Skip to content

Commit d4ad39f

Browse files
committed
feat(cli): auto install after add
1 parent c6c0e2e commit d4ad39f

File tree

3 files changed

+78
-46
lines changed

3 files changed

+78
-46
lines changed

src/cli.ts

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { parseArgs } from "node:util";
44

55
import { c } from "./utils/colors.ts";
66
import { addSkill, findSkillsConfig } from "./config.ts";
7-
import { installSkills } from "./skills.ts";
7+
import { installSkillSource, installSkills } from "./skills.ts";
88

99
const name = "skillman";
1010
const version = "0.0.0";
@@ -60,18 +60,17 @@ ${c.dim}$${c.reset} npx ${name} add ${c.cyan}vercel-labs/skills${c.reset}
6060
showUsage("add");
6161
throw new Error("Missing skill source.");
6262
}
63+
const agents = values.agent || ["claude-code"];
6364
for (const rawSource of sources) {
6465
const { source, skills: parsedSkills } = parseSource(rawSource);
6566
const skills = [...parsedSkills, ...(values.skill ?? [])];
67+
68+
// Install the skill first
69+
await installSkillSource({ source, skills }, { agents, yes: true });
70+
71+
// Then add to skills.json
6672
await addSkill(source, skills);
67-
const normalizedSkills = skills
68-
.map((skill) => skill.trim())
69-
.filter((skill) => skill.length > 0 && skill !== "*");
70-
const skillsSuffix =
71-
normalizedSkills.length > 0 ? ` ${c.dim}(${normalizedSkills.join(", ")})${c.reset}` : "";
72-
console.log(
73-
`${c.green}${c.reset} Added ${c.cyan}${source}${c.reset} to skills.json${skillsSuffix}`,
74-
);
73+
console.log(`${c.green}${c.reset} Added ${c.cyan}${source}${c.reset} to skills.json`);
7574
}
7675
return;
7776
}
@@ -89,6 +88,7 @@ ${c.bold}Arguments:${c.reset}
8988
9089
${c.bold}Options:${c.reset}
9190
${c.cyan}--skill${c.reset} <name> Specific skill to add ${c.dim}(can be repeated)${c.reset}
91+
${c.cyan}--agent${c.reset} <name> Target agent ${c.dim}(default: claude-code, can be repeated)${c.reset}
9292
${c.cyan}-h, --help${c.reset} Show this help message
9393
9494
${c.bold}Examples:${c.reset}

src/skills.ts

Lines changed: 60 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { spawn } from "node:child_process";
22
import { existsSync } from "node:fs";
3-
import { dirname, join, resolve } from "node:path";
3+
import { dirname, join } from "node:path";
4+
import { fileURLToPath } from "node:url";
45

56
import { c } from "./utils/colors.ts";
67
import { readSkillsConfig } from "./config.ts";
@@ -13,27 +14,34 @@ export interface InstallSkillsOptions {
1314
yes?: boolean;
1415
}
1516

