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.
- package/CHANGELOG.md +262 -1
- package/README.md +57 -10
- 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 +247 -141
- 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-CbtMTkxd.js +1 -0
- package/dist-ui/assets/CatalogPage-W5MqylAz.css +1 -0
- package/dist-ui/assets/DoctorPage-oUZyX91t.js +1 -0
- package/dist-ui/assets/Skeleton-B_xm5L3P.js +1 -0
- package/dist-ui/assets/Skeleton-_Ooiw1nN.css +1 -0
- package/dist-ui/assets/SkillDetailPage-5JHQLq3q.js +1 -0
- package/dist-ui/assets/SkillDetailPage-CBAaWpcc.css +1 -0
- package/dist-ui/assets/{index-UBECch6X.css → index-CWm7zQTg.css} +1 -1
- package/dist-ui/assets/index-I0b-syhc.js +26 -0
- package/dist-ui/assets/recommended-D_i10hwH.js +1 -0
- package/dist-ui/index.html +2 -2
- package/package.json +2 -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,81 @@
|
|
|
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
|
+
]);
|
|
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>
|
|
23
|
-
--ref <ref>
|
|
24
|
-
--adapter <id>
|
|
25
|
-
--auto-sync
|
|
26
|
-
--
|
|
27
|
-
--
|
|
28
|
-
--
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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.
|
|
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.
|
|
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.
|
|
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 (
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
882
|
-
if (
|
|
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
|
-
|
|
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("
|
|
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>;
|