skild 0.4.4 → 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 +524 -406
  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,57 +99,107 @@ 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
  }
118
- const root = { id: "", name: ".", depth: 0, children: [allNode], leafIndices: allNode.leafIndices.slice(), isLeaf: false };
119
- return root;
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) {
128
+ while (allNode.children.length === 1 && !allNode.children[0].isLeaf && allNode.children[0].children.length > 0) {
129
+ const singleChild = allNode.children[0];
130
+ for (const grandchild of singleChild.children) {
131
+ adjustDepth(grandchild, -1);
132
+ }
133
+ allNode.children = singleChild.children;
134
+ }
135
+ }
136
+ function adjustDepth(node, delta) {
137
+ node.depth += delta;
138
+ for (const child of node.children) {
139
+ adjustDepth(child, delta);
140
+ }
120
141
  }
121
142
  function flattenTree(root) {
122
143
  const result = [];
123
144
  function walk(node) {
124
- if (node.id !== "") {
125
- result.push(node);
126
- }
127
- for (const child of node.children) {
128
- walk(child);
129
- }
130
- }
131
- for (const child of root.children) {
132
- walk(child);
145
+ if (node.id !== "") result.push(node);
146
+ for (const child of node.children) walk(child);
133
147
  }
148
+ for (const child of root.children) walk(child);
134
149
  return result;
135
150
  }
136
- function getNodeState(node, selected) {
151
+ function getNodeSelection(node, selected) {
137
152
  const total = node.leafIndices.length;
138
- if (total === 0) return "none";
139
- let count = 0;
153
+ if (total === 0) return { state: "none", selectedCount: 0 };
154
+ let selectedCount = 0;
140
155
  for (const idx of node.leafIndices) {
141
- if (selected.has(idx)) count++;
156
+ if (selected.has(idx)) selectedCount++;
157
+ }
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;
142
180
  }
143
- if (count === 0) return "none";
144
- if (count === total) return "all";
145
- return "partial";
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);
146
193
  }
147
194
  async function interactiveTreeSelect(items, options) {
148
- const { title, subtitle, buildTree, formatNode, defaultAll } = options;
195
+ const { title, subtitle, buildTree, formatNode, defaultAll, defaultSelected } = options;
149
196
  const root = buildTree(items);
150
197
  const flatNodes = flattenTree(root);
151
198
  if (flatNodes.length === 0) return null;
152
199
  const selected = /* @__PURE__ */ new Set();
153
- if (defaultAll) {
200
+ if (defaultSelected) {
201
+ for (const idx of defaultSelected) selected.add(idx);
202
+ } else if (defaultAll) {
154
203
  for (let i = 0; i < items.length; i++) selected.add(i);
155
204
  }
156
205
  let cursor = 0;
@@ -163,186 +212,161 @@ async function interactiveTreeSelect(items, options) {
163
212
  stdin.setRawMode(true);
164
213
  stdin.resume();
165
214
  readline.emitKeypressEvents(stdin);
166
- function render() {
167
- stdout.write("\x1B[?25l");
168
- const totalLines = flatNodes.length + 4;
169
- stdout.write(`\x1B[${totalLines}A`);
170
- stdout.write("\x1B[0J");
171
- stdout.write(`
172
- ${chalk2.bold.cyan(title)}
173
- `);
174
- stdout.write(`${chalk2.dim(subtitle)}
175
-
176
- `);
177
- for (let i = 0; i < flatNodes.length; i++) {
178
- const node = flatNodes[i];
179
- const state = getNodeState(node, selected);
180
- const isCursor = i === cursor;
181
- stdout.write(formatNode(node, state, isCursor) + "\n");
182
- }
183
- stdout.write("\n");
184
- }
185
- function initialRender() {
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
- initialRender();
215
+ const renderer = createRenderer(title, subtitle, flatNodes, selected, () => cursor, formatNode);
216
+ writeToTerminal(stdout, renderer.renderContent());
201
217
  return new Promise((resolve) => {
202
- 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
+ }
203
224
  stdin.setRawMode(wasRaw);
204
225
  stdin.pause();
205
226
  stdin.removeListener("keypress", onKeypress);
206
227
  stdout.write("\x1B[?25h");
207
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
+ }
208
247
  function onKeypress(_str, key) {
209
248
  if (key.ctrl && key.name === "c") {
210
- cleanup();
249
+ cleanup(true);
211
250
  resolve(null);
212
251
  return;
213
252
  }
214
253
  if (key.name === "return" || key.name === "enter") {
215
- cleanup();
216
- const result = Array.from(selected);
217
- if (result.length === 0) {
218
- resolve(null);
219
- } else {
220
- resolve(result);
221
- }
254
+ cleanup(true);
255
+ resolve(selected.size > 0 ? Array.from(selected) : null);
222
256
  return;
223
257
  }
224
258
  if (key.name === "up") {
225
259
  cursor = (cursor - 1 + flatNodes.length) % flatNodes.length;
226
- render();
260
+ rerender();
227
261
  return;
228
262
  }
229
263
  if (key.name === "down") {
230
264
  cursor = (cursor + 1) % flatNodes.length;
231
- render();
265
+ rerender();
232
266
  return;
233
267
  }
234
268
  if (key.name === "space") {
235
- const node = flatNodes[cursor];
236
- const state = getNodeState(node, selected);
237
- if (state === "all") {
238
- for (const idx of node.leafIndices) {
239
- selected.delete(idx);
240
- }
241
- } else {
242
- for (const idx of node.leafIndices) {
243
- selected.add(idx);
244
- }
245
- }
246
- render();
269
+ toggleNode(flatNodes[cursor]);
270
+ rerender();
247
271
  return;
248
272
  }
249
273
  if (key.name === "a") {
250
- const allSelected = selected.size === items.length;
251
- if (allSelected) {
252
- selected.clear();
253
- } else {
254
- for (let i = 0; i < items.length; i++) selected.add(i);
255
- }
256
- render();
274
+ toggleAll();
275
+ rerender();
257
276
  return;
258
277
  }
259
278
  }
260
279
  stdin.on("keypress", onKeypress);
261
280
  });
262
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
+ }
263
306
  async function promptSkillsInteractive(skills, options = {}) {
264
307
  if (skills.length === 0) return null;
308
+ const targetPlatforms = options.targetPlatforms || [];
309
+ const hasInstalledCheck = targetPlatforms.length > 0;
310
+ const defaultSelected = /* @__PURE__ */ new Set();
311
+ for (let i = 0; i < skills.length; i++) {
312
+ const skill = skills[i];
313
+ const installedOnTargets = skill.installedPlatforms?.filter((p) => targetPlatforms.includes(p)) || [];
314
+ const isFullyInstalled = hasInstalledCheck && installedOnTargets.length === targetPlatforms.length;
315
+ if (options.defaultAll !== false && !isFullyInstalled) {
316
+ defaultSelected.add(i);
317
+ }
318
+ }
265
319
  const selectedIndices = await interactiveTreeSelect(skills, {
266
320
  title: "Select skills to install",
267
321
  subtitle: "\u2191\u2193 navigate \u2022 Space toggle \u2022 Enter confirm",
268
322
  buildTree: buildSkillTree,
269
- formatNode: (node, state, isCursor) => {
270
- const indent = " ".repeat(node.depth - 1);
271
- const checkbox = state === "all" ? chalk2.green("\u25CF") : state === "partial" ? chalk2.yellow("\u25D0") : chalk2.dim("\u25CB");
272
- const name = isCursor ? chalk2.cyan.underline(node.name) : node.name;
273
- const cursor = isCursor ? chalk2.cyan("\u203A ") : " ";
274
- const count = node.leafIndices.length > 1 ? chalk2.dim(` (${node.leafIndices.length})`) : "";
275
- let hint = "";
276
- if (isCursor && node.leafIndices.length > 0) {
277
- hint = state === "all" ? chalk2.dim(" \u2190 Space to deselect") : chalk2.dim(" \u2190 Space to select");
323
+ formatNode: (node, selection, isCursor) => {
324
+ let suffix = "";
325
+ if (node.isLeaf && node.leafIndices.length === 1) {
326
+ const skill = skills[node.leafIndices[0]];
327
+ if (skill?.installedPlatforms?.length) {
328
+ if (skill.installedPlatforms.length === targetPlatforms.length && targetPlatforms.length > 0) {
329
+ suffix = chalk2.dim(" [installed]");
330
+ } else if (skill.installedPlatforms.length > 0) {
331
+ suffix = chalk2.dim(` [installed on ${skill.installedPlatforms.length}]`);
332
+ }
333
+ }
278
334
  }
279
- return `${cursor}${indent}${checkbox} ${name}${count}${hint}`;
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");
338
+ }
339
+ return formatted;
280
340
  },
281
- defaultAll: options.defaultAll !== false
341
+ defaultAll: false,
342
+ defaultSelected
282
343
  });
283
- if (!selectedIndices || selectedIndices.length === 0) {
284
- return null;
285
- }
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);
286
347
  console.log(chalk2.green(`
287
- \u2713 ${selectedIndices.length} skill${selectedIndices.length > 1 ? "s" : ""} selected
348
+ \u2713 Selected ${selectedSkills.length} skill${selectedSkills.length > 1 ? "s" : ""}: ${chalk2.cyan(names.join(", "))}
288
349
  `));
289
- return selectedIndices.map((i) => skills[i]);
350
+ return selectedSkills;
290
351
  }
291
352
  async function promptPlatformsInteractive(options = {}) {
292
353
  const platformItems = PLATFORMS.map((p) => ({ platform: p }));
293
354
  const selectedIndices = await interactiveTreeSelect(platformItems, {
294
355
  title: "Select target platforms",
295
356
  subtitle: "\u2191\u2193 navigate \u2022 Space toggle \u2022 Enter confirm",
296
- buildTree: (items) => {
297
- const allNode = {
298
- id: "all",
299
- name: "All Platforms",
300
- depth: 1,
301
- children: [],
302
- leafIndices: [],
303
- isLeaf: false
304
- };
305
- for (let i = 0; i < items.length; i++) {
306
- const p = items[i].platform;
307
- allNode.children.push({
308
- id: p,
309
- name: p,
310
- depth: 2,
311
- children: [],
312
- leafIndices: [i],
313
- isLeaf: true
314
- });
315
- allNode.leafIndices.push(i);
316
- }
317
- const root = { id: "", name: ".", depth: 0, children: [allNode], leafIndices: allNode.leafIndices.slice(), isLeaf: false };
318
- return root;
319
- },
320
- formatNode: (node, state, isCursor) => {
321
- const indent = " ".repeat(node.depth - 1);
322
- const checkbox = state === "all" ? chalk2.green("\u25CF") : state === "partial" ? chalk2.yellow("\u25D0") : chalk2.dim("\u25CB");
323
- let displayName = node.name;
324
- if (node.name !== "All Platforms") {
325
- const platform = node.name;
326
- const display = PLATFORM_DISPLAY[platform];
327
- if (display) displayName = display.name;
328
- }
329
- const name = isCursor ? chalk2.cyan.underline(displayName) : displayName;
330
- const cursor = isCursor ? chalk2.cyan("\u203A ") : " ";
331
- const count = node.leafIndices.length > 1 ? chalk2.dim(` (${node.leafIndices.length})`) : "";
332
- let hint = "";
333
- if (isCursor && node.leafIndices.length > 0) {
334
- hint = state === "all" ? chalk2.dim(" \u2190 Space to deselect") : chalk2.dim(" \u2190 Space to select");
335
- }
336
- 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);
337
362
  },
338
363
  defaultAll: options.defaultAll !== false
339
364
  });
340
- if (!selectedIndices || selectedIndices.length === 0) {
341
- return null;
342
- }
365
+ if (!selectedIndices) return null;
343
366
  const selected = selectedIndices.map((i) => PLATFORMS[i]);
367
+ const names = selected.map((p) => PLATFORM_DISPLAY[p] || p);
344
368
  console.log(chalk2.green(`
345
- \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(", "))}
346
370
  `));
347
371
  return selected;
348
372
  }
@@ -448,287 +472,381 @@ function asDiscoveredSkills(discovered, toSuggestedSource, toMaterializedDir) {
448
472
  materializedDir: toMaterializedDir ? toMaterializedDir(d) : void 0
449
473
  }));
450
474
  }
451
- async function install(source, options = {}) {
475
+ function createContext(source, options) {
452
476
  const scope = options.local ? "project" : "global";
453
477
  const auth = loadRegistryAuth();
454
- const registryUrlForDeps = options.registry || auth?.registryUrl;
478
+ const registryUrl = options.registry || auth?.registryUrl;
455
479
  const jsonOnly = Boolean(options.json);
456
- const recursive = Boolean(options.recursive);
457
480
  const yes = Boolean(options.yes);
458
- const maxDepth = parsePositiveInt(options.depth, 6);
459
- const maxSkills = parsePositiveInt(options.maxSkills, 200);
460
481
  const interactive = Boolean(process.stdin.isTTY && process.stdout.isTTY) && !jsonOnly;
461
- const requestedAll = Boolean(options.all);
462
- const requestedTarget = options.target;
463
- if (requestedAll && options.target) {
464
- const message = "Invalid options: use either --all or --target, not both.";
465
- if (jsonOnly) printJson({ ok: false, error: message });
466
- else console.error(chalk3.red(message));
467
- process.exitCode = 1;
468
- return;
469
- }
470
482
  let targets = [];
471
- let deferPlatformSelection = false;
472
- if (requestedAll) {
483
+ let needsPlatformPrompt = false;
484
+ if (options.all) {
473
485
  targets = [...PLATFORMS2];
474
- } else if (requestedTarget) {
475
- targets = [requestedTarget];
486
+ } else if (options.target) {
487
+ targets = [options.target];
476
488
  } else if (yes) {
477
489
  targets = [...PLATFORMS2];
478
490
  } else if (interactive) {
479
- deferPlatformSelection = true;
491
+ needsPlatformPrompt = true;
480
492
  targets = [...PLATFORMS2];
481
493
  } else {
482
494
  targets = ["claude"];
483
495
  }
484
- const allPlatformsSelected = targets.length === PLATFORMS2.length;
485
- 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
+ }
486
529
  try {
487
- if (looksLikeAlias(resolvedSource)) {
488
- const registryUrl = resolveRegistryUrl(registryUrlForDeps);
489
- const resolved = await resolveRegistryAlias(registryUrl, resolvedSource);
490
- if (!jsonOnly) logger.info(`Resolved ${chalk3.cyan(resolvedSource)} \u2192 ${chalk3.cyan(resolved.spec)} (${resolved.type})`);
491
- resolvedSource = resolved.spec;
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;
492
538
  }
493
539
  } catch (error) {
494
540
  const message = error instanceof SkildError ? error.message : error instanceof Error ? error.message : String(error);
495
- if (jsonOnly) printJson({ ok: false, error: message });
541
+ if (ctx.jsonOnly) printJson({ ok: false, error: message });
496
542
  else console.error(chalk3.red(message));
497
543
  process.exitCode = 1;
498
- return;
544
+ return false;
499
545
  }
500
- const results = [];
501
- const errors = [];
502
- let effectiveRecursive = recursive;
503
- let recursiveSkillCount = null;
504
- async function installOne(inputSource, materializedDir) {
505
- for (const targetPlatform of targets) {
506
- try {
507
- const record = inputSource.startsWith("@") && inputSource.includes("/") ? await installRegistrySkill(
508
- { spec: inputSource, registryUrl: registryUrlForDeps },
509
- { platform: targetPlatform, scope, force: Boolean(options.force) }
510
- ) : await installSkill(
511
- { source: inputSource, materializedDir },
512
- { platform: targetPlatform, scope, force: Boolean(options.force), registryUrl: registryUrlForDeps }
513
- );
514
- results.push(record);
515
- void reportDownload(record, registryUrlForDeps);
516
- } catch (error) {
517
- const message = error instanceof SkildError ? error.message : error instanceof Error ? error.message : String(error);
518
- errors.push({ platform: targetPlatform, error: message, inputSource });
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
+ }
553
+ try {
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;
519
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;
520
582
  }
521
- }
522
- async function resolveDiscoveredSelection(found, spinner) {
523
- if (found.length === 0) return null;
524
- if (found.length > maxSkills) {
525
- const message = `Found more than ${maxSkills} skills. Increase --max-skills to proceed.`;
526
- if (jsonOnly) printJson({ ok: false, error: "TOO_MANY_SKILLS", message, source, resolvedSource, maxSkills });
527
- else console.error(chalk3.red(message));
528
- process.exitCode = 1;
529
- return null;
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;
530
591
  }
531
- if (!effectiveRecursive) {
592
+ const discovered = discoverSkillDirsWithHeuristics(ctx.materializedDir, { maxDepth, maxSkills });
593
+ if (discovered.length === 0) {
594
+ const message = `No SKILL.md found in source "${resolvedSource}".`;
532
595
  if (jsonOnly) {
533
- const foundOutput = found.map(({ relPath, suggestedSource }) => ({ relPath, suggestedSource }));
534
- printJson({
535
- ok: false,
536
- error: "MULTI_SKILL_SOURCE",
537
- message: "Source is not a skill root (missing SKILL.md). Multiple skills were found.",
538
- source,
539
- resolvedSource,
540
- found: foundOutput
541
- });
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;
603
+ }
604
+ ctx.discoveredSkills = asDiscoveredSkills(
605
+ discovered,
606
+ (d) => deriveChildSource(resolvedSource, d.relPath),
607
+ (d) => d.absDir
608
+ );
609
+ return true;
610
+ } catch (error) {
611
+ const message = error instanceof SkildError ? error.message : error instanceof Error ? error.message : String(error);
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."));
542
629
  process.exitCode = 1;
543
- return null;
630
+ return false;
544
631
  }
545
- if (spinner) spinner.stop();
546
- const headline = found.length === 1 ? `No SKILL.md found at root. Found 1 skill.
547
- ` : `No SKILL.md found at root. Found ${found.length} skills.
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 });
643
+ else console.error(chalk3.red(message));
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.
548
677
  `;
549
678
  console.log(chalk3.yellow(headline));
550
- if (!interactive && !yes) {
551
- console.log(chalk3.dim(`Tip: rerun with ${chalk3.cyan("skild install <source> --recursive")} to install all.`));
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."));
552
691
  process.exitCode = 1;
553
- return null;
554
- }
555
- if (!yes) {
556
- const selected = await promptSkillsInteractive(found, { defaultAll: true });
557
- if (!selected) {
558
- console.log(chalk3.red("No skills selected."));
559
- process.exitCode = 1;
560
- return null;
561
- }
562
- if (deferPlatformSelection) {
563
- const selectedPlatforms = await promptPlatformsInteractive({ defaultAll: true });
564
- if (!selectedPlatforms) {
565
- console.log(chalk3.red("No platforms selected."));
566
- process.exitCode = 1;
567
- return null;
568
- }
569
- targets = selectedPlatforms;
570
- deferPlatformSelection = false;
571
- }
572
- effectiveRecursive = true;
573
- if (spinner) spinner.start();
574
- recursiveSkillCount = selected.length;
575
- return selected;
692
+ return false;
576
693
  }
577
- effectiveRecursive = true;
694
+ ctx.targets = selectedPlatforms;
695
+ ctx.needsPlatformPrompt = false;
578
696
  }
579
- recursiveSkillCount = found.length;
580
- return found;
697
+ if (ctx.spinner) ctx.spinner.start();
698
+ } else {
699
+ ctx.selectedSkills = discoveredSkills;
581
700
  }
582
- try {
583
- const spinner = jsonOnly ? null : createSpinner(
584
- 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})...`
585
- );
586
- let cleanupMaterialized = null;
587
- let materializedRoot = null;
588
- try {
589
- async function ensurePlatformSelection() {
590
- if (!deferPlatformSelection) return true;
591
- const selectedPlatforms = await promptPlatformsInteractive({ defaultAll: true });
592
- if (!selectedPlatforms) {
593
- console.log(chalk3.red("No platforms selected."));
594
- process.exitCode = 1;
595
- return false;
596
- }
597
- targets = selectedPlatforms;
598
- deferPlatformSelection = false;
599
- return true;
600
- }
601
- if (resolvedSource.startsWith("@") && resolvedSource.includes("/")) {
602
- if (!await ensurePlatformSelection()) return;
603
- await installOne(resolvedSource);
604
- } else {
605
- const maybeLocalRoot = path2.resolve(resolvedSource);
606
- const isLocal = fs2.existsSync(maybeLocalRoot);
607
- if (isLocal) {
608
- const hasSkillMd = fs2.existsSync(path2.join(maybeLocalRoot, "SKILL.md"));
609
- if (hasSkillMd) {
610
- if (!await ensurePlatformSelection()) return;
611
- await installOne(resolvedSource);
612
- } else {
613
- const discovered = discoverSkillDirsWithHeuristics(maybeLocalRoot, { maxDepth, maxSkills });
614
- if (discovered.length === 0) {
615
- const message = `No SKILL.md found at ${maybeLocalRoot} (or within subdirectories).`;
616
- if (jsonOnly) {
617
- printJson({ ok: false, error: "SKILL_MD_NOT_FOUND", message, source, resolvedSource });
618
- } else {
619
- if (spinner) spinner.stop();
620
- console.error(chalk3.red(message));
621
- }
622
- process.exitCode = 1;
623
- return;
624
- }
625
- const found = asDiscoveredSkills(discovered, (d) => path2.join(maybeLocalRoot, d.relPath));
626
- const selected = await resolveDiscoveredSelection(found, spinner);
627
- if (!selected) return;
628
- if (spinner) spinner.text = `Installing ${chalk3.cyan(source)} \u2014 discovered ${selected.length} skills...`;
629
- for (const skill of selected) {
630
- if (spinner) spinner.text = `Installing ${chalk3.cyan(skill.relPath)} (${scope})...`;
631
- await installOne(skill.suggestedSource, skill.materializedDir);
632
- }
633
- }
634
- } else {
635
- const materialized = await materializeSourceToTemp(resolvedSource);
636
- cleanupMaterialized = materialized.cleanup;
637
- materializedRoot = materialized.dir;
638
- const hasSkillMd = fs2.existsSync(path2.join(materializedRoot, "SKILL.md"));
639
- if (hasSkillMd) {
640
- await installOne(resolvedSource, materializedRoot);
641
- } else {
642
- const discovered = discoverSkillDirsWithHeuristics(materializedRoot, { maxDepth, maxSkills });
643
- if (discovered.length === 0) {
644
- const message = `No SKILL.md found in source "${resolvedSource}".`;
645
- if (jsonOnly) {
646
- printJson({ ok: false, error: "SKILL_MD_NOT_FOUND", message, source, resolvedSource });
647
- } else {
648
- if (spinner) spinner.stop();
649
- console.error(chalk3.red(message));
650
- }
651
- process.exitCode = 1;
652
- return;
653
- }
654
- const found = asDiscoveredSkills(
655
- discovered,
656
- (d) => deriveChildSource(resolvedSource, d.relPath),
657
- (d) => d.absDir
658
- );
659
- const selected = await resolveDiscoveredSelection(found, spinner);
660
- if (!selected) return;
661
- if (spinner) spinner.text = `Installing ${chalk3.cyan(source)} \u2014 discovered ${selected.length} skills...`;
662
- for (const skill of selected) {
663
- if (spinner) spinner.text = `Installing ${chalk3.cyan(skill.relPath)} (${scope})...`;
664
- await installOne(skill.suggestedSource, skill.materializedDir);
665
- }
666
- }
701
+ return true;
702
+ }
703
+ async function executeInstalls(ctx) {
704
+ const { selectedSkills, targets, scope, forceOverwrite, registryUrl, spinner } = ctx;
705
+ if (!selectedSkills || selectedSkills.length === 0) {
706
+ return;
707
+ }
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) {
716
+ try {
717
+ const record = skill.suggestedSource.startsWith("@") && skill.suggestedSource.includes("/") ? await installRegistrySkill(
718
+ { spec: skill.suggestedSource, registryUrl },
719
+ { platform, scope, force: forceOverwrite }
720
+ ) : await installSkill(
721
+ { source: skill.suggestedSource, materializedDir: skill.materializedDir },
722
+ { platform, scope, force: forceOverwrite, registryUrl }
723
+ );
724
+ ctx.results.push(record);
725
+ void reportDownload(record, registryUrl);
726
+ } catch (error) {
727
+ if (error instanceof SkildError && error.code === "ALREADY_INSTALLED") {
728
+ const details = error.details;
729
+ ctx.skipped.push({
730
+ skillName: details?.skillName || skill.suggestedSource,
731
+ platform,
732
+ installDir: details?.installDir || ""
733
+ });
734
+ continue;
667
735
  }
736
+ const message = error instanceof SkildError ? error.message : error instanceof Error ? error.message : String(error);
737
+ ctx.errors.push({ platform, error: message, inputSource: skill.suggestedSource });
668
738
  }
669
- } finally {
670
- if (cleanupMaterialized) cleanupMaterialized();
671
739
  }
672
- if (jsonOnly) {
673
- if (!effectiveRecursive && targets.length === 1) {
674
- if (errors.length) printJson({ ok: false, error: errors[0]?.error || "Install failed." });
675
- else printJson(results[0] ?? null);
676
- } else {
677
- printJson({
678
- ok: errors.length === 0,
679
- source,
680
- resolvedSource,
681
- scope,
682
- recursive: effectiveRecursive,
683
- all: allPlatformsSelected,
684
- recursiveSkillCount,
685
- results,
686
- errors
687
- });
688
- }
689
- process.exitCode = errors.length ? 1 : 0;
690
- return;
740
+ }
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
+ });
691
765
  }
692
- if (errors.length === 0) {
693
- const displayName = results[0]?.canonicalName || results[0]?.name || source;
694
- spinner.succeed(
695
- 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 || "")}`
766
+ process.exitCode = errors.length ? 1 : 0;
767
+ return;
768
+ }
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`)}`
696
774
  );
697
775
  } else {
698
- const attempted = results.length + errors.length;
699
- spinner.fail(
700
- 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 || "")}`
701
778
  );
702
- process.exitCode = 1;
703
- if (!effectiveRecursive && targets.length === 1 && errors[0]) console.error(chalk3.red(errors[0].error));
704
779
  }
705
- if (!effectiveRecursive && targets.length === 1 && results[0]) {
706
- const record = results[0];
707
- if (record.hasSkillMd) logger.installDetail("SKILL.md found \u2713");
708
- else logger.installDetail("Warning: No SKILL.md found", true);
709
- if (record.skill?.validation && !record.skill.validation.ok) {
710
- logger.installDetail(`Validation: ${chalk3.yellow("failed")} (${record.skill.validation.issues.length} issues)`, true);
711
- } else if (record.skill?.validation?.ok) {
712
- logger.installDetail(`Validation: ${chalk3.green("ok")}`);
713
- }
714
- } else if (effectiveRecursive || targets.length > 1) {
715
- for (const r of results.slice(0, 60)) {
716
- const displayName = r.canonicalName || r.name;
717
- const suffix = r.hasSkillMd ? chalk3.green("\u2713") : chalk3.yellow("\u26A0");
718
- console.log(` ${suffix} ${chalk3.cyan(displayName)} \u2192 ${chalk3.dim(r.platform)}`);
719
- }
720
- if (results.length > 60) console.log(chalk3.dim(` ... and ${results.length - 60} more`));
721
- if (errors.length) {
722
- console.log(chalk3.yellow("\nFailures:"));
723
- for (const e of errors) console.log(chalk3.yellow(` - ${e.platform}: ${e.error}`));
724
- }
725
- 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));
788
+ }
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")}`);
726
810
  }
811
+ }
812
+ if (skipped.length > 0) {
813
+ const uniqueSkills = [...new Set(skipped.map((s) => s.skillName))];
814
+ console.log(chalk3.dim(`
815
+ Skipped ${skipped.length} already installed (${uniqueSkills.length} skill${uniqueSkills.length > 1 ? "s" : ""}):`));
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})`));
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);
727
841
  } catch (error) {
728
842
  const message = error instanceof SkildError ? error.message : error instanceof Error ? error.message : String(error);
729
- if (jsonOnly) printJson({ ok: false, error: message });
730
- 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
+ }
731
848
  process.exitCode = 1;
849
+ if (ctx.cleanupMaterialized) ctx.cleanupMaterialized();
732
850
  }
733
851
  }
734
852
  async function reportDownload(record, registryOverride) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "skild",
3
- "version": "0.4.4",
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.4"
41
+ "@skild/core": "^0.4.6"
42
42
  },
43
43
  "devDependencies": {
44
44
  "@types/node": "^20.10.0",