skillex 0.3.1 → 0.4.1

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 (51) hide show
  1. package/CHANGELOG.md +286 -1
  2. package/README.md +82 -16
  3. package/dist/auto-sync.d.ts +66 -0
  4. package/dist/auto-sync.js +91 -0
  5. package/dist/catalog.js +5 -29
  6. package/dist/cli.d.ts +13 -0
  7. package/dist/cli.js +266 -144
  8. package/dist/confirm.js +3 -1
  9. package/dist/direct-github.d.ts +60 -0
  10. package/dist/direct-github.js +177 -0
  11. package/dist/doctor.d.ts +31 -0
  12. package/dist/doctor.js +172 -0
  13. package/dist/downloader.d.ts +42 -0
  14. package/dist/downloader.js +41 -0
  15. package/dist/fs.d.ts +21 -1
  16. package/dist/fs.js +30 -3
  17. package/dist/http.d.ts +28 -7
  18. package/dist/http.js +143 -42
  19. package/dist/install.d.ts +23 -9
  20. package/dist/install.js +75 -348
  21. package/dist/lockfile.d.ts +46 -0
  22. package/dist/lockfile.js +169 -0
  23. package/dist/output.d.ts +11 -0
  24. package/dist/output.js +49 -0
  25. package/dist/recommended.d.ts +13 -0
  26. package/dist/recommended.js +21 -0
  27. package/dist/runner.js +9 -9
  28. package/dist/skill.d.ts +2 -0
  29. package/dist/skill.js +3 -0
  30. package/dist/sync.js +12 -9
  31. package/dist/types.d.ts +39 -0
  32. package/dist/types.js +28 -0
  33. package/dist/ui.js +1 -1
  34. package/dist/user-config.d.ts +5 -0
  35. package/dist/user-config.js +22 -1
  36. package/dist/web-ui.js +5 -0
  37. package/dist-ui/assets/CatalogPage-CKEfRSvG.js +1 -0
  38. package/dist-ui/assets/CatalogPage-W5MqylAz.css +1 -0
  39. package/dist-ui/assets/DoctorPage-C92pEVl_.js +1 -0
  40. package/dist-ui/assets/Skeleton-BISmLuhY.js +1 -0
  41. package/dist-ui/assets/Skeleton-_Ooiw1nN.css +1 -0
  42. package/dist-ui/assets/SkillDetailPage-CBAaWpcc.css +1 -0
  43. package/dist-ui/assets/SkillDetailPage-CWGjTH2M.js +1 -0
  44. package/dist-ui/assets/{index-UBECch6X.css → index-CWm7zQTg.css} +1 -1
  45. package/dist-ui/assets/index-DAVP4Xp_.js +26 -0
  46. package/dist-ui/assets/recommended-D_i10hwH.js +1 -0
  47. package/dist-ui/index.html +2 -2
  48. package/package.json +6 -2
  49. package/dist-ui/assets/CatalogPage-B_qic36n.js +0 -1
  50. package/dist-ui/assets/SkillDetailPage-BJ3onKk4.js +0 -1
  51. package/dist-ui/assets/index-DN-z--cR.js +0 -25
package/dist/cli.js CHANGED
@@ -1,16 +1,84 @@
1
1
  import * as path from "node:path";
2
2
  import { listAdapters } from "./adapters.js";
3
- import { computeCatalogCacheKey, readCatalogCache, searchCatalogSkills, } from "./catalog.js";
3
+ import { buildRawGitHubUrl, searchCatalogSkills, } from "./catalog.js";
4
+ import { runDoctorChecks } from "./doctor.js";
5
+ import { fetchText } from "./http.js";
4
6
  import { DEFAULT_INSTALL_SCOPE, getScopedStatePaths } from "./config.js";
5
- import { addProjectSource, getInstalledSkills, initProject, installSkills, listProjectSources, loadProjectCatalogs, removeProjectSource, removeSkills, resolveProjectSource, syncInstalledSkills, updateInstalledSkills, } from "./install.js";
7
+ import { addProjectSource, getInstalledSkills, initProject, installSkills, listProjectSources, loadProjectCatalogs, removeProjectSource, removeSkills, syncInstalledSkills, updateInstalledSkills, } from "./install.js";
6
8
  import * as output from "./output.js";
7
- import { setVerbose } from "./output.js";
9
+ import { setVerbose, suggestClosest } from "./output.js";
8
10
  import { parseSkillCommandReference, runSkillScript } from "./runner.js";
