diff --git a/docs/mac/skills.md b/docs/mac/skills.md index b85426e7e..4ab81ca8a 100644 --- a/docs/mac/skills.md +++ b/docs/mac/skills.md @@ -13,7 +13,7 @@ The macOS app surfaces Clawdis skills via the gateway; it does not parse skills - Requirements are derived from `metadata.clawdis.requires` in each `SKILL.md`. ## Install actions -- `metadata.clawdis.install` defines install options (brew/node/go/shell). +- `metadata.clawdis.install` defines install options (brew/node/go/uv). - The app calls `skills.install` to run installers on the gateway host. - The gateway surfaces only one preferred installer when multiple are provided (brew when available, otherwise node manager from `skillsInstall`, default npm). diff --git a/docs/skills.md b/docs/skills.md index 0a2a28d46..3bccd5028 100644 --- a/docs/skills.md +++ b/docs/skills.md @@ -62,7 +62,7 @@ Fields under `metadata.clawdis`: - `requires.env` — list; env var must exist **or** be provided in config. - `requires.config` — list of `clawdis.json` paths that must be truthy. - `primaryEnv` — env var name associated with `skills..apiKey`. -- `install` — optional array of installer specs used by the macOS Skills UI (brew/node/go/shell). +- `install` — optional array of installer specs used by the macOS Skills UI (brew/node/go/uv). Installer example: diff --git a/skills/nano-pdf/SKILL.md b/skills/nano-pdf/SKILL.md index 3f2deef4e..ae632eada 100644 --- a/skills/nano-pdf/SKILL.md +++ b/skills/nano-pdf/SKILL.md @@ -1,7 +1,7 @@ --- name: nano-pdf description: Edit PDFs with natural-language instructions using the nano-pdf CLI. -metadata: {"clawdis":{"emoji":"📄","requires":{"bins":["nano-pdf"]},"install":[{"id":"pipx","kind":"shell","command":"python3 -m pip install --user pipx && python3 -m pipx ensurepath && pipx install nano-pdf","bins":["nano-pdf"],"label":"Install nano-pdf (pipx)"},{"id":"pip","kind":"shell","command":"python3 -m pip install --user nano-pdf","bins":["nano-pdf"],"label":"Install nano-pdf (pip --user)"}]}} +metadata: {"clawdis":{"emoji":"📄","requires":{"bins":["nano-pdf"]},"install":[{"id":"uv","kind":"uv","package":"nano-pdf","bins":["nano-pdf"],"label":"Install nano-pdf (uv)"}]}} --- # nano-pdf diff --git a/src/agents/skills-install.ts b/src/agents/skills-install.ts index a1c2dcdf4..90025836a 100644 --- a/src/agents/skills-install.ts +++ b/src/agents/skills-install.ts @@ -1,13 +1,14 @@ +import type { ClawdisConfig } from "../config/config.js"; import { runCommandWithTimeout } from "../process/exec.js"; import { resolveUserPath } from "../utils.js"; import { + hasBinary, loadWorkspaceSkillEntries, resolveSkillsInstallPreferences, type SkillEntry, type SkillInstallSpec, type SkillsInstallPreferences, } from "./skills.js"; -import type { ClawdisConfig } from "../config/config.js"; export type SkillInstallRequest = { workspaceDir: string; @@ -40,10 +41,6 @@ function findInstallSpec( return undefined; } -function runShell(command: string, timeoutMs: number) { - return runCommandWithTimeout(["/bin/zsh", "-lc", command], { timeoutMs }); -} - function buildNodeInstallCommand( packageName: string, prefs: SkillsInstallPreferences, @@ -63,36 +60,33 @@ function buildInstallCommand( prefs: SkillsInstallPreferences, ): { argv: string[] | null; - shell: string | null; - cwd?: string; error?: string; } { switch (spec.kind) { case "brew": { if (!spec.formula) - return { argv: null, shell: null, error: "missing brew formula" }; - return { argv: ["brew", "install", spec.formula], shell: null }; + return { argv: null, error: "missing brew formula" }; + return { argv: ["brew", "install", spec.formula] }; } case "node": { if (!spec.package) - return { argv: null, shell: null, error: "missing node package" }; + return { argv: null, error: "missing node package" }; return { argv: buildNodeInstallCommand(spec.package, prefs), - shell: null, }; } case "go": { if (!spec.module) - return { argv: null, shell: null, error: "missing go module" }; - return { argv: ["go", "install", spec.module], shell: null }; + return { argv: null, error: "missing go module" }; + return { argv: ["go", "install", spec.module] }; } - case "shell": { - if (!spec.command) - return { argv: null, shell: null, error: "missing shell command" }; - return { argv: null, shell: spec.command }; + case "uv": { + if (!spec.package) + return { argv: null, error: "missing uv package" }; + return { argv: ["uv", "tool", "install", spec.package] }; } default: - return { argv: null, shell: null, error: "unsupported installer" }; + return { argv: null, error: "unsupported installer" }; } } @@ -138,7 +132,34 @@ export async function installSkill( code: null, }; } - if (!command.shell && (!command.argv || command.argv.length === 0)) { + if (spec.kind === "uv" && !hasBinary("uv")) { + if (hasBinary("brew")) { + const brewResult = await runCommandWithTimeout( + ["brew", "install", "uv"], + { + timeoutMs, + }, + ); + if (brewResult.code !== 0) { + return { + ok: false, + message: "Failed to install uv (brew)", + stdout: brewResult.stdout.trim(), + stderr: brewResult.stderr.trim(), + code: brewResult.code, + }; + } + } else { + return { + ok: false, + message: "uv not installed (install via brew)", + stdout: "", + stderr: "", + code: null, + }; + } + } + if (!command.argv || command.argv.length === 0) { return { ok: false, message: "invalid install command", @@ -149,14 +170,12 @@ export async function installSkill( } const result = await (async () => { - if (command.shell) return runShell(command.shell, timeoutMs); const argv = command.argv; if (!argv || argv.length === 0) { return { code: null, stdout: "", stderr: "invalid install command" }; } return runCommandWithTimeout(argv, { timeoutMs, - cwd: command.cwd, }); })(); diff --git a/src/agents/skills-status.ts b/src/agents/skills-status.ts index 7f41bf6f4..af0eee27d 100644 --- a/src/agents/skills-status.ts +++ b/src/agents/skills-status.ts @@ -76,13 +76,13 @@ function selectPreferredInstallSpec( const brewSpec = findKind("brew"); const nodeSpec = findKind("node"); const goSpec = findKind("go"); - const shellSpec = findKind("shell"); + const uvSpec = findKind("uv"); if (prefs.preferBrew && hasBinary("brew") && brewSpec) return brewSpec; + if (uvSpec) return uvSpec; if (nodeSpec) return nodeSpec; if (brewSpec) return brewSpec; if (goSpec) return goSpec; - if (shellSpec) return shellSpec; return indexed[0]; } @@ -108,6 +108,8 @@ function normalizeInstallOptions( label = `Install ${spec.package} (${prefs.nodeManager})`; } else if (spec.kind === "go" && spec.module) { label = `Install ${spec.module} (go)`; + } else if (spec.kind === "uv" && spec.package) { + label = `Install ${spec.package} (uv)`; } else { label = "Run installer"; } diff --git a/src/agents/skills.ts b/src/agents/skills.ts index 68cb00a30..4fcbc5315 100644 --- a/src/agents/skills.ts +++ b/src/agents/skills.ts @@ -13,13 +13,12 @@ import { CONFIG_DIR, resolveUserPath } from "../utils.js"; export type SkillInstallSpec = { id?: string; - kind: "brew" | "node" | "go" | "shell"; + kind: "brew" | "node" | "go" | "uv"; label?: string; bins?: string[]; formula?: string; package?: string; module?: string; - command?: string; }; export type ClawdisSkillMetadata = { @@ -143,7 +142,7 @@ function parseInstallSpec(input: unknown): SkillInstallSpec | undefined { kind !== "brew" && kind !== "node" && kind !== "go" && - kind !== "shell" + kind !== "uv" ) { return undefined; } @@ -159,7 +158,6 @@ function parseInstallSpec(input: unknown): SkillInstallSpec | undefined { if (typeof raw.formula === "string") spec.formula = raw.formula; if (typeof raw.package === "string") spec.package = raw.package; if (typeof raw.module === "string") spec.module = raw.module; - if (typeof raw.command === "string") spec.command = raw.command; return spec; }