16-
export async function findSkillsBinary(cwd: string = process.cwd()): Promise<string | undefined> {
17-
let dir = resolve(cwd);
18-
const root = dirname(dir);
17+
let _skillsBinaryCache: string | undefined | null = null;
1918

20-
while (dir !== root) {
19+
export function findSkillsBinary(options?: { cache?: boolean }): string | undefined {
20+
const useCache = options?.cache !== false;
21+
if (useCache && _skillsBinaryCache !== null) {
22+
return _skillsBinaryCache;
23+
}
24+
25+
let dir = dirname(fileURLToPath(import.meta.url));
26+
27+
while (true) {
2128
const candidate = join(dir, "node_modules", ".bin", "skills");
2229
if (existsSync(candidate)) {
30+
_skillsBinaryCache = candidate;
2331
return candidate;
2432
}
2533
const parent = dirname(dir);
2634
if (parent === dir) break;
2735
dir = parent;
2836
}
2937

38+
_skillsBinaryCache = undefined;
3039
return undefined;
3140
}
3241

3342
export async function installSkills(options: InstallSkillsOptions = {}): Promise<void> {
3443
const { config, path: configPath } = await readSkillsConfig({ cwd: options.cwd });
3544
const configDir = dirname(configPath);
36-
const skillsBinary = await findSkillsBinary(options.cwd);
3745

3846
// Ensure .agents is in .gitignore
3947
await addGitignoreEntry(".agents", { cwd: configDir });
@@ -45,47 +53,62 @@ export async function installSkills(options: InstallSkillsOptions = {}): Promise
4553
let i = 0;
4654
for (const entry of config.skills) {
4755
i++;
48-
const skillList =
49-
(entry.skills?.length || 0) > 0 ? ` ${c.dim}(${entry.skills!.join(", ")})${c.reset}` : "";
50-
console.log(`${c.cyan}${c.reset} [${i}/${total}] Installing ${entry.source}${skillList}`);
51-
52-
const [command, args] = skillsBinary
53-
? [skillsBinary, ["add", entry.source]]
54-
: ["npx", ["skills", "add", entry.source]];
55-
56-
if ((entry.skills?.length || 0) > 0) {
57-
args.push("--skill", ...entry.skills!);
58-
} else {
59-
args.push("--skill", "*");
60-
}
56+
await installSkillSource(entry, { ...options, prefix: `[${i}/${total}] ` });
57+
}
6158

62-
if (options.agents && options.agents.length > 0) {
63-
args.push("--agent", ...options.agents);
64-
}
59+
const totalDuration = formatDuration(performance.now() - totalStart);
60+
console.log(
61+
`🎉 Done! ${total} skill${total === 1 ? "" : "s"} installed in ${c.green}${totalDuration}${c.reset}.`,
62+
);
63+
}
6564

66-
if (options.global) {
67-
args.push("--global");
68-
}
65+
export interface InstallSkillSourceOptions extends InstallSkillsOptions {
66+
prefix?: string;
67+
}
6968

70-
if (options.yes) {
71-
args.push("--yes");
72-
}
69+
export async function installSkillSource(
70+
entry: { source: string; skills?: string[] },
71+
options: InstallSkillSourceOptions,
72+
): Promise<void> {
73+
const skillsBinary = findSkillsBinary();
7374

74-
if (process.env.DEBUG) {
75-
console.log(`${c.dim}$ ${["skills", ...args].join(" ")}${c.reset}\n`);
76-
}
75+
const skillList =
76+
(entry.skills?.length || 0) > 0 ? ` ${c.dim}(${entry.skills!.join(", ")})${c.reset}` : "";
77+
console.log(`${c.cyan}${c.reset} ${options.prefix || ""}Installing ${entry.source}${skillList}`);
78+
79+
const [command, args] = skillsBinary
80+
? [skillsBinary, ["add", entry.source]]
81+
: ["npx", ["skills", "add", entry.source]];
82+
83+
if ((entry.skills?.length || 0) > 0) {
84+
args.push("--skill", ...entry.skills!);
85+
} else {
86+
args.push("--skill", "*");
87+
}
88+
89+
if (options.agents && options.agents.length > 0) {
90+
args.push("--agent", ...options.agents);
91+
}
7792

78-
const skillStart = performance.now();
79-
await runCommand(command, args);
80-
const skillDuration = formatDuration(performance.now() - skillStart);
93+
if (options.global) {
94+
args.push("--global");
95+
}
96+
97+
if (options.yes) {
98+
args.push("--yes");
99+
}
100+
101+
if (process.env.DEBUG) {
81102
console.log(
82-
`${c.green}${c.reset} Installed ${entry.source} ${c.dim}(${skillDuration})${c.reset}\n`,
103+
`${c.dim}$ ${[command.replace(process.cwd(), "."), ...args].join(" ")}${c.reset}\n`,
83104
);
84105
}
85106

86-
const totalDuration = formatDuration(performance.now() - totalStart);
107+
const skillStart = performance.now();
108+
await runCommand(command, args);
109+
const skillDuration = formatDuration(performance.now() - skillStart);
87110
console.log(
88-
`🎉 Done! ${total} skill${total === 1 ? "" : "s"} installed in ${c.green}${totalDuration}${c.reset}.`,
111+
`${c.green}${c.reset} Installed ${entry.source} ${c.dim}(${skillDuration})${c.reset}\n`,
89112
);
90113
}
91114

test/index.test.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { mkdir, rm, writeFile } from "node:fs/promises";
22
import { join } from "node:path";
33
import { describe, expect, it } from "vitest";
44
import { findSkillsConfig, readSkillsConfig } from "../src/config.ts";
5+
import { findSkillsBinary } from "../src/skills.ts";
56

67
describe("findSkillsConfig", () => {
78
const testDir = join(import.meta.dirname, ".tmp");
@@ -58,3 +59,11 @@ describe("readSkillsConfig", () => {
5859
await expect(readSkillsConfig({ cwd: "/" })).rejects.toThrow("skills.json not found");
5960
});
6061
});
62+
63+
describe("findSkillsBinary", () => {
64+
it("finds skills binary in node_modules", () => {
65+
const binary = findSkillsBinary({ cache: false });
66+
expect(binary).toBeDefined();
67+
expect(binary).toMatch(/node_modules[/\\]\.bin[/\\]skills$/);
68+
});
69+
});

0 commit comments

Comments
 (0)