skild 0.4.5 → 0.4.6

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 +482 -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,97 @@ 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
+ };
288
+ function formatTreeNode(node, selection, isCursor, options = {}) {
289
+ const { state, selectedCount } = selection;
290
+ const totalCount = node.leafIndices.length;
291
+ const indent = " ".repeat(node.depth - 1);
292
+ const checkbox = state === "all" ? chalk2.green("\u25CF") : state === "partial" ? chalk2.yellow("\u25D0") : chalk2.dim("\u25CB");
293
+ const name = isCursor ? chalk2.cyan.underline(node.name) : node.name;
294
+ const cursorMark = isCursor ? chalk2.cyan("\u203A ") : " ";
295
+ let count = "";
296
+ if (totalCount > 1) {
297
+ count = chalk2.dim(` (${selectedCount}/${totalCount})`);
298
+ }
299
+ const suffix = options.suffix || "";
300
+ let hint = "";
301
+ if (isCursor && totalCount > 0) {
302
+ hint = state === "all" ? chalk2.dim(" \u2190 Space to deselect") : chalk2.dim(" \u2190 Space to select");
303
+ }
304
+ return `${cursorMark}${indent}${checkbox} ${name}${count}${suffix}${hint}`;
305
+ }
278
306
  async function promptSkillsInteractive(skills, options = {}) {
279
307
  if (skills.length === 0) return null;
280
308
  const targetPlatforms = options.targetPlatforms || [];
@@ -292,100 +320,53 @@ async function promptSkillsInteractive(skills, options = {}) {
292
320
  title: "Select skills to install",
293
321
  subtitle: "\u2191\u2193 navigate \u2022 Space toggle \u2022 Enter confirm",
294
322
  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 = "";
323
+ formatNode: (node, selection, isCursor) => {
324
+ let suffix = "";
299
325
  if (node.isLeaf && node.leafIndices.length === 1) {
300
326
  const skill = skills[node.leafIndices[0]];
301
327
  if (skill?.installedPlatforms?.length) {
302
328
  if (skill.installedPlatforms.length === targetPlatforms.length && targetPlatforms.length > 0) {
303
- installedTag = chalk2.dim(" [installed]");
329
+ suffix = chalk2.dim(" [installed]");
304
330
  } else if (skill.installedPlatforms.length > 0) {
305
- installedTag = chalk2.dim(` [installed on ${skill.installedPlatforms.length}]`);
331
+ suffix = chalk2.dim(` [installed on ${skill.installedPlatforms.length}]`);
306
332
  }
307
333
  }
308
334
  }
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
- }
335
+ const formatted = formatTreeNode(node, selection, isCursor, { suffix });
336
+ if (isCursor && suffix && selection.state !== "all") {
337
+ return formatted.replace("\u2190 Space to select", "\u2190 Space to reinstall");
319
338
  }
320
- return `${cursor}${indent}${checkbox} ${name}${count}${installedTag}${hint}`;
339
+ return formatted;
321
340
  },
322
341
  defaultAll: false,
323
- // We handle default selection manually
324
342
  defaultSelected
325
343
  });
326
- if (!selectedIndices || selectedIndices.length === 0) {
327
- return null;
328
- }
344
+ if (!selectedIndices) return null;
345
+ const selectedSkills = selectedIndices.map((i) => skills[i]);
346
+ const names = selectedSkills.map((s) => s.relPath === "." ? s.suggestedSource : s.relPath);
329
347
  console.log(chalk2.green(`
330
- \u2713 ${selectedIndices.length} skill${selectedIndices.length > 1 ? "s" : ""} selected
348
+ \u2713 Selected ${selectedSkills.length} skill${selectedSkills.length > 1 ? "s" : ""}: ${chalk2.cyan(names.join(", "))}
331
349
  `));
332
- return selectedIndices.map((i) => skills[i]);
350
+ return selectedSkills;
333
351
  }
