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.
- package/CHANGELOG.md +286 -1
- package/README.md +82 -16
- package/dist/auto-sync.d.ts +66 -0
- package/dist/auto-sync.js +91 -0
- package/dist/catalog.js +5 -29
- package/dist/cli.d.ts +13 -0
- package/dist/cli.js +266 -144
- package/dist/confirm.js +3 -1
- package/dist/direct-github.d.ts +60 -0
- package/dist/direct-github.js +177 -0
- package/dist/doctor.d.ts +31 -0
- package/dist/doctor.js +172 -0
- package/dist/downloader.d.ts +42 -0
- package/dist/downloader.js +41 -0
- package/dist/fs.d.ts +21 -1
- package/dist/fs.js +30 -3
- package/dist/http.d.ts +28 -7
- package/dist/http.js +143 -42
- package/dist/install.d.ts +23 -9
- package/dist/install.js +75 -348
- package/dist/lockfile.d.ts +46 -0
- package/dist/lockfile.js +169 -0
- package/dist/output.d.ts +11 -0
- package/dist/output.js +49 -0
- package/dist/recommended.d.ts +13 -0
- package/dist/recommended.js +21 -0
- package/dist/runner.js +9 -9
- package/dist/skill.d.ts +2 -0
- package/dist/skill.js +3 -0
- package/dist/sync.js +12 -9
- package/dist/types.d.ts +39 -0
- package/dist/types.js +28 -0
- package/dist/ui.js +1 -1
- package/dist/user-config.d.ts +5 -0
- package/dist/user-config.js +22 -1
- package/dist/web-ui.js +5 -0
- package/dist-ui/assets/CatalogPage-CKEfRSvG.js +1 -0
- package/dist-ui/assets/CatalogPage-W5MqylAz.css +1 -0
- package/dist-ui/assets/DoctorPage-C92pEVl_.js +1 -0
- package/dist-ui/assets/Skeleton-BISmLuhY.js +1 -0
- package/dist-ui/assets/Skeleton-_Ooiw1nN.css +1 -0
- package/dist-ui/assets/SkillDetailPage-CBAaWpcc.css +1 -0
- package/dist-ui/assets/SkillDetailPage-CWGjTH2M.js +1 -0
- package/dist-ui/assets/{index-UBECch6X.css → index-CWm7zQTg.css} +1 -1
- package/dist-ui/assets/index-DAVP4Xp_.js +26 -0
- package/dist-ui/assets/recommended-D_i10hwH.js +1 -0
- package/dist-ui/index.html +2 -2
- package/package.json +6 -2
- package/dist-ui/assets/CatalogPage-B_qic36n.js +0 -1
- package/dist-ui/assets/SkillDetailPage-BJ3onKk4.js +0 -1
- 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 {
|
|
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,
|
|
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>
|
|
23
|
-
--ref <ref>
|
|
24
|
-
--adapter <id>
|
|
25
|
-
--auto-sync
|
|
26
|
-
--
|
|
27
|
-
--
|
|
28
|
-
--
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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.
|
|
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.
|
|
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.
|
|
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 (
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
882
|
-
if (
|
|
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
|
-
|
|
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("
|
|
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 {
|