mirror of
https://github.com/openswarm-ai/openswarm.git
synced 2026-08-17 18:25:42 +02:00
[eric] skills: the Add box accepts the install commands people paste from READMEs (npx/npm/pnpm/bunx, URLs, bare names)
This commit is contained in:
@@ -0,0 +1,55 @@
|
||||
"""Turn the install commands people already paste from READMEs into a skill id we can install.
|
||||
|
||||
The ecosystem's grammar is `npx skills add <name>`, and every neighbouring form (npm/pnpm/bunx,
|
||||
`install` instead of `add`, a bare `@scope/name`, a skills.sh URL, or just the name) means the same
|
||||
thing to the person pasting it. Accepting only our own button was the friction."""
|
||||
|
||||
import re
|
||||
from typing import Optional
|
||||
|
||||
from typeguard import typechecked
|
||||
|
||||
# The runners people actually have in their muscle memory.
|
||||
P_RUNNERS = ("npx", "npm", "pnpm", "pnpx", "yarn", "bunx", "bun", "deno")
|
||||
P_VERBS = ("add", "install", "i")
|
||||
P_SKILL_ID = re.compile(r"^[A-Za-z0-9@._/-]+$")
|
||||
|
||||
|
||||
@typechecked
|
||||
def parse_install_command(raw: str) -> Optional[str]:
|
||||
"""Return the skill id a pasted command refers to, or None when it is not an install command.
|
||||
|
||||
None means "I could not read this", never a guess: installing the wrong skill because a paste
|
||||
was ambiguous is worse than asking the user to pick from the list."""
|
||||
text = (raw or "").strip()
|
||||
if not text:
|
||||
return None
|
||||
# A skills.sh (or GitHub) URL carries the id in its last meaningful path segment.
|
||||
if text.startswith(("http://", "https://")):
|
||||
parts = [p for p in text.split("?")[0].split("#")[0].rstrip("/").split("/") if p]
|
||||
tail = parts[-1] if parts else ""
|
||||
return tail if tail and P_SKILL_ID.match(tail) else None
|
||||
|
||||
# Strip a leading shell prompt or copy artifact ("$ npx ...").
|
||||
text = re.sub(r"^[$>#]\s*", "", text)
|
||||
tokens = text.split()
|
||||
if not tokens:
|
||||
return None
|
||||
|
||||
if tokens[0].lower() in P_RUNNERS:
|
||||
# npx skills add <id> | npm i skills <id> | bunx skills add <id>
|
||||
rest = [t for t in tokens[1:] if not t.startswith("-")]
|
||||
# The verb and the package name arrive in either order ("npx skills add x", "npm i skills x"),
|
||||
# so strip both, in whichever order they appear.
|
||||
for _ in range(2):
|
||||
if rest and rest[0].lower() in ("skills", "skill", "@skills/cli", "openswarm"):
|
||||
rest = rest[1:]
|
||||
elif rest and rest[0].lower() in P_VERBS:
|
||||
rest = rest[1:]
|
||||
candidate = rest[0] if rest else ""
|
||||
return candidate if candidate and P_SKILL_ID.match(candidate) else None
|
||||
|
||||
# A bare id or scoped package pasted on its own.
|
||||
if len(tokens) == 1 and P_SKILL_ID.match(tokens[0]) and "." not in tokens[0].split("/")[-1][:1]:
|
||||
return tokens[0]
|
||||
return None
|
||||
@@ -115,6 +115,20 @@ class p_InstallRequest(BaseModel):
|
||||
confirm: bool = False
|
||||
|
||||
|
||||
class p_ParseCommandRequest(BaseModel):
|
||||
# What the user pasted: "npx skills add pdf-filler", a skills.sh URL, or a bare name.
|
||||
command: str
|
||||
|
||||
|
||||
@skill_registry.router.post("/parse-command")
|
||||
def registry_parse_command(req: p_ParseCommandRequest) -> dict:
|
||||
"""Resolve a pasted install command to a skill id so the Marketplace's Add box accepts the
|
||||
grammar people already copy out of READMEs. Resolution only; nothing is installed here, and an
|
||||
unreadable paste returns null rather than a guess."""
|
||||
from backend.apps.skill_registry.parse_install_command import parse_install_command
|
||||
return {"skill_id": parse_install_command(req.command)}
|
||||
|
||||
|
||||
@skill_registry.router.post("/install")
|
||||
async def registry_install(req: p_InstallRequest):
|
||||
"""Install a community (skills.sh) skill, in two honest steps.
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
"""Pasting the command from a README must install the skill. Guessing wrong is worse than asking,
|
||||
so anything unreadable returns None rather than a best effort."""
|
||||
|
||||
import pytest
|
||||
|
||||
from backend.apps.skill_registry.parse_install_command import parse_install_command
|
||||
|
||||
|
||||
@pytest.mark.parametrize("cmd", [
|
||||
"npx skills add pdf-filler",
|
||||
"npx skills install pdf-filler",
|
||||
"npm i skills pdf-filler",
|
||||
"pnpm skills add pdf-filler",
|
||||
"bunx skills add pdf-filler",
|
||||
"$ npx skills add pdf-filler",
|
||||
"npx --yes skills add pdf-filler",
|
||||
"npx skills add pdf-filler ",
|
||||
])
|
||||
def test_every_common_runner_and_verb_resolves_the_same_skill(cmd):
|
||||
assert parse_install_command(cmd) == "pdf-filler"
|
||||
|
||||
|
||||
def test_scoped_names_survive():
|
||||
assert parse_install_command("npx skills add @anthropic/docx") == "@anthropic/docx"
|
||||
assert parse_install_command("@anthropic/docx") == "@anthropic/docx"
|
||||
|
||||
|
||||
def test_a_bare_name_is_accepted():
|
||||
assert parse_install_command("pdf-filler") == "pdf-filler"
|
||||
|
||||
|
||||
def test_urls_carry_their_id_in_the_last_segment():
|
||||
assert parse_install_command("https://skills.sh/s/pdf-filler") == "pdf-filler"
|
||||
assert parse_install_command("https://skills.sh/s/pdf-filler/") == "pdf-filler"
|
||||
assert parse_install_command("https://skills.sh/s/pdf-filler?ref=x") == "pdf-filler"
|
||||
|
||||
|
||||
@pytest.mark.parametrize("junk", [
|
||||
"", " ", "npx skills add", "npm install", "how do i install a skill",
|
||||
"rm -rf /", "npx skills add ; rm -rf /",
|
||||
])
|
||||
def test_unreadable_input_returns_none_rather_than_a_guess(junk):
|
||||
assert parse_install_command(junk) is None
|
||||
Reference in New Issue
Block a user