11
+ import { getRecommendedSkillIds } from "./recommended.js";
9
12
  import { runInteractiveUi } from "./ui.js";
10
13
  import { startWebUiServer } from "./web-ui.js";
11
14
  import { CliError } from "./types.js";
12
15
  import { VALID_CONFIG_KEYS, readUserConfig, writeUserConfig } from "./user-config.js";
13
16
  // ---------------------------------------------------------------------------
17
+ // Flag schema (drives parsing, validation, and "did you mean" suggestions)
18
+ // ---------------------------------------------------------------------------
19
+ /** Flags that accept a value (e.g. `--repo foo` or `--repo=foo`). */
20
+ const STRING_FLAGS = new Set([
21
+ "repo",
22
+ "ref",
23
+ "adapter",
24
+ "scope",
25
+ "cwd",
26
+ "mode",
27
+ "tag",
28
+ "tags",
29
+ "compatibility",
30
+ "agent-skills-dir",
31
+ "catalog-path",
32
+ "catalog-url",
33
+ "skills-dir",
34
+ "label",
35
+ "timeout",
36
+ "host",
37
+ "port",
38
+ ]);
39
+ /** Flags that are pure booleans (presence = true; supports `--name=value` parsing too). */
40
+ const BOOLEAN_FLAGS = new Set([
41
+ "help",
42
+ "verbose",
43
+ "v",
44
+ "json",
45
+ "no-cache",
46
+ "all",
47
+ "trust",
48
+ "yes",
49
+ "global",
50
+ "auto-sync",
51
+ "dry-run",
52
+ "exit-code",
53
+ "raw",
54
+ "install-recommended",
55
+ "no-open",
56
+ ]);
57
+ /** Union of all flags the parser accepts anywhere in the CLI. */
58
+ const KNOWN_FLAGS = new Set([...STRING_FLAGS, ...BOOLEAN_FLAGS]);
59
+ const COMMANDS = [
60
+ "init",
61
+ "list",
62
+ "search",
63
+ "install",
64
+ "update",
65
+ "remove",
66
+ "sync",
67
+ "run",
68
+ "show",
69
+ "browse",
70
+ "tui",
71
+ "ui",
72
+ "status",
73
+ "doctor",
74
+ "config",
75
+ "source",
76
+ "ls",
77
+ "rm",
78
+ "uninstall",
79
+ "help",
80
+ ];
81
+ // ---------------------------------------------------------------------------
14
82
  // Per-command help text
15
83
  // ---------------------------------------------------------------------------
