feat: refresh skills metadata and toggles
This commit is contained in:
@@ -2,15 +2,19 @@ import { runCommandWithTimeout } from "../process/exec.js";
|
||||
import { resolveUserPath } from "../utils.js";
|
||||
import {
|
||||
loadWorkspaceSkillEntries,
|
||||
resolveSkillsInstallPreferences,
|
||||
type SkillEntry,
|
||||
type SkillInstallSpec,
|
||||
type SkillsInstallPreferences,
|
||||
} from "./skills.js";
|
||||
import type { ClawdisConfig } from "../config/config.js";
|
||||
|
||||
export type SkillInstallRequest = {
|
||||
workspaceDir: string;
|
||||
skillName: string;
|
||||
installId: string;
|
||||
timeoutMs?: number;
|
||||
config?: ClawdisConfig;
|
||||
};
|
||||
|
||||
export type SkillInstallResult = {
|
||||
@@ -40,7 +44,24 @@ function runShell(command: string, timeoutMs: number) {
|
||||
return runCommandWithTimeout(["/bin/zsh", "-lc", command], { timeoutMs });
|
||||
}
|
||||
|
||||
function buildInstallCommand(spec: SkillInstallSpec): {
|
||||
function buildNodeInstallCommand(
|
||||
packageName: string,
|
||||
prefs: SkillsInstallPreferences,
|
||||
): string[] {
|
||||
switch (prefs.nodeManager) {
|
||||
case "pnpm":
|
||||
return ["pnpm", "add", "-g", packageName];
|
||||
case "bun":
|
||||
return ["bun", "add", "-g", packageName];
|
||||
default:
|
||||
return ["npm", "install", "-g", packageName];
|
||||
}
|
||||
}
|
||||
|
||||
function buildInstallCommand(
|
||||
spec: SkillInstallSpec,
|
||||
prefs: SkillsInstallPreferences,
|
||||
): {
|
||||
argv: string[] | null;
|
||||
shell: string | null;
|
||||
cwd?: string;
|
||||
@@ -55,7 +76,10 @@ function buildInstallCommand(spec: SkillInstallSpec): {
|
||||
case "node": {
|
||||
if (!spec.package)
|
||||
return { argv: null, shell: null, error: "missing node package" };
|
||||
return { argv: ["npm", "install", "-g", spec.package], shell: null };
|
||||
return {
|
||||
argv: buildNodeInstallCommand(spec.package, prefs),
|
||||
shell: null,
|
||||
};
|
||||
}
|
||||
case "go": {
|
||||
if (!spec.module)
|
||||
@@ -74,18 +98,6 @@ function buildInstallCommand(spec: SkillInstallSpec): {
|
||||
const cmd = `cd ${JSON.stringify(repoPath)} && pnpm install && pnpm run ${JSON.stringify(spec.script)}`;
|
||||
return { argv: null, shell: cmd };
|
||||
}
|
||||
case "git": {
|
||||
if (!spec.url || !spec.destination) {
|
||||
return {
|
||||
argv: null,
|
||||
shell: null,
|
||||
error: "missing git url/destination",
|
||||
};
|
||||
}
|
||||
const dest = resolveUserPath(spec.destination);
|
||||
const cmd = `if [ -d ${JSON.stringify(dest)} ]; then echo "Already cloned"; else git clone ${JSON.stringify(spec.url)} ${JSON.stringify(dest)}; fi`;
|
||||
return { argv: null, shell: cmd };
|
||||
}
|
||||
case "shell": {
|
||||
if (!spec.command)
|
||||
return { argv: null, shell: null, error: "missing shell command" };
|
||||
@@ -127,7 +139,8 @@ export async function installSkill(
|
||||
};
|
||||
}
|
||||
|
||||
const command = buildInstallCommand(spec);
|
||||
const prefs = resolveSkillsInstallPreferences(params.config);
|
||||
const command = buildInstallCommand(spec, prefs);
|
||||
if (command.error) {
|
||||
return {
|
||||
ok: false,
|
||||
|
||||
@@ -8,8 +8,10 @@ import {
|
||||
loadWorkspaceSkillEntries,
|
||||
resolveConfigPath,
|
||||
resolveSkillConfig,
|
||||
resolveSkillsInstallPreferences,
|
||||
type SkillEntry,
|
||||
type SkillInstallSpec,
|
||||
type SkillsInstallPreferences,
|
||||
} from "./skills.js";
|
||||
|
||||
export type SkillStatusConfigCheck = {
|
||||
@@ -33,6 +35,7 @@ export type SkillStatusEntry = {
|
||||
baseDir: string;
|
||||
skillKey: string;
|
||||
primaryEnv?: string;
|
||||
emoji?: string;
|
||||
always: boolean;
|
||||
disabled: boolean;
|
||||
eligible: boolean;
|
||||
@@ -60,45 +63,78 @@ function resolveSkillKey(entry: SkillEntry): string {
|
||||
return entry.clawdis?.skillKey ?? entry.skill.name;
|
||||
}
|
||||
|
||||
function normalizeInstallOptions(entry: SkillEntry): SkillInstallOption[] {
|
||||
function selectPreferredInstallSpec(
|
||||
install: SkillInstallSpec[],
|
||||
prefs: SkillsInstallPreferences,
|
||||
): { spec: SkillInstallSpec; index: number } | undefined {
|
||||
if (install.length === 0) return undefined;
|
||||
const indexed = install.map((spec, index) => ({ spec, index }));
|
||||
const findKind = (kind: SkillInstallSpec["kind"]) =>
|
||||
indexed.find((item) => item.spec.kind === kind);
|
||||
|
||||
const brewSpec = findKind("brew");
|
||||
const nodeSpec = findKind("node");
|
||||
const goSpec = findKind("go");
|
||||
const pnpmSpec = findKind("pnpm");
|
||||
const shellSpec = findKind("shell");
|
||||
|
||||
if (prefs.preferBrew && hasBinary("brew") && brewSpec) return brewSpec;
|
||||
if (nodeSpec) return nodeSpec;
|
||||
if (brewSpec) return brewSpec;
|
||||
if (goSpec) return goSpec;
|
||||
if (pnpmSpec) return pnpmSpec;
|
||||
if (shellSpec) return shellSpec;
|
||||
return indexed[0];
|
||||
}
|
||||
|
||||
function normalizeInstallOptions(
|
||||
entry: SkillEntry,
|
||||
prefs: SkillsInstallPreferences,
|
||||
): SkillInstallOption[] {
|
||||
const install = entry.clawdis?.install ?? [];
|
||||
if (install.length === 0) return [];
|
||||
return install.map((spec, index) => {
|
||||
const id = (spec.id ?? `${spec.kind}-${index}`).trim();
|
||||
const bins = spec.bins ?? [];
|
||||
let label = (spec.label ?? "").trim();
|
||||
if (!label) {
|
||||
if (spec.kind === "brew" && spec.formula) {
|
||||
label = `Install ${spec.formula} (brew)`;
|
||||
} else if (spec.kind === "node" && spec.package) {
|
||||
label = `Install ${spec.package} (node)`;
|
||||
} else if (spec.kind === "go" && spec.module) {
|
||||
label = `Install ${spec.module} (go)`;
|
||||
} else if (spec.kind === "pnpm" && spec.repoPath) {
|
||||
label = `Install ${spec.repoPath} (pnpm)`;
|
||||
} else if (spec.kind === "git" && spec.url) {
|
||||
label = `Clone ${spec.url}`;
|
||||
} else {
|
||||
label = "Run installer";
|
||||
}
|
||||
const preferred = selectPreferredInstallSpec(install, prefs);
|
||||
if (!preferred) return [];
|
||||
const { spec, index } = preferred;
|
||||
const id = (spec.id ?? `${spec.kind}-${index}`).trim();
|
||||
const bins = spec.bins ?? [];
|
||||
let label = (spec.label ?? "").trim();
|
||||
if (spec.kind === "node" && spec.package) {
|
||||
label = `Install ${spec.package} (${prefs.nodeManager})`;
|
||||
}
|
||||
if (!label) {
|
||||
if (spec.kind === "brew" && spec.formula) {
|
||||
label = `Install ${spec.formula} (brew)`;
|
||||
} else if (spec.kind === "node" && spec.package) {
|
||||
label = `Install ${spec.package} (${prefs.nodeManager})`;
|
||||
} else if (spec.kind === "go" && spec.module) {
|
||||
label = `Install ${spec.module} (go)`;
|
||||
} else if (spec.kind === "pnpm" && spec.repoPath) {
|
||||
label = `Install ${spec.repoPath} (pnpm)`;
|
||||
} else {
|
||||
label = "Run installer";
|
||||
}
|
||||
return {
|
||||
}
|
||||
return [
|
||||
{
|
||||
id,
|
||||
kind: spec.kind,
|
||||
label,
|
||||
bins,
|
||||
};
|
||||
});
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
function buildSkillStatus(
|
||||
entry: SkillEntry,
|
||||
config?: ClawdisConfig,
|
||||
prefs?: SkillsInstallPreferences,
|
||||
): SkillStatusEntry {
|
||||
const skillKey = resolveSkillKey(entry);
|
||||
const skillConfig = resolveSkillConfig(config, skillKey);
|
||||
const disabled = skillConfig?.enabled === false;
|
||||
const always = entry.clawdis?.always === true;
|
||||
const emoji = entry.clawdis?.emoji ?? entry.frontmatter.emoji;
|
||||
|
||||
const requiredBins = entry.clawdis?.requires?.bins ?? [];
|
||||
const requiredEnv = entry.clawdis?.requires?.env ?? [];
|
||||
@@ -145,6 +181,7 @@ function buildSkillStatus(
|
||||
baseDir: entry.skill.baseDir,
|
||||
skillKey,
|
||||
primaryEnv: entry.clawdis?.primaryEnv,
|
||||
emoji,
|
||||
always,
|
||||
disabled,
|
||||
eligible,
|
||||
@@ -155,7 +192,10 @@ function buildSkillStatus(
|
||||
},
|
||||
missing,
|
||||
configChecks,
|
||||
install: normalizeInstallOptions(entry),
|
||||
install: normalizeInstallOptions(
|
||||
entry,
|
||||
prefs ?? resolveSkillsInstallPreferences(config),
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -171,9 +211,12 @@ export function buildWorkspaceSkillStatus(
|
||||
opts?.managedSkillsDir ?? path.join(CONFIG_DIR, "skills");
|
||||
const skillEntries =
|
||||
opts?.entries ?? loadWorkspaceSkillEntries(workspaceDir, opts);
|
||||
const prefs = resolveSkillsInstallPreferences(opts?.config);
|
||||
return {
|
||||
workspaceDir,
|
||||
managedSkillsDir,
|
||||
skills: skillEntries.map((entry) => buildSkillStatus(entry, opts?.config)),
|
||||
skills: skillEntries.map((entry) =>
|
||||
buildSkillStatus(entry, opts?.config, prefs),
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ import { CONFIG_DIR, resolveUserPath } from "../utils.js";
|
||||
|
||||
export type SkillInstallSpec = {
|
||||
id?: string;
|
||||
kind: "brew" | "node" | "go" | "pnpm" | "git" | "shell";
|
||||
kind: "brew" | "node" | "go" | "pnpm" | "shell";
|
||||
label?: string;
|
||||
bins?: string[];
|
||||
formula?: string;
|
||||
@@ -21,8 +21,6 @@ export type SkillInstallSpec = {
|
||||
module?: string;
|
||||
repoPath?: string;
|
||||
script?: string;
|
||||
url?: string;
|
||||
destination?: string;
|
||||
command?: string;
|
||||
};
|
||||
|
||||
@@ -30,6 +28,7 @@ export type ClawdisSkillMetadata = {
|
||||
always?: boolean;
|
||||
skillKey?: string;
|
||||
primaryEnv?: string;
|
||||
emoji?: string;
|
||||
requires?: {
|
||||
bins?: string[];
|
||||
env?: string[];
|
||||
@@ -38,6 +37,11 @@ export type ClawdisSkillMetadata = {
|
||||
install?: SkillInstallSpec[];
|
||||
};
|
||||
|
||||
export type SkillsInstallPreferences = {
|
||||
preferBrew: boolean;
|
||||
nodeManager: "npm" | "pnpm" | "bun";
|
||||
};
|
||||
|
||||
type ParsedSkillFrontmatter = Record<string, string>;
|
||||
|
||||
export type SkillEntry = {
|
||||
@@ -141,7 +145,6 @@ function parseInstallSpec(input: unknown): SkillInstallSpec | undefined {
|
||||
kind !== "node" &&
|
||||
kind !== "go" &&
|
||||
kind !== "pnpm" &&
|
||||
kind !== "git" &&
|
||||
kind !== "shell"
|
||||
) {
|
||||
return undefined;
|
||||
@@ -160,8 +163,6 @@ function parseInstallSpec(input: unknown): SkillInstallSpec | undefined {
|
||||
if (typeof raw.module === "string") spec.module = raw.module;
|
||||
if (typeof raw.repoPath === "string") spec.repoPath = raw.repoPath;
|
||||
if (typeof raw.script === "string") spec.script = raw.script;
|
||||
if (typeof raw.url === "string") spec.url = raw.url;
|
||||
if (typeof raw.destination === "string") spec.destination = raw.destination;
|
||||
if (typeof raw.command === "string") spec.command = raw.command;
|
||||
|
||||
return spec;
|
||||
@@ -179,6 +180,21 @@ const DEFAULT_CONFIG_VALUES: Record<string, boolean> = {
|
||||
"browser.enabled": true,
|
||||
};
|
||||
|
||||
export function resolveSkillsInstallPreferences(
|
||||
config?: ClawdisConfig,
|
||||
): SkillsInstallPreferences {
|
||||
const raw = config?.skillsInstall;
|
||||
const preferBrew = raw?.preferBrew ?? true;
|
||||
const managerRaw =
|
||||
typeof raw?.nodeManager === "string" ? raw.nodeManager.trim() : "";
|
||||
const manager = managerRaw.toLowerCase();
|
||||
const nodeManager =
|
||||
manager === "pnpm" || manager === "bun" || manager === "npm"
|
||||
? (manager as SkillsInstallPreferences["nodeManager"])
|
||||
: "npm";
|
||||
return { preferBrew, nodeManager };
|
||||
}
|
||||
|
||||
export function resolveConfigPath(
|
||||
config: ClawdisConfig | undefined,
|
||||
pathStr: string,
|
||||
@@ -253,6 +269,8 @@ function resolveClawdisMetadata(
|
||||
return {
|
||||
always:
|
||||
typeof clawdisObj.always === "boolean" ? clawdisObj.always : undefined,
|
||||
emoji:
|
||||
typeof clawdisObj.emoji === "string" ? clawdisObj.emoji : undefined,
|
||||
skillKey:
|
||||
typeof clawdisObj.skillKey === "string"
|
||||
? clawdisObj.skillKey
|
||||
|
||||
Reference in New Issue
Block a user