skillex 0.3.1 → 0.4.0

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 +262 -1
  2. package/README.md +57 -10
  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 +247 -141
  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-CbtMTkxd.js +1 -0
  38. package/dist-ui/assets/CatalogPage-W5MqylAz.css +1 -0
  39. package/dist-ui/assets/DoctorPage-oUZyX91t.js +1 -0
  40. package/dist-ui/assets/Skeleton-B_xm5L3P.js +1 -0
  41. package/dist-ui/assets/Skeleton-_Ooiw1nN.css +1 -0
  42. package/dist-ui/assets/SkillDetailPage-5JHQLq3q.js +1 -0
  43. package/dist-ui/assets/SkillDetailPage-CBAaWpcc.css +1 -0
  44. package/dist-ui/assets/{index-UBECch6X.css → index-CWm7zQTg.css} +1 -1
  45. package/dist-ui/assets/index-I0b-syhc.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 +2 -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,81 @@
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
+ ]);
37
+ /** Flags that are pure booleans (presence = true; supports `--name=value` parsing too). */
38
+ const BOOLEAN_FLAGS = new Set([
39
+ "help",
40
+ "verbose",
41
+ "v",
42
+ "json",
43
+ "no-cache",
44
+ "all",
45
+ "trust",
46
+ "yes",
47
+ "global",
48
+ "auto-sync",
49
+ "dry-run",
50
+ "exit-code",
51
+ "raw",
52
+ "install-recommended",
53
+ ]);
54
+ /** Union of all flags the parser accepts anywhere in the CLI. */
55
+ const KNOWN_FLAGS = new Set([...STRING_FLAGS, ...BOOLEAN_FLAGS]);
56
+ const COMMANDS = [
57
+ "init",
58
+ "list",
59
+ "search",
60
+ "install",
61
+ "update",
62
+ "remove",
63
+ "sync",
64
+ "run",
65
+ "show",
66
+ "browse",
67
+ "tui",
68
+ "ui",
69
+ "status",
70
+ "doctor",
71
+ "config",
72
+ "source",
73
+ "ls",
74
+ "rm",
75
+ "uninstall",
76
+ "help",
77
+ ];
78
+ // ---------------------------------------------------------------------------
14
79
  // Per-command help text
15
80
  // ---------------------------------------------------------------------------
