skild 0.4.2 → 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 +284 -48
  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, spinner) {
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,31 +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
545
  if (spinner) spinner.stop();
328
- const headline = found.length === 1 ? `No SKILL.md found at root. Found 1 skill:
329
- ${previewDiscovered(found)}
330
- ` : `No SKILL.md found at root. Found ${found.length} skills:
331
- ${previewDiscovered(found)}
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.
332
548
  `;
333
549
  console.log(chalk2.yellow(headline));
334
- const question = found.length === 1 ? "Install the discovered skill?" : "Install all discovered skills?";
335
- const confirm = yes || interactive && await promptConfirm(question, { defaultValue: found.length === 1 });
336
- if (!confirm) {
550
+ if (!interactive && !yes) {
337
551
  console.log(chalk2.dim(`Tip: rerun with ${chalk2.cyan("skild install <source> --recursive")} to install all.`));
338
552
  process.exitCode = 1;
339
- 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;
340
567
  }
341
568
  effectiveRecursive = true;
342
- if (spinner) spinner.start();
343
569
  }
344
570
  recursiveSkillCount = found.length;
345
- return false;
571
+ return found;
346
572
  }
347
573
  try {
348
574
  const spinner = jsonOnly ? null : createSpinner(
349
- 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})...`
350
576
  );
351
577
  let cleanupMaterialized = null;
352
578
  let materializedRoot = null;
@@ -374,10 +600,10 @@ ${previewDiscovered(found)}
374
600
  return;
375
601
  }
376
602
  const found = asDiscoveredSkills(discovered, (d) => path2.join(maybeLocalRoot, d.relPath));
377
- const didReturn = await maybeEnableRecursiveAndInstall(found, spinner);
378
- if (didReturn) return;
379
- if (spinner) spinner.text = `Installing ${chalk2.cyan(source)} \u2014 discovered ${found.length} skills...`;
380
- 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) {
381
607
  if (spinner) spinner.text = `Installing ${chalk2.cyan(skill.relPath)} (${scope})...`;
382
608
  await installOne(skill.suggestedSource, skill.materializedDir);
383
609
  }
@@ -407,10 +633,10 @@ ${previewDiscovered(found)}
407
633
  (d) => deriveChildSource(resolvedSource, d.relPath),
408
634
  (d) => d.absDir
409
635
  );
410
- const didReturn = await maybeEnableRecursiveAndInstall(found, spinner);
411
- if (didReturn) return;
412
- if (spinner) spinner.text = `Installing ${chalk2.cyan(source)} \u2014 discovered ${found.length} skills...`;
413
- 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) {
414
640
  if (spinner) spinner.text = `Installing ${chalk2.cyan(skill.relPath)} (${scope})...`;
415
641
  await installOne(skill.suggestedSource, skill.materializedDir);
416
642
  }
@@ -421,11 +647,21 @@ ${previewDiscovered(found)}
421
647
  if (cleanupMaterialized) cleanupMaterialized();
422
648
  }
423
649
  if (jsonOnly) {
424
- if (!all && !effectiveRecursive) {
650
+ if (!effectiveRecursive && targets.length === 1) {
425
651
  if (errors.length) printJson({ ok: false, error: errors[0]?.error || "Install failed." });
426
652
  else printJson(results[0] ?? null);
427
653
  } else {
428
- 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
+ });
429
665
  }
430
666
  process.exitCode = errors.length ? 1 : 0;
431
667
  return;
@@ -433,7 +669,7 @@ ${previewDiscovered(found)}
433
669
  if (errors.length === 0) {
434
670
  const displayName = results[0]?.canonicalName || results[0]?.name || source;
435
671
  spinner.succeed(
436
- 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 || "")}`
437
673
  );
438
674
  } else {
439
675
  const attempted = results.length + errors.length;
@@ -441,9 +677,9 @@ ${previewDiscovered(found)}
441
677
  effectiveRecursive ? `Install had failures (${errors.length}/${attempted} installs failed)` : `Failed to install ${chalk2.red(source)} to ${errors.length}/${targets.length} platforms`
442
678
  );
443
679
  process.exitCode = 1;
444
- 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));
445
681
  }
446
- if (!effectiveRecursive && !all && results[0]) {
682
+ if (!effectiveRecursive && targets.length === 1 && results[0]) {
447
683
  const record = results[0];
448
684
  if (record.hasSkillMd) logger.installDetail("SKILL.md found \u2713");
449
685
  else logger.installDetail("Warning: No SKILL.md found", true);
@@ -452,7 +688,7 @@ ${previewDiscovered(found)}
452
688
  } else if (record.skill?.validation?.ok) {
453
689
  logger.installDetail(`Validation: ${chalk2.green("ok")}`);
454
690
  }
455
- } else if (effectiveRecursive || all) {
691
+ } else if (effectiveRecursive || targets.length > 1) {
456
692
  for (const r of results.slice(0, 60)) {
457
693
  const displayName = r.canonicalName || r.name;
458
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.2",
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.2"
40
+ "@skild/core": "^0.4.3"
41
41
  },
42
42
  "devDependencies": {
43
43
  "@types/node": "^20.10.0",