@agent-native/skills 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,699 @@
1
+ import { spawn } from "node:child_process";
2
+ import fs from "node:fs";
3
+ import os from "node:os";
4
+ import path from "node:path";
5
+ import { createInterface } from "node:readline/promises";
6
+ export const MANAGED_BLOCK_START = "<!-- BEGIN @agent-native/skills managed block -->";
7
+ export const MANAGED_BLOCK_END = "<!-- END @agent-native/skills managed block -->";
8
+ export const HELP = `skills
9
+
10
+ Usage:
11
+ skills list <source> [--json]
12
+ skills add <source> [--skill <name> ...|--all] [--agent codex|claude|all] [--scope user|project] [--project <dir>] [--instructions agents|claude|both] [--yes] [--force] [--dry-run] [--json]
13
+
14
+ Sources:
15
+ ./skills
16
+ ./skills/my-skill
17
+ owner/repo
18
+ owner/repo/path/to/skills#ref
19
+ github:owner/repo/path/to/skills#ref
20
+ https://github.com/owner/repo/tree/ref/path/to/skills
21
+
22
+ Targets:
23
+ project codex -> <project>/.agents/skills
24
+ project claude -> <project>/.claude/skills
25
+ user codex -> $CODEX_HOME/skills or ~/.codex/skills
26
+ user claude -> ~/.claude/skills
27
+
28
+ Options:
29
+ -s, --skill <name> Install one skill. Repeat or comma-separate.
30
+ --all Install every discovered skill.
31
+ -a, --agent <target> codex, claude, or all. Defaults to codex.
32
+ --scope <scope> user or project. Defaults to user.
33
+ --project <dir> Project root for project-scoped installs.
34
+ --ref <git-ref> Git ref for GitHub-style sources.
35
+ --instructions <set> agents, claude, both/all, or none.
36
+ --with-agents-md Append/update the AGENTS.md managed block.
37
+ --with-claude-md Append/update the CLAUDE.md managed block.
38
+ --agents-file <path> Override the AGENTS.md path.
39
+ --claude-file <path> Override the CLAUDE.md path.
40
+ -y, --yes Accept prompts.
41
+ --force Overwrite existing skill folders.
42
+ --dry-run Print what would change.
43
+ --json Print machine-readable output.`;
44
+ function valueFor(args, index, flag) {
45
+ const arg = args[index];
46
+ if (arg === flag) {
47
+ const value = args[index + 1];
48
+ if (!value || value.startsWith("-")) {
49
+ throw new Error(`Missing value for ${flag}.`);
50
+ }
51
+ return { value, nextIndex: index + 1 };
52
+ }
53
+ if (arg.startsWith(`${flag}=`)) {
54
+ const value = arg.slice(flag.length + 1);
55
+ if (!value)
56
+ throw new Error(`Missing value for ${flag}.`);
57
+ return { value, nextIndex: index };
58
+ }
59
+ return null;
60
+ }
61
+ function splitList(value) {
62
+ return value
63
+ .split(",")
64
+ .map((part) => part.trim())
65
+ .filter(Boolean);
66
+ }
67
+ function normalizeAgent(value) {
68
+ const out = [];
69
+ for (const part of splitList(value)) {
70
+ const key = part.toLowerCase();
71
+ if (key === "all") {
72
+ out.push("codex", "claude");
73
+ }
74
+ else if (key === "codex") {
75
+ out.push("codex");
76
+ }
77
+ else if (key === "claude" || key === "claude-code") {
78
+ out.push("claude");
79
+ }
80
+ else {
81
+ throw new Error(`Unknown agent "${part}". Expected codex, claude, or all.`);
82
+ }
83
+ }
84
+ return [...new Set(out)];
85
+ }
86
+ function normalizeInstructions(value) {
87
+ const key = value.trim().toLowerCase();
88
+ if (key === "none")
89
+ return [];
90
+ if (key === "agents" || key === "agents.md" || key === "agent") {
91
+ return ["agents"];
92
+ }
93
+ if (key === "claude" || key === "claude.md")
94
+ return ["claude"];
95
+ if (key === "both" || key === "all")
96
+ return ["agents", "claude"];
97
+ throw new Error(`Unknown instructions target "${value}". Expected agents, claude, both, or none.`);
98
+ }
99
+ function pushUnique(items, value) {
100
+ if (!items.includes(value))
101
+ items.push(value);
102
+ }
103
+ export function parseSkillsCliArgs(argv, cwd = process.cwd()) {
104
+ const first = argv[0];
105
+ let command = "add";
106
+ let args = argv;
107
+ if (!first || first === "help" || first === "--help" || first === "-h") {
108
+ command = "help";
109
+ args = argv.slice(first ? 1 : 0);
110
+ }
111
+ else if (first === "add" || first === "list") {
112
+ command = first;
113
+ args = argv.slice(1);
114
+ }
115
+ const parsed = {
116
+ command,
117
+ skills: [],
118
+ all: false,
119
+ agents: ["codex"],
120
+ scope: "user",
121
+ projectDir: cwd,
122
+ instructionTargets: [],
123
+ agentsFile: "AGENTS.md",
124
+ claudeFile: "CLAUDE.md",
125
+ yes: false,
126
+ force: false,
127
+ dryRun: false,
128
+ json: false,
129
+ };
130
+ for (let i = 0; i < args.length; i++) {
131
+ const arg = args[i];
132
+ let consumed;
133
+ if ((consumed = valueFor(args, i, "--skill")) ||
134
+ (consumed = valueFor(args, i, "-s"))) {
135
+ for (const skill of splitList(consumed.value))
136
+ parsed.skills.push(skill);
137
+ i = consumed.nextIndex;
138
+ }
139
+ else if ((consumed = valueFor(args, i, "--agent")) ||
140
+ (consumed = valueFor(args, i, "-a"))) {
141
+ parsed.agents = normalizeAgent(consumed.value);
142
+ i = consumed.nextIndex;
143
+ }
144
+ else if ((consumed = valueFor(args, i, "--scope"))) {
145
+ if (consumed.value !== "user" && consumed.value !== "project") {
146
+ throw new Error("--scope must be either user or project.");
147
+ }
148
+ parsed.scope = consumed.value;
149
+ i = consumed.nextIndex;
150
+ }
151
+ else if ((consumed = valueFor(args, i, "--project"))) {
152
+ parsed.projectDir = path.resolve(cwd, consumed.value);
153
+ i = consumed.nextIndex;
154
+ }
155
+ else if ((consumed = valueFor(args, i, "--ref"))) {
156
+ parsed.ref = consumed.value;
157
+ i = consumed.nextIndex;
158
+ }
159
+ else if ((consumed = valueFor(args, i, "--instructions"))) {
160
+ parsed.instructionTargets = normalizeInstructions(consumed.value);
161
+ i = consumed.nextIndex;
162
+ }
163
+ else if ((consumed = valueFor(args, i, "--agents-file"))) {
164
+ parsed.agentsFile = consumed.value;
165
+ pushUnique(parsed.instructionTargets, "agents");
166
+ i = consumed.nextIndex;
167
+ }
168
+ else if ((consumed = valueFor(args, i, "--claude-file"))) {
169
+ parsed.claudeFile = consumed.value;
170
+ pushUnique(parsed.instructionTargets, "claude");
171
+ i = consumed.nextIndex;
172
+ }
173
+ else if (arg === "--with-agents-md") {
174
+ pushUnique(parsed.instructionTargets, "agents");
175
+ }
176
+ else if (arg === "--with-claude-md") {
177
+ pushUnique(parsed.instructionTargets, "claude");
178
+ }
179
+ else if (arg === "--all") {
180
+ parsed.all = true;
181
+ }
182
+ else if (arg === "--global") {
183
+ parsed.scope = "user";
184
+ }
185
+ else if (arg === "--project-scope") {
186
+ parsed.scope = "project";
187
+ }
188
+ else if (arg === "--yes" || arg === "-y") {
189
+ parsed.yes = true;
190
+ }
191
+ else if (arg === "--force") {
192
+ parsed.force = true;
193
+ }
194
+ else if (arg === "--dry-run") {
195
+ parsed.dryRun = true;
196
+ }
197
+ else if (arg === "--json") {
198
+ parsed.json = true;
199
+ }
200
+ else if (arg.startsWith("-")) {
201
+ throw new Error(`Unknown option: ${arg}`);
202
+ }
203
+ else if (!parsed.source) {
204
+ parsed.source = arg;
205
+ }
206
+ else {
207
+ throw new Error(`Unexpected argument: ${arg}`);
208
+ }
209
+ }
210
+ parsed.skills = [...new Set(parsed.skills)];
211
+ parsed.agents = [...new Set(parsed.agents)];
212
+ return parsed;
213
+ }
214
+ function stripGitSuffix(value) {
215
+ return value.endsWith(".git") ? value.slice(0, -4) : value;
216
+ }
217
+ function parsePathGitHubSource(input, ref) {
218
+ const hashIndex = input.indexOf("#");
219
+ const withoutHash = hashIndex >= 0 ? input.slice(0, hashIndex) : input;
220
+ const hashRef = hashIndex >= 0 ? input.slice(hashIndex + 1) : undefined;
221
+ const raw = withoutHash.startsWith("github:")
222
+ ? withoutHash.slice("github:".length)
223
+ : withoutHash;
224
+ if (/^[./~]/.test(raw))
225
+ return null;
226
+ const parts = raw.split("/").filter(Boolean);
227
+ if (parts.length < 2)
228
+ return null;
229
+ if (!/^[A-Za-z0-9_.-]+$/.test(parts[0]))
230
+ return null;
231
+ if (!/^[A-Za-z0-9_.-]+(?:\.git)?$/.test(parts[1]))
232
+ return null;
233
+ const owner = parts[0];
234
+ const repo = stripGitSuffix(parts[1]);
235
+ const subdir = parts.slice(2).join("/");
236
+ return {
237
+ cloneUrl: `https://github.com/${owner}/${repo}.git`,
238
+ ref: ref ?? hashRef,
239
+ subdir,
240
+ display: `github:${owner}/${repo}${subdir ? `/${subdir}` : ""}${(ref ?? hashRef) ? `#${ref ?? hashRef}` : ""}`,
241
+ };
242
+ }
243
+ export function parseGitHubSource(input, ref) {
244
+ if (input.startsWith("git@github.com:")) {
245
+ const withoutPrefix = input.slice("git@github.com:".length);
246
+ return parsePathGitHubSource(withoutPrefix, ref);
247
+ }
248
+ if (!input.startsWith("http://") && !input.startsWith("https://")) {
249
+ return parsePathGitHubSource(input, ref);
250
+ }
251
+ let url;
252
+ try {
253
+ url = new URL(input);
254
+ }
255
+ catch {
256
+ return null;
257
+ }
258
+ if (url.hostname !== "github.com")
259
+ return null;
260
+ const parts = url.pathname.split("/").filter(Boolean);
261
+ if (parts.length < 2)
262
+ return null;
263
+ const owner = parts[0];
264
+ const repo = stripGitSuffix(parts[1]);
265
+ let sourceRef = ref ?? (url.hash ? url.hash.slice(1) : undefined);
266
+ let subdir = "";
267
+ if (parts[2] === "tree" || parts[2] === "blob") {
268
+ sourceRef = ref ?? parts[3] ?? sourceRef;
269
+ subdir = parts.slice(4).join("/");
270
+ }
271
+ else {
272
+ subdir = parts.slice(2).join("/");
273
+ }
274
+ return {
275
+ cloneUrl: `https://github.com/${owner}/${repo}.git`,
276
+ ref: sourceRef,
277
+ subdir,
278
+ display: `github:${owner}/${repo}${subdir ? `/${subdir}` : ""}${sourceRef ? `#${sourceRef}` : ""}`,
279
+ };
280
+ }
281
+ function runCommand(cmd, args, options = {}) {
282
+ return new Promise((resolve, reject) => {
283
+ const child = spawn(cmd, args, {
284
+ cwd: options.cwd,
285
+ stdio: "inherit",
286
+ shell: process.platform === "win32",
287
+ env: process.env,
288
+ });
289
+ child.on("error", reject);
290
+ child.on("exit", (code, signal) => {
291
+ if (signal)
292
+ reject(new Error(`${cmd} interrupted by ${signal}.`));
293
+ else
294
+ resolve(code ?? 0);
295
+ });
296
+ });
297
+ }
298
+ async function resolveSource(source, parsed, options) {
299
+ const local = path.resolve(options.cwd ?? process.cwd(), source);
300
+ if (fs.existsSync(local)) {
301
+ return { root: local, display: source, cleanup: () => { } };
302
+ }
303
+ const github = parseGitHubSource(source, parsed.ref);
304
+ if (!github) {
305
+ throw new Error(`Source does not exist and is not a GitHub source: ${source}`);
306
+ }
307
+ const tmpRoot = fs.mkdtempSync(path.join(os.tmpdir(), "an-skills-"));
308
+ const repoDir = path.join(tmpRoot, "repo");
309
+ const args = ["clone", "--depth", "1"];
310
+ if (github.ref)
311
+ args.push("--branch", github.ref);
312
+ args.push(github.cloneUrl, repoDir);
313
+ const code = await (options.runCommand ?? runCommand)("git", args);
314
+ if (code !== 0) {
315
+ fs.rmSync(tmpRoot, { recursive: true, force: true });
316
+ throw new Error(`git clone exited with ${code}.`);
317
+ }
318
+ const root = github.subdir ? path.join(repoDir, github.subdir) : repoDir;
319
+ if (!fs.existsSync(root)) {
320
+ fs.rmSync(tmpRoot, { recursive: true, force: true });
321
+ throw new Error(`GitHub source path not found: ${github.subdir}`);
322
+ }
323
+ return {
324
+ root,
325
+ display: github.display,
326
+ cleanup: () => fs.rmSync(tmpRoot, { recursive: true, force: true }),
327
+ };
328
+ }
329
+ function parseFrontmatterField(content, field) {
330
+ const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
331
+ if (!match)
332
+ return undefined;
333
+ const fieldMatch = match[1].match(new RegExp(`^${field}:\\s*(.+)$`, "m"));
334
+ if (!fieldMatch)
335
+ return undefined;
336
+ return fieldMatch[1].trim().replace(/^['"]|['"]$/g, "");
337
+ }
338
+ function safeSkillName(value) {
339
+ const name = value.trim();
340
+ if (!/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(name)) {
341
+ throw new Error(`Invalid skill name "${value}".`);
342
+ }
343
+ return name;
344
+ }
345
+ function skillFromDir(dir) {
346
+ const file = path.join(dir, "SKILL.md");
347
+ if (!fs.existsSync(file))
348
+ return null;
349
+ const content = fs.readFileSync(file, "utf-8");
350
+ return {
351
+ name: safeSkillName(parseFrontmatterField(content, "name") ?? path.basename(dir)),
352
+ description: parseFrontmatterField(content, "description"),
353
+ dir,
354
+ };
355
+ }
356
+ function addSkillDir(out, seen, dir) {
357
+ const skill = skillFromDir(dir);
358
+ if (!skill)
359
+ return;
360
+ const key = `${skill.name}:${path.resolve(skill.dir)}`;
361
+ if (seen.has(key))
362
+ return;
363
+ seen.add(key);
364
+ out.push(skill);
365
+ }
366
+ export function discoverSkillFolders(root) {
367
+ const out = [];
368
+ const seen = new Set();
369
+ addSkillDir(out, seen, root);
370
+ const containers = [
371
+ path.join(root, "skills"),
372
+ path.join(root, ".agents", "skills"),
373
+ path.join(root, ".claude", "skills"),
374
+ root,
375
+ ];
376
+ for (const container of containers) {
377
+ if (!fs.existsSync(container))
378
+ continue;
379
+ const stat = fs.statSync(container);
380
+ if (!stat.isDirectory())
381
+ continue;
382
+ for (const entry of fs.readdirSync(container, { withFileTypes: true })) {
383
+ if (!entry.isDirectory())
384
+ continue;
385
+ if (entry.name === "node_modules" || entry.name === ".git")
386
+ continue;
387
+ addSkillDir(out, seen, path.join(container, entry.name));
388
+ }
389
+ }
390
+ return out.sort((a, b) => a.name.localeCompare(b.name));
391
+ }
392
+ function homeDir(env) {
393
+ return env.HOME || os.homedir();
394
+ }
395
+ export function targetRootsFor(input) {
396
+ const env = input.env ?? process.env;
397
+ const roots = [];
398
+ for (const agent of input.agents) {
399
+ if (input.scope === "project") {
400
+ roots.push({
401
+ agent,
402
+ scope: "project",
403
+ root: agent === "codex"
404
+ ? path.join(input.projectDir, ".agents", "skills")
405
+ : path.join(input.projectDir, ".claude", "skills"),
406
+ });
407
+ }
408
+ else if (agent === "codex") {
409
+ roots.push({
410
+ agent,
411
+ scope: "user",
412
+ root: env.CODEX_HOME
413
+ ? path.join(env.CODEX_HOME, "skills")
414
+ : path.join(homeDir(env), ".codex", "skills"),
415
+ });
416
+ }
417
+ else {
418
+ roots.push({
419
+ agent,
420
+ scope: "user",
421
+ root: path.join(homeDir(env), ".claude", "skills"),
422
+ });
423
+ }
424
+ }
425
+ return roots;
426
+ }
427
+ function shouldPrompt(options, parsed) {
428
+ if (parsed.yes || parsed.json)
429
+ return false;
430
+ if (options.isInteractive)
431
+ return options.isInteractive();
432
+ if (process.env.CI === "true")
433
+ return false;
434
+ return Boolean(process.stdin.isTTY && process.stdout.isTTY);
435
+ }
436
+ async function promptForSkills(skills) {
437
+ process.stdout.write("Select skills to install:\n");
438
+ skills.forEach((skill, index) => {
439
+ const suffix = skill.description ? ` - ${skill.description}` : "";
440
+ process.stdout.write(` ${index + 1}. ${skill.name}${suffix}\n`);
441
+ });
442
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
443
+ try {
444
+ const answer = await rl.question("Enter numbers or names separated by commas, or all: ");
445
+ const trimmed = answer.trim();
446
+ if (!trimmed)
447
+ return null;
448
+ if (trimmed.toLowerCase() === "all")
449
+ return skills.map((skill) => skill.name);
450
+ const selected = [];
451
+ for (const part of splitList(trimmed)) {
452
+ const index = Number(part);
453
+ if (Number.isInteger(index) && index >= 1 && index <= skills.length) {
454
+ selected.push(skills[index - 1].name);
455
+ }
456
+ else {
457
+ selected.push(part);
458
+ }
459
+ }
460
+ return selected;
461
+ }
462
+ finally {
463
+ rl.close();
464
+ }
465
+ }
466
+ async function promptForOverwrite(paths) {
467
+ process.stdout.write("Existing skill folders will be overwritten:\n");
468
+ for (const target of paths)
469
+ process.stdout.write(` ${target}\n`);
470
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
471
+ try {
472
+ const answer = await rl.question("Overwrite? [y/N] ");
473
+ return answer.trim().toLowerCase() === "y";
474
+ }
475
+ finally {
476
+ rl.close();
477
+ }
478
+ }
479
+ async function selectSkills(discovered, parsed, options) {
480
+ if (discovered.length === 0) {
481
+ throw new Error("No skill folders found. Expected folders with SKILL.md.");
482
+ }
483
+ let names = parsed.skills;
484
+ if (parsed.all) {
485
+ names = discovered.map((skill) => skill.name);
486
+ }
487
+ else if (names.length === 0 && discovered.length === 1) {
488
+ names = [discovered[0].name];
489
+ }
490
+ else if (names.length === 0 && shouldPrompt(options, parsed)) {
491
+ const picked = await (options.promptSkills ?? promptForSkills)(discovered);
492
+ if (!picked || picked.length === 0)
493
+ throw new Error("No skills selected.");
494
+ names = picked;
495
+ }
496
+ else if (names.length === 0) {
497
+ throw new Error("Multiple skills found. Pass --skill <name>, --all, or run interactively.");
498
+ }
499
+ const byName = new Map(discovered.map((skill) => [skill.name, skill]));
500
+ return names.map((name) => {
501
+ const skill = byName.get(name);
502
+ if (!skill) {
503
+ throw new Error(`Skill "${name}" not found. Available: ${discovered
504
+ .map((item) => item.name)
505
+ .join(", ")}`);
506
+ }
507
+ return skill;
508
+ });
509
+ }
510
+ function assertWithin(root, target) {
511
+ const rel = path.relative(path.resolve(root), path.resolve(target));
512
+ if (rel.startsWith("..") || path.isAbsolute(rel)) {
513
+ throw new Error(`Refusing to write outside target root: ${target}`);
514
+ }
515
+ }
516
+ function copySkillDir(from, to, dryRun) {
517
+ if (dryRun)
518
+ return;
519
+ fs.rmSync(to, { recursive: true, force: true });
520
+ fs.mkdirSync(path.dirname(to), { recursive: true });
521
+ fs.cpSync(from, to, {
522
+ recursive: true,
523
+ filter(source) {
524
+ const base = path.basename(source);
525
+ return base !== ".git" && base !== "node_modules";
526
+ },
527
+ });
528
+ }
529
+ function instructionFilePath(projectDir, target, parsed) {
530
+ const file = target === "agents" ? parsed.agentsFile : parsed.claudeFile;
531
+ return path.isAbsolute(file) ? file : path.join(projectDir, file);
532
+ }
533
+ function buildManagedBlock(input) {
534
+ const agentText = [...new Set(input.roots.map((root) => root.agent))].join(", ");
535
+ const scopeText = [...new Set(input.roots.map((root) => root.scope))].join(", ");
536
+ const skillLines = input.skills
537
+ .map((skill) => `- \`${skill.name}\``)
538
+ .join("\n");
539
+ return `${MANAGED_BLOCK_START}
540
+ ## Installed Agent Skills
541
+
542
+ This block is managed by \`@agent-native/skills\`. Re-run the installer to update
543
+ it.
544
+
545
+ Source: \`${input.source}\`
546
+ Agents: ${agentText}
547
+ Scope: ${scopeText}
548
+
549
+ ${skillLines}
550
+ ${MANAGED_BLOCK_END}
551
+ `;
552
+ }
553
+ export function upsertManagedBlock(file, block, dryRun = false) {
554
+ const current = fs.existsSync(file) ? fs.readFileSync(file, "utf-8") : "";
555
+ const start = current.indexOf(MANAGED_BLOCK_START);
556
+ const end = current.indexOf(MANAGED_BLOCK_END);
557
+ let next;
558
+ if (start >= 0 && end >= start) {
559
+ const afterEnd = end + MANAGED_BLOCK_END.length;
560
+ next = `${current.slice(0, start).trimEnd()}\n\n${block.trimEnd()}\n${current
561
+ .slice(afterEnd)
562
+ .trimStart()}`;
563
+ }
564
+ else {
565
+ next = `${current.trimEnd()}${current.trim() ? "\n\n" : ""}${block.trimEnd()}\n`;
566
+ }
567
+ if (next === current)
568
+ return false;
569
+ if (!dryRun) {
570
+ fs.mkdirSync(path.dirname(file), { recursive: true });
571
+ fs.writeFileSync(file, next, "utf-8");
572
+ }
573
+ return true;
574
+ }
575
+ export async function installSkills(parsed, options = {}) {
576
+ if (!parsed.source)
577
+ throw new Error("Missing source.");
578
+ const source = await resolveSource(parsed.source, parsed, options);
579
+ try {
580
+ const discovered = discoverSkillFolders(source.root);
581
+ const selected = await selectSkills(discovered, parsed, options);
582
+ const roots = targetRootsFor({
583
+ agents: parsed.agents,
584
+ scope: parsed.scope,
585
+ projectDir: parsed.projectDir,
586
+ env: options.env,
587
+ });
588
+ const existing = [];
589
+ for (const root of roots) {
590
+ for (const skill of selected) {
591
+ const dest = path.join(root.root, safeSkillName(skill.name));
592
+ assertWithin(root.root, dest);
593
+ if (fs.existsSync(dest))
594
+ existing.push(dest);
595
+ }
596
+ }
597
+ if (existing.length > 0 && !parsed.force && !parsed.yes && !parsed.dryRun) {
598
+ if (!shouldPrompt(options, parsed)) {
599
+ throw new Error(`Refusing to overwrite existing skill folders without --force: ${existing.join(", ")}`);
600
+ }
601
+ const ok = await (options.promptOverwrite ?? promptForOverwrite)(existing);
602
+ if (!ok)
603
+ throw new Error("Install cancelled.");
604
+ }
605
+ const copied = [];
606
+ for (const root of roots) {
607
+ for (const skill of selected) {
608
+ const dest = path.join(root.root, safeSkillName(skill.name));
609
+ assertWithin(root.root, dest);
610
+ copySkillDir(skill.dir, dest, parsed.dryRun);
611
+ copied.push({
612
+ skillName: skill.name,
613
+ agent: root.agent,
614
+ scope: root.scope,
615
+ from: skill.dir,
616
+ to: dest,
617
+ });
618
+ }
619
+ }
620
+ const instructionFiles = [];
621
+ if (parsed.instructionTargets.length > 0) {
622
+ const block = buildManagedBlock({
623
+ skills: selected,
624
+ source: source.display,
625
+ roots,
626
+ });
627
+ for (const target of parsed.instructionTargets) {
628
+ const file = instructionFilePath(parsed.projectDir, target, parsed);
629
+ upsertManagedBlock(file, block, parsed.dryRun);
630
+ instructionFiles.push(file);
631
+ }
632
+ }
633
+ return {
634
+ source: source.display,
635
+ dryRun: parsed.dryRun,
636
+ skills: selected.map((skill) => skill.name),
637
+ copied,
638
+ instructionFiles,
639
+ };
640
+ }
641
+ finally {
642
+ source.cleanup();
643
+ }
644
+ }
645
+ function formatList(skills) {
646
+ if (skills.length === 0)
647
+ return "No skill folders found.\n";
648
+ return skills
649
+ .map((skill) => {
650
+ const suffix = skill.description ? ` - ${skill.description}` : "";
651
+ return `${skill.name}${suffix}\n ${skill.dir}`;
652
+ })
653
+ .join("\n");
654
+ }
655
+ function formatInstallResult(result) {
656
+ const verb = result.dryRun ? "Would install" : "Installed";
657
+ const rows = result.copied.map((item) => ` ${item.skillName} -> ${item.to} (${item.agent}/${item.scope})`);
658
+ const instructions = result.instructionFiles.length > 0
659
+ ? `\nUpdated managed instruction blocks:\n${result.instructionFiles
660
+ .map((file) => ` ${file}`)
661
+ .join("\n")}`
662
+ : "";
663
+ return `${verb} ${result.skills.length} skill${result.skills.length === 1 ? "" : "s"} from ${result.source}:\n${rows.join("\n")}${instructions}\n`;
664
+ }
665
+ export async function runSkillsCli(argv, options = {}) {
666
+ const cwd = options.cwd ?? process.cwd();
667
+ const parsed = parseSkillsCliArgs(argv, cwd);
668
+ const write = options.log ?? ((message) => process.stdout.write(message));
669
+ if (parsed.command === "help") {
670
+ write(`${HELP}\n`);
671
+ return;
672
+ }
673
+ if (!parsed.source)
674
+ throw new Error("Missing source.");
675
+ if (parsed.command === "list") {
676
+ const source = await resolveSource(parsed.source, parsed, options);
677
+ try {
678
+ const skills = discoverSkillFolders(source.root);
679
+ if (parsed.json) {
680
+ write(`${JSON.stringify({ source: source.display, skills }, null, 2)}\n`);
681
+ }
682
+ else {
683
+ write(`${formatList(skills)}\n`);
684
+ }
685
+ }
686
+ finally {
687
+ source.cleanup();
688
+ }
689
+ return;
690
+ }
691
+ const result = await installSkills(parsed, options);
692
+ if (parsed.json) {
693
+ write(`${JSON.stringify(result, null, 2)}\n`);
694
+ }
695
+ else {
696
+ write(formatInstallResult(result));
697
+ }
698
+ }
699
+ //# sourceMappingURL=install.js.map