skild 0.4.5 → 0.4.7

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.
Files changed (2) hide show
  1. package/dist/index.js +484 -435
  2. package/package.json +2 -2
package/dist/index.js CHANGED
@@ -9,7 +9,19 @@ import { createRequire } from "module";
9
9
  import fs2 from "fs";
10
10
  import path2 from "path";
11
11
  import chalk3 from "chalk";
12
- import { deriveChildSource, fetchWithTimeout, installRegistrySkill, installSkill, isValidAlias, loadRegistryAuth, materializeSourceToTemp, resolveRegistryAlias, resolveRegistryUrl, SkildError, PLATFORMS as PLATFORMS2 } from "@skild/core";
12
+ import {
13
+ deriveChildSource,
14
+ fetchWithTimeout,
15
+ installRegistrySkill,
16
+ installSkill,
17
+ isValidAlias,
18
+ loadRegistryAuth,
19
+ materializeSourceToTemp,
20
+ resolveRegistryAlias,
21
+ resolveRegistryUrl,
22
+ SkildError,
23
+ PLATFORMS as PLATFORMS2
24
+ } from "@skild/core";
13
25
 
14
26
  // src/utils/logger.ts
15
27
  import chalk from "chalk";
@@ -75,21 +87,8 @@ var logger = {
75
87
  import readline from "readline";
76
88
  import chalk2 from "chalk";
77
89
  import { PLATFORMS } from "@skild/core";
78
- var PLATFORM_DISPLAY = {
79
- claude: { name: "Claude" },
80
- codex: { name: "Codex" },
81
- copilot: { name: "Copilot" },
82
- antigravity: { name: "Antigravity" }
83
- };
84
90
  function buildSkillTree(skills) {
85
- const allNode = {
86
- id: "all",
87
- name: "All Skills",
88
- depth: 1,
89
- children: [],
90
- leafIndices: [],
91
- isLeaf: false
92
- };
91
+ const allNode = createTreeNode("all", "All Skills", 1, false);
93
92
  for (let i = 0; i < skills.length; i++) {
94
93
  const relPath = skills[i].relPath;
95
94
  const parts = relPath === "." ? ["."] : relPath.split("/").filter(Boolean);
@@ -100,21 +99,32 @@ function buildSkillTree(skills) {
100
99
  const nodeId = parts.slice(0, d + 1).join("/");
101
100
  let child = current.children.find((c2) => c2.name === part);
102
101
  if (!child) {
103
- child = {
104
- id: nodeId,
105
- name: part,
106
- depth: d + 2,
107
- // +2 because allNode is depth 1
108
- children: [],
109
- leafIndices: [],
110
- isLeaf: d === parts.length - 1
111
- };
102
+ child = createTreeNode(nodeId, part, d + 2, d === parts.length - 1);
112
103
  current.children.push(child);
113
104
  }
114
105
  child.leafIndices.push(i);
115
106
  current = child;
116
107
  }
117
108
  }
109
+ collapseIntermediateNodes(allNode);
110
+ return wrapWithRoot(allNode);
111
+ }
112
+ function buildPlatformTree(items) {
113
+ const allNode = createTreeNode("all", "All Platforms", 1, false);
114
+ for (let i = 0; i < items.length; i++) {
115
+ const platform = items[i].platform;
116
+ allNode.children.push(createTreeNode(platform, platform, 2, true, [i]));
117
+ allNode.leafIndices.push(i);
118
+ }
119
+ return wrapWithRoot(allNode);
120
+ }
121
+ function createTreeNode(id, name, depth, isLeaf, leafIndices = []) {
122
+ return { id, name, depth, children: [], leafIndices, isLeaf };
123
+ }
124
+ function wrapWithRoot(allNode) {
125
+ return { id: "", name: ".", depth: 0, children: [allNode], leafIndices: [...allNode.leafIndices], isLeaf: false };
126
+ }
127
+ function collapseIntermediateNodes(allNode) {
118
128
  while (allNode.children.length === 1 && !allNode.children[0].isLeaf && allNode.children[0].children.length > 0) {
119
129
  const singleChild = allNode.children[0];
120
130
  for (const grandchild of singleChild.children) {
@@ -122,8 +132,6 @@ function buildSkillTree(skills) {
122
132
  }
123
133
  allNode.children = singleChild.children;
124
134
  }
125
- const root = { id: "", name: ".", depth: 0, children: [allNode], leafIndices: allNode.leafIndices.slice(), isLeaf: false };
126
- return root;
127
135
  }
128
136
  function adjustDepth(node, delta) {
129
137
  node.depth += delta;
@@ -134,28 +142,54 @@ function adjustDepth(node, delta) {
134
142
  function flattenTree(root) {
135
143
  const result = [];
136
144
  function walk(node) {
137
- if (node.id !== "") {
138
- result.push(node);
139
- }
140
- for (const child of node.children) {
141
- walk(child);
142
- }
143
- }
144
- for (const child of root.children) {
145
- walk(child);
145
+ if (node.id !== "") result.push(node);
146
+ for (const child of node.children) walk(child);
146
147
  }
148
+ for (const child of root.children) walk(child);
147
149
  return result;
148
150
  }
149
- function getNodeState(node, selected) {
151
+ function getNodeSelection(node, selected) {
150
152
  const total = node.leafIndices.length;
151
- if (total === 0) return "none";
152
- let count = 0;
153
+ if (total === 0) return { state: "none", selectedCount: 0 };
154
+ let selectedCount = 0;
153
155
  for (const idx of node.leafIndices) {
154
- if (selected.has(idx)) count++;
156
+ if (selected.has(idx)) selectedCount++;
155
157
  }
156
- if (count === 0) return "none";
157
- if (count === total) return "all";
158
- return "partial";
158
+ let state = "none";
159
+ if (selectedCount === total) state = "all";
160
+ else if (selectedCount > 0) state = "partial";
161
+ return { state, selectedCount };
162
+ }
163
+ function createRenderer(title, subtitle, flatNodes, selected, getCursor, formatNode) {
164
+ function renderContent() {
165
+ const lines = [];
166
+ lines.push(chalk2.bold.cyan(title));
167
+ lines.push(chalk2.dim(subtitle));
168
+ lines.push("");
169
+ const cursor = getCursor();
170
+ for (let i = 0; i < flatNodes.length; i++) {
171
+ const node = flatNodes[i];
172
+ const selection = getNodeSelection(node, selected);
173
+ lines.push(formatNode(node, selection, i === cursor));
174
+ }
175
+ lines.push("");
176
+ return lines;
177
+ }
178
+ function getLineCount() {
179
+ return 4 + flatNodes.length;
180
+ }
181
+ return { renderContent, getLineCount };
182
+ }
183
+ function writeToTerminal(stdout, lines) {
184
+ for (const line of lines) {
185
+ stdout.write(line + "\n");
186
+ }
187
+ }
188
+ function clearAndRerender(stdout, lineCount, lines) {
189
+ stdout.write("\x1B[?25l");
190
+ stdout.write(`\x1B[${lineCount}A`);
191
+ stdout.write("\x1B[0J");
192
+ writeToTerminal(stdout, lines);
159
193
  }
160
194
  async function interactiveTreeSelect(items, options) {
161
195
  const { title, subtitle, buildTree, formatNode, defaultAll, defaultSelected } = options;
@@ -178,103 +212,98 @@ async function interactiveTreeSelect(items, options) {
178
212
  stdin.setRawMode(true);
179
213
  stdin.resume();
180
214
  readline.emitKeypressEvents(stdin);
181
- function render() {
182
- stdout.write("\x1B[?25l");
183
- const totalLines = flatNodes.length + 4;
184
- stdout.write(`\x1B[${totalLines}A`);
185
- stdout.write("\x1B[0J");
186
- stdout.write(`
187
- ${chalk2.bold.cyan(title)}
188
- `);
189
- stdout.write(`${chalk2.dim(subtitle)}
190
-
191
- `);
192
- for (let i = 0; i < flatNodes.length; i++) {
193
- const node = flatNodes[i];
194
- const state = getNodeState(node, selected);
195
- const isCursor = i === cursor;
196
- stdout.write(formatNode(node, state, isCursor) + "\n");
197
- }
198
- stdout.write("\n");
199
- }
200
- function initialRender() {
201
- stdout.write(`
202
- ${chalk2.bold.cyan(title)}
203
- `);
204
- stdout.write(`${chalk2.dim(subtitle)}
205
-
206
- `);
207
- for (let i = 0; i < flatNodes.length; i++) {
208
- const node = flatNodes[i];
209
- const state = getNodeState(node, selected);
210
- const isCursor = i === cursor;
211
- stdout.write(formatNode(node, state, isCursor) + "\n");
212
- }
213
- stdout.write("\n");
214
- }
215
- initialRender();
215
+ const renderer = createRenderer(title, subtitle, flatNodes, selected, () => cursor, formatNode);
216
+ writeToTerminal(stdout, renderer.renderContent());
216
217
  return new Promise((resolve) => {
217
- function cleanup() {
218
+ function cleanup(clear = false) {
219
+ if (clear) {
220
+ const lineCount = renderer.getLineCount();
221
+ stdout.write(`\x1B[${lineCount}A`);
222
+ stdout.write("\x1B[0J");
223
+ }
218
224
  stdin.setRawMode(wasRaw);
219
225
  stdin.pause();
220
226
  stdin.removeListener("keypress", onKeypress);
221
227
  stdout.write("\x1B[?25h");
222
228
  }
229
+ function rerender() {
230
+ clearAndRerender(stdout, renderer.getLineCount(), renderer.renderContent());
231
+ }
232
+ function toggleNode(node) {
233
+ const { state } = getNodeSelection(node, selected);
234
+ if (state === "all") {
235
+ for (const idx of node.leafIndices) selected.delete(idx);
236
+ } else {
237
+ for (const idx of node.leafIndices) selected.add(idx);
238
+ }
239
+ }
240
+ function toggleAll() {
241
+ if (selected.size === items.length) {
242
+ selected.clear();
243
+ } else {
244
+ for (let i = 0; i < items.length; i++) selected.add(i);
245
+ }
246
+ }
223
247
  function onKeypress(_str, key) {
224
248
  if (key.ctrl && key.name === "c") {
225
- cleanup();
249
+ cleanup(true);
226
250
  resolve(null);
227
251
  return;
228
252
  }
229
253
  if (key.name === "return" || key.name === "enter") {
230
- cleanup();
231
- const result = Array.from(selected);
232
- if (result.length === 0) {
233
- resolve(null);
234
- } else {
235
- resolve(result);
236
- }
254
+ cleanup(true);
255
+ resolve(selected.size > 0 ? Array.from(selected) : null);
237
256
  return;
238
257
  }
239
258
  if (key.name === "up") {
240
259
  cursor = (cursor - 1 + flatNodes.length) % flatNodes.length;
241
- render();
260
+ rerender();
242
261
  return;
243
262
  }
244
263
  if (key.name === "down") {
245
264
  cursor = (cursor + 1) % flatNodes.length;
246
- render();
265
+ rerender();
247
266
  return;
248
267
  }
249
268
  if (key.name === "space") {
250
- const node = flatNodes[cursor];
251
- const state = getNodeState(node, selected);
252
- if (state === "all") {
253
- for (const idx of node.leafIndices) {
254
- selected.delete(idx);
255
- }
256
- } else {
257
- for (const idx of node.leafIndices) {
258
- selected.add(idx);
259
- }
260
- }
261
- render();
269
+ toggleNode(flatNodes[cursor]);
270
+ rerender();
262
271
  return;
263
272
  }
264
273
  if (key.name === "a") {
265
- const allSelected = selected.size === items.length;
266
- if (allSelected) {
267
- selected.clear();
268
- } else {
269
- for (let i = 0; i < items.length; i++) selected.add(i);
270
- }
271
- render();
274
+ toggleAll();
275
+ rerender();
272
276
  return;
273
277
  }
274
278
  }
275
279
  stdin.on("keypress", onKeypress);
276
280
  });
277
281
  }
282
+ var PLATFORM_DISPLAY = {
283
+ claude: "Claude",
284
+ codex: "Codex",
285
+ copilot: "Copilot",
286
+ antigravity: "Antigravity",
287
+ opencode: "OpenCode"
288
+ };
289
+ function formatTreeNode(node, selection, isCursor, options = {}) {
290
+ const { state, selectedCount } = selection;
291
+ const totalCount = node.leafIndices.length;
292
+ const indent = " ".repeat(node.depth - 1);
293
+ const checkbox = state === "all" ? chalk2.green("\u25CF") : state === "partial" ? chalk2.yellow("\u25D0") : chalk2.dim("\u25CB");
294
+ const name = isCursor ? chalk2.cyan.underline(node.name) : node.name;
295
+ const cursorMark = isCursor ? chalk2.cyan("\u203A ") : " ";
296
+ let count = "";
297
+ if (totalCount > 1) {
298
+ count = chalk2.dim(` (${selectedCount}/${totalCount})`);
299
+ }
300
+ const suffix = options.suffix || "";
301
+ let hint = "";
302
+ if (isCursor && totalCount > 0) {
303
+ hint = state === "all" ? chalk2.dim(" \u2190 Space to deselect") : chalk2.dim(" \u2190 Space to select");
304
+ }
305
+ return `${cursorMark}${indent}${checkbox} ${name}${count}${suffix}${hint}`;
306
+ }
278
307
  async function promptSkillsInteractive(skills, options = {}) {
279
308
  if (skills.length === 0) return null;
280
309
  const targetPlatforms = options.targetPlatforms || [];
@@ -292,100 +321,53 @@ async function promptSkillsInteractive(skills, options = {}) {
292
321
  title: "Select skills to install",
293
322
  subtitle: "\u2191\u2193 navigate \u2022 Space toggle \u2022 Enter confirm",
294
323
  buildTree: buildSkillTree,
295
- formatNode: (node, state, isCursor) => {
296
- const indent = " ".repeat(node.depth - 1);
297
- const checkbox = state === "all" ? chalk2.green("\u25CF") : state === "partial" ? chalk2.yellow("\u25D0") : chalk2.dim("\u25CB");
298
- let installedTag = "";
324
+ formatNode: (node, selection, isCursor) => {
325
+ let suffix = "";
299
326
  if (node.isLeaf && node.leafIndices.length === 1) {
300
327
  const skill = skills[node.leafIndices[0]];
301
328
  if (skill?.installedPlatforms?.length) {
302
329
  if (skill.installedPlatforms.length === targetPlatforms.length && targetPlatforms.length > 0) {
303
- installedTag = chalk2.dim(" [installed]");
330
+ suffix = chalk2.dim(" [installed]");
304
331
  } else if (skill.installedPlatforms.length > 0) {
305
- installedTag = chalk2.dim(` [installed on ${skill.installedPlatforms.length}]`);
332
+ suffix = chalk2.dim(` [installed on ${skill.installedPlatforms.length}]`);
306
333
  }
307
334
  }
308
335
  }
309
- const name = isCursor ? chalk2.cyan.underline(node.name) : node.name;
310
- const cursor = isCursor ? chalk2.cyan("\u203A ") : " ";
311
- const count = node.leafIndices.length > 1 ? chalk2.dim(` (${node.leafIndices.length})`) : "";
312
- let hint = "";
313
- if (isCursor && node.leafIndices.length > 0) {
314
- if (installedTag && state !== "all") {
315
- hint = chalk2.dim(" \u2190 Space to reinstall");
316
- } else {
317
- hint = state === "all" ? chalk2.dim(" \u2190 Space to deselect") : chalk2.dim(" \u2190 Space to select");
318
- }
336
+ const formatted = formatTreeNode(node, selection, isCursor, { suffix });
337
+ if (isCursor && suffix && selection.state !== "all") {
338
+ return formatted.replace("\u2190 Space to select", "\u2190 Space to reinstall");
319
339
  }
320
- return `${cursor}${indent}${checkbox} ${name}${count}${installedTag}${hint}`;
340
+ return formatted;
321
341
  },
322
342
  defaultAll: false,
323
- // We handle default selection manually
324
343
  defaultSelected
325
344
  });
326
- if (!selectedIndices || selectedIndices.length === 0) {
327
- return null;
328
- }
345
+ if (!selectedIndices) return null;
346
+ const selectedSkills = selectedIndices.map((i) => skills[i]);
347
+ const names = selectedSkills.map((s) => s.relPath === "." ? s.suggestedSource : s.relPath);
329
348
  console.log(chalk2.green(`
330
- \u2713 ${selectedIndices.length} skill${selectedIndices.length > 1 ? "s" : ""} selected
349
+ \u2713 Selected ${selectedSkills.length} skill${selectedSkills.length > 1 ? "s" : ""}: ${chalk2.cyan(names.join(", "))}
331
350
  `));
332
- return selectedIndices.map((i) => skills[i]);
351
+ return selectedSkills;
333
352
  }
334
353
  async function promptPlatformsInteractive(options = {}) {
335
354
  const platformItems = PLATFORMS.map((p) => ({ platform: p }));
336
355
  const selectedIndices = await interactiveTreeSelect(platformItems, {
337
356
  title: "Select target platforms",
338
357
  subtitle: "\u2191\u2193 navigate \u2022 Space toggle \u2022 Enter confirm",
339
- buildTree: (items) => {
340
- const allNode = {
341
- id: "all",
342
- name: "All Platforms",
343
- depth: 1,
344
- children: [],
345
- leafIndices: [],
346
- isLeaf: false
347
- };
348
- for (let i = 0; i < items.length; i++) {
349
- const p = items[i].platform;
350
- allNode.children.push({
351
- id: p,
352
- name: p,
353
- depth: 2,
354
- children: [],
355
- leafIndices: [i],
356
- isLeaf: true
357
- });
358
- allNode.leafIndices.push(i);
359
- }
360
- const root = { id: "", name: ".", depth: 0, children: [allNode], leafIndices: allNode.leafIndices.slice(), isLeaf: false };
361
- return root;
362
- },
363
- formatNode: (node, state, isCursor) => {
364
- const indent = " ".repeat(node.depth - 1);
365
- const checkbox = state === "all" ? chalk2.green("\u25CF") : state === "partial" ? chalk2.yellow("\u25D0") : chalk2.dim("\u25CB");
366
- let displayName = node.name;
367
- if (node.name !== "All Platforms") {
368
- const platform = node.name;
369
- const display = PLATFORM_DISPLAY[platform];
370
- if (display) displayName = display.name;
371
- }
372
- const name = isCursor ? chalk2.cyan.underline(displayName) : displayName;
373
- const cursor = isCursor ? chalk2.cyan("\u203A ") : " ";
374
- const count = node.leafIndices.length > 1 ? chalk2.dim(` (${node.leafIndices.length})`) : "";
375
- let hint = "";
376
- if (isCursor && node.leafIndices.length > 0) {
377
- hint = state === "all" ? chalk2.dim(" \u2190 Space to deselect") : chalk2.dim(" \u2190 Space to select");
378
- }
379
- return `${cursor}${indent}${checkbox} ${name}${count}${hint}`;
358
+ buildTree: buildPlatformTree,
359
+ formatNode: (node, selection, isCursor) => {
360
+ const displayName = node.name === "All Platforms" ? node.name : PLATFORM_DISPLAY[node.name] || node.name;
361
+ const modifiedNode = { ...node, name: displayName };
362
+ return formatTreeNode(modifiedNode, selection, isCursor);
380
363
  },
381
364
  defaultAll: options.defaultAll !== false
382
365
  });
383
- if (!selectedIndices || selectedIndices.length === 0) {
384
- return null;
385
- }
366
+ if (!selectedIndices) return null;
386
367
  const selected = selectedIndices.map((i) => PLATFORMS[i]);
368
+ const names = selected.map((p) => PLATFORM_DISPLAY[p] || p);
387
369
  console.log(chalk2.green(`
388
- \u2713 Installing to ${selected.length} platform${selected.length > 1 ? "s" : ""}
370
+ \u2713 Installing to ${selected.length} platform${selected.length > 1 ? "s" : ""}: ${chalk2.cyan(names.join(", "))}
389
371
  `));
390
372
  return selected;
391
373
  }
@@ -450,6 +432,7 @@ function discoverSkillDirsWithHeuristics(rootDir, options) {
450
432
  path.join(".agent", "skills"),
451
433
  path.join(".claude", "skills"),
452
434
  path.join(".codex", "skills"),
435
+ path.join(".opencode", "skill"),
453
436
  path.join(".github", "skills")
454
437
  ];
455
438
  for (const rel of candidates) {
@@ -491,315 +474,381 @@ function asDiscoveredSkills(discovered, toSuggestedSource, toMaterializedDir) {
491
474
  materializedDir: toMaterializedDir ? toMaterializedDir(d) : void 0
492
475
  }));
493
476
  }
494
- async function install(source, options = {}) {
477
+ function createContext(source, options) {
495
478
  const scope = options.local ? "project" : "global";
496
479
  const auth = loadRegistryAuth();
497
- const registryUrlForDeps = options.registry || auth?.registryUrl;
480
+ const registryUrl = options.registry || auth?.registryUrl;
498
481
  const jsonOnly = Boolean(options.json);
499
- const recursive = Boolean(options.recursive);
500
482
  const yes = Boolean(options.yes);
501
- const maxDepth = parsePositiveInt(options.depth, 6);
502
- const maxSkills = parsePositiveInt(options.maxSkills, 200);
503
483
  const interactive = Boolean(process.stdin.isTTY && process.stdout.isTTY) && !jsonOnly;
504
- const requestedAll = Boolean(options.all);
505
- const requestedTarget = options.target;
506
- if (requestedAll && options.target) {
507
- const message = "Invalid options: use either --all or --target, not both.";
508
- if (jsonOnly) printJson({ ok: false, error: message });
509
- else console.error(chalk3.red(message));
510
- process.exitCode = 1;
511
- return;
512
- }
513
484
  let targets = [];
514
- let deferPlatformSelection = false;
515
- if (requestedAll) {
485
+ let needsPlatformPrompt = false;
486
+ if (options.all) {
516
487
  targets = [...PLATFORMS2];
517
- } else if (requestedTarget) {
518
- targets = [requestedTarget];
488
+ } else if (options.target) {
489
+ targets = [options.target];
519
490
  } else if (yes) {
520
491
  targets = [...PLATFORMS2];
521
492
  } else if (interactive) {
522
- deferPlatformSelection = true;
493
+ needsPlatformPrompt = true;
523
494
  targets = [...PLATFORMS2];
524
495
  } else {
525
496
  targets = ["claude"];
526
497
  }
527
- const allPlatformsSelected = targets.length === PLATFORMS2.length;
528
- let resolvedSource = source.trim();
498
+ return {
499
+ source,
500
+ options,
501
+ scope,
502
+ registryUrl,
503
+ jsonOnly,
504
+ interactive,
505
+ yes,
506
+ maxDepth: parsePositiveInt(options.depth, 6),
507
+ maxSkills: parsePositiveInt(options.maxSkills, 200),
508
+ resolvedSource: source.trim(),
509
+ targets,
510
+ forceOverwrite: Boolean(options.force),
511
+ needsPlatformPrompt,
512
+ discoveredSkills: null,
513
+ selectedSkills: null,
514
+ isSingleSkill: false,
515
+ materializedDir: null,
516
+ cleanupMaterialized: null,
517
+ results: [],
518
+ errors: [],
519
+ skipped: [],
520
+ spinner: null
521
+ };
522
+ }
523
+ async function resolveSource(ctx) {
524
+ if (ctx.options.all && ctx.options.target) {
525
+ const message = "Invalid options: use either --all or --target, not both.";
526
+ if (ctx.jsonOnly) printJson({ ok: false, error: message });
527
+ else console.error(chalk3.red(message));
528
+ process.exitCode = 1;
529
+ return false;
530
+ }
531
+ try {
532
+ if (looksLikeAlias(ctx.resolvedSource)) {
533
+ if (ctx.spinner) ctx.spinner.text = `Resolving ${chalk3.cyan(ctx.resolvedSource)}...`;
534
+ const registryUrl = resolveRegistryUrl(ctx.registryUrl);
535
+ const resolved = await resolveRegistryAlias(registryUrl, ctx.resolvedSource);
536
+ if (!ctx.jsonOnly) {
537
+ logger.info(`Resolved ${chalk3.cyan(ctx.resolvedSource)} \u2192 ${chalk3.cyan(resolved.spec)} (${resolved.type})`);
538
+ }
539
+ ctx.resolvedSource = resolved.spec;
540
+ }
541
+ } catch (error) {
542
+ const message = error instanceof SkildError ? error.message : error instanceof Error ? error.message : String(error);
543
+ if (ctx.jsonOnly) printJson({ ok: false, error: message });
544
+ else console.error(chalk3.red(message));
545
+ process.exitCode = 1;
546
+ return false;
547
+ }
548
+ return true;
549
+ }
550
+ async function discoverSkills(ctx) {
551
+ const { resolvedSource, maxDepth, maxSkills, jsonOnly } = ctx;
552
+ if (ctx.spinner) {
553
+ ctx.spinner.text = `Discovery at ${chalk3.cyan(ctx.source)}...`;
554
+ }
529
555
  try {
530
- if (looksLikeAlias(resolvedSource)) {
531
- const registryUrl = resolveRegistryUrl(registryUrlForDeps);
532
- const resolved = await resolveRegistryAlias(registryUrl, resolvedSource);
533
- if (!jsonOnly) logger.info(`Resolved ${chalk3.cyan(resolvedSource)} \u2192 ${chalk3.cyan(resolved.spec)} (${resolved.type})`);
534
- resolvedSource = resolved.spec;
556
+ if (resolvedSource.startsWith("@") && resolvedSource.includes("/")) {
557
+ ctx.isSingleSkill = true;
558
+ ctx.selectedSkills = [{ relPath: resolvedSource, suggestedSource: resolvedSource }];
559
+ return true;
560
+ }
561
+ const maybeLocalRoot = path2.resolve(resolvedSource);
562
+ const isLocal = fs2.existsSync(maybeLocalRoot);
563
+ if (isLocal) {
564
+ const hasSkillMd2 = fs2.existsSync(path2.join(maybeLocalRoot, "SKILL.md"));
565
+ if (hasSkillMd2) {
566
+ ctx.isSingleSkill = true;
567
+ ctx.selectedSkills = [{ relPath: maybeLocalRoot, suggestedSource: resolvedSource }];
568
+ return true;
569
+ }
570
+ const discovered2 = discoverSkillDirsWithHeuristics(maybeLocalRoot, { maxDepth, maxSkills });
571
+ if (discovered2.length === 0) {
572
+ const message = `No SKILL.md found at ${maybeLocalRoot} (or within subdirectories).`;
573
+ if (jsonOnly) {
574
+ printJson({ ok: false, error: "SKILL_MD_NOT_FOUND", message, source: ctx.source, resolvedSource });
575
+ } else {
576
+ if (ctx.spinner) ctx.spinner.stop();
577
+ console.error(chalk3.red(message));
578
+ }
579
+ process.exitCode = 1;
580
+ return false;
581
+ }
582
+ ctx.discoveredSkills = asDiscoveredSkills(discovered2, (d) => path2.join(maybeLocalRoot, d.relPath));
583
+ return true;
584
+ }
585
+ const materialized = await materializeSourceToTemp(resolvedSource);
586
+ ctx.materializedDir = materialized.dir;
587
+ ctx.cleanupMaterialized = materialized.cleanup;
588
+ const hasSkillMd = fs2.existsSync(path2.join(ctx.materializedDir, "SKILL.md"));
589
+ if (hasSkillMd) {
590
+ ctx.isSingleSkill = true;
591
+ ctx.selectedSkills = [{ relPath: ".", suggestedSource: resolvedSource, materializedDir: ctx.materializedDir }];
592
+ return true;
593
+ }
594
+ const discovered = discoverSkillDirsWithHeuristics(ctx.materializedDir, { maxDepth, maxSkills });
595
+ if (discovered.length === 0) {
596
+ const message = `No SKILL.md found in source "${resolvedSource}".`;
597
+ if (jsonOnly) {
598
+ printJson({ ok: false, error: "SKILL_MD_NOT_FOUND", message, source: ctx.source, resolvedSource });
599
+ } else {
600
+ if (ctx.spinner) ctx.spinner.stop();
601
+ console.error(chalk3.red(message));
602
+ }
603
+ process.exitCode = 1;
604
+ return false;
535
605
  }
606
+ ctx.discoveredSkills = asDiscoveredSkills(
607
+ discovered,
608
+ (d) => deriveChildSource(resolvedSource, d.relPath),
609
+ (d) => d.absDir
610
+ );
611
+ return true;
536
612
  } catch (error) {
537
613
  const message = error instanceof SkildError ? error.message : error instanceof Error ? error.message : String(error);
538
614
  if (jsonOnly) printJson({ ok: false, error: message });
615
+ else {
616
+ if (ctx.spinner) ctx.spinner.stop();
617
+ console.error(chalk3.red(message));
618
+ }
619
+ process.exitCode = 1;
620
+ return false;
621
+ }
622
+ }
623
+ async function promptSelections(ctx) {
624
+ const { discoveredSkills, isSingleSkill, jsonOnly, interactive, yes, options } = ctx;
625
+ if (isSingleSkill) {
626
+ if (ctx.needsPlatformPrompt) {
627
+ if (ctx.spinner) ctx.spinner.stop();
628
+ const selectedPlatforms = await promptPlatformsInteractive({ defaultAll: true });
629
+ if (!selectedPlatforms) {
630
+ console.log(chalk3.red("No platforms selected."));
631
+ process.exitCode = 1;
632
+ return false;
633
+ }
634
+ ctx.targets = selectedPlatforms;
635
+ ctx.needsPlatformPrompt = false;
636
+ }
637
+ return true;
638
+ }
639
+ if (!discoveredSkills || discoveredSkills.length === 0) {
640
+ return false;
641
+ }
642
+ if (discoveredSkills.length > ctx.maxSkills) {
643
+ const message = `Found more than ${ctx.maxSkills} skills. Increase --max-skills to proceed.`;
644
+ if (jsonOnly) printJson({ ok: false, error: "TOO_MANY_SKILLS", message, source: ctx.source, resolvedSource: ctx.resolvedSource, maxSkills: ctx.maxSkills });
539
645
  else console.error(chalk3.red(message));
540
646
  process.exitCode = 1;
647
+ return false;
648
+ }
649
+ if (!options.recursive && !yes) {
650
+ if (jsonOnly) {
651
+ const foundOutput = discoveredSkills.map(({ relPath, suggestedSource }) => ({ relPath, suggestedSource }));
652
+ printJson({
653
+ ok: false,
654
+ error: "MULTI_SKILL_SOURCE",
655
+ message: "Source is not a skill root (missing SKILL.md). Multiple skills were found.",
656
+ source: ctx.source,
657
+ resolvedSource: ctx.resolvedSource,
658
+ found: foundOutput
659
+ });
660
+ process.exitCode = 1;
661
+ return false;
662
+ }
663
+ if (ctx.spinner) ctx.spinner.stop();
664
+ const headline = discoveredSkills.length === 1 ? `No SKILL.md found at root. Found 1 skill.
665
+ ` : `No SKILL.md found at root. Found ${discoveredSkills.length} skills.
666
+ `;
667
+ console.log(chalk3.yellow(headline));
668
+ if (!interactive) {
669
+ console.log(chalk3.dim(`Tip: rerun with ${chalk3.cyan("skild install <source> --recursive")} to install all.`));
670
+ process.exitCode = 1;
671
+ return false;
672
+ }
673
+ }
674
+ if (!yes && interactive) {
675
+ if (ctx.spinner) ctx.spinner.stop();
676
+ if (options.recursive) {
677
+ const headline = discoveredSkills.length === 1 ? `No SKILL.md found at root. Found 1 skill.
678
+ ` : `No SKILL.md found at root. Found ${discoveredSkills.length} skills.
679
+ `;
680
+ console.log(chalk3.yellow(headline));
681
+ }
682
+ const selected = await promptSkillsInteractive(discoveredSkills, { defaultAll: true });
683
+ if (!selected) {
684
+ console.log(chalk3.red("No skills selected."));
685
+ process.exitCode = 1;
686
+ return false;
687
+ }
688
+ ctx.selectedSkills = selected;
689
+ if (ctx.needsPlatformPrompt) {
690
+ const selectedPlatforms = await promptPlatformsInteractive({ defaultAll: true });
691
+ if (!selectedPlatforms) {
692
+ console.log(chalk3.red("No platforms selected."));
693
+ process.exitCode = 1;
694
+ return false;
695
+ }
696
+ ctx.targets = selectedPlatforms;
697
+ ctx.needsPlatformPrompt = false;
698
+ }
699
+ if (ctx.spinner) ctx.spinner.start();
700
+ } else {
701
+ ctx.selectedSkills = discoveredSkills;
702
+ }
703
+ return true;
704
+ }
705
+ async function executeInstalls(ctx) {
706
+ const { selectedSkills, targets, scope, forceOverwrite, registryUrl, spinner } = ctx;
707
+ if (!selectedSkills || selectedSkills.length === 0) {
541
708
  return;
542
709
  }
543
- const results = [];
544
- const errors = [];
545
- const skipped = [];
546
- let effectiveRecursive = recursive;
547
- let recursiveSkillCount = null;
548
- let forceOverwriteAll = Boolean(options.force);
549
- async function installOne(inputSource, materializedDir) {
550
- for (const targetPlatform of targets) {
710
+ if (spinner) {
711
+ spinner.text = selectedSkills.length > 1 ? `Installing ${chalk3.cyan(ctx.source)} \u2014 ${selectedSkills.length} skills...` : `Installing ${chalk3.cyan(ctx.source)}...`;
712
+ }
713
+ for (const skill of selectedSkills) {
714
+ if (spinner) {
715
+ spinner.text = `Installing ${chalk3.cyan(skill.relPath === "." ? ctx.source : skill.relPath)}...`;
716
+ }
717
+ for (const platform of targets) {
551
718
  try {
552
- const record = inputSource.startsWith("@") && inputSource.includes("/") ? await installRegistrySkill(
553
- { spec: inputSource, registryUrl: registryUrlForDeps },
554
- { platform: targetPlatform, scope, force: forceOverwriteAll }
719
+ const record = skill.suggestedSource.startsWith("@") && skill.suggestedSource.includes("/") ? await installRegistrySkill(
720
+ { spec: skill.suggestedSource, registryUrl },
721
+ { platform, scope, force: forceOverwrite }
555
722
  ) : await installSkill(
556
- { source: inputSource, materializedDir },
557
- { platform: targetPlatform, scope, force: forceOverwriteAll, registryUrl: registryUrlForDeps }
723
+ { source: skill.suggestedSource, materializedDir: skill.materializedDir },
724
+ { platform, scope, force: forceOverwrite, registryUrl }
558
725
  );
559
- results.push(record);
560
- void reportDownload(record, registryUrlForDeps);
726
+ ctx.results.push(record);
727
+ void reportDownload(record, registryUrl);
561
728
  } catch (error) {
562
729
  if (error instanceof SkildError && error.code === "ALREADY_INSTALLED") {
563
730
  const details = error.details;
564
- skipped.push({
565
- skillName: details?.skillName || inputSource,
566
- platform: targetPlatform,
731
+ ctx.skipped.push({
732
+ skillName: details?.skillName || skill.suggestedSource,
733
+ platform,
567
734
  installDir: details?.installDir || ""
568
735
  });
569
736
  continue;
570
737
  }
571
738
  const message = error instanceof SkildError ? error.message : error instanceof Error ? error.message : String(error);
572
- errors.push({ platform: targetPlatform, error: message, inputSource });
739
+ ctx.errors.push({ platform, error: message, inputSource: skill.suggestedSource });
573
740
  }
574
741
  }
575
742
  }
576
- async function resolveDiscoveredSelection(found, spinner) {
577
- if (found.length === 0) return null;
578
- if (found.length > maxSkills) {
579
- const message = `Found more than ${maxSkills} skills. Increase --max-skills to proceed.`;
580
- if (jsonOnly) printJson({ ok: false, error: "TOO_MANY_SKILLS", message, source, resolvedSource, maxSkills });
581
- else console.error(chalk3.red(message));
582
- process.exitCode = 1;
583
- return null;
584
- }
585
- if (!effectiveRecursive) {
586
- if (jsonOnly) {
587
- const foundOutput = found.map(({ relPath, suggestedSource }) => ({ relPath, suggestedSource }));
588
- printJson({
589
- ok: false,
590
- error: "MULTI_SKILL_SOURCE",
591
- message: "Source is not a skill root (missing SKILL.md). Multiple skills were found.",
592
- source,
593
- resolvedSource,
594
- found: foundOutput
595
- });
596
- process.exitCode = 1;
597
- return null;
598
- }
599
- if (spinner) spinner.stop();
600
- const headline = found.length === 1 ? `No SKILL.md found at root. Found 1 skill.
601
- ` : `No SKILL.md found at root. Found ${found.length} skills.
602
- `;
603
- console.log(chalk3.yellow(headline));
604
- if (!interactive && !yes) {
605
- console.log(chalk3.dim(`Tip: rerun with ${chalk3.cyan("skild install <source> --recursive")} to install all.`));
606
- process.exitCode = 1;
607
- return null;
608
- }
609
- if (!yes) {
610
- const selected = await promptSkillsInteractive(found, { defaultAll: true });
611
- if (!selected) {
612
- console.log(chalk3.red("No skills selected."));
613
- process.exitCode = 1;
614
- return null;
615
- }
616
- if (deferPlatformSelection) {
617
- const selectedPlatforms = await promptPlatformsInteractive({ defaultAll: true });
618
- if (!selectedPlatforms) {
619
- console.log(chalk3.red("No platforms selected."));
620
- process.exitCode = 1;
621
- return null;
622
- }
623
- targets = selectedPlatforms;
624
- deferPlatformSelection = false;
625
- }
626
- effectiveRecursive = true;
627
- if (spinner) spinner.start();
628
- recursiveSkillCount = selected.length;
629
- return selected;
630
- }
631
- effectiveRecursive = true;
743
+ }
744
+ function reportResults(ctx) {
745
+ const { results, errors, skipped, spinner, jsonOnly, selectedSkills, targets } = ctx;
746
+ if (ctx.cleanupMaterialized) {
747
+ ctx.cleanupMaterialized();
748
+ }
749
+ const isMultiSkill = (selectedSkills?.length ?? 0) > 1;
750
+ if (jsonOnly) {
751
+ if (!isMultiSkill && targets.length === 1) {
752
+ if (errors.length) printJson({ ok: false, error: errors[0]?.error || "Install failed." });
753
+ else printJson(results[0] ?? null);
754
+ } else {
755
+ printJson({
756
+ ok: errors.length === 0,
757
+ source: ctx.source,
758
+ resolvedSource: ctx.resolvedSource,
759
+ scope: ctx.scope,
760
+ recursive: isMultiSkill,
761
+ all: targets.length === PLATFORMS2.length,
762
+ skillCount: selectedSkills?.length ?? 0,
763
+ results,
764
+ errors,
765
+ skipped
766
+ });
632
767
  }
633
- recursiveSkillCount = found.length;
634
- return found;
768
+ process.exitCode = errors.length ? 1 : 0;
769
+ return;
635
770
  }
636
- try {
637
- const spinner = jsonOnly ? null : createSpinner(
638
- allPlatformsSelected ? `Installing ${chalk3.cyan(source)} to ${chalk3.dim("all platforms")} (${scope})...` : targets.length > 1 ? `Installing ${chalk3.cyan(source)} to ${chalk3.dim(`${targets.length} platforms`)} (${scope})...` : `Installing ${chalk3.cyan(source)} to ${chalk3.dim(targets[0])} (${scope})...`
639
- );
640
- let cleanupMaterialized = null;
641
- let materializedRoot = null;
642
- try {
643
- async function ensurePlatformSelection() {
644
- if (!deferPlatformSelection) return true;
645
- const selectedPlatforms = await promptPlatformsInteractive({ defaultAll: true });
646
- if (!selectedPlatforms) {
647
- console.log(chalk3.red("No platforms selected."));
648
- process.exitCode = 1;
649
- return false;
650
- }
651
- targets = selectedPlatforms;
652
- deferPlatformSelection = false;
653
- return true;
654
- }
655
- if (resolvedSource.startsWith("@") && resolvedSource.includes("/")) {
656
- if (!await ensurePlatformSelection()) return;
657
- await installOne(resolvedSource);
658
- } else {
659
- const maybeLocalRoot = path2.resolve(resolvedSource);
660
- const isLocal = fs2.existsSync(maybeLocalRoot);
661
- if (isLocal) {
662
- const hasSkillMd = fs2.existsSync(path2.join(maybeLocalRoot, "SKILL.md"));
663
- if (hasSkillMd) {
664
- if (!await ensurePlatformSelection()) return;
665
- await installOne(resolvedSource);
666
- } else {
667
- const discovered = discoverSkillDirsWithHeuristics(maybeLocalRoot, { maxDepth, maxSkills });
668
- if (discovered.length === 0) {
669
- const message = `No SKILL.md found at ${maybeLocalRoot} (or within subdirectories).`;
670
- if (jsonOnly) {
671
- printJson({ ok: false, error: "SKILL_MD_NOT_FOUND", message, source, resolvedSource });
672
- } else {
673
- if (spinner) spinner.stop();
674
- console.error(chalk3.red(message));
675
- }
676
- process.exitCode = 1;
677
- return;
678
- }
679
- const found = asDiscoveredSkills(discovered, (d) => path2.join(maybeLocalRoot, d.relPath));
680
- const selected = await resolveDiscoveredSelection(found, spinner);
681
- if (!selected) return;
682
- if (spinner) spinner.text = `Installing ${chalk3.cyan(source)} \u2014 discovered ${selected.length} skills...`;
683
- for (const skill of selected) {
684
- if (spinner) spinner.text = `Installing ${chalk3.cyan(skill.relPath)} (${scope})...`;
685
- await installOne(skill.suggestedSource, skill.materializedDir);
686
- }
687
- }
688
- } else {
689
- const materialized = await materializeSourceToTemp(resolvedSource);
690
- cleanupMaterialized = materialized.cleanup;
691
- materializedRoot = materialized.dir;
692
- const hasSkillMd = fs2.existsSync(path2.join(materializedRoot, "SKILL.md"));
693
- if (hasSkillMd) {
694
- await installOne(resolvedSource, materializedRoot);
695
- } else {
696
- const discovered = discoverSkillDirsWithHeuristics(materializedRoot, { maxDepth, maxSkills });
697
- if (discovered.length === 0) {
698
- const message = `No SKILL.md found in source "${resolvedSource}".`;
699
- if (jsonOnly) {
700
- printJson({ ok: false, error: "SKILL_MD_NOT_FOUND", message, source, resolvedSource });
701
- } else {
702
- if (spinner) spinner.stop();
703
- console.error(chalk3.red(message));
704
- }
705
- process.exitCode = 1;
706
- return;
707
- }
708
- const found = asDiscoveredSkills(
709
- discovered,
710
- (d) => deriveChildSource(resolvedSource, d.relPath),
711
- (d) => d.absDir
712
- );
713
- const selected = await resolveDiscoveredSelection(found, spinner);
714
- if (!selected) return;
715
- if (spinner) spinner.text = `Installing ${chalk3.cyan(source)} \u2014 discovered ${selected.length} skills...`;
716
- for (const skill of selected) {
717
- if (spinner) spinner.text = `Installing ${chalk3.cyan(skill.relPath)} (${scope})...`;
718
- await installOne(skill.suggestedSource, skill.materializedDir);
719
- }
720
- }
721
- }
722
- }
723
- } finally {
724
- if (cleanupMaterialized) cleanupMaterialized();
725
- }
726
- if (jsonOnly) {
727
- if (!effectiveRecursive && targets.length === 1) {
728
- if (errors.length) printJson({ ok: false, error: errors[0]?.error || "Install failed." });
729
- else printJson(results[0] ?? null);
730
- } else {
731
- printJson({
732
- ok: errors.length === 0,
733
- source,
734
- resolvedSource,
735
- scope,
736
- recursive: effectiveRecursive,
737
- all: allPlatformsSelected,
738
- recursiveSkillCount,
739
- results,
740
- errors
741
- });
742
- }
743
- process.exitCode = errors.length ? 1 : 0;
744
- return;
745
- }
746
- if (errors.length === 0) {
747
- const displayName = results[0]?.canonicalName || results[0]?.name || source;
748
- spinner.succeed(
749
- effectiveRecursive ? `Installed ${chalk3.green(String(recursiveSkillCount ?? results.length))}${chalk3.dim(" skills")} to ${chalk3.dim(`${targets.length} platforms`)}` : targets.length > 1 ? `Installed ${chalk3.green(displayName)} to ${chalk3.dim(`${results.length} platforms`)}` : `Installed ${chalk3.green(displayName)} to ${chalk3.dim(results[0]?.installDir || "")}`
771
+ if (errors.length === 0 && (results.length > 0 || skipped.length > 0)) {
772
+ const displayName = results[0]?.canonicalName || results[0]?.name || ctx.source;
773
+ if (skipped.length > 0) {
774
+ spinner?.succeed(
775
+ `Installed ${chalk3.green(results.length)} and skipped ${chalk3.dim(skipped.length)} (already installed) to ${chalk3.dim(`${targets.length} platforms`)}`
750
776
  );
751
777
  } else {
752
- const attempted = results.length + errors.length;
753
- spinner.fail(
754
- effectiveRecursive ? `Install had failures (${errors.length}/${attempted} installs failed)` : `Failed to install ${chalk3.red(source)} to ${errors.length}/${targets.length} platforms`
778
+ spinner?.succeed(
779
+ isMultiSkill ? `Installed ${chalk3.green(String(selectedSkills?.length ?? results.length))}${chalk3.dim(" skills")} to ${chalk3.dim(`${targets.length} platforms`)}` : targets.length > 1 ? `Installed ${chalk3.green(displayName)} to ${chalk3.dim(`${results.length} platforms`)}` : `Installed ${chalk3.green(displayName)} to ${chalk3.dim(results[0]?.installDir || "")}`
755
780
  );
756
- process.exitCode = 1;
757
- if (!effectiveRecursive && targets.length === 1 && errors[0]) console.error(chalk3.red(errors[0].error));
758
781
  }
759
- if (!effectiveRecursive && targets.length === 1 && results[0]) {
760
- const record = results[0];
761
- if (record.hasSkillMd) logger.installDetail("SKILL.md found \u2713");
762
- else logger.installDetail("Warning: No SKILL.md found", true);
763
- if (record.skill?.validation && !record.skill.validation.ok) {
764
- logger.installDetail(`Validation: ${chalk3.yellow("failed")} (${record.skill.validation.issues.length} issues)`, true);
765
- } else if (record.skill?.validation?.ok) {
766
- logger.installDetail(`Validation: ${chalk3.green("ok")}`);
767
- }
768
- } else if (effectiveRecursive || targets.length > 1) {
769
- for (const r of results.slice(0, 60)) {
770
- const displayName = r.canonicalName || r.name;
771
- const suffix = r.hasSkillMd ? chalk3.green("\u2713") : chalk3.yellow("\u26A0");
772
- console.log(` ${suffix} ${chalk3.cyan(displayName)} \u2192 ${chalk3.dim(r.platform)}`);
773
- }
774
- if (results.length > 60) console.log(chalk3.dim(` ... and ${results.length - 60} more`));
775
- if (errors.length) {
776
- console.log(chalk3.yellow("\nFailures:"));
777
- for (const e of errors) console.log(chalk3.yellow(` - ${e.platform}: ${e.error}`));
778
- }
779
- process.exitCode = errors.length ? 1 : 0;
782
+ } else if (errors.length > 0) {
783
+ const attempted = results.length + errors.length;
784
+ spinner?.fail(
785
+ isMultiSkill ? `Install had failures (${errors.length}/${attempted} installs failed)` : `Failed to install ${chalk3.red(ctx.source)} to ${errors.length}/${targets.length} platforms`
786
+ );
787
+ process.exitCode = 1;
788
+ if (!isMultiSkill && targets.length === 1 && errors[0]) {
789
+ console.error(chalk3.red(errors[0].error));
780
790
  }
781
- if (skipped.length > 0) {
782
- const uniqueSkills = [...new Set(skipped.map((s) => s.skillName))];
783
- console.log(chalk3.dim(`
791
+ }
792
+ if (isMultiSkill || targets.length > 1) {
793
+ for (const r of results.slice(0, 60)) {
794
+ const displayName = r.canonicalName || r.name;
795
+ const suffix = r.hasSkillMd ? chalk3.green("\u2713") : chalk3.yellow("\u26A0");
796
+ console.log(` ${suffix} ${chalk3.cyan(displayName)} \u2192 ${chalk3.dim(r.platform)}`);
797
+ }
798
+ if (results.length > 60) console.log(chalk3.dim(` ... and ${results.length - 60} more`));
799
+ if (errors.length) {
800
+ console.log(chalk3.yellow("\nFailures:"));
801
+ for (const e of errors) console.log(chalk3.yellow(` - ${e.platform}: ${e.error}`));
802
+ }
803
+ process.exitCode = errors.length ? 1 : 0;
804
+ } else if (!isMultiSkill && targets.length === 1 && results[0]) {
805
+ const record = results[0];
806
+ if (record.hasSkillMd) logger.installDetail("SKILL.md found \u2713");
807
+ else logger.installDetail("Warning: No SKILL.md found", true);
808
+ if (record.skill?.validation && !record.skill.validation.ok) {
809
+ logger.installDetail(`Validation: ${chalk3.yellow("failed")} (${record.skill.validation.issues.length} issues)`, true);
810
+ } else if (record.skill?.validation?.ok) {
811
+ logger.installDetail(`Validation: ${chalk3.green("ok")}`);
812
+ }
813
+ }
814
+ if (skipped.length > 0) {
815
+ const uniqueSkills = [...new Set(skipped.map((s) => s.skillName))];
816
+ console.log(chalk3.dim(`
784
817
  Skipped ${skipped.length} already installed (${uniqueSkills.length} skill${uniqueSkills.length > 1 ? "s" : ""}):`));
785
- const bySkill = /* @__PURE__ */ new Map();
786
- for (const s of skipped) {
787
- const platforms = bySkill.get(s.skillName) || [];
788
- platforms.push(s.platform);
789
- bySkill.set(s.skillName, platforms);
790
- }
791
- for (const [skillName, platforms] of bySkill.entries()) {
792
- const platformsStr = platforms.length === PLATFORMS2.length ? "all platforms" : platforms.join(", ");
793
- console.log(chalk3.dim(` - ${skillName} (${platformsStr})`));
794
- }
795
- console.log(chalk3.dim(`
796
- To reinstall, use: ${chalk3.cyan("skild install <source> --force")}`));
818
+ const bySkill = /* @__PURE__ */ new Map();
819
+ for (const s of skipped) {
820
+ const platforms = bySkill.get(s.skillName) || [];
821
+ platforms.push(s.platform);
822
+ bySkill.set(s.skillName, platforms);
823
+ }
824
+ for (const [skillName, platforms] of bySkill.entries()) {
825
+ const platformsStr = platforms.length === PLATFORMS2.length ? "all platforms" : platforms.join(", ");
826
+ console.log(chalk3.dim(` - ${skillName} (${platformsStr})`));
797
827
  }
828
+ console.log(chalk3.dim(`
829
+ To reinstall, use: ${chalk3.cyan("skild install <source> --force")}`));
830
+ }
831
+ }
832
+ async function install(source, options = {}) {
833
+ const ctx = createContext(source, options);
834
+ if (!ctx.jsonOnly) {
835
+ ctx.spinner = createSpinner(`Interpreting ${chalk3.cyan(ctx.source)}...`);
836
+ }
837
+ try {
838
+ if (!await resolveSource(ctx)) return;
839
+ if (!await discoverSkills(ctx)) return;
840
+ if (!await promptSelections(ctx)) return;
841
+ await executeInstalls(ctx);
842
+ reportResults(ctx);
798
843
  } catch (error) {
799
844
  const message = error instanceof SkildError ? error.message : error instanceof Error ? error.message : String(error);
800
- if (jsonOnly) printJson({ ok: false, error: message });
801
- else console.error(chalk3.red(message));
845
+ if (ctx.jsonOnly) printJson({ ok: false, error: message });
846
+ else {
847
+ if (ctx.spinner) ctx.spinner.fail(chalk3.red(message));
848
+ else console.error(chalk3.red(message));
849
+ }
802
850
  process.exitCode = 1;
851
+ if (ctx.cleanupMaterialized) ctx.cleanupMaterialized();
803
852
  }
804
853
  }
805
854
  async function reportDownload(record, registryOverride) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "skild",
3
- "version": "0.4.5",
3
+ "version": "0.4.7",
4
4
  "description": "The npm for Agent Skills — Discover, install, manage, and publish AI Agent Skills with ease.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -38,7 +38,7 @@
38
38
  "commander": "^12.1.0",
39
39
  "ora": "^8.0.1",
40
40
  "tar": "^7.4.3",
41
- "@skild/core": "^0.4.5"
41
+ "@skild/core": "^0.4.7"
42
42
  },
43
43
  "devDependencies": {
44
44
  "@types/node": "^20.10.0",