16
84
  const COMMAND_HELP = {
@@ -19,16 +87,18 @@ const COMMAND_HELP = {
19
87
  Initialize Skillex state for the local workspace or global user scope.
20
88
 
21
89
  Options:
22
- --repo <owner/repo> GitHub repository with skills (default: lgili/skillex)
23
- --ref <ref> Branch, tag, or commit (default: main)
24
- --adapter <id> Force a specific adapter
25
- --auto-sync Enable or disable auto-sync (default: on)
26
- --scope <scope> local or global (default: local)
27
- --global Shortcut for --scope global
28
- --cwd <path> Target project directory (default: current directory)
90
+ --repo <owner/repo> GitHub repository with skills (default: lgili/skillex)
91
+ --ref <ref> Branch, tag, or commit (default: main)
92
+ --adapter <id> Force a specific adapter
93
+ --auto-sync Enable or disable auto-sync (default: on)
94
+ --install-recommended After init, install a curated starter pack
95
+ --scope <scope> local or global (default: local)
96
+ --global Shortcut for --scope global
97
+ --cwd <path> Target project directory (default: current directory)
29
98
 
30
99
  Example:
31
100
  skillex init
101
+ skillex init --install-recommended
32
102
  skillex init --repo myorg/my-skills
33
103
  skillex init --global --adapter codex`,
34
104
  list: `Usage: skillex list [options]
@@ -51,12 +121,13 @@ Search skills by text, compatibility, or tags.
51
121
  Options:
52
122
  --repo <owner/repo> GitHub repository (limits this command to one source)
53
123
  --compatibility <id> Filter by adapter compatibility
54
- --tag <tag> Filter by tag
124
+ --tag <tag> Filter by tag (alias: --tags for compatibility)
55
125
  --no-cache Bypass local catalog cache
56
126
  --json Output results as JSON
57
127
 
58
128
  Example:
59
- skillex search git --compatibility claude`,
129
+ skillex search git --compatibility claude
130
+ skillex search --tag workflow`,
60
131
  install: `Usage: skillex install <skill-id...> [options]
61
132
  skillex install --all [options]
62
133
  skillex install <owner/repo[@ref]> [options]
@@ -107,6 +178,7 @@ Synchronize installed skills to adapter targets.
107
178
  Options:
108
179
  --adapter <id> Target adapter (overrides saved config)
109
180
  --dry-run Preview changes without writing to disk
181
+ --exit-code With --dry-run, exit 1 when adapters would change (CI)
110
182
  --mode <symlink|copy> Sync write mode (default: symlink)
111
183
  --scope <scope> local or global (default: local)
112
184
  --global Shortcut for --scope global
@@ -114,6 +186,7 @@ Options:
114
186
  Example:
115
187
  skillex sync
116
188
  skillex sync --adapter cursor --dry-run
189
+ skillex sync --dry-run --exit-code # CI: fail when out of sync
117
190
  skillex sync --global --adapter codex`,
118
191
  run: `Usage: skillex run <skill-id:command> [options]
119
192
 
@@ -125,6 +198,20 @@ Options:
125
198
 
126
199
  Example:
127
200
  skillex run git-master:cleanup --yes`,
201
+ show: `Usage: skillex show <skill-id> [options]
202
+
203
+ Print the manifest summary and rendered SKILL.md content of a skill from
204
+ the configured catalog sources without installing it.
205
+
206
+ Options:
207
+ --repo <owner/repo> Limit resolution to one source
208
+ --raw Print SKILL.md verbatim (no manifest header)
209
+ --json Print manifest + raw SKILL.md as a single JSON object
210
+ --no-cache Bypass local catalog cache
211
+
212
+ Example:
213
+ skillex show git-master
214
+ skillex show code-review --raw`,
128
215
  browse: `Usage: skillex browse [options]
129
216
  skillex tui [options]
130
217
  skillex [options]
@@ -147,10 +234,14 @@ Options:
147
234
  --scope <scope> local or global (default: local)
148
235
  --global Shortcut for --scope global
149
236
  --no-cache Bypass local catalog cache
237
+ --host <host> Bind address (default: 127.0.0.1; loopback only)
238
+ --port <number> Listen port (default: pick a free port)
239
+ --no-open Do not open the system browser; just print the URL
150
240
 
151
241
  Example:
152
242
  skillex ui
153
- skillex ui --global`,
243
+ skillex ui --global
244
+ skillex ui --port 4173 --no-open # for the dev orchestrator`,
154
245
  status: `Usage: skillex status [options]
155
246
 
156
247
  Show the installation status of the selected scope.
@@ -270,6 +361,9 @@ export async function main(argv) {
270
361
  case "run":
271
362
  await handleRun(positionals, flags, userConfig);
272
363
  return;
364
+ case "show":
365
+ await handleShow(positionals, flags, userConfig);
366
+ return;
273
367
  case "ui":
274
368
  await handleWebUi(flags, userConfig);
275
369
  return;
@@ -285,8 +379,12 @@ export async function main(argv) {
285
379
  case "source":
286
380
  await handleSource(positionals, flags, userConfig);
287
381
  return;
288
- default:
289
- throw new CliError(`Unknown command: ${resolvedCommand}. Run "skillex help" to see available commands.`);
382
+ default: {
383
+ const suggestion = suggestClosest(resolvedCommand, COMMANDS);
384
+ throw new CliError(suggestion
385
+ ? `Unknown command: ${resolvedCommand}. Did you mean: ${suggestion}? Run "skillex help" for the full list.`
386
+ : `Unknown command: ${resolvedCommand}. Run "skillex help" to see available commands.`);
387
+ }
290
388
  }
291
389
  }
292
390
  // ---------------------------------------------------------------------------
@@ -294,6 +392,7 @@ export async function main(argv) {
294
392
  // ---------------------------------------------------------------------------
295
393
  async function handleInit(flags, userConfig) {
296
394
  const repo = asOptionalString(flags.repo) ?? userConfig.defaultRepo;
395
+ const installRecommended = parseBooleanFlag(flags["install-recommended"], "install-recommended") ?? false;
297
396
  const opts = commonOptions(flags, userConfig);
298
397
  const result = await initProject({
299
398
  ...opts,
@@ -321,7 +420,24 @@ async function handleInit(flags, userConfig) {
321
420
  if (result.lockfile.adapters.detected.length > 0) {
322
421
  output.info(` Detected : ${result.lockfile.adapters.detected.join(", ")}`);
323
422
  }
324
- output.info("\nNext: run 'skillex list' to browse available skills");
423
+ if (installRecommended) {
424
+ const recommended = getRecommendedSkillIds();
425
+ output.info(`\nInstalling ${recommended.length} recommended skill(s)...`);
426
+ const installResult = await installSkills(recommended, {
427
+ ...opts,
428
+ onProgress: (current, total, skillId) => output.progress(current, total, skillId),
429
+ });
430
+ output.success(`Installed ${installResult.installedCount} skill(s) from the recommended pack`);
431
+ for (const skill of installResult.installedSkills) {
432
+ output.info(` + ${skill.id}@${skill.version}`);
433
+ }
434
+ printAutoSyncResult(installResult.autoSync);
435
+ return;
436
+ }
437
+ output.info("\nNext steps:");
438
+ output.info(" • Browse and install interactively: skillex");
439
+ output.info(" • Install a curated starter pack: skillex init --install-recommended");
440
+ output.info(" • List the full catalog: skillex list");
325
441
  }
326
442
  async function handleList(flags, userConfig) {
327
443
  const opts = commonOptions(flags, userConfig);
@@ -352,7 +468,9 @@ async function handleSearch(positionals, flags, userConfig) {
352
468
  const aggregated = await loadProjectCatalogs({ ...opts, ...cacheOptions(opts) });
353
469
  const searchOptions = { query: positionals.join(" ") };
354
470
  const compatibility = asOptionalString(flags.compatibility);
355
- const tag = asOptionalString(flags.tag);
471
+ // `--tag` is canonical; `--tags` is accepted as an alias because earlier README
472
+ // versions documented the plural form. The parser already permits both names.
473
+ const tag = asOptionalString(flags.tag) ?? asOptionalString(flags.tags);
356
474
  if (compatibility)
357
475
  searchOptions.compatibility = compatibility;
358
476
  if (tag)
@@ -426,9 +544,13 @@ async function handleRemove(positionals, flags, userConfig) {
426
544
  for (const skillId of result.missingSkills) {
427
545
  output.warn(`${skillId} is not installed`);
428
546
  }
429
- printAutoSyncResult(result.autoSync);
547
+ // Remove can fan out across multiple previously-synced adapters; report each.
548
+ for (const sync of result.autoSyncs) {
549
+ printAutoSyncResult(sync);
550
+ }
430
551
  }
431
552
  async function handleSync(flags, userConfig) {
553
+ const exitCodeFlag = parseBooleanFlag(flags["exit-code"], "exit-code") ?? false;
432
554
  const result = await syncInstalledSkills(commonOptions(flags, userConfig));
433
555
  if (result.dryRun) {
434
556
  output.info(`Preview: ${result.skillCount} skill(s)`);
@@ -436,6 +558,10 @@ async function handleSync(flags, userConfig) {
436
558
  output.info(` ${entry.adapter} → ${entry.targetPath} [${entry.syncMode}]${entry.changed ? "" : " (no changes)"}`);
437
559
  }
438
560
  process.stdout.write(result.diff);
561
+ // Mirror `git diff --exit-code`: when --exit-code is set, drift is a non-zero exit.
562
+ if (exitCodeFlag && result.changed) {
563
+ process.exitCode = 1;
564
+ }
439
565
  return;
440
566
  }
441
567
  output.success(`Synced ${result.skillCount} skill(s)`);
@@ -461,6 +587,52 @@ async function handleRun(positionals, flags, userConfig) {
461
587
  process.exitCode = exitCode;
462
588
  }
463
589
  }
590
+ async function handleShow(positionals, flags, userConfig) {
591
+ const skillId = positionals[0];
592
+ if (!skillId) {
593
+ throw new CliError("Provide a skill id. Usage: skillex show <skill-id> [--raw|--json]", "SHOW_REQUIRES_SKILL");
594
+ }
595
+ const opts = commonOptions(flags, userConfig);
596
+ const aggregated = await loadProjectCatalogs({ ...opts, ...cacheOptions(opts) });
597
+ const matches = aggregated.skills.filter((s) => s.id === skillId);
598
+ if (matches.length === 0) {
599
+ throw new CliError(`Skill "${skillId}" not found in the configured sources.`, "SHOW_SKILL_NOT_FOUND");
600
+ }
601
+ if (matches.length > 1) {
602
+ const sourceList = matches.map((m) => `${m.source.repo}@${m.source.ref}`).join(", ");
603
+ throw new CliError(`Skill "${skillId}" exists in multiple sources: ${sourceList}. Use --repo to choose one.`, "SHOW_AMBIGUOUS_SOURCE");
604
+ }
605
+ const skill = matches[0];
606
+ const skillFile = skill.entry || "SKILL.md";
607
+ const remotePath = skill.path ? `${skill.path}/${skillFile}` : skillFile;
608
+ const url = buildRawGitHubUrl(skill.source.repo, skill.source.ref, remotePath);
609
+ const body = await fetchText(url, { headers: { Accept: "text/plain" } });
610
+ const raw = parseBooleanFlag(flags.raw, "raw") ?? false;
611
+ if (flags.json === true) {
612
+ output.info(JSON.stringify({
613
+ ...skill,
614
+ entryContent: body,
615
+ }, null, 2));
616
+ return;
617
+ }
618
+ if (raw) {
619
+ process.stdout.write(body.endsWith("\n") ? body : `${body}\n`);
620
+ return;
621
+ }
622
+ output.info(`${skill.name} (${skill.id}) — v${skill.version}`);
623
+ output.info(`Source : ${skill.source.repo}@${skill.source.ref}${skill.source.label ? ` [${skill.source.label}]` : ""}`);
624
+ if (skill.author)
625
+ output.info(`Author : ${skill.author}`);
626
+ if (skill.tags.length)
627
+ output.info(`Tags : ${skill.tags.join(", ")}`);
628
+ if (skill.compatibility.length)
629
+ output.info(`Compatibility: ${skill.compatibility.join(", ")}`);
630
+ output.info(`Files : ${skill.files.length}`);
631
+ output.info("");
632
+ output.info("─".repeat(60));
633
+ output.info("");
634
+ process.stdout.write(body.endsWith("\n") ? body : `${body}\n`);
635
+ }
464
636
  async function handleBrowse(flags, userConfig) {
465
637
  const options = commonOptions(flags, userConfig);
466
638
  const state = await getInstalledSkills(options);
@@ -500,9 +672,18 @@ async function handleBrowse(flags, userConfig) {
500
672
  }
501
673
  async function handleWebUi(flags, userConfig) {
502
674
  const options = commonOptions(flags, userConfig);
503
- const session = await startWebUiServer(options);
675
+ const host = asOptionalString(flags.host);
676
+ const portRaw = asOptionalString(flags.port);
677
+ const port = portRaw !== undefined ? parsePositiveInt(portRaw) : undefined;
678
+ const noOpen = parseBooleanFlag(flags["no-open"], "no-open") ?? false;
679
+ const session = await startWebUiServer({
680
+ ...options,
681
+ ...(host ? { host } : {}),
682
+ ...(port !== undefined ? { port } : {}),
683
+ autoOpen: !noOpen,
684
+ });
504
685
  output.success(`Skillex Web UI running at ${session.url}`);
505
- if (!session.opened) {
686
+ if (!noOpen && !session.opened) {
506
687
  output.warn("Could not open the browser automatically. Open the URL above manually.");
507
688
  }
508
689
  output.info("Press Ctrl+C to stop the local server.");
@@ -550,113 +731,12 @@ async function handleStatus(flags, userConfig) {
550
731
  }
551
732
  async function handleDoctor(flags, userConfig) {
552
733
  const opts = commonOptions(flags, userConfig);
553
- const cwd = opts.cwd ?? process.cwd();
554
- const statePaths = getScopedStatePaths(cwd, {
555
- scope: opts.scope,
556
- baseDir: opts.agentSkillsDir,
557
- });
558
- const checks = [];
559
- // 1. Lockfile
560
- const state = await getInstalledSkills(opts);
561
- if (state) {
562
- checks.push({ name: "lockfile", passed: true, message: `Found at ${statePaths.lockfilePath}` });
563
- }
564
- else {
565
- checks.push({
566
- name: "lockfile",
567
- passed: false,
568
- message: "Lockfile not found",
569
- hint: "Run: skillex init",
570
- });
571
- }
572
- // 2. Sources configured
573
- const stateSources = state?.sources ?? [];
574
- if (stateSources.length > 0) {
575
- checks.push({
576
- name: "source",
577
- passed: true,
578
- message: stateSources.map((source) => `${source.repo}@${source.ref}`).join(", "),
579
- });
580
- }
581
- else {
582
- checks.push({
583
- name: "source",
584
- passed: false,
585
- message: "No catalog source configured",
586
- hint: "Run: skillex init",
587
- });
588
- }
589
- // 3. Adapter detected
590
- const hasAdapter = Boolean(state?.adapters?.active || (state?.adapters?.detected?.length ?? 0) > 0);
591
- if (hasAdapter) {
592
- const adapter = state?.adapters?.active ?? state?.adapters?.detected?.[0];
593
- checks.push({ name: "adapter", passed: true, message: `Active: ${adapter}` });
594
- }
595
- else {
596
- checks.push({
597
- name: "adapter",
598
- passed: false,
599
- message: "No adapter detected",
600
- hint: `Use --adapter <id>. Available: ${listAdapters().map((a) => a.id).join(", ")}`,
601
- });
602
- }
603
- // 4. GitHub reachable
604
- try {
605
- const response = await fetch("https://api.github.com", {
606
- method: "HEAD",
607
- headers: { "User-Agent": "skillex" },
608
- signal: AbortSignal.timeout(5000),
609
- });
610
- if (response.status < 500) {
611
- checks.push({ name: "github", passed: true, message: "GitHub API is reachable" });
612
- }
613
- else {
614
- checks.push({
615
- name: "github",
616
- passed: false,
617
- message: `GitHub API returned ${response.status}`,
618
- hint: "Try again in a moment.",
619
- });
620
- }
621
- }
622
- catch {
623
- checks.push({
624
- name: "github",
625
- passed: false,
626
- message: "GitHub API is unreachable",
627
- hint: "Check your internet connection or proxy settings.",
628
- });
629
- }
630
- // 5. GitHub token (warning only — never fails)
631
- const token = process.env.GITHUB_TOKEN;
632
- if (token) {
633
- checks.push({ name: "token", passed: true, message: "GitHub token set (authenticated — 5,000 req/hr)" });
634
- }
635
- else {
636
- checks.push({ name: "token", passed: true, message: "No GitHub token (unauthenticated — 60 req/hr)" });
637
- }
638
- // 6. Cache
639
- const cacheDir = path.join(statePaths.stateDir, ".cache");
640
- if ((state?.sources?.length ?? 0) > 0) {
641
- const source = await resolveProjectSource(opts);
642
- const cacheKey = computeCatalogCacheKey(source);
643
- const cached = await readCatalogCache(cacheDir, cacheKey);
644
- if (cached) {
645
- checks.push({ name: "cache", passed: true, message: "Catalog cache is fresh" });
646
- }
647
- else {
648
- checks.push({ name: "cache", passed: true, message: "No cached catalog (will fetch on next command)" });
649
- }
650
- }
651
- else {
652
- checks.push({ name: "cache", passed: true, message: "Cache not checked (no repo configured)" });
653
- }
654
- const anyFailed = checks.some((c) => !c.passed);
734
+ const report = await runDoctorChecks(opts);
655
735
  if (flags.json === true) {
656
736
  const jsonResult = {};
657
- for (const check of checks) {
737
+ for (const check of report.checks) {
658
738
  jsonResult[check.name] = {
659
- passed: check.passed,
739
+ passed: check.status !== "fail",
660
740
  message: check.message,
661
741
  ...(check.hint ? { hint: check.hint } : {}),
662
742
  };
@@ -664,21 +744,27 @@ async function handleDoctor(flags, userConfig) {
664
744
  output.info(JSON.stringify(jsonResult, null, 2));
665
745
  }
666
746
  else {
667
- for (const check of checks) {
668
- const symbol = check.passed ? "" : "";
747
+ for (const check of report.checks) {
748
+ const symbol = check.status === "fail" ? "" : check.status === "warn" ? "⚠" : "✓";
669
749
  const line = `${symbol} ${check.name.padEnd(10)} ${check.message}`;
670
- if (check.passed) {
671
- output.info(line);
672
- }
673
- else {
750
+ if (check.status === "fail") {
674
751
  output.error(line);
675
752
  if (check.hint) {
676
753
  output.info(` Hint: ${check.hint}`);
677
754
  }
678
755
  }
756
+ else if (check.status === "warn") {
757
+ output.warn(line);
758
+ if (check.hint) {
759
+ output.info(` Hint: ${check.hint}`);
760
+ }
761
+ }
762
+ else {
763
+ output.info(line);
764
+ }
679
765
  }
680
766
  }
681
- if (anyFailed) {
767
+ if (report.hasFailures) {
682
768
  process.exitCode = 1;
683
769
  }
684
770
  }
@@ -791,13 +877,13 @@ function commonOptions(flags, userConfig = {}) {
791
877
  const skillsDir = asOptionalString(flags["skills-dir"]);
792
878
  const agentSkillsDir = asOptionalString(flags["agent-skills-dir"]);
793
879
  const adapter = asOptionalString(flags.adapter) ?? userConfig.defaultAdapter;
794
- const autoSync = parseBooleanFlag(flags["auto-sync"]) ?? (userConfig.disableAutoSync ? false : undefined);
795
- const dryRun = parseBooleanFlag(flags["dry-run"]);
796
- const trust = parseBooleanFlag(flags.trust);
797
- const yes = parseBooleanFlag(flags.yes);
880
+ const autoSync = parseBooleanFlag(flags["auto-sync"], "auto-sync") ?? (userConfig.disableAutoSync ? false : undefined);
881
+ const dryRun = parseBooleanFlag(flags["dry-run"], "dry-run");
882
+ const trust = parseBooleanFlag(flags.trust, "trust");
883
+ const yes = parseBooleanFlag(flags.yes, "yes");
798
884
  const mode = parseSyncMode(asOptionalString(flags.mode));
799
885
  const timeout = parsePositiveInt(asOptionalString(flags.timeout));
800
- const noCache = parseBooleanFlag(flags["no-cache"]);
886
+ const noCache = parseBooleanFlag(flags["no-cache"], "no-cache");
801
887
  if (repo)
802
888
  options.repo = repo;
803
889
  if (ref)
@@ -842,7 +928,7 @@ function cacheOptions(opts) {
842
928
  }
843
929
  function resolveScope(flags) {
844
930
  const rawScope = asOptionalString(flags.scope);
845
- const globalFlag = parseBooleanFlag(flags.global);
931
+ const globalFlag = parseBooleanFlag(flags.global, "global");
846
932
  if (rawScope && rawScope !== "local" && rawScope !== "global") {
847
933
  throw new CliError(`Invalid scope: ${rawScope}. Use "local" or "global".`, "INVALID_SCOPE");
848
934
  }
@@ -854,14 +940,35 @@ function resolveScope(flags) {
854
940
  }
855
941
  return rawScope || DEFAULT_INSTALL_SCOPE;
856
942
  }
857
- function parseArgs(argv) {
943
+ /**
944
+ * Parses argv into a typed `ParsedArgs` shape with strict validation:
945
+ *
946
+ * - Unknown flags raise `UNKNOWN_FLAG` with a "did you mean" suggestion.
947
+ * - Boolean flags (`BOOLEAN_FLAGS`) accept presence-only or `--flag=value` forms.
948
+ * - String flags (`STRING_FLAGS`) require a value via `--flag=value` or
949
+ * `--flag value`. Missing values raise `MISSING_FLAG_VALUE`.
950
+ * - The literal `--` token marks end-of-options; remaining tokens become
951
+ * `positionalAfter` and are forwarded to handlers (used by `run` to pass
952
+ * arguments to the underlying script without flag interpretation).
953
+ */
954
+ export function parseArgs(argv) {
858
955
  const flags = {};
859
956
  const positionals = [];
957
+ const positionalAfter = [];
860
958
  let command;
959
+ let endOfOptions = false;
861
960
  for (let index = 0; index < argv.length; index += 1) {
862
961
  const token = argv[index];
863
962
  if (token === undefined)
864
963
  continue;
964
+ if (endOfOptions) {
965
+ positionalAfter.push(token);
966
+ continue;
967
+ }
968
+ if (token === "--") {
969
+ endOfOptions = true;
970
+ continue;
971
+ }
865
972
  if (!command && !token.startsWith("-")) {
866
973
  command = token;
867
974
  continue;
@@ -871,25 +978,39 @@ function parseArgs(argv) {
871
978
  continue;
872
979
  }
873
980
  if (token.startsWith("--")) {
874
- const [rawKey, inlineValue] = token.slice(2).split("=", 2);
981
+ const eq = token.indexOf("=");
982
+ const rawKey = eq === -1 ? token.slice(2) : token.slice(2, eq);
983
+ const inlineValue = eq === -1 ? undefined : token.slice(eq + 1);
875
984
  if (!rawKey)
876
985
  continue;
986
+ if (!KNOWN_FLAGS.has(rawKey)) {
987
+ const suggestion = suggestClosest(rawKey, [...KNOWN_FLAGS]);
988
+ throw new CliError(suggestion
989
+ ? `Unknown flag: --${rawKey}. Did you mean --${suggestion}?`
990
+ : `Unknown flag: --${rawKey}. Run 'skillex --help' to list flags.`, "UNKNOWN_FLAG");
991
+ }
877
992
  if (inlineValue !== undefined) {
878
993
  flags[rawKey] = inlineValue;
879
994
  continue;
880
995
  }
881
- const next = argv[index + 1];
882
- if (!next || next.startsWith("-")) {
996
+ // Boolean flag without an inline value: presence = true.
997
+ if (BOOLEAN_FLAGS.has(rawKey)) {
883
998
  flags[rawKey] = true;
884
999
  continue;
885
1000
  }
1001
+ // String flag: require a following value that is not another flag and not the
1002
+ // end-of-options sentinel.
1003
+ const next = argv[index + 1];
1004
+ if (next === undefined || next === "--" || next.startsWith("-")) {
1005
+ throw new CliError(`Missing value for --${rawKey}. Pass --${rawKey} <value> or --${rawKey}=<value>.`, "MISSING_FLAG_VALUE");
1006
+ }
886
1007
  flags[rawKey] = next;
887
1008
  index += 1;
888
1009
  continue;
889
1010
  }
890
1011
  positionals.push(token);
891
1012
  }
892
- return { command, positionals, flags };
1013
+ return { command, positionals, positionalAfter, flags };
893
1014
  }
894
1015
  function printHelp() {
895
1016
  output.info(`skillex — AI agent skill manager
@@ -943,7 +1064,7 @@ function truncate(value, maxLength) {
943
1064
  return value;
944
1065
  return `${value.slice(0, maxLength - 3)}...`;
945
1066
  }
946
- function parseBooleanFlag(value) {
1067
+ function parseBooleanFlag(value, flagName) {
947
1068
  if (value === undefined)
948
1069
  return undefined;
949
1070
  if (value === true)
@@ -953,7 +1074,8 @@ function parseBooleanFlag(value) {
953
1074
  return true;
954
1075
  if (["false", "0", "no", "off"].includes(normalized))
955
1076
  return false;
956
- throw new CliError(`Invalid boolean value: ${value}`, "INVALID_BOOLEAN_FLAG");
1077
+ const target = flagName ? `--${flagName}` : "boolean flag";
1078
+ throw new CliError(`Invalid value "${value}" for ${target}. Use true, false, yes, no, on, off, 1, or 0.`, "INVALID_BOOLEAN_FLAG");
957
1079
  }
958
1080
  function parsePositiveInt(value) {
959
1081
  if (!value)
package/dist/confirm.js CHANGED
@@ -10,12 +10,14 @@ import { CliError } from "./types.js";
10
10
  */
11
11
  export async function confirmAction(message) {
12
12
  if (!input.isTTY || !output.isTTY) {
13
- throw new CliError("Confirmacao interativa indisponivel neste terminal. Use a flag correspondente.", "TTY_REQUIRED");
13
+ throw new CliError("Interactive confirmation is unavailable in this terminal. Use the matching CLI flag (e.g. --trust, --yes) to skip the prompt.", "TTY_REQUIRED");
14
14
  }
15
15
  const rl = createInterface({ input, output });
16
16
  try {
17
17
  const answer = await rl.question(`${message} [y/N] `);
18
18
  const normalized = answer.trim().toLowerCase();
19
+ // Accept English (y/yes) and historical Portuguese (s/sim) responses to avoid
20
+ // breaking muscle memory for existing users.
19
21
  return normalized === "y" || normalized === "yes" || normalized === "s" || normalized === "sim";
20
22
  }
21
23
  finally {