feat: add uv skill installers
This commit is contained in:
@@ -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`.
|
- Requirements are derived from `metadata.clawdis.requires` in each `SKILL.md`.
|
||||||
|
|
||||||
## Install actions
|
## 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 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).
|
- The gateway surfaces only one preferred installer when multiple are provided (brew when available, otherwise node manager from `skillsInstall`, default npm).
|
||||||
|
|
||||||
|
|||||||
@@ -62,7 +62,7 @@ Fields under `metadata.clawdis`:
|
|||||||
- `requires.env` — list; env var must exist **or** be provided in config.
|
- `requires.env` — list; env var must exist **or** be provided in config.
|
||||||
- `requires.config` — list of `clawdis.json` paths that must be truthy.
|
- `requires.config` — list of `clawdis.json` paths that must be truthy.
|
||||||
- `primaryEnv` — env var name associated with `skills.<name>.apiKey`.
|
- `primaryEnv` — env var name associated with `skills.<name>.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:
|
Installer example:
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
---
|
---
|
||||||
name: nano-pdf
|
name: nano-pdf
|
||||||
description: Edit PDFs with natural-language instructions using the nano-pdf CLI.
|
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
|
# nano-pdf
|
||||||
|
|||||||
@@ -1,13 +1,14 @@
|
|||||||
|
import type { ClawdisConfig } from "../config/config.js";
|
||||||
import { runCommandWithTimeout } from "../process/exec.js";
|
import { runCommandWithTimeout } from "../process/exec.js";
|
||||||
import { resolveUserPath } from "../utils.js";
|
import { resolveUserPath } from "../utils.js";
|
||||||
import {
|
import {
|
||||||
|
hasBinary,
|
||||||
loadWorkspaceSkillEntries,
|
loadWorkspaceSkillEntries,
|
||||||
resolveSkillsInstallPreferences,
|
resolveSkillsInstallPreferences,
|
||||||
type SkillEntry,
|
type SkillEntry,
|
||||||
type SkillInstallSpec,
|
type SkillInstallSpec,
|
||||||
type SkillsInstallPreferences,
|
type SkillsInstallPreferences,
|
||||||
} from "./skills.js";
|
} from "./skills.js";
|
||||||
import type { ClawdisConfig } from "../config/config.js";
|
|
||||||
|
|
||||||
export type SkillInstallRequest = {
|
export type SkillInstallRequest = {
|
||||||
workspaceDir: string;
|
workspaceDir: string;
|
||||||
@@ -40,10 +41,6 @@ function findInstallSpec(
|
|||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
function runShell(command: string, timeoutMs: number) {
|
|
||||||
return runCommandWithTimeout(["/bin/zsh", "-lc", command], { timeoutMs });
|
|
||||||
}
|
|
||||||
|
|
||||||
function buildNodeInstallCommand(
|
function buildNodeInstallCommand(
|
||||||
packageName: string,
|
packageName: string,
|
||||||
prefs: SkillsInstallPreferences,
|
prefs: SkillsInstallPreferences,
|
||||||
@@ -63,36 +60,33 @@ function buildInstallCommand(
|
|||||||
prefs: SkillsInstallPreferences,
|
prefs: SkillsInstallPreferences,
|
||||||
): {
|
): {
|
||||||
argv: string[] | null;
|
argv: string[] | null;
|
||||||
shell: string | null;
|
|
||||||
cwd?: string;
|
|
||||||
error?: string;
|
error?: string;
|
||||||
} {
|
} {
|
||||||
switch (spec.kind) {
|
switch (spec.kind) {
|
||||||
case "brew": {
|
case "brew": {
|
||||||
if (!spec.formula)
|
if (!spec.formula)
|
||||||
return { argv: null, shell: null, error: "missing brew formula" };
|
return { argv: null, error: "missing brew formula" };
|
||||||
return { argv: ["brew", "install", spec.formula], shell: null };
|
return { argv: ["brew", "install", spec.formula] };
|
||||||
}
|
}
|
||||||
case "node": {
|
case "node": {
|
||||||
if (!spec.package)
|
if (!spec.package)
|
||||||
return { argv: null, shell: null, error: "missing node package" };
|
return { argv: null, error: "missing node package" };
|
||||||
return {
|
return {
|
||||||
argv: buildNodeInstallCommand(spec.package, prefs),
|
argv: buildNodeInstallCommand(spec.package, prefs),
|
||||||
shell: null,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
case "go": {
|
case "go": {
|
||||||
if (!spec.module)
|
if (!spec.module)
|
||||||
return { argv: null, shell: null, error: "missing go module" };
|
return { argv: null, error: "missing go module" };
|
||||||
return { argv: ["go", "install", spec.module], shell: null };
|
return { argv: ["go", "install", spec.module] };
|
||||||
}
|
}
|
||||||
case "shell": {
|
case "uv": {
|
||||||
if (!spec.command)
|
if (!spec.package)
|
||||||
return { argv: null, shell: null, error: "missing shell command" };
|
return { argv: null, error: "missing uv package" };
|
||||||
return { argv: null, shell: spec.command };
|
return { argv: ["uv", "tool", "install", spec.package] };
|
||||||
}
|
}
|
||||||
default:
|
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,
|
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 {
|
return {
|
||||||
ok: false,
|
ok: false,
|
||||||
message: "invalid install command",
|
message: "invalid install command",
|
||||||
@@ -149,14 +170,12 @@ export async function installSkill(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const result = await (async () => {
|
const result = await (async () => {
|
||||||
if (command.shell) return runShell(command.shell, timeoutMs);
|
|
||||||
const argv = command.argv;
|
const argv = command.argv;
|
||||||
if (!argv || argv.length === 0) {
|
if (!argv || argv.length === 0) {
|
||||||
return { code: null, stdout: "", stderr: "invalid install command" };
|
return { code: null, stdout: "", stderr: "invalid install command" };
|
||||||
}
|
}
|
||||||
return runCommandWithTimeout(argv, {
|
return runCommandWithTimeout(argv, {
|
||||||
timeoutMs,
|
timeoutMs,
|
||||||
cwd: command.cwd,
|
|
||||||
});
|
});
|
||||||
})();
|
})();
|
||||||
|
|
||||||
|
|||||||
@@ -76,13 +76,13 @@ function selectPreferredInstallSpec(
|
|||||||
const brewSpec = findKind("brew");
|
const brewSpec = findKind("brew");
|
||||||
const nodeSpec = findKind("node");
|
const nodeSpec = findKind("node");
|
||||||
const goSpec = findKind("go");
|
const goSpec = findKind("go");
|
||||||
const shellSpec = findKind("shell");
|
const uvSpec = findKind("uv");
|
||||||
|
|
||||||
if (prefs.preferBrew && hasBinary("brew") && brewSpec) return brewSpec;
|
if (prefs.preferBrew && hasBinary("brew") && brewSpec) return brewSpec;
|
||||||
|
if (uvSpec) return uvSpec;
|
||||||
if (nodeSpec) return nodeSpec;
|
if (nodeSpec) return nodeSpec;
|
||||||
if (brewSpec) return brewSpec;
|
if (brewSpec) return brewSpec;
|
||||||
if (goSpec) return goSpec;
|
if (goSpec) return goSpec;
|
||||||
if (shellSpec) return shellSpec;
|
|
||||||
return indexed[0];
|
return indexed[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -108,6 +108,8 @@ function normalizeInstallOptions(
|
|||||||
label = `Install ${spec.package} (${prefs.nodeManager})`;
|
label = `Install ${spec.package} (${prefs.nodeManager})`;
|
||||||
} else if (spec.kind === "go" && spec.module) {
|
} else if (spec.kind === "go" && spec.module) {
|
||||||
label = `Install ${spec.module} (go)`;
|
label = `Install ${spec.module} (go)`;
|
||||||
|
} else if (spec.kind === "uv" && spec.package) {
|
||||||
|
label = `Install ${spec.package} (uv)`;
|
||||||
} else {
|
} else {
|
||||||
label = "Run installer";
|
label = "Run installer";
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,13 +13,12 @@ import { CONFIG_DIR, resolveUserPath } from "../utils.js";
|
|||||||
|
|
||||||
export type SkillInstallSpec = {
|
export type SkillInstallSpec = {
|
||||||
id?: string;
|
id?: string;
|
||||||
kind: "brew" | "node" | "go" | "shell";
|
kind: "brew" | "node" | "go" | "uv";
|
||||||
label?: string;
|
label?: string;
|
||||||
bins?: string[];
|
bins?: string[];
|
||||||
formula?: string;
|
formula?: string;
|
||||||
package?: string;
|
package?: string;
|
||||||
module?: string;
|
module?: string;
|
||||||
command?: string;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ClawdisSkillMetadata = {
|
export type ClawdisSkillMetadata = {
|
||||||
@@ -143,7 +142,7 @@ function parseInstallSpec(input: unknown): SkillInstallSpec | undefined {
|
|||||||
kind !== "brew" &&
|
kind !== "brew" &&
|
||||||
kind !== "node" &&
|
kind !== "node" &&
|
||||||
kind !== "go" &&
|
kind !== "go" &&
|
||||||
kind !== "shell"
|
kind !== "uv"
|
||||||
) {
|
) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
@@ -159,7 +158,6 @@ function parseInstallSpec(input: unknown): SkillInstallSpec | undefined {
|
|||||||
if (typeof raw.formula === "string") spec.formula = raw.formula;
|
if (typeof raw.formula === "string") spec.formula = raw.formula;
|
||||||
if (typeof raw.package === "string") spec.package = raw.package;
|
if (typeof raw.package === "string") spec.package = raw.package;
|
||||||
if (typeof raw.module === "string") spec.module = raw.module;
|
if (typeof raw.module === "string") spec.module = raw.module;
|
||||||
if (typeof raw.command === "string") spec.command = raw.command;
|
|
||||||
|
|
||||||
return spec;
|
return spec;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user