16
81
  const COMMAND_HELP = {
@@ -19,16 +84,18 @@ const COMMAND_HELP = {
19
84
  Initialize Skillex state for the local workspace or global user scope.
20
85
 
21
86
  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)
87
+ --repo <owner/repo> GitHub repository with skills (default: lgili/skillex)
88
+ --ref <ref> Branch, tag, or commit (default: main)
89
+ --adapter <id> Force a specific adapter
90
+ --auto-sync Enable or disable auto-sync (default: on)
91
+ --install-recommended After init, install a curated starter pack
92
+ --scope <scope> local or global (default: local)
93
+ --global Shortcut for --scope global
94
+ --cwd <path> Target project directory (default: current directory)
29
95
 
30
96
  Example:
31
97
  skillex init
98
+ skillex init --install-recommended
32
99
  skillex init --repo myorg/my-skills
33
100
  skillex init --global --adapter codex`,
34
101
  list: `Usage: skillex list [options]
@@ -51,12 +118,13 @@ Search skills by text, compatibility, or tags.
51
118
  Options:
52
119
  --repo <owner/repo> GitHub repository (limits this command to one source)
53
120
  --compatibility <id> Filter by adapter compatibility
54
- --tag <tag> Filter by tag
121
+ --tag <tag> Filter by tag (alias: --tags for compatibility)
55
122
  --no-cache Bypass local catalog cache
56
123
  --json Output results as JSON
57
124
 
58
125
  Example:
59
- skillex search git --compatibility claude`,
126
+ skillex search git --compatibility claude
127
+ skillex search --tag workflow`,
60
128
  install: `Usage: skillex install <skill-id...> [options]
61
129
  skillex install --all [options]
62
130
  skillex install <owner/repo[@ref]> [options]
@@ -107,6 +175,7 @@ Synchronize installed skills to adapter targets.
107
175
  Options:
108
176
  --adapter <id> Target adapter (overrides saved config)
109
177
  --dry-run Preview changes without writing to disk
178
+ --exit-code With --dry-run, exit 1 when adapters would change (CI)
110
179
  --mode <symlink|copy> Sync write mode (default: symlink)
111
180
  --scope <scope> local or global (default: local)
112
181
  --global Shortcut for --scope global
@@ -114,6 +183,7 @@ Options:
114
183
  Example:
115
184
  skillex sync
116
185
  skillex sync --adapter cursor --dry-run
186
+ skillex sync --dry-run --exit-code # CI: fail when out of sync
117
187
  skillex sync --global --adapter codex`,
118
188
  run: `Usage: skillex run <skill-id:command> [options]
119
189
 
@@ -125,6 +195,20 @@ Options:
125
195
 
126
196
  Example:
127
197
  skillex run git-master:cleanup --yes`,
198
+ show: `Usage: skillex show <skill-id> [options]
199
+
200
+ Print the manifest summary and rendered SKILL.md content of a skill from
201
+ the configured catalog sources without installing it.
202
+
203
+ Options:
204
+ --repo <owner/repo> Limit resolution to one source
205
+ --raw Print SKILL.md verbatim (no manifest header)
206
+ --json Print manifest + raw SKILL.md as a single JSON object
207
+ --no-cache Bypass local catalog cache
208
+
209
+ Example:
210
+ skillex show git-master
211
+ skillex show code-review --raw`,
128
212
  browse: `Usage: skillex browse [options]
129
213
  skillex tui [options]
130
214
  skillex [options]
@@ -270,6 +354,9 @@ export async function main(argv) {
270
354
  case "run":
271
355
  await handleRun(positionals, flags, userConfig);
272
356
  return;
357
+ case "show":
358
+ await handleShow(positionals, flags, userConfig);
359
+ return;
273
360
  case "ui":
274
361
  await handleWebUi(flags, userConfig);
275
362
  return;
@@ -285,8 +372,12 @@ export async function main(argv) {
285
372
  case "source":
286
373
  await handleSource(positionals, flags, userConfig);
287
374
  return;
288
- default:
289
- throw new CliError(`Unknown command: ${resolvedCommand}. Run "skillex help" to see available commands.`);
375
+ default: {
376
+ const suggestion = suggestClosest(resolvedCommand, COMMANDS);
377
+ throw new CliError(suggestion
378
+ ? `Unknown command: ${resolvedCommand}. Did you mean: ${suggestion}? Run "skillex help" for the full list.`
379
+ : `Unknown command: ${resolvedCommand}. Run "skillex help" to see available commands.`);
380
+ }
290
381
  }
291
382
  }
292
383
  // ---------------------------------------------------------------------------
@@ -294,6 +385,7 @@ export async function main(argv) {
294
385
  // ---------------------------------------------------------------------------
295
386
  async function handleInit(flags, userConfig) {
296
387
  const repo = asOptionalString(flags.repo) ?? userConfig.defaultRepo;
388
+ const installRecommended = parseBooleanFlag(flags["install-recommended"], "install-recommended") ?? false;
297
389
  const opts = commonOptions(flags, userConfig);
298
390
  const result = await initProject({
299
391
  ...opts,
@@ -321,7 +413,24 @@ async function handleInit(flags, userConfig) {
321
413
  if (result.lockfile.adapters.detected.length > 0) {
322
414
  output.info(` Detected : ${result.lockfile.adapters.detected.join(", ")}`);
323
415
  }
324
- output.info("\nNext: run 'skillex list' to browse available skills");
416
+ if (installRecommended) {
417
+ const recommended = getRecommendedSkillIds();
418
+ output.info(`\nInstalling ${recommended.length} recommended skill(s)...`);
419
+ const installResult = await installSkills(recommended, {
420
+ ...opts,
421
+ onProgress: (current, total, skillId) => output.progress(current, total, skillId),
422
+ });
423
+ output.success(`Installed ${installResult.installedCount} skill(s) from the recommended pack`);
424
+ for (const skill of installResult.installedSkills) {
425
+ output.info(` + ${skill.id}@${skill.version}`);
426
+ }
427
+ printAutoSyncResult(installResult.autoSync);
428
+ return;
429
+ }
430
+ output.info("\nNext steps:");
431
+ output.info(" • Browse and install interactively: skillex");
432
+ output.info(" • Install a curated starter pack: skillex init --install-recommended");
433
+ output.info(" • List the full catalog: skillex list");
325
434
  }
326
435
  async function handleList(flags, userConfig) {
327
436
  const opts = commonOptions(flags, userConfig);
@@ -352,7 +461,9 @@ async function handleSearch(positionals, flags, userConfig) {
352
461
  const aggregated = await loadProjectCatalogs({ ...opts, ...cacheOptions(opts) });
353
462
  const searchOptions = { query: positionals.join(" ") };
354
463
  const compatibility = asOptionalString(flags.compatibility);
355
- const tag = asOptionalString(flags.tag);
464
+ // `--tag` is canonical; `--tags` is accepted as an alias because earlier README
465
+ // versions documented the plural form. The parser already permits both names.
466
+ const tag = asOptionalString(flags.tag) ?? asOptionalString(flags.tags);
356
467
  if (compatibility)
357
468
  searchOptions.compatibility = compatibility;
358
469
  if (tag)
@@ -426,9 +537,13 @@ async function handleRemove(positionals, flags, userConfig) {
426
537
  for (const skillId of result.missingSkills) {
427
538
  output.warn(`${skillId} is not installed`);
428
539
  }
429
- printAutoSyncResult(result.autoSync);
540
+ // Remove can fan out across multiple previously-synced adapters; report each.
541
+ for (const sync of result.autoSyncs) {
542
+ printAutoSyncResult(sync);
543
+ }
430
544
  }
431
545
  async function handleSync(flags, userConfig) {
546
+ const exitCodeFlag = parseBooleanFlag(flags["exit-code"], "exit-code") ?? false;
432
547
  const result = await syncInstalledSkills(commonOptions(flags, userConfig));
433
548
  if (result.dryRun) {
434
549
  output.info(`Preview: ${result.skillCount} skill(s)`);
@@ -436,6 +551,10 @@ async function handleSync(flags, userConfig) {
436
551
  output.info(` ${entry.adapter} → ${entry.targetPath} [${entry.syncMode}]${entry.changed ? "" : " (no changes)"}`);
437
552
  }
438
553
  process.stdout.write(result.diff);
554
+ // Mirror `git diff --exit-code`: when --exit-code is set, drift is a non-zero exit.
555
+ if (exitCodeFlag && result.changed) {
556
+ process.exitCode = 1;
557
+ }
439
558
  return;
440
559
  }
441
560
  output.success(`Synced ${result.skillCount} skill(s)`);
@@ -461,6 +580,52 @@ async function handleRun(positionals, flags, userConfig) {
461
580
  process.exitCode = exitCode;
462
581
  }
463
582
  }
583
+ async function handleShow(positionals, flags, userConfig) {
584
+ const skillId = positionals[0];
585
+ if (!skillId) {
586
+ throw new CliError("Provide a skill id. Usage: skillex show <skill-id> [--raw|--json]", "SHOW_REQUIRES_SKILL");
587
+ }
588
+ const opts = commonOptions(flags, userConfig);
589
+ const aggregated = await loadProjectCatalogs({ ...opts, ...cacheOptions(opts) });
590
+ const matches = aggregated.skills.filter((s) => s.id === skillId);
591
+ if (matches.length === 0) {
592
+ throw new CliError(`Skill "${skillId}" not found in the configured sources.`, "SHOW_SKILL_NOT_FOUND");
593
+ }
594
+ if (matches.length > 1) {
595
+ const sourceList = matches.map((m) => `${m.source.repo}@${m.source.ref}`).join(", ");
596
+ throw new CliError(`Skill "${skillId}" exists in multiple sources: ${sourceList}. Use --repo to choose one.`, "SHOW_AMBIGUOUS_SOURCE");
597
+ }
598
+ const skill = matches[0];
599
+ const skillFile = skill.entry || "SKILL.md";
600
+ const remotePath = skill.path ? `${skill.path}/${skillFile}` : skillFile;
601
+ const url = buildRawGitHubUrl(skill.source.repo, skill.source.ref, remotePath);
602
+ const body = await fetchText(url, { headers: { Accept: "text/plain" } });
603
+ const raw = parseBooleanFlag(flags.raw, "raw") ?? false;
604
+ if (flags.json === true) {
605
+ output.info(JSON.stringify({
606
+ ...skill,
607
+ entryContent: body,
608
+ }, null, 2));
609
+ return;
610
+ }
611
+ if (raw) {
612
+ process.stdout.write(body.endsWith("\n") ? body : `${body}\n`);
613
+ return;
614
+ }
615
+ output.info(`${skill.name} (${skill.id}) — v${skill.version}`);
616
+ output.info(`Source : ${skill.source.repo}@${skill.source.ref}${skill.source.label ? ` [${skill.source.label}]` : ""}`);
617
+ if (skill.author)
618
+ output.info(`Author : ${skill.author}`);
619
+ if (skill.tags.length)
620
+ output.info(`Tags : ${skill.tags.join(", ")}`);
621
+ if (skill.compatibility.length)
622
+ output.info(`Compatibility: ${skill.compatibility.join(", ")}`);
623
+ output.info(`Files : ${skill.files.length}`);
624
+ output.info("");
625
+ output.info("─".repeat(60));
626
+ output.info("");
627
+ process.stdout.write(body.endsWith("\n") ? body : `${body}\n`);
628
+ }
464
629
  async function handleBrowse(flags, userConfig) {
465
630
  const options = commonOptions(flags, userConfig);
466
631
  const state = await getInstalledSkills(options);
@@ -550,113 +715,12 @@ async function handleStatus(flags, userConfig) {
550
715
  }
551
716
  async function handleDoctor(flags, userConfig) {
552
717
  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);
718
+ const report = await runDoctorChecks(opts);
655
719
  if (flags.json === true) {
656
720
  const jsonResult = {};
657
- for (const check of checks) {
721
+ for (const check of report.checks) {
658
722
  jsonResult[check.name] = {
659
- passed: check.passed,
723
+ passed: check.status !== "fail",
660
724
  message: check.message,
661
725
  ...(check.hint ? { hint: check.hint } : {}),
662
726
  };
@@ -664,21 +728,27 @@ async function handleDoctor(flags, userConfig) {
664
728
  output.info(JSON.stringify(jsonResult, null, 2));
665
729
  }
666
730
  else {
667
- for (const check of checks) {
668
- const symbol = check.passed ? "" : "";
731
+ for (const check of report.checks) {
732
+ const symbol = check.status === "fail" ? "" : check.status === "warn" ? "⚠" : "✓";
669
733
  const line = `${symbol} ${check.name.padEnd(10)} ${check.message}`;
670
- if (check.passed) {
671
- output.info(line);
672
- }
673
- else {
734
+ if (check.status === "fail") {
674
735
  output.error(line);
675
736
  if (check.hint) {
676
737
  output.info(` Hint: ${check.hint}`);
677
738
  }
678
739
  }
740
+ else if (check.status === "warn") {
741
+ output.warn(line);
742
+ if (check.hint) {
743
+ output.info(` Hint: ${check.hint}`);
744
+ }
745
+ }
746
+ else {
747
+ output.info(line);
748
+ }
679
749
  }
680
750
  }
681
- if (anyFailed) {
751
+ if (report.hasFailures) {
682
752
  process.exitCode = 1;
683
753
  }
684
754
  }
@@ -791,13 +861,13 @@ function commonOptions(flags, userConfig = {}) {
791
861
  const skillsDir = asOptionalString(flags["skills-dir"]);
792
862
  const agentSkillsDir = asOptionalString(flags["agent-skills-dir"]);
793
863
  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);
864
+ const autoSync = parseBooleanFlag(flags["auto-sync"], "auto-sync") ?? (userConfig.disableAutoSync ? false : undefined);
865
+ const dryRun = parseBooleanFlag(flags["dry-run"], "dry-run");
866
+ const trust = parseBooleanFlag(flags.trust, "trust");
867
+ const yes = parseBooleanFlag(flags.yes, "yes");
798
868
  const mode = parseSyncMode(asOptionalString(flags.mode));
799
869
  const timeout = parsePositiveInt(asOptionalString(flags.timeout));
800
- const noCache = parseBooleanFlag(flags["no-cache"]);
870
+ const noCache = parseBooleanFlag(flags["no-cache"], "no-cache");
801
871
  if (repo)
802
872
  options.repo = repo;
803
873
  if (ref)
@@ -842,7 +912,7 @@ function cacheOptions(opts) {
842
912
  }
843
913
  function resolveScope(flags) {
844
914
  const rawScope = asOptionalString(flags.scope);
845
- const globalFlag = parseBooleanFlag(flags.global);
915
+ const globalFlag = parseBooleanFlag(flags.global, "global");
846
916
  if (rawScope && rawScope !== "local" && rawScope !== "global") {
847
917
  throw new CliError(`Invalid scope: ${rawScope}. Use "local" or "global".`, "INVALID_SCOPE");
848
918
  }
@@ -854,14 +924,35 @@ function resolveScope(flags) {
854
924
  }
855
925
  return rawScope || DEFAULT_INSTALL_SCOPE;
856
926
  }
857
- function parseArgs(argv) {
927
+ /**
928
+ * Parses argv into a typed `ParsedArgs` shape with strict validation:
929
+ *
930
+ * - Unknown flags raise `UNKNOWN_FLAG` with a "did you mean" suggestion.
931
+ * - Boolean flags (`BOOLEAN_FLAGS`) accept presence-only or `--flag=value` forms.
932
+ * - String flags (`STRING_FLAGS`) require a value via `--flag=value` or
933
+ * `--flag value`. Missing values raise `MISSING_FLAG_VALUE`.
934
+ * - The literal `--` token marks end-of-options; remaining tokens become
935
+ * `positionalAfter` and are forwarded to handlers (used by `run` to pass
936
+ * arguments to the underlying script without flag interpretation).
937
+ */
938
+ export function parseArgs(argv) {
858
939
  const flags = {};
859
940
  const positionals = [];
941
+ const positionalAfter = [];
860
942
  let command;
943
+ let endOfOptions = false;
861
944
  for (let index = 0; index < argv.length; index += 1) {
862
945
  const token = argv[index];
863
946
  if (token === undefined)
864
947
  continue;
948
+ if (endOfOptions) {
949
+ positionalAfter.push(token);
950
+ continue;
951
+ }
952
+ if (token === "--") {
953
+ endOfOptions = true;
954
+ continue;
955
+ }
865
956
  if (!command && !token.startsWith("-")) {
866
957
  command = token;
867
958
  continue;
@@ -871,25 +962,39 @@ function parseArgs(argv) {
871
962
  continue;
872
963
  }
873
964
  if (token.startsWith("--")) {
874
- const [rawKey, inlineValue] = token.slice(2).split("=", 2);
965
+ const eq = token.indexOf("=");
966
+ const rawKey = eq === -1 ? token.slice(2) : token.slice(2, eq);
967
+ const inlineValue = eq === -1 ? undefined : token.slice(eq + 1);
875
968
  if (!rawKey)
876
969
  continue;
970
+ if (!KNOWN_FLAGS.has(rawKey)) {
971
+ const suggestion = suggestClosest(rawKey, [...KNOWN_FLAGS]);
972
+ throw new CliError(suggestion
973
+ ? `Unknown flag: --${rawKey}. Did you mean --${suggestion}?`
974
+ : `Unknown flag: --${rawKey}. Run 'skillex --help' to list flags.`, "UNKNOWN_FLAG");
975
+ }
877
976
  if (inlineValue !== undefined) {
878
977
  flags[rawKey] = inlineValue;
879
978
  continue;
880
979
  }
881
- const next = argv[index + 1];
882
- if (!next || next.startsWith("-")) {
980
+ // Boolean flag without an inline value: presence = true.
981
+ if (BOOLEAN_FLAGS.has(rawKey)) {
883
982
  flags[rawKey] = true;
884
983
  continue;
885
984
  }
985
+ // String flag: require a following value that is not another flag and not the
986
+ // end-of-options sentinel.
987
+ const next = argv[index + 1];
988
+ if (next === undefined || next === "--" || next.startsWith("-")) {
989
+ throw new CliError(`Missing value for --${rawKey}. Pass --${rawKey} <value> or --${rawKey}=<value>.`, "MISSING_FLAG_VALUE");
990
+ }
886
991
  flags[rawKey] = next;
887
992
  index += 1;
888
993
  continue;
889
994
  }
890
995
  positionals.push(token);
891
996
  }
892
- return { command, positionals, flags };
997
+ return { command, positionals, positionalAfter, flags };
893
998
  }
894
999
  function printHelp() {
895
1000
  output.info(`skillex — AI agent skill manager
@@ -943,7 +1048,7 @@ function truncate(value, maxLength) {
943
1048
  return value;
944
1049
  return `${value.slice(0, maxLength - 3)}...`;
945
1050
  }
946
- function parseBooleanFlag(value) {
1051
+ function parseBooleanFlag(value, flagName) {
947
1052
  if (value === undefined)
948
1053
  return undefined;
949
1054
  if (value === true)
@@ -953,7 +1058,8 @@ function parseBooleanFlag(value) {
953
1058
  return true;
954
1059
  if (["false", "0", "no", "off"].includes(normalized))
955
1060
  return false;
956
- throw new CliError(`Invalid boolean value: ${value}`, "INVALID_BOOLEAN_FLAG");
1061
+ const target = flagName ? `--${flagName}` : "boolean flag";
1062
+ throw new CliError(`Invalid value "${value}" for ${target}. Use true, false, yes, no, on, off, 1, or 0.`, "INVALID_BOOLEAN_FLAG");
957
1063
  }
958
1064
  function parsePositiveInt(value) {
959
1065
  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 {
@@ -0,0 +1,60 @@
1
+ /**
2
+ * Direct-GitHub install path. Parallels the catalog install path but reads
3
+ * `skill.json` (or `SKILL.md` frontmatter) directly from a GitHub
4
+ * repository, allowing users to install skills that are not yet published
5
+ * in any catalog source.
6
+ */
7
+ import type { DirectGitHubRef, SkillManifest } from "./types.js";
8
+ /** Resolved direct-install payload as returned by `fetchDirectGitHubSkill`. */
9
+ export interface DirectInstallPayload {
10
+ manifest: SkillManifest;
11
+ repo: string;
12
+ ref: string;
13
+ source: string;
14
+ }
15
+ /** Trust-prompt options accepted by `confirmDirectInstall`. */
16
+ export interface ConfirmDirectInstallOptions {
17
+ trust?: boolean | undefined;
18
+ confirm?: (() => Promise<boolean>) | undefined;
19
+ warn?: ((message: string) => void) | undefined;
20
+ }
21
+ /**
22
+ * Parses a direct GitHub install reference in `owner/repo[@ref]` format.
23
+ *
24
+ * The ref segment (when present) MUST match `^[A-Za-z0-9_.\-/]+$`. Empty
25
+ * refs (e.g. `owner/repo@`) and refs containing whitespace, newlines, or
26
+ * shell metacharacters are rejected with `CliError("INVALID_DIRECT_REF")`
27
+ * rather than silently defaulting to `main`.
28
+ *
29
+ * @param input - User-supplied install argument.
30
+ * @returns Parsed direct GitHub reference or `null` when the value is not a direct ref.
31
+ * @throws {CliError} When the value looks like a direct ref but the ref portion is invalid.
32
+ */
33
+ export declare function parseDirectGitHubRef(input: string): DirectGitHubRef | null;
34
+ /**
35
+ * Parses a `github:owner/repo@ref` source string from the lockfile back into a
36
+ * `DirectGitHubRef`.
37
+ */
38
+ export declare function parseGitHubSource(source: string): DirectGitHubRef | null;
39
+ /**
40
+ * Fetches the manifest for a direct-install skill, falling back to SKILL.md
41
+ * frontmatter when no `skill.json` is present at the repository root.
42
+ */
43
+ export declare function fetchDirectGitHubSkill(reference: DirectGitHubRef): Promise<DirectInstallPayload>;
44
+ /**
45
+ * Downloads the resolved direct-install skill into the managed skills store.
46
+ */
47
+ export declare function downloadDirectGitHubSkill(skill: DirectInstallPayload, skillsDirPath: string): Promise<void>;
48
+ /**
49
+ * Promotes a partial direct-install manifest into a fully-typed `SkillManifest`,
50
+ * providing safe defaults for missing fields.
51
+ */
52
+ export declare function normalizeDirectManifest(manifest: Partial<SkillManifest> & {
53
+ scripts?: Record<string, string>;
54
+ }, reference: DirectGitHubRef): SkillManifest;
55
+ /**
56
+ * Prompts the user to confirm a direct GitHub install (skipped when the
57
+ * caller passes `trust: true`). Throws `InstallError` with code
58
+ * `INSTALL_CANCELLED` on rejection.
59
+ */
60
+ export declare function confirmDirectInstall(skillRef: string, options: ConfirmDirectInstallOptions): Promise<void>;