334
352
  async function promptPlatformsInteractive(options = {}) {
335
353
  const platformItems = PLATFORMS.map((p) => ({ platform: p }));
336
354
  const selectedIndices = await interactiveTreeSelect(platformItems, {
337
355
  title: "Select target platforms",
338
356
  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}`;
357
+ buildTree: buildPlatformTree,
358
+ formatNode: (node, selection, isCursor) => {
359
+ const displayName = node.name === "All Platforms" ? node.name : PLATFORM_DISPLAY[node.name] || node.name;
360
+ const modifiedNode = { ...node, name: displayName };
361
+ return formatTreeNode(modifiedNode, selection, isCursor);
380
362
  },
381
363
  defaultAll: options.defaultAll !== false
382
364
  });
383
- if (!selectedIndices || selectedIndices.length === 0) {
384
- return null;
385
- }
365
+ if (!selectedIndices) return null;
386
366
  const selected = selectedIndices.map((i) => PLATFORMS[i]);
367
+ const names = selected.map((p) => PLATFORM_DISPLAY[p] || p);
387
368
  console.log(chalk2.green(`
388
- \u2713 Installing to ${selected.length} platform${selected.length > 1 ? "s" : ""}
369
+ \u2713 Installing to ${selected.length} platform${selected.length > 1 ? "s" : ""}: ${chalk2.cyan(names.join(", "))}
389
370
  `));
390
371
  return selected;
391
372
  }
@@ -491,315 +472,381 @@ function asDiscoveredSkills(discovered, toSuggestedSource, toMaterializedDir) {
491
472
  materializedDir: toMaterializedDir ? toMaterializedDir(d) : void 0
492
473
  }));
493
474
  }
494
- async function install(source, options = {}) {
475
+ function createContext(source, options) {
495
476
  const scope = options.local ? "project" : "global";
496
477
  const auth = loadRegistryAuth();
497
- const registryUrlForDeps = options.registry || auth?.registryUrl;
478
+ const registryUrl = options.registry || auth?.registryUrl;
498
479
  const jsonOnly = Boolean(options.json);
499
- const recursive = Boolean(options.recursive);
500
480
  const yes = Boolean(options.yes);
501
- const maxDepth = parsePositiveInt(options.depth, 6);
502
- const maxSkills = parsePositiveInt(options.maxSkills, 200);
503
481
  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
482
  let targets = [];
514
- let deferPlatformSelection = false;
515
- if (requestedAll) {
483
+ let needsPlatformPrompt = false;
484
+ if (options.all) {
516
485
  targets = [...PLATFORMS2];
517
- } else if (requestedTarget) {
518
- targets = [requestedTarget];
486
+ } else if (options.target) {
487
+ targets = [options.target];
519
488
  } else if (yes) {
520
489
  targets = [...PLATFORMS2];
521
490
  } else if (interactive) {
522
- deferPlatformSelection = true;
491
+ needsPlatformPrompt = true;
523
492
  targets = [...PLATFORMS2];
524
493
  } else {
525
494
  targets = ["claude"];
526
495
  }
527
- const allPlatformsSelected = targets.length === PLATFORMS2.length;
528
- let resolvedSource = source.trim();
496
+ return {
497
+ source,
498
+ options,
499
+ scope,
500
+ registryUrl,
501
+ jsonOnly,
502
+ interactive,
503
+ yes,
504
+ maxDepth: parsePositiveInt(options.depth, 6),
505
+ maxSkills: parsePositiveInt(options.maxSkills, 200),
506
+ resolvedSource: source.trim(),
507
+ targets,
508
+ forceOverwrite: Boolean(options.force),
509
+ needsPlatformPrompt,
510
+ discoveredSkills: null,
511
+ selectedSkills: null,
512
+ isSingleSkill: false,
513
+ materializedDir: null,
514
+ cleanupMaterialized: null,
515
+ results: [],
516
+ errors: [],
517
+ skipped: [],
518
+ spinner: null
519
+ };
520
+ }
521
+ async function resolveSource(ctx) {
522
+ if (ctx.options.all && ctx.options.target) {
523
+ const message = "Invalid options: use either --all or --target, not both.";
524
+ if (ctx.jsonOnly) printJson({ ok: false, error: message });
525
+ else console.error(chalk3.red(message));
526
+ process.exitCode = 1;
527
+ return false;
528
+ }
529
+ try {
530
+ if (looksLikeAlias(ctx.resolvedSource)) {
531
+ if (ctx.spinner) ctx.spinner.text = `Resolving ${chalk3.cyan(ctx.resolvedSource)}...`;
532
+ const registryUrl = resolveRegistryUrl(ctx.registryUrl);
533
+ const resolved = await resolveRegistryAlias(registryUrl, ctx.resolvedSource);
534
+ if (!ctx.jsonOnly) {
535
+ logger.info(`Resolved ${chalk3.cyan(ctx.resolvedSource)} \u2192 ${chalk3.cyan(resolved.spec)} (${resolved.type})`);
536
+ }
537
+ ctx.resolvedSource = resolved.spec;
538
+ }
539
+ } catch (error) {
540
+ const message = error instanceof SkildError ? error.message : error instanceof Error ? error.message : String(error);
541
+ if (ctx.jsonOnly) printJson({ ok: false, error: message });
542
+ else console.error(chalk3.red(message));
543
+ process.exitCode = 1;
544
+ return false;
545
+ }
546
+ return true;
547
+ }
548
+ async function discoverSkills(ctx) {
549
+ const { resolvedSource, maxDepth, maxSkills, jsonOnly } = ctx;
550
+ if (ctx.spinner) {
551
+ ctx.spinner.text = `Discovery at ${chalk3.cyan(ctx.source)}...`;
552
+ }
529
553
  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;
554
+ if (resolvedSource.startsWith("@") && resolvedSource.includes("/")) {
555
+ ctx.isSingleSkill = true;
556
+ ctx.selectedSkills = [{ relPath: resolvedSource, suggestedSource: resolvedSource }];
557
+ return true;
558
+ }
559
+ const maybeLocalRoot = path2.resolve(resolvedSource);
560
+ const isLocal = fs2.existsSync(maybeLocalRoot);
561
+ if (isLocal) {
562
+ const hasSkillMd2 = fs2.existsSync(path2.join(maybeLocalRoot, "SKILL.md"));
563
+ if (hasSkillMd2) {
564
+ ctx.isSingleSkill = true;
565
+ ctx.selectedSkills = [{ relPath: maybeLocalRoot, suggestedSource: resolvedSource }];
566
+ return true;
567
+ }
568
+ const discovered2 = discoverSkillDirsWithHeuristics(maybeLocalRoot, { maxDepth, maxSkills });
569
+ if (discovered2.length === 0) {
570
+ const message = `No SKILL.md found at ${maybeLocalRoot} (or within subdirectories).`;
571
+ if (jsonOnly) {
572
+ printJson({ ok: false, error: "SKILL_MD_NOT_FOUND", message, source: ctx.source, resolvedSource });
573
+ } else {
574
+ if (ctx.spinner) ctx.spinner.stop();
575
+ console.error(chalk3.red(message));
576
+ }
577
+ process.exitCode = 1;
578
+ return false;
579
+ }
580
+ ctx.discoveredSkills = asDiscoveredSkills(discovered2, (d) => path2.join(maybeLocalRoot, d.relPath));
581
+ return true;
582
+ }
583
+ const materialized = await materializeSourceToTemp(resolvedSource);
584
+ ctx.materializedDir = materialized.dir;
585
+ ctx.cleanupMaterialized = materialized.cleanup;
586
+ const hasSkillMd = fs2.existsSync(path2.join(ctx.materializedDir, "SKILL.md"));
587
+ if (hasSkillMd) {
588
+ ctx.isSingleSkill = true;
589
+ ctx.selectedSkills = [{ relPath: ".", suggestedSource: resolvedSource, materializedDir: ctx.materializedDir }];
590
+ return true;
591
+ }
592
+ const discovered = discoverSkillDirsWithHeuristics(ctx.materializedDir, { maxDepth, maxSkills });
593
+ if (discovered.length === 0) {
594
+ const message = `No SKILL.md found in source "${resolvedSource}".`;
595
+ if (jsonOnly) {
596
+ printJson({ ok: false, error: "SKILL_MD_NOT_FOUND", message, source: ctx.source, resolvedSource });
597
+ } else {
598
+ if (ctx.spinner) ctx.spinner.stop();
599
+ console.error(chalk3.red(message));
600
+ }
601
+ process.exitCode = 1;
602
+ return false;
535
603
  }
604
+ ctx.discoveredSkills = asDiscoveredSkills(
605
+ discovered,
606
+ (d) => deriveChildSource(resolvedSource, d.relPath),
607
+ (d) => d.absDir
608
+ );
609
+ return true;
536
610
  } catch (error) {
537
611
  const message = error instanceof SkildError ? error.message : error instanceof Error ? error.message : String(error);
538
612
  if (jsonOnly) printJson({ ok: false, error: message });
613
+ else {
614
+ if (ctx.spinner) ctx.spinner.stop();
615
+ console.error(chalk3.red(message));
616
+ }
617
+ process.exitCode = 1;
618
+ return false;
619
+ }
620
+ }
621
+ async function promptSelections(ctx) {
622
+ const { discoveredSkills, isSingleSkill, jsonOnly, interactive, yes, options } = ctx;
623
+ if (isSingleSkill) {
624
+ if (ctx.needsPlatformPrompt) {
625
+ if (ctx.spinner) ctx.spinner.stop();
626
+ const selectedPlatforms = await promptPlatformsInteractive({ defaultAll: true });
627
+ if (!selectedPlatforms) {
628
+ console.log(chalk3.red("No platforms selected."));
629
+ process.exitCode = 1;
630
+ return false;
631
+ }
632
+ ctx.targets = selectedPlatforms;
633
+ ctx.needsPlatformPrompt = false;
634
+ }
635
+ return true;
636
+ }
637
+ if (!discoveredSkills || discoveredSkills.length === 0) {
638
+ return false;
639
+ }
640
+ if (discoveredSkills.length > ctx.maxSkills) {
641
+ const message = `Found more than ${ctx.maxSkills} skills. Increase --max-skills to proceed.`;
642
+ if (jsonOnly) printJson({ ok: false, error: "TOO_MANY_SKILLS", message, source: ctx.source, resolvedSource: ctx.resolvedSource, maxSkills: ctx.maxSkills });
539
643
  else console.error(chalk3.red(message));
540
644
  process.exitCode = 1;
645
+ return false;
646
+ }
647
+ if (!options.recursive && !yes) {
648
+ if (jsonOnly) {
649
+ const foundOutput = discoveredSkills.map(({ relPath, suggestedSource }) => ({ relPath, suggestedSource }));
650
+ printJson({
651
+ ok: false,
652
+ error: "MULTI_SKILL_SOURCE",
653
+ message: "Source is not a skill root (missing SKILL.md). Multiple skills were found.",
654
+ source: ctx.source,
655
+ resolvedSource: ctx.resolvedSource,
656
+ found: foundOutput
657
+ });
658
+ process.exitCode = 1;
659
+ return false;
660
+ }
661
+ if (ctx.spinner) ctx.spinner.stop();
662
+ const headline = discoveredSkills.length === 1 ? `No SKILL.md found at root. Found 1 skill.
663
+ ` : `No SKILL.md found at root. Found ${discoveredSkills.length} skills.
664
+ `;
665
+ console.log(chalk3.yellow(headline));
666
+ if (!interactive) {
667
+ console.log(chalk3.dim(`Tip: rerun with ${chalk3.cyan("skild install <source> --recursive")} to install all.`));
668
+ process.exitCode = 1;
669
+ return false;
670
+ }
671
+ }
672
+ if (!yes && interactive) {
673
+ if (ctx.spinner) ctx.spinner.stop();
674
+ if (options.recursive) {
675
+ const headline = discoveredSkills.length === 1 ? `No SKILL.md found at root. Found 1 skill.
676
+ ` : `No SKILL.md found at root. Found ${discoveredSkills.length} skills.
677
+ `;
678
+ console.log(chalk3.yellow(headline));
679
+ }
680
+ const selected = await promptSkillsInteractive(discoveredSkills, { defaultAll: true });
681
+ if (!selected) {
682
+ console.log(chalk3.red("No skills selected."));
683
+ process.exitCode = 1;
684
+ return false;
685
+ }
686
+ ctx.selectedSkills = selected;
687
+ if (ctx.needsPlatformPrompt) {
688
+ const selectedPlatforms = await promptPlatformsInteractive({ defaultAll: true });
689
+ if (!selectedPlatforms) {
690
+ console.log(chalk3.red("No platforms selected."));
691
+ process.exitCode = 1;
692
+ return false;
693
+ }
694
+ ctx.targets = selectedPlatforms;
695
+ ctx.needsPlatformPrompt = false;
696
+ }
697
+ if (ctx.spinner) ctx.spinner.start();
698
+ } else {
699
+ ctx.selectedSkills = discoveredSkills;
700
+ }
701
+ return true;
702
+ }
703
+ async function executeInstalls(ctx) {
704
+ const { selectedSkills, targets, scope, forceOverwrite, registryUrl, spinner } = ctx;
705
+ if (!selectedSkills || selectedSkills.length === 0) {
541
706
  return;
542
707
  }
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) {
708
+ if (spinner) {
709
+ spinner.text = selectedSkills.length > 1 ? `Installing ${chalk3.cyan(ctx.source)} \u2014 ${selectedSkills.length} skills...` : `Installing ${chalk3.cyan(ctx.source)}...`;
710
+ }
711
+ for (const skill of selectedSkills) {
712
+ if (spinner) {
713
+ spinner.text = `Installing ${chalk3.cyan(skill.relPath === "." ? ctx.source : skill.relPath)}...`;
714
+ }
715
+ for (const platform of targets) {
551
716
  try {
552
- const record = inputSource.startsWith("@") && inputSource.includes("/") ? await installRegistrySkill(
553
- { spec: inputSource, registryUrl: registryUrlForDeps },
554
- { platform: targetPlatform, scope, force: forceOverwriteAll }
717
+ const record = skill.suggestedSource.startsWith("@") && skill.suggestedSource.includes("/") ? await installRegistrySkill(
718
+ { spec: skill.suggestedSource, registryUrl },
719
+ { platform, scope, force: forceOverwrite }
555
720
  ) : await installSkill(
556
- { source: inputSource, materializedDir },
557
- { platform: targetPlatform, scope, force: forceOverwriteAll, registryUrl: registryUrlForDeps }
721
+ { source: skill.suggestedSource, materializedDir: skill.materializedDir },
722
+ { platform, scope, force: forceOverwrite, registryUrl }
558
723
  );
559
- results.push(record);
560
- void reportDownload(record, registryUrlForDeps);
724
+ ctx.results.push(record);
725
+ void reportDownload(record, registryUrl);
561
726
  } catch (error) {
562
727
  if (error instanceof SkildError && error.code === "ALREADY_INSTALLED") {
563
728
  const details = error.details;
564
- skipped.push({
565
- skillName: details?.skillName || inputSource,
566
- platform: targetPlatform,
729
+ ctx.skipped.push({
730
+ skillName: details?.skillName || skill.suggestedSource,
731
+ platform,
567
732
  installDir: details?.installDir || ""
568
733
  });
569
734
  continue;
570
735
  }
571
736
  const message = error instanceof SkildError ? error.message : error instanceof Error ? error.message : String(error);
572
- errors.push({ platform: targetPlatform, error: message, inputSource });
737
+ ctx.errors.push({ platform, error: message, inputSource: skill.suggestedSource });
573
738
  }
574
739
  }
575
740
  }
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;
741
+ }
742
+ function reportResults(ctx) {
743
+ const { results, errors, skipped, spinner, jsonOnly, selectedSkills, targets } = ctx;
744
+ if (ctx.cleanupMaterialized) {
745
+ ctx.cleanupMaterialized();
746
+ }
747
+ const isMultiSkill = (selectedSkills?.length ?? 0) > 1;
748
+ if (jsonOnly) {
749
+ if (!isMultiSkill && targets.length === 1) {
750
+ if (errors.length) printJson({ ok: false, error: errors[0]?.error || "Install failed." });
751
+ else printJson(results[0] ?? null);
752
+ } else {
753
+ printJson({
754
+ ok: errors.length === 0,
755
+ source: ctx.source,
756
+ resolvedSource: ctx.resolvedSource,
757
+ scope: ctx.scope,
758
+ recursive: isMultiSkill,
759
+ all: targets.length === PLATFORMS2.length,
760
+ skillCount: selectedSkills?.length ?? 0,
761
+ results,
762
+ errors,
763
+ skipped
764
+ });
632
765
  }
633
- recursiveSkillCount = found.length;
634
- return found;
766
+ process.exitCode = errors.length ? 1 : 0;
767
+ return;
635
768
  }
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 || "")}`
769
+ if (errors.length === 0 && (results.length > 0 || skipped.length > 0)) {
770
+ const displayName = results[0]?.canonicalName || results[0]?.name || ctx.source;
771
+ if (skipped.length > 0) {
772
+ spinner?.succeed(
773
+ `Installed ${chalk3.green(results.length)} and skipped ${chalk3.dim(skipped.length)} (already installed) to ${chalk3.dim(`${targets.length} platforms`)}`
750
774
  );
751
775
  } 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`
776
+ spinner?.succeed(
777
+ 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
778
  );
756
- process.exitCode = 1;
757
- if (!effectiveRecursive && targets.length === 1 && errors[0]) console.error(chalk3.red(errors[0].error));
758
779
  }
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;
780
+ } else if (errors.length > 0) {
781
+ const attempted = results.length + errors.length;
782
+ spinner?.fail(
783
+ isMultiSkill ? `Install had failures (${errors.length}/${attempted} installs failed)` : `Failed to install ${chalk3.red(ctx.source)} to ${errors.length}/${targets.length} platforms`
784
+ );
785
+ process.exitCode = 1;
786
+ if (!isMultiSkill && targets.length === 1 && errors[0]) {
787
+ console.error(chalk3.red(errors[0].error));
780
788
  }
781
- if (skipped.length > 0) {
782
- const uniqueSkills = [...new Set(skipped.map((s) => s.skillName))];
783
- console.log(chalk3.dim(`
789
+ }
790
+ if (isMultiSkill || targets.length > 1) {
791
+ for (const r of results.slice(0, 60)) {
792
+ const displayName = r.canonicalName || r.name;
793
+ const suffix = r.hasSkillMd ? chalk3.green("\u2713") : chalk3.yellow("\u26A0");
794
+ console.log(` ${suffix} ${chalk3.cyan(displayName)} \u2192 ${chalk3.dim(r.platform)}`);
795
+ }
796
+ if (results.length > 60) console.log(chalk3.dim(` ... and ${results.length - 60} more`));
797
+ if (errors.length) {
798
+ console.log(chalk3.yellow("\nFailures:"));
799
+ for (const e of errors) console.log(chalk3.yellow(` - ${e.platform}: ${e.error}`));
800
+ }
801
+ process.exitCode = errors.length ? 1 : 0;
802
+ } else if (!isMultiSkill && targets.length === 1 && results[0]) {
803
+ const record = results[0];
804
+ if (record.hasSkillMd) logger.installDetail("SKILL.md found \u2713");
805
+ else logger.installDetail("Warning: No SKILL.md found", true);
806
+ if (record.skill?.validation && !record.skill.validation.ok) {
807
+ logger.installDetail(`Validation: ${chalk3.yellow("failed")} (${record.skill.validation.issues.length} issues)`, true);
808
+ } else if (record.skill?.validation?.ok) {
809
+ logger.installDetail(`Validation: ${chalk3.green("ok")}`);
810
+ }
811
+ }
812
+ if (skipped.length > 0) {
813
+ const uniqueSkills = [...new Set(skipped.map((s) => s.skillName))];
814
+ console.log(chalk3.dim(`
784
815
  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")}`));
816
+ const bySkill = /* @__PURE__ */ new Map();
817
+ for (const s of skipped) {
818
+ const platforms = bySkill.get(s.skillName) || [];
819
+ platforms.push(s.platform);
820
+ bySkill.set(s.skillName, platforms);
821
+ }
822
+ for (const [skillName, platforms] of bySkill.entries()) {
823
+ const platformsStr = platforms.length === PLATFORMS2.length ? "all platforms" : platforms.join(", ");
824
+ console.log(chalk3.dim(` - ${skillName} (${platformsStr})`));
797
825
  }
826
+ console.log(chalk3.dim(`
827
+ To reinstall, use: ${chalk3.cyan("skild install <source> --force")}`));
828
+ }
829
+ }
830
+ async function install(source, options = {}) {
831
+ const ctx = createContext(source, options);
832
+ if (!ctx.jsonOnly) {
833
+ ctx.spinner = createSpinner(`Interpreting ${chalk3.cyan(ctx.source)}...`);
834
+ }
835
+ try {
836
+ if (!await resolveSource(ctx)) return;
837
+ if (!await discoverSkills(ctx)) return;
838
+ if (!await promptSelections(ctx)) return;
839
+ await executeInstalls(ctx);
840
+ reportResults(ctx);
798
841
  } catch (error) {
799
842
  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));
843
+ if (ctx.jsonOnly) printJson({ ok: false, error: message });
844
+ else {
845
+ if (ctx.spinner) ctx.spinner.fail(chalk3.red(message));
846
+ else console.error(chalk3.red(message));
847
+ }
802
848
  process.exitCode = 1;
849
+ if (ctx.cleanupMaterialized) ctx.cleanupMaterialized();
803
850
  }
804
851
  }
805
852
  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.6",
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.6"
42
42
  },
43
43
  "devDependencies": {
44
44
  "@types/node": "^20.10.0",