skild 0.4.1 → 0.4.3

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 +287 -47
  2. package/package.json +2 -2
package/dist/index.js CHANGED
@@ -128,16 +128,6 @@ async function promptPassword(question) {
128
128
  stdin.on("keypress", onKeypress);
129
129
  });
130
130
  }
131
- async function promptConfirm(question, options = {}) {
132
- const defaultValue = options.defaultValue ?? false;
133
- const suffix = defaultValue ? " (Y/n)" : " (y/N)";
134
- const answer = await promptLine(`${question}${suffix}`);
135
- const v = answer.trim().toLowerCase();
136
- if (!v) return defaultValue;
137
- if (v === "y" || v === "yes") return true;
138
- if (v === "n" || v === "no") return false;
139
- return defaultValue;
140
- }
141
131
 
142
132
  // src/commands/install-discovery.ts
143
133
  import fs from "fs";
@@ -233,10 +223,6 @@ function printJson(value) {
233
223
  process.stdout.write(`${JSON.stringify(value, null, 2)}
234
224
  `);
235
225
  }
236
- function previewDiscovered(found, limit = 12) {
237
- const preview = found.slice(0, limit).map((s) => ` - ${s.relPath}`).join("\n");
238
- return `${preview}${found.length > limit ? "\n ..." : ""}`;
239
- }
240
226
  function asDiscoveredSkills(discovered, toSuggestedSource, toMaterializedDir) {
241
227
  return discovered.map((d) => ({
242
228
  relPath: d.relPath,
@@ -244,24 +230,258 @@ function asDiscoveredSkills(discovered, toSuggestedSource, toMaterializedDir) {
244
230
  materializedDir: toMaterializedDir ? toMaterializedDir(d) : void 0
245
231
  }));
246
232
  }
233
+ function buildSkillTree(found) {
234
+ const root = { id: "", name: ".", children: [], leafIndices: [] };
235
+ const byId = /* @__PURE__ */ new Map([["", root]]);
236
+ for (let i = 0; i < found.length; i += 1) {
237
+ const relPath = found[i].relPath;
238
+ const parts = relPath === "." ? ["."] : relPath.split("/").filter(Boolean);
239
+ let currentId = "";
240
+ let current = root;
241
+ current.leafIndices.push(i);
242
+ for (const part of parts) {
243
+ const nextId = currentId ? `${currentId}/${part}` : part;
244
+ let node = byId.get(nextId);
245
+ if (!node) {
246
+ node = { id: nextId, name: part, children: [], leafIndices: [] };
247
+ byId.set(nextId, node);
248
+ current.children.push(node);
249
+ }
250
+ node.leafIndices.push(i);
251
+ current = node;
252
+ currentId = nextId;
253
+ }
254
+ }
255
+ return root;
256
+ }
257
+ function getNodeState(node, selected) {
258
+ const total = node.leafIndices.length;
259
+ if (total === 0) return "none";
260
+ let selectedCount = 0;
261
+ for (const idx of node.leafIndices) {
262
+ if (selected.has(idx)) selectedCount += 1;
263
+ }
264
+ if (selectedCount === 0) return "none";
265
+ if (selectedCount === total) return "all";
266
+ return "partial";
267
+ }
268
+ function renderSkillTree(root, selected) {
269
+ const items = [];
270
+ const stack = [];
271
+ for (let i = root.children.length - 1; i >= 0; i -= 1) {
272
+ stack.push({ node: root.children[i], depth: 0 });
273
+ }
274
+ while (stack.length) {
275
+ const current = stack.pop();
276
+ items.push(current);
277
+ const children = current.node.children;
278
+ for (let i = children.length - 1; i >= 0; i -= 1) {
279
+ stack.push({ node: children[i], depth: current.depth + 1 });
280
+ }
281
+ }
282
+ return items;
283
+ }
284
+ function parseSelectionInput(input, maxIndex) {
285
+ const trimmed = input.trim().toLowerCase();
286
+ if (!trimmed) return { command: "done" };
287
+ if (["q", "quit", "exit", "cancel"].includes(trimmed)) return { command: "cancel" };
288
+ if (["a", "all"].includes(trimmed)) return { command: "all" };
289
+ if (["n", "none"].includes(trimmed)) return { command: "none" };
290
+ if (["i", "invert"].includes(trimmed)) return { command: "invert" };
291
+ const tokens = trimmed.split(/[,\s]+/).filter(Boolean);
292
+ const indices = [];
293
+ for (const token of tokens) {
294
+ if (/^\d+$/.test(token)) {
295
+ const value = Number(token);
296
+ if (value < 1 || value > maxIndex) return null;
297
+ indices.push(value);
298
+ continue;
299
+ }
300
+ if (/^\d+-\d+$/.test(token)) {
301
+ const [startRaw, endRaw] = token.split("-");
302
+ const start = Number(startRaw);
303
+ const end = Number(endRaw);
304
+ if (!Number.isFinite(start) || !Number.isFinite(end) || start < 1 || end < 1 || start > maxIndex || end > maxIndex) {
305
+ return null;
306
+ }
307
+ const from = Math.min(start, end);
308
+ const to = Math.max(start, end);
309
+ for (let i = from; i <= to; i += 1) indices.push(i);
310
+ continue;
311
+ }
312
+ return null;
313
+ }
314
+ return { indices };
315
+ }
316
+ async function promptSkillSelection(found, options) {
317
+ const root = buildSkillTree(found);
318
+ const selected = /* @__PURE__ */ new Set();
319
+ if (options.defaultAll) {
320
+ for (let i = 0; i < found.length; i += 1) selected.add(i);
321
+ }
322
+ while (true) {
323
+ console.log(chalk2.cyan("\nSelect skills to install"));
324
+ console.log(chalk2.dim("Toggle by number. Commands: a=all, n=none, i=invert, enter=continue, q=cancel."));
325
+ const display = renderSkillTree(root, selected);
326
+ for (let i = 0; i < display.length; i += 1) {
327
+ const item = display[i];
328
+ const state = getNodeState(item.node, selected);
329
+ const box = state === "all" ? "[x]" : state === "partial" ? "[-]" : "[ ]";
330
+ const indent = " ".repeat(item.depth);
331
+ const idx = String(i + 1).padStart(2, " ");
332
+ console.log(`${idx} ${box} ${indent}${item.node.name}`);
333
+ }
334
+ const answer = await promptLine("Selection");
335
+ const parsed = parseSelectionInput(answer, display.length);
336
+ if (!parsed) {
337
+ console.log(chalk2.yellow('Invalid selection. Use numbers like "1,3-5" or a/n/i.'));
338
+ continue;
339
+ }
340
+ if (parsed.command === "done") break;
341
+ if (parsed.command === "cancel") return null;
342
+ if (parsed.command === "all") {
343
+ selected.clear();
344
+ for (let i = 0; i < found.length; i += 1) selected.add(i);
345
+ continue;
346
+ }
347
+ if (parsed.command === "none") {
348
+ selected.clear();
349
+ continue;
350
+ }
351
+ if (parsed.command === "invert") {
352
+ const next = /* @__PURE__ */ new Set();
353
+ for (let i = 0; i < found.length; i += 1) {
354
+ if (!selected.has(i)) next.add(i);
355
+ }
356
+ selected.clear();
357
+ for (const idx of next) selected.add(idx);
358
+ continue;
359
+ }
360
+ if (parsed.indices) {
361
+ for (const index of parsed.indices) {
362
+ const item = display[index - 1];
363
+ if (!item) continue;
364
+ const state = getNodeState(item.node, selected);
365
+ if (state === "all") {
366
+ for (const leaf of item.node.leafIndices) selected.delete(leaf);
367
+ } else {
368
+ for (const leaf of item.node.leafIndices) selected.add(leaf);
369
+ }
370
+ }
371
+ }
372
+ }
373
+ if (selected.size === 0) return null;
374
+ const selectedFound = [];
375
+ for (let i = 0; i < found.length; i += 1) {
376
+ if (selected.has(i)) selectedFound.push(found[i]);
377
+ }
378
+ return selectedFound;
379
+ }
380
+ async function promptPlatformSelection(defaultAll) {
381
+ const selected = /* @__PURE__ */ new Set();
382
+ if (defaultAll) {
383
+ for (let i = 0; i < PLATFORMS.length; i += 1) selected.add(i);
384
+ }
385
+ while (true) {
386
+ console.log(chalk2.cyan("\nSelect platforms to install to"));
387
+ console.log(chalk2.dim("Toggle by number. Commands: a=all, n=none, i=invert, enter=continue, q=cancel."));
388
+ for (let i = 0; i < PLATFORMS.length; i += 1) {
389
+ const label = PLATFORMS[i];
390
+ const box = selected.has(i) ? "[x]" : "[ ]";
391
+ const idx = String(i + 1).padStart(2, " ");
392
+ console.log(`${idx} ${box} ${label}`);
393
+ }
394
+ const answer = await promptLine("Selection");
395
+ const parsed = parseSelectionInput(answer, PLATFORMS.length);
396
+ if (!parsed) {
397
+ console.log(chalk2.yellow('Invalid selection. Use numbers like "1,3-5" or a/n/i.'));
398
+ continue;
399
+ }
400
+ if (parsed.command === "done") break;
401
+ if (parsed.command === "cancel") return null;
402
+ if (parsed.command === "all") {
403
+ selected.clear();
404
+ for (let i = 0; i < PLATFORMS.length; i += 1) selected.add(i);
405
+ continue;
406
+ }
407
+ if (parsed.command === "none") {
408
+ selected.clear();
409
+ continue;
410
+ }
411
+ if (parsed.command === "invert") {
412
+ const next = /* @__PURE__ */ new Set();
413
+ for (let i = 0; i < PLATFORMS.length; i += 1) {
414
+ if (!selected.has(i)) next.add(i);
415
+ }
416
+ selected.clear();
417
+ for (const idx of next) selected.add(idx);
418
+ continue;
419
+ }
420
+ if (parsed.indices) {
421
+ for (const index of parsed.indices) {
422
+ const idx = index - 1;
423
+ if (idx < 0 || idx >= PLATFORMS.length) continue;
424
+ if (selected.has(idx)) selected.delete(idx);
425
+ else selected.add(idx);
426
+ }
427
+ }
428
+ }
429
+ if (selected.size === 0) return null;
430
+ const chosen = [];
431
+ for (let i = 0; i < PLATFORMS.length; i += 1) {
432
+ if (selected.has(i)) chosen.push(PLATFORMS[i]);
433
+ }
434
+ return chosen;
435
+ }
436
+ function printSelectedSkills(found) {
437
+ const names = found.map((skill) => skill.relPath);
438
+ if (names.length === 0) return;
439
+ const preview = names.slice(0, 20).map((name) => ` - ${name}`).join("\n");
440
+ console.log(chalk2.dim(`
441
+ Selected skills (${names.length}):`));
442
+ console.log(preview);
443
+ if (names.length > 20) {
444
+ console.log(chalk2.dim(` ... and ${names.length - 20} more`));
445
+ }
446
+ }
247
447
  async function install(source, options = {}) {
248
448
  const scope = options.local ? "project" : "global";
249
449
  const auth = loadRegistryAuth();
250
450
  const registryUrlForDeps = options.registry || auth?.registryUrl;
251
- const all = Boolean(options.all);
252
451
  const jsonOnly = Boolean(options.json);
253
452
  const recursive = Boolean(options.recursive);
254
453
  const yes = Boolean(options.yes);
255
454
  const maxDepth = parsePositiveInt(options.depth, 6);
256
455
  const maxSkills = parsePositiveInt(options.maxSkills, 200);
257
- const platform = options.target || "claude";
258
- if (all && options.target) {
456
+ const interactive = Boolean(process.stdin.isTTY && process.stdout.isTTY) && !jsonOnly;
457
+ const requestedAll = Boolean(options.all);
458
+ const requestedTarget = options.target;
459
+ if (requestedAll && options.target) {
259
460
  const message = "Invalid options: use either --all or --target, not both.";
260
461
  if (jsonOnly) printJson({ ok: false, error: message });
261
462
  else console.error(chalk2.red(message));
262
463
  process.exitCode = 1;
263
464
  return;
264
465
  }
466
+ let targets = [];
467
+ if (requestedAll) {
468
+ targets = [...PLATFORMS];
469
+ } else if (requestedTarget) {
470
+ targets = [requestedTarget];
471
+ } else if (yes) {
472
+ targets = [...PLATFORMS];
473
+ } else if (interactive) {
474
+ const selectedTargets = await promptPlatformSelection(true);
475
+ if (!selectedTargets) {
476
+ console.error(chalk2.red("No platforms selected."));
477
+ process.exitCode = 1;
478
+ return;
479
+ }
480
+ targets = selectedTargets;
481
+ } else {
482
+ targets = ["claude"];
483
+ }
484
+ const allPlatformsSelected = targets.length === PLATFORMS.length;
265
485
  let resolvedSource = source.trim();
266
486
  try {
267
487
  if (looksLikeAlias(resolvedSource)) {
@@ -277,12 +497,10 @@ async function install(source, options = {}) {
277
497
  process.exitCode = 1;
278
498
  return;
279
499
  }
280
- const targets = all ? [...PLATFORMS] : [platform];
281
500
  const results = [];
282
501
  const errors = [];
283
502
  let effectiveRecursive = recursive;
284
503
  let recursiveSkillCount = null;
285
- const interactive = Boolean(process.stdin.isTTY && process.stdout.isTTY) && !jsonOnly;
286
504
  async function installOne(inputSource, materializedDir) {
287
505
  for (const targetPlatform of targets) {
288
506
  try {
@@ -301,14 +519,14 @@ async function install(source, options = {}) {
301
519
  }
302
520
  }
303
521
  }
304
- async function maybeEnableRecursiveAndInstall(found) {
305
- if (found.length === 0) return false;
522
+ async function resolveDiscoveredSelection(found, spinner) {
523
+ if (found.length === 0) return null;
306
524
  if (found.length > maxSkills) {
307
525
  const message = `Found more than ${maxSkills} skills. Increase --max-skills to proceed.`;
308
526
  if (jsonOnly) printJson({ ok: false, error: "TOO_MANY_SKILLS", message, source, resolvedSource, maxSkills });
309
527
  else console.error(chalk2.red(message));
310
528
  process.exitCode = 1;
311
- return true;
529
+ return null;
312
530
  }
313
531
  if (!effectiveRecursive) {
314
532
  if (jsonOnly) {
@@ -322,29 +540,39 @@ async function install(source, options = {}) {
322
540
  found: foundOutput
323
541
  });
324
542
  process.exitCode = 1;
325
- return true;
543
+ return null;
326
544
  }
327
- const headline = found.length === 1 ? `No SKILL.md found at root. Found 1 skill:
328
- ${previewDiscovered(found)}
329
- ` : `No SKILL.md found at root. Found ${found.length} skills:
330
- ${previewDiscovered(found)}
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.
331
548
  `;
332
549
  console.log(chalk2.yellow(headline));
333
- const question = found.length === 1 ? "Install the discovered skill?" : "Install all discovered skills?";
334
- const confirm = yes || interactive && await promptConfirm(question, { defaultValue: found.length === 1 });
335
- if (!confirm) {
550
+ if (!interactive && !yes) {
336
551
  console.log(chalk2.dim(`Tip: rerun with ${chalk2.cyan("skild install <source> --recursive")} to install all.`));
337
552
  process.exitCode = 1;
338
- return true;
553
+ return null;
554
+ }
555
+ if (!yes) {
556
+ const selected = await promptSkillSelection(found, { defaultAll: true });
557
+ if (!selected) {
558
+ console.log(chalk2.red("No skills selected."));
559
+ process.exitCode = 1;
560
+ return null;
561
+ }
562
+ printSelectedSkills(selected);
563
+ effectiveRecursive = true;
564
+ if (spinner) spinner.start();
565
+ recursiveSkillCount = selected.length;
566
+ return selected;
339
567
  }
340
568
  effectiveRecursive = true;
341
569
  }
342
570
  recursiveSkillCount = found.length;
343
- return false;
571
+ return found;
344
572
  }
345
573
  try {
346
574
  const spinner = jsonOnly ? null : createSpinner(
347
- all ? `Installing ${chalk2.cyan(source)} to ${chalk2.dim("all platforms")} (${scope})...` : `Installing ${chalk2.cyan(source)} to ${chalk2.dim(platform)} (${scope})...`
575
+ allPlatformsSelected ? `Installing ${chalk2.cyan(source)} to ${chalk2.dim("all platforms")} (${scope})...` : targets.length > 1 ? `Installing ${chalk2.cyan(source)} to ${chalk2.dim(`${targets.length} platforms`)} (${scope})...` : `Installing ${chalk2.cyan(source)} to ${chalk2.dim(targets[0])} (${scope})...`
348
576
  );
349
577
  let cleanupMaterialized = null;
350
578
  let materializedRoot = null;
@@ -365,16 +593,17 @@ ${previewDiscovered(found)}
365
593
  if (jsonOnly) {
366
594
  printJson({ ok: false, error: "SKILL_MD_NOT_FOUND", message, source, resolvedSource });
367
595
  } else {
596
+ if (spinner) spinner.stop();
368
597
  console.error(chalk2.red(message));
369
598
  }
370
599
  process.exitCode = 1;
371
600
  return;
372
601
  }
373
602
  const found = asDiscoveredSkills(discovered, (d) => path2.join(maybeLocalRoot, d.relPath));
374
- const didReturn = await maybeEnableRecursiveAndInstall(found);
375
- if (didReturn) return;
376
- if (spinner) spinner.text = `Installing ${chalk2.cyan(source)} \u2014 discovered ${found.length} skills...`;
377
- for (const skill of found) {
603
+ const selected = await resolveDiscoveredSelection(found, spinner);
604
+ if (!selected) return;
605
+ if (spinner) spinner.text = `Installing ${chalk2.cyan(source)} \u2014 discovered ${selected.length} skills...`;
606
+ for (const skill of selected) {
378
607
  if (spinner) spinner.text = `Installing ${chalk2.cyan(skill.relPath)} (${scope})...`;
379
608
  await installOne(skill.suggestedSource, skill.materializedDir);
380
609
  }
@@ -393,6 +622,7 @@ ${previewDiscovered(found)}
393
622
  if (jsonOnly) {
394
623
  printJson({ ok: false, error: "SKILL_MD_NOT_FOUND", message, source, resolvedSource });
395
624
  } else {
625
+ if (spinner) spinner.stop();
396
626
  console.error(chalk2.red(message));
397
627
  }
398
628
  process.exitCode = 1;
@@ -403,10 +633,10 @@ ${previewDiscovered(found)}
403
633
  (d) => deriveChildSource(resolvedSource, d.relPath),
404
634
  (d) => d.absDir
405
635
  );
406
- const didReturn = await maybeEnableRecursiveAndInstall(found);
407
- if (didReturn) return;
408
- if (spinner) spinner.text = `Installing ${chalk2.cyan(source)} \u2014 discovered ${found.length} skills...`;
409
- for (const skill of found) {
636
+ const selected = await resolveDiscoveredSelection(found, spinner);
637
+ if (!selected) return;
638
+ if (spinner) spinner.text = `Installing ${chalk2.cyan(source)} \u2014 discovered ${selected.length} skills...`;
639
+ for (const skill of selected) {
410
640
  if (spinner) spinner.text = `Installing ${chalk2.cyan(skill.relPath)} (${scope})...`;
411
641
  await installOne(skill.suggestedSource, skill.materializedDir);
412
642
  }
@@ -417,11 +647,21 @@ ${previewDiscovered(found)}
417
647
  if (cleanupMaterialized) cleanupMaterialized();
418
648
  }
419
649
  if (jsonOnly) {
420
- if (!all && !effectiveRecursive) {
650
+ if (!effectiveRecursive && targets.length === 1) {
421
651
  if (errors.length) printJson({ ok: false, error: errors[0]?.error || "Install failed." });
422
652
  else printJson(results[0] ?? null);
423
653
  } else {
424
- printJson({ ok: errors.length === 0, source, resolvedSource, scope, recursive: effectiveRecursive, all, recursiveSkillCount, results, errors });
654
+ printJson({
655
+ ok: errors.length === 0,
656
+ source,
657
+ resolvedSource,
658
+ scope,
659
+ recursive: effectiveRecursive,
660
+ all: allPlatformsSelected,
661
+ recursiveSkillCount,
662
+ results,
663
+ errors
664
+ });
425
665
  }
426
666
  process.exitCode = errors.length ? 1 : 0;
427
667
  return;
@@ -429,7 +669,7 @@ ${previewDiscovered(found)}
429
669
  if (errors.length === 0) {
430
670
  const displayName = results[0]?.canonicalName || results[0]?.name || source;
431
671
  spinner.succeed(
432
- effectiveRecursive ? `Installed ${chalk2.green(String(recursiveSkillCount ?? results.length))}${chalk2.dim(" skills")} to ${chalk2.dim(`${targets.length} platforms`)}` : all ? `Installed ${chalk2.green(displayName)} to ${chalk2.dim(`${results.length} platforms`)}` : `Installed ${chalk2.green(displayName)} to ${chalk2.dim(results[0]?.installDir || "")}`
672
+ effectiveRecursive ? `Installed ${chalk2.green(String(recursiveSkillCount ?? results.length))}${chalk2.dim(" skills")} to ${chalk2.dim(`${targets.length} platforms`)}` : targets.length > 1 ? `Installed ${chalk2.green(displayName)} to ${chalk2.dim(`${results.length} platforms`)}` : `Installed ${chalk2.green(displayName)} to ${chalk2.dim(results[0]?.installDir || "")}`
433
673
  );
434
674
  } else {
435
675
  const attempted = results.length + errors.length;
@@ -437,9 +677,9 @@ ${previewDiscovered(found)}
437
677
  effectiveRecursive ? `Install had failures (${errors.length}/${attempted} installs failed)` : `Failed to install ${chalk2.red(source)} to ${errors.length}/${targets.length} platforms`
438
678
  );
439
679
  process.exitCode = 1;
440
- if (!all && errors[0]) console.error(chalk2.red(errors[0].error));
680
+ if (!effectiveRecursive && targets.length === 1 && errors[0]) console.error(chalk2.red(errors[0].error));
441
681
  }
442
- if (!effectiveRecursive && !all && results[0]) {
682
+ if (!effectiveRecursive && targets.length === 1 && results[0]) {
443
683
  const record = results[0];
444
684
  if (record.hasSkillMd) logger.installDetail("SKILL.md found \u2713");
445
685
  else logger.installDetail("Warning: No SKILL.md found", true);
@@ -448,7 +688,7 @@ ${previewDiscovered(found)}
448
688
  } else if (record.skill?.validation?.ok) {
449
689
  logger.installDetail(`Validation: ${chalk2.green("ok")}`);
450
690
  }
451
- } else if (effectiveRecursive || all) {
691
+ } else if (effectiveRecursive || targets.length > 1) {
452
692
  for (const r of results.slice(0, 60)) {
453
693
  const displayName = r.canonicalName || r.name;
454
694
  const suffix = r.hasSkillMd ? chalk2.green("\u2713") : chalk2.yellow("\u26A0");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "skild",
3
- "version": "0.4.1",
3
+ "version": "0.4.3",
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",
@@ -37,7 +37,7 @@
37
37
  "commander": "^12.1.0",
38
38
  "ora": "^8.0.1",
39
39
  "tar": "^7.4.3",
40
- "@skild/core": "^0.4.1"
40
+ "@skild/core": "^0.4.3"
41
41
  },
42
42
  "devDependencies": {
43
43
  "@types/node": "^20.10.0",