skillex 0.2.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 +64 -0
- package/LICENSE +21 -0
- package/README.md +631 -0
- package/bin/skillex.js +11 -0
- package/dist/adapters.d.ts +60 -0
- package/dist/adapters.js +213 -0
- package/dist/catalog.d.ts +73 -0
- package/dist/catalog.js +356 -0
- package/dist/cli.d.ts +7 -0
- package/dist/cli.js +885 -0
- package/dist/config.d.ts +17 -0
- package/dist/config.js +25 -0
- package/dist/confirm.d.ts +8 -0
- package/dist/confirm.js +24 -0
- package/dist/fs.d.ts +79 -0
- package/dist/fs.js +184 -0
- package/dist/http.d.ts +43 -0
- package/dist/http.js +123 -0
- package/dist/install.d.ts +115 -0
- package/dist/install.js +895 -0
- package/dist/output.d.ts +46 -0
- package/dist/output.js +78 -0
- package/dist/runner.d.ts +31 -0
- package/dist/runner.js +94 -0
- package/dist/skill.d.ts +23 -0
- package/dist/skill.js +61 -0
- package/dist/sync.d.ts +41 -0
- package/dist/sync.js +384 -0
- package/dist/types.d.ts +442 -0
- package/dist/types.js +83 -0
- package/dist/ui.d.ts +43 -0
- package/dist/ui.js +78 -0
- package/dist/user-config.d.ts +39 -0
- package/dist/user-config.js +54 -0
- package/package.json +93 -0
package/dist/cli.js
ADDED
|
@@ -0,0 +1,885 @@
|
|
|
1
|
+
import * as path from "node:path";
|
|
2
|
+
import { listAdapters } from "./adapters.js";
|
|
3
|
+
import { computeCatalogCacheKey, loadCatalog, readCatalogCache, searchCatalogSkills, } from "./catalog.js";
|
|
4
|
+
import { DEFAULT_AGENT_SKILLS_DIR, getStatePaths } from "./config.js";
|
|
5
|
+
import { addProjectSource, getInstalledSkills, initProject, installSkills, listProjectSources, loadProjectCatalogs, removeProjectSource, removeSkills, resolveProjectSource, syncInstalledSkills, updateInstalledSkills, } from "./install.js";
|
|
6
|
+
import * as output from "./output.js";
|
|
7
|
+
import { setVerbose } from "./output.js";
|
|
8
|
+
import { parseSkillCommandReference, runSkillScript } from "./runner.js";
|
|
9
|
+
import { runInteractiveUi } from "./ui.js";
|
|
10
|
+
import { CliError } from "./types.js";
|
|
11
|
+
import { VALID_CONFIG_KEYS, readUserConfig, writeUserConfig } from "./user-config.js";
|
|
12
|
+
// ---------------------------------------------------------------------------
|
|
13
|
+
// Per-command help text
|
|
14
|
+
// ---------------------------------------------------------------------------
|
|
15
|
+
const COMMAND_HELP = {
|
|
16
|
+
init: `Usage: skillex init [--repo <owner/repo>] [options]
|
|
17
|
+
|
|
18
|
+
Initialize the local Skillex workspace.
|
|
19
|
+
|
|
20
|
+
Options:
|
|
21
|
+
--repo <owner/repo> GitHub repository with skills (default: lgili/skillex)
|
|
22
|
+
--ref <ref> Branch, tag, or commit (default: main)
|
|
23
|
+
--adapter <id> Force a specific adapter
|
|
24
|
+
--auto-sync Enable auto-sync after install/update/remove
|
|
25
|
+
--cwd <path> Target project directory (default: current directory)
|
|
26
|
+
|
|
27
|
+
Example:
|
|
28
|
+
skillex init
|
|
29
|
+
skillex init --repo myorg/my-skills`,
|
|
30
|
+
list: `Usage: skillex list [options]
|
|
31
|
+
|
|
32
|
+
List all skills in the configured sources.
|
|
33
|
+
|
|
34
|
+
Options:
|
|
35
|
+
--repo <owner/repo> GitHub repository (limits this command to one source)
|
|
36
|
+
--ref <ref> Branch, tag, or commit
|
|
37
|
+
--no-cache Bypass local catalog cache
|
|
38
|
+
--json Output results as JSON
|
|
39
|
+
|
|
40
|
+
Example:
|
|
41
|
+
skillex list
|
|
42
|
+
skillex list --repo myorg/my-skills --json`,
|
|
43
|
+
search: `Usage: skillex search [query] [options]
|
|
44
|
+
|
|
45
|
+
Search skills by text, compatibility, or tags.
|
|
46
|
+
|
|
47
|
+
Options:
|
|
48
|
+
--repo <owner/repo> GitHub repository (limits this command to one source)
|
|
49
|
+
--compatibility <id> Filter by adapter compatibility
|
|
50
|
+
--tag <tag> Filter by tag
|
|
51
|
+
--no-cache Bypass local catalog cache
|
|
52
|
+
--json Output results as JSON
|
|
53
|
+
|
|
54
|
+
Example:
|
|
55
|
+
skillex search git --compatibility claude`,
|
|
56
|
+
install: `Usage: skillex install <skill-id...> [options]
|
|
57
|
+
skillex install --all [options]
|
|
58
|
+
skillex install <owner/repo[@ref]> [options]
|
|
59
|
+
|
|
60
|
+
Install one or more skills from the catalog or directly from GitHub.
|
|
61
|
+
|
|
62
|
+
Options:
|
|
63
|
+
--all Install all skills from the catalog
|
|
64
|
+
--repo <owner/repo> GitHub repository (limits this command to one source)
|
|
65
|
+
--ref <ref> Branch, tag, or commit
|
|
66
|
+
--trust Skip confirmation for direct GitHub installs
|
|
67
|
+
--adapter <id> Target adapter
|
|
68
|
+
--no-cache Bypass local catalog cache
|
|
69
|
+
|
|
70
|
+
Example:
|
|
71
|
+
skillex install git-master code-review
|
|
72
|
+
skillex install --all --repo myorg/my-skills
|
|
73
|
+
skillex install octocat/my-skill@main --trust`,
|
|
74
|
+
update: `Usage: skillex update [skill-id...] [options]
|
|
75
|
+
|
|
76
|
+
Update installed skills to the latest catalog version.
|
|
77
|
+
|
|
78
|
+
Options:
|
|
79
|
+
--repo <owner/repo> GitHub repository
|
|
80
|
+
--no-cache Bypass local catalog cache
|
|
81
|
+
|
|
82
|
+
Example:
|
|
83
|
+
skillex update
|
|
84
|
+
skillex update git-master`,
|
|
85
|
+
remove: `Usage: skillex remove <skill-id...>
|
|
86
|
+
|
|
87
|
+
Remove installed skills from the local workspace.
|
|
88
|
+
|
|
89
|
+
Example:
|
|
90
|
+
skillex remove git-master code-review`,
|
|
91
|
+
sync: `Usage: skillex sync [options]
|
|
92
|
+
|
|
93
|
+
Synchronize installed skills to adapter target files.
|
|
94
|
+
|
|
95
|
+
Options:
|
|
96
|
+
--adapter <id> Target adapter (overrides saved config)
|
|
97
|
+
--dry-run Preview changes without writing to disk
|
|
98
|
+
--mode <symlink|copy> Sync write mode (default: symlink)
|
|
99
|
+
|
|
100
|
+
Example:
|
|
101
|
+
skillex sync
|
|
102
|
+
skillex sync --adapter cursor --dry-run`,
|
|
103
|
+
run: `Usage: skillex run <skill-id:command> [options]
|
|
104
|
+
|
|
105
|
+
Execute a script bundled inside an installed skill.
|
|
106
|
+
|
|
107
|
+
Options:
|
|
108
|
+
--yes Skip confirmation prompt
|
|
109
|
+
--timeout <seconds> Script timeout in seconds (default: 30)
|
|
110
|
+
|
|
111
|
+
Example:
|
|
112
|
+
skillex run git-master:cleanup --yes`,
|
|
113
|
+
ui: `Usage: skillex ui [options]
|
|
114
|
+
|
|
115
|
+
Open the interactive terminal browser to browse and install skills.
|
|
116
|
+
|
|
117
|
+
Options:
|
|
118
|
+
--repo <owner/repo> GitHub repository
|
|
119
|
+
--no-cache Bypass local catalog cache
|
|
120
|
+
|
|
121
|
+
Example:
|
|
122
|
+
skillex ui`,
|
|
123
|
+
status: `Usage: skillex status [options]
|
|
124
|
+
|
|
125
|
+
Show the installation status of the current workspace.
|
|
126
|
+
|
|
127
|
+
Options:
|
|
128
|
+
--cwd <path> Target project directory
|
|
129
|
+
--json Output as JSON
|
|
130
|
+
|
|
131
|
+
Example:
|
|
132
|
+
skillex status
|
|
133
|
+
skillex status --json`,
|
|
134
|
+
doctor: `Usage: skillex doctor [options]
|
|
135
|
+
|
|
136
|
+
Run environment and configuration checks to diagnose issues.
|
|
137
|
+
|
|
138
|
+
Options:
|
|
139
|
+
--json Output results as JSON
|
|
140
|
+
|
|
141
|
+
Example:
|
|
142
|
+
skillex doctor`,
|
|
143
|
+
config: `Usage: skillex config set <key> <value>
|
|
144
|
+
skillex config get <key>
|
|
145
|
+
|
|
146
|
+
Manage global Skillex preferences stored in ~/.askillrc.json.
|
|
147
|
+
CLI flags always take precedence over these values.
|
|
148
|
+
|
|
149
|
+
Valid keys: ${VALID_CONFIG_KEYS.join(", ")}
|
|
150
|
+
|
|
151
|
+
Precedence order: CLI flag > GITHUB_TOKEN env > global config > default
|
|
152
|
+
|
|
153
|
+
Example:
|
|
154
|
+
skillex config set defaultRepo myorg/my-skills
|
|
155
|
+
skillex config get defaultRepo`,
|
|
156
|
+
source: `Usage: skillex source <add|remove|list> [repo] [options]
|
|
157
|
+
|
|
158
|
+
Manage the workspace source list.
|
|
159
|
+
|
|
160
|
+
Commands:
|
|
161
|
+
skillex source list
|
|
162
|
+
skillex source add <owner/repo> [--ref main] [--label work]
|
|
163
|
+
skillex source remove <owner/repo>
|
|
164
|
+
|
|
165
|
+
Options:
|
|
166
|
+
--ref <ref> Branch, tag, or commit (default: main)
|
|
167
|
+
--label <label> Human-readable source label
|
|
168
|
+
|
|
169
|
+
Example:
|
|
170
|
+
skillex source add myorg/my-skills --label work`,
|
|
171
|
+
};
|
|
172
|
+
// ---------------------------------------------------------------------------
|
|
173
|
+
// Entrypoint
|
|
174
|
+
// ---------------------------------------------------------------------------
|
|
175
|
+
/**
|
|
176
|
+
* Runs the Skillex CLI entrypoint.
|
|
177
|
+
*
|
|
178
|
+
* @param argv - Raw CLI arguments without the Node executable prefix.
|
|
179
|
+
* @throws {CliError} When the command or flag values are invalid.
|
|
180
|
+
*/
|
|
181
|
+
export async function main(argv) {
|
|
182
|
+
const { command, positionals, flags } = parseArgs(argv);
|
|
183
|
+
// Apply verbose flag early so debug output works from the start
|
|
184
|
+
if (flags.verbose === true || flags.v === true) {
|
|
185
|
+
setVerbose(true);
|
|
186
|
+
}
|
|
187
|
+
// Load global user config once at startup
|
|
188
|
+
const userConfig = await readUserConfig();
|
|
189
|
+
// Apply githubToken from user config as env fallback (env always wins)
|
|
190
|
+
if (userConfig.githubToken && !process.env.GITHUB_TOKEN) {
|
|
191
|
+
process.env.GITHUB_TOKEN = userConfig.githubToken;
|
|
192
|
+
}
|
|
193
|
+
// Per-command --help
|
|
194
|
+
if (flags.help === true && command && command !== "help") {
|
|
195
|
+
const helpText = COMMAND_HELP[command];
|
|
196
|
+
if (helpText) {
|
|
197
|
+
output.info(helpText);
|
|
198
|
+
}
|
|
199
|
+
else {
|
|
200
|
+
printHelp();
|
|
201
|
+
}
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
// Resolve command aliases
|
|
205
|
+
const resolvedCommand = resolveAlias(command);
|
|
206
|
+
switch (resolvedCommand) {
|
|
207
|
+
case "help":
|
|
208
|
+
case undefined:
|
|
209
|
+
printHelp();
|
|
210
|
+
return;
|
|
211
|
+
case "init":
|
|
212
|
+
await handleInit(flags, userConfig);
|
|
213
|
+
return;
|
|
214
|
+
case "list":
|
|
215
|
+
await handleList(flags, userConfig);
|
|
216
|
+
return;
|
|
217
|
+
case "search":
|
|
218
|
+
await handleSearch(positionals, flags, userConfig);
|
|
219
|
+
return;
|
|
220
|
+
case "install":
|
|
221
|
+
await handleInstall(positionals, flags, userConfig);
|
|
222
|
+
return;
|
|
223
|
+
case "update":
|
|
224
|
+
await handleUpdate(positionals, flags, userConfig);
|
|
225
|
+
return;
|
|
226
|
+
case "remove":
|
|
227
|
+
await handleRemove(positionals, flags, userConfig);
|
|
228
|
+
return;
|
|
229
|
+
case "sync":
|
|
230
|
+
await handleSync(flags, userConfig);
|
|
231
|
+
return;
|
|
232
|
+
case "run":
|
|
233
|
+
await handleRun(positionals, flags, userConfig);
|
|
234
|
+
return;
|
|
235
|
+
case "ui":
|
|
236
|
+
await handleUi(flags, userConfig);
|
|
237
|
+
return;
|
|
238
|
+
case "status":
|
|
239
|
+
await handleStatus(flags, userConfig);
|
|
240
|
+
return;
|
|
241
|
+
case "doctor":
|
|
242
|
+
await handleDoctor(flags, userConfig);
|
|
243
|
+
return;
|
|
244
|
+
case "config":
|
|
245
|
+
await handleConfig(positionals, flags);
|
|
246
|
+
return;
|
|
247
|
+
case "source":
|
|
248
|
+
await handleSource(positionals, flags, userConfig);
|
|
249
|
+
return;
|
|
250
|
+
default:
|
|
251
|
+
throw new CliError(`Unknown command: ${resolvedCommand}. Run "skillex help" to see available commands.`);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
// ---------------------------------------------------------------------------
|
|
255
|
+
// Command handlers
|
|
256
|
+
// ---------------------------------------------------------------------------
|
|
257
|
+
async function handleInit(flags, userConfig) {
|
|
258
|
+
const repo = asOptionalString(flags.repo) ?? userConfig.defaultRepo;
|
|
259
|
+
const opts = commonOptions(flags, userConfig);
|
|
260
|
+
const result = await initProject({
|
|
261
|
+
...opts,
|
|
262
|
+
...(repo ? { repo } : {}),
|
|
263
|
+
});
|
|
264
|
+
if (result.created) {
|
|
265
|
+
output.success(`Initialized at ${result.statePaths.stateDir}`);
|
|
266
|
+
}
|
|
267
|
+
else {
|
|
268
|
+
output.info(`Already configured at ${result.statePaths.stateDir}`);
|
|
269
|
+
}
|
|
270
|
+
const primarySource = result.lockfile.sources[0];
|
|
271
|
+
output.info(` Source : ${primarySource?.repo}@${primarySource?.ref}`);
|
|
272
|
+
if (result.lockfile.sources.length > 1) {
|
|
273
|
+
output.info(` Sources : ${result.lockfile.sources.length}`);
|
|
274
|
+
}
|
|
275
|
+
if (result.lockfile.adapters.active) {
|
|
276
|
+
output.info(` Adapter : ${result.lockfile.adapters.active}`);
|
|
277
|
+
}
|
|
278
|
+
else {
|
|
279
|
+
output.warn("No adapter detected in this directory.\n" +
|
|
280
|
+
` Use --adapter <id> to specify one. Available: ${listAdapters().map((a) => a.id).join(", ")}`);
|
|
281
|
+
}
|
|
282
|
+
output.info(` Auto-sync: ${result.lockfile.settings.autoSync ? "enabled" : "disabled"}`);
|
|
283
|
+
output.info("\nNext: run 'skillex list' to browse available skills");
|
|
284
|
+
}
|
|
285
|
+
async function handleList(flags, userConfig) {
|
|
286
|
+
const opts = commonOptions(flags, userConfig);
|
|
287
|
+
const aggregated = await loadProjectCatalogs({ ...opts, ...cacheOptions(opts) });
|
|
288
|
+
if (aggregated.skills.length === 0) {
|
|
289
|
+
output.info("No skills found.");
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
292
|
+
if (flags.json === true) {
|
|
293
|
+
output.info(JSON.stringify(aggregated.skills, null, 2));
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
296
|
+
for (const source of aggregated.sources) {
|
|
297
|
+
output.info(`Source: ${source.repo}@${source.ref}${source.label ? ` [${source.label}]` : ""}`);
|
|
298
|
+
printTable(aggregated.skills
|
|
299
|
+
.filter((skill) => skill.source.repo === source.repo && skill.source.ref === source.ref)
|
|
300
|
+
.map((skill) => ({
|
|
301
|
+
id: skill.id,
|
|
302
|
+
version: skill.version,
|
|
303
|
+
name: skill.name,
|
|
304
|
+
description: truncate(skill.description, 96),
|
|
305
|
+
})));
|
|
306
|
+
output.info("");
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
async function handleSearch(positionals, flags, userConfig) {
|
|
310
|
+
const opts = commonOptions(flags, userConfig);
|
|
311
|
+
const aggregated = await loadProjectCatalogs({ ...opts, ...cacheOptions(opts) });
|
|
312
|
+
const searchOptions = { query: positionals.join(" ") };
|
|
313
|
+
const compatibility = asOptionalString(flags.compatibility);
|
|
314
|
+
const tag = asOptionalString(flags.tag);
|
|
315
|
+
if (compatibility)
|
|
316
|
+
searchOptions.compatibility = compatibility;
|
|
317
|
+
if (tag)
|
|
318
|
+
searchOptions.tags = tag;
|
|
319
|
+
const filtered = searchCatalogSkills(aggregated.skills, searchOptions);
|
|
320
|
+
if (filtered.length === 0) {
|
|
321
|
+
output.info("No skills match the given filters.");
|
|
322
|
+
return;
|
|
323
|
+
}
|
|
324
|
+
if (flags.json === true) {
|
|
325
|
+
output.info(JSON.stringify(filtered, null, 2));
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
328
|
+
for (const source of aggregated.sources) {
|
|
329
|
+
const sourceMatches = filtered.filter((skill) => skill.source.repo === source.repo && skill.source.ref === source.ref);
|
|
330
|
+
if (sourceMatches.length === 0) {
|
|
331
|
+
continue;
|
|
332
|
+
}
|
|
333
|
+
output.info(`Source: ${source.repo}@${source.ref}${source.label ? ` [${source.label}]` : ""}`);
|
|
334
|
+
printTable(sourceMatches.map((skill) => ({
|
|
335
|
+
id: skill.id,
|
|
336
|
+
version: skill.version,
|
|
337
|
+
compatibility: skill.compatibility.join(","),
|
|
338
|
+
description: truncate(skill.description, 72),
|
|
339
|
+
})));
|
|
340
|
+
output.info("");
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
async function handleInstall(positionals, flags, userConfig) {
|
|
344
|
+
const installAll = Boolean(flags.all);
|
|
345
|
+
const opts = commonOptions(flags, userConfig);
|
|
346
|
+
const result = await installSkills(positionals, {
|
|
347
|
+
...opts,
|
|
348
|
+
installAll,
|
|
349
|
+
onProgress: (current, total, skillId) => {
|
|
350
|
+
output.info(`[${current}/${total}] Installing ${skillId}...`);
|
|
351
|
+
},
|
|
352
|
+
});
|
|
353
|
+
output.success(`Installed ${result.installedCount} skill(s) to ${result.statePaths.stateDir}`);
|
|
354
|
+
for (const skill of result.installedSkills) {
|
|
355
|
+
output.info(` + ${skill.id}@${skill.version}`);
|
|
356
|
+
}
|
|
357
|
+
printAutoSyncResult(result.autoSync);
|
|
358
|
+
}
|
|
359
|
+
async function handleUpdate(positionals, flags, userConfig) {
|
|
360
|
+
const opts = commonOptions(flags, userConfig);
|
|
361
|
+
const result = await updateInstalledSkills(positionals, opts);
|
|
362
|
+
if (result.updatedSkills.length === 0) {
|
|
363
|
+
output.info("No skills updated.");
|
|
364
|
+
}
|
|
365
|
+
else {
|
|
366
|
+
output.success(`Updated ${result.updatedSkills.length} skill(s)`);
|
|
367
|
+
for (const skill of result.updatedSkills) {
|
|
368
|
+
output.info(` ↑ ${skill.id}@${skill.version}`);
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
for (const skillId of result.missingFromCatalog) {
|
|
372
|
+
output.warn(`${skillId} no longer exists in the remote catalog`);
|
|
373
|
+
}
|
|
374
|
+
printAutoSyncResult(result.autoSync);
|
|
375
|
+
}
|
|
376
|
+
async function handleRemove(positionals, flags, userConfig) {
|
|
377
|
+
const result = await removeSkills(positionals, commonOptions(flags, userConfig));
|
|
378
|
+
if (result.removedSkills.length > 0) {
|
|
379
|
+
output.success(`Removed ${result.removedSkills.length} skill(s)`);
|
|
380
|
+
for (const skillId of result.removedSkills) {
|
|
381
|
+
output.info(` - ${skillId}`);
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
for (const skillId of result.missingSkills) {
|
|
385
|
+
output.warn(`${skillId} is not installed`);
|
|
386
|
+
}
|
|
387
|
+
printAutoSyncResult(result.autoSync);
|
|
388
|
+
}
|
|
389
|
+
async function handleSync(flags, userConfig) {
|
|
390
|
+
const result = await syncInstalledSkills(commonOptions(flags, userConfig));
|
|
391
|
+
if (result.dryRun) {
|
|
392
|
+
output.info(`Preview: ${result.skillCount} skill(s) → ${result.sync.adapter}`);
|
|
393
|
+
output.info(`Target file : ${result.sync.targetPath}`);
|
|
394
|
+
output.info(`Sync mode : ${result.syncMode}`);
|
|
395
|
+
process.stdout.write(result.diff);
|
|
396
|
+
return;
|
|
397
|
+
}
|
|
398
|
+
output.success(`Synced ${result.skillCount} skill(s) → ${result.sync.adapter}`);
|
|
399
|
+
output.info(`Target file : ${result.sync.targetPath}`);
|
|
400
|
+
output.info(`Sync mode : ${result.syncMode}`);
|
|
401
|
+
if (!result.changed) {
|
|
402
|
+
output.info("No changes to the target file.");
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
async function handleRun(positionals, flags, userConfig) {
|
|
406
|
+
const target = positionals[0];
|
|
407
|
+
if (!target) {
|
|
408
|
+
throw new CliError('Provide a target in the format "skill-id:command".', "RUN_REQUIRES_TARGET");
|
|
409
|
+
}
|
|
410
|
+
const parsed = parseSkillCommandReference(target);
|
|
411
|
+
const exitCode = await runSkillScript(parsed.skillId, parsed.command, {
|
|
412
|
+
...commonOptions(flags, userConfig),
|
|
413
|
+
yes: parseBooleanFlag(flags.yes) || false,
|
|
414
|
+
timeout: parsePositiveInt(asOptionalString(flags.timeout)),
|
|
415
|
+
});
|
|
416
|
+
if (exitCode !== 0) {
|
|
417
|
+
process.exitCode = exitCode;
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
async function handleUi(flags, userConfig) {
|
|
421
|
+
const options = commonOptions(flags, userConfig);
|
|
422
|
+
const state = await getInstalledSkills(options);
|
|
423
|
+
const source = await resolveProjectSource(options);
|
|
424
|
+
const catalog = await loadCatalog({ ...source, ...cacheOptions(options) });
|
|
425
|
+
if (catalog.skills.length === 0) {
|
|
426
|
+
output.info("No skills available in the catalog.");
|
|
427
|
+
return;
|
|
428
|
+
}
|
|
429
|
+
const installedIds = Object.keys(state?.installed || {});
|
|
430
|
+
const selection = await runInteractiveUi({ skills: catalog.skills, installedIds });
|
|
431
|
+
if (selection.visibleIds.length === 0) {
|
|
432
|
+
output.info(selection.query
|
|
433
|
+
? `No skills match the filter "${selection.query}".`
|
|
434
|
+
: "No skills available in the catalog.");
|
|
435
|
+
return;
|
|
436
|
+
}
|
|
437
|
+
const installResult = selection.toInstall.length > 0 ? await installSkills(selection.toInstall, options) : null;
|
|
438
|
+
const removeResult = selection.toRemove.length > 0 ? await removeSkills(selection.toRemove, options) : null;
|
|
439
|
+
if (!installResult && !removeResult) {
|
|
440
|
+
output.info("No changes applied.");
|
|
441
|
+
return;
|
|
442
|
+
}
|
|
443
|
+
output.success("UI summary:");
|
|
444
|
+
if (installResult) {
|
|
445
|
+
output.info(` Installed : ${installResult.installedSkills.map((s) => s.id).join(", ")}`);
|
|
446
|
+
}
|
|
447
|
+
if (removeResult) {
|
|
448
|
+
output.info(` Removed : ${removeResult.removedSkills.join(", ")}`);
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
async function handleStatus(flags, userConfig) {
|
|
452
|
+
const state = await getInstalledSkills(commonOptions(flags, userConfig));
|
|
453
|
+
if (!state) {
|
|
454
|
+
output.warn("No local installation found. Run: skillex init");
|
|
455
|
+
return;
|
|
456
|
+
}
|
|
457
|
+
if (flags.json === true) {
|
|
458
|
+
output.info(JSON.stringify(state, null, 2));
|
|
459
|
+
return;
|
|
460
|
+
}
|
|
461
|
+
const installedEntries = Object.entries(state.installed || {});
|
|
462
|
+
output.info(`Sources : ${state.sources.map((source) => `${source.repo}@${source.ref}`).join(", ")}`);
|
|
463
|
+
output.info(`Active adapter: ${state.adapters.active || "(none)"}`);
|
|
464
|
+
output.info(`Auto-sync : ${state.settings.autoSync ? "enabled" : "disabled"}`);
|
|
465
|
+
output.info(`Sync mode : ${state.syncMode || "(none)"}`);
|
|
466
|
+
if (state.adapters.detected.length > 0) {
|
|
467
|
+
output.info(`Detected : ${state.adapters.detected.join(", ")}`);
|
|
468
|
+
}
|
|
469
|
+
if (state.sync) {
|
|
470
|
+
output.info(`Last sync : ${state.sync.adapter} → ${state.sync.targetPath} at ${state.sync.syncedAt}`);
|
|
471
|
+
}
|
|
472
|
+
if (installedEntries.length === 0) {
|
|
473
|
+
output.info("No skills installed.");
|
|
474
|
+
return;
|
|
475
|
+
}
|
|
476
|
+
printTable(installedEntries.map(([id, metadata]) => ({
|
|
477
|
+
id,
|
|
478
|
+
version: metadata.version,
|
|
479
|
+
source: metadata.source || "catalog",
|
|
480
|
+
installedAt: metadata.installedAt,
|
|
481
|
+
})));
|
|
482
|
+
}
|
|
483
|
+
async function handleDoctor(flags, userConfig) {
|
|
484
|
+
const opts = commonOptions(flags, userConfig);
|
|
485
|
+
const cwd = opts.cwd ?? process.cwd();
|
|
486
|
+
const statePaths = getStatePaths(cwd, opts.agentSkillsDir ?? DEFAULT_AGENT_SKILLS_DIR);
|
|
487
|
+
const checks = [];
|
|
488
|
+
// 1. Lockfile
|
|
489
|
+
const state = await getInstalledSkills(opts);
|
|
490
|
+
if (state) {
|
|
491
|
+
checks.push({ name: "lockfile", passed: true, message: `Found at ${statePaths.lockfilePath}` });
|
|
492
|
+
}
|
|
493
|
+
else {
|
|
494
|
+
checks.push({
|
|
495
|
+
name: "lockfile",
|
|
496
|
+
passed: false,
|
|
497
|
+
message: "Lockfile not found",
|
|
498
|
+
hint: "Run: skillex init",
|
|
499
|
+
});
|
|
500
|
+
}
|
|
501
|
+
// 2. Sources configured
|
|
502
|
+
const stateSources = state?.sources ?? [];
|
|
503
|
+
if (stateSources.length > 0) {
|
|
504
|
+
checks.push({
|
|
505
|
+
name: "source",
|
|
506
|
+
passed: true,
|
|
507
|
+
message: stateSources.map((source) => `${source.repo}@${source.ref}`).join(", "),
|
|
508
|
+
});
|
|
509
|
+
}
|
|
510
|
+
else {
|
|
511
|
+
checks.push({
|
|
512
|
+
name: "source",
|
|
513
|
+
passed: false,
|
|
514
|
+
message: "No catalog source configured",
|
|
515
|
+
hint: "Run: skillex init",
|
|
516
|
+
});
|
|
517
|
+
}
|
|
518
|
+
// 3. Adapter detected
|
|
519
|
+
const hasAdapter = Boolean(state?.adapters?.active || (state?.adapters?.detected?.length ?? 0) > 0);
|
|
520
|
+
if (hasAdapter) {
|
|
521
|
+
const adapter = state?.adapters?.active ?? state?.adapters?.detected?.[0];
|
|
522
|
+
checks.push({ name: "adapter", passed: true, message: `Active: ${adapter}` });
|
|
523
|
+
}
|
|
524
|
+
else {
|
|
525
|
+
checks.push({
|
|
526
|
+
name: "adapter",
|
|
527
|
+
passed: false,
|
|
528
|
+
message: "No adapter detected",
|
|
529
|
+
hint: `Use --adapter <id>. Available: ${listAdapters().map((a) => a.id).join(", ")}`,
|
|
530
|
+
});
|
|
531
|
+
}
|
|
532
|
+
// 4. GitHub reachable
|
|
533
|
+
try {
|
|
534
|
+
const response = await fetch("https://api.github.com", {
|
|
535
|
+
method: "HEAD",
|
|
536
|
+
headers: { "User-Agent": "skillex" },
|
|
537
|
+
signal: AbortSignal.timeout(5000),
|
|
538
|
+
});
|
|
539
|
+
if (response.status < 500) {
|
|
540
|
+
checks.push({ name: "github", passed: true, message: "GitHub API is reachable" });
|
|
541
|
+
}
|
|
542
|
+
else {
|
|
543
|
+
checks.push({
|
|
544
|
+
name: "github",
|
|
545
|
+
passed: false,
|
|
546
|
+
message: `GitHub API returned ${response.status}`,
|
|
547
|
+
hint: "Try again in a moment.",
|
|
548
|
+
});
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
catch {
|
|
552
|
+
checks.push({
|
|
553
|
+
name: "github",
|
|
554
|
+
passed: false,
|
|
555
|
+
message: "GitHub API is unreachable",
|
|
556
|
+
hint: "Check your internet connection or proxy settings.",
|
|
557
|
+
});
|
|
558
|
+
}
|
|
559
|
+
// 5. GitHub token (warning only — never fails)
|
|
560
|
+
const token = process.env.GITHUB_TOKEN;
|
|
561
|
+
if (token) {
|
|
562
|
+
checks.push({ name: "token", passed: true, message: "GitHub token set (authenticated — 5,000 req/hr)" });
|
|
563
|
+
}
|
|
564
|
+
else {
|
|
565
|
+
checks.push({ name: "token", passed: true, message: "No GitHub token (unauthenticated — 60 req/hr)" });
|
|
566
|
+
}
|
|
567
|
+
// 6. Cache
|
|
568
|
+
const cacheDir = path.join(statePaths.stateDir, ".cache");
|
|
569
|
+
if ((state?.sources?.length ?? 0) > 0) {
|
|
570
|
+
const source = await resolveProjectSource(opts);
|
|
571
|
+
const cacheKey = computeCatalogCacheKey(source);
|
|
572
|
+
const cached = await readCatalogCache(cacheDir, cacheKey);
|
|
573
|
+
if (cached) {
|
|
574
|
+
checks.push({ name: "cache", passed: true, message: "Catalog cache is fresh" });
|
|
575
|
+
}
|
|
576
|
+
else {
|
|
577
|
+
checks.push({ name: "cache", passed: true, message: "No cached catalog (will fetch on next command)" });
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
else {
|
|
581
|
+
checks.push({ name: "cache", passed: true, message: "Cache not checked (no repo configured)" });
|
|
582
|
+
}
|
|
583
|
+
const anyFailed = checks.some((c) => !c.passed);
|
|
584
|
+
if (flags.json === true) {
|
|
585
|
+
const jsonResult = {};
|
|
586
|
+
for (const check of checks) {
|
|
587
|
+
jsonResult[check.name] = {
|
|
588
|
+
passed: check.passed,
|
|
589
|
+
message: check.message,
|
|
590
|
+
...(check.hint ? { hint: check.hint } : {}),
|
|
591
|
+
};
|
|
592
|
+
}
|
|
593
|
+
output.info(JSON.stringify(jsonResult, null, 2));
|
|
594
|
+
}
|
|
595
|
+
else {
|
|
596
|
+
for (const check of checks) {
|
|
597
|
+
const symbol = check.passed ? "✓" : "✗";
|
|
598
|
+
const line = `${symbol} ${check.name.padEnd(10)} ${check.message}`;
|
|
599
|
+
if (check.passed) {
|
|
600
|
+
output.info(line);
|
|
601
|
+
}
|
|
602
|
+
else {
|
|
603
|
+
output.error(line);
|
|
604
|
+
if (check.hint) {
|
|
605
|
+
output.info(` Hint: ${check.hint}`);
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
if (anyFailed) {
|
|
611
|
+
process.exitCode = 1;
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
async function handleSource(positionals, flags, userConfig) {
|
|
615
|
+
const subcommand = positionals[0];
|
|
616
|
+
const options = commonOptions(flags, userConfig);
|
|
617
|
+
if (!subcommand || subcommand === "help" || flags.help === true) {
|
|
618
|
+
output.info(COMMAND_HELP.source ?? "");
|
|
619
|
+
return;
|
|
620
|
+
}
|
|
621
|
+
if (subcommand === "list") {
|
|
622
|
+
const sources = await listProjectSources(options);
|
|
623
|
+
if (flags.json === true) {
|
|
624
|
+
output.info(JSON.stringify(sources, null, 2));
|
|
625
|
+
return;
|
|
626
|
+
}
|
|
627
|
+
printTable(sources.map((source) => ({
|
|
628
|
+
repo: source.repo,
|
|
629
|
+
ref: source.ref,
|
|
630
|
+
label: source.label || "",
|
|
631
|
+
})));
|
|
632
|
+
return;
|
|
633
|
+
}
|
|
634
|
+
if (subcommand === "add") {
|
|
635
|
+
const repo = positionals[1];
|
|
636
|
+
if (!repo) {
|
|
637
|
+
throw new CliError("Usage: skillex source add <owner/repo>", "SOURCE_ADD_REQUIRES_REPO");
|
|
638
|
+
}
|
|
639
|
+
const lockfile = await addProjectSource({
|
|
640
|
+
repo,
|
|
641
|
+
ref: asOptionalString(flags.ref),
|
|
642
|
+
label: asOptionalString(flags.label),
|
|
643
|
+
}, options);
|
|
644
|
+
output.success(`Added source ${repo}`);
|
|
645
|
+
output.info(`Configured sources: ${lockfile.sources.length}`);
|
|
646
|
+
return;
|
|
647
|
+
}
|
|
648
|
+
if (subcommand === "remove") {
|
|
649
|
+
const repo = positionals[1];
|
|
650
|
+
if (!repo) {
|
|
651
|
+
throw new CliError("Usage: skillex source remove <owner/repo>", "SOURCE_REMOVE_REQUIRES_REPO");
|
|
652
|
+
}
|
|
653
|
+
const lockfile = await removeProjectSource(repo, options);
|
|
654
|
+
output.success(`Removed source ${repo}`);
|
|
655
|
+
output.info(`Configured sources: ${lockfile.sources.length}`);
|
|
656
|
+
return;
|
|
657
|
+
}
|
|
658
|
+
throw new CliError(`Unknown source subcommand: ${subcommand}.`, "SOURCE_UNKNOWN_SUBCOMMAND");
|
|
659
|
+
}
|
|
660
|
+
async function handleConfig(positionals, flags) {
|
|
661
|
+
const subcommand = positionals[0];
|
|
662
|
+
if (!subcommand || subcommand === "help" || flags.help === true) {
|
|
663
|
+
output.info(COMMAND_HELP.config ?? "");
|
|
664
|
+
return;
|
|
665
|
+
}
|
|
666
|
+
if (subcommand === "get") {
|
|
667
|
+
const key = positionals[1];
|
|
668
|
+
if (!key) {
|
|
669
|
+
throw new CliError("Usage: skillex config get <key>", "CONFIG_GET_REQUIRES_KEY");
|
|
670
|
+
}
|
|
671
|
+
if (!VALID_CONFIG_KEYS.includes(key)) {
|
|
672
|
+
throw new CliError(`Unknown config key: ${key}. Valid keys: ${VALID_CONFIG_KEYS.join(", ")}`, "CONFIG_INVALID_KEY");
|
|
673
|
+
}
|
|
674
|
+
const config = await readUserConfig();
|
|
675
|
+
const value = config[key];
|
|
676
|
+
output.info(value !== undefined ? String(value) : "(not set)");
|
|
677
|
+
return;
|
|
678
|
+
}
|
|
679
|
+
if (subcommand === "set") {
|
|
680
|
+
const key = positionals[1];
|
|
681
|
+
const value = positionals[2];
|
|
682
|
+
if (!key || value === undefined) {
|
|
683
|
+
throw new CliError("Usage: skillex config set <key> <value>", "CONFIG_SET_REQUIRES_KEY_VALUE");
|
|
684
|
+
}
|
|
685
|
+
if (!VALID_CONFIG_KEYS.includes(key)) {
|
|
686
|
+
throw new CliError(`Unknown config key: ${key}. Valid keys: ${VALID_CONFIG_KEYS.join(", ")}`, "CONFIG_INVALID_KEY");
|
|
687
|
+
}
|
|
688
|
+
// Coerce boolean keys
|
|
689
|
+
const coerced = key === "disableAutoSync" ? value === "true" : value;
|
|
690
|
+
await writeUserConfig({ [key]: coerced });
|
|
691
|
+
output.success(`Set ${key} = ${coerced} in ~/.askillrc.json`);
|
|
692
|
+
return;
|
|
693
|
+
}
|
|
694
|
+
throw new CliError(`Unknown config subcommand: ${subcommand}. Use "get" or "set".`, "CONFIG_UNKNOWN_SUBCOMMAND");
|
|
695
|
+
}
|
|
696
|
+
// ---------------------------------------------------------------------------
|
|
697
|
+
// Helpers
|
|
698
|
+
// ---------------------------------------------------------------------------
|
|
699
|
+
function resolveAlias(command) {
|
|
700
|
+
const ALIASES = {
|
|
701
|
+
ls: "list",
|
|
702
|
+
rm: "remove",
|
|
703
|
+
uninstall: "remove",
|
|
704
|
+
};
|
|
705
|
+
return command !== undefined ? (ALIASES[command] ?? command) : undefined;
|
|
706
|
+
}
|
|
707
|
+
function commonOptions(flags, userConfig = {}) {
|
|
708
|
+
const options = {
|
|
709
|
+
cwd: path.resolve(asOptionalString(flags.cwd) || process.cwd()),
|
|
710
|
+
};
|
|
711
|
+
const repo = asOptionalString(flags.repo) ?? userConfig.defaultRepo;
|
|
712
|
+
const ref = asOptionalString(flags.ref);
|
|
713
|
+
const catalogPath = asOptionalString(flags["catalog-path"]);
|
|
714
|
+
const catalogUrl = asOptionalString(flags["catalog-url"]);
|
|
715
|
+
const skillsDir = asOptionalString(flags["skills-dir"]);
|
|
716
|
+
const agentSkillsDir = asOptionalString(flags["agent-skills-dir"]);
|
|
717
|
+
const adapter = asOptionalString(flags.adapter) ?? userConfig.defaultAdapter;
|
|
718
|
+
const autoSync = parseBooleanFlag(flags["auto-sync"]) ?? (userConfig.disableAutoSync ? false : undefined);
|
|
719
|
+
const dryRun = parseBooleanFlag(flags["dry-run"]);
|
|
720
|
+
const trust = parseBooleanFlag(flags.trust);
|
|
721
|
+
const yes = parseBooleanFlag(flags.yes);
|
|
722
|
+
const mode = parseSyncMode(asOptionalString(flags.mode));
|
|
723
|
+
const timeout = parsePositiveInt(asOptionalString(flags.timeout));
|
|
724
|
+
const noCache = parseBooleanFlag(flags["no-cache"]);
|
|
725
|
+
if (repo)
|
|
726
|
+
options.repo = repo;
|
|
727
|
+
if (ref)
|
|
728
|
+
options.ref = ref;
|
|
729
|
+
if (catalogPath)
|
|
730
|
+
options.catalogPath = catalogPath;
|
|
731
|
+
if (catalogUrl)
|
|
732
|
+
options.catalogUrl = catalogUrl;
|
|
733
|
+
if (skillsDir)
|
|
734
|
+
options.skillsDir = skillsDir;
|
|
735
|
+
if (agentSkillsDir)
|
|
736
|
+
options.agentSkillsDir = agentSkillsDir;
|
|
737
|
+
if (adapter)
|
|
738
|
+
options.adapter = adapter;
|
|
739
|
+
if (autoSync !== undefined)
|
|
740
|
+
options.autoSync = autoSync;
|
|
741
|
+
if (dryRun !== undefined)
|
|
742
|
+
options.dryRun = dryRun;
|
|
743
|
+
if (trust !== undefined)
|
|
744
|
+
options.trust = trust;
|
|
745
|
+
if (yes !== undefined)
|
|
746
|
+
options.yes = yes;
|
|
747
|
+
if (mode)
|
|
748
|
+
options.mode = mode;
|
|
749
|
+
if (timeout !== undefined)
|
|
750
|
+
options.timeout = timeout;
|
|
751
|
+
if (noCache !== undefined)
|
|
752
|
+
options.noCache = noCache;
|
|
753
|
+
return options;
|
|
754
|
+
}
|
|
755
|
+
/** Returns cache-related options to spread into a loadCatalog call. */
|
|
756
|
+
function cacheOptions(opts) {
|
|
757
|
+
const cwd = opts.cwd ?? process.cwd();
|
|
758
|
+
const stateDir = path.join(cwd, opts.agentSkillsDir ?? DEFAULT_AGENT_SKILLS_DIR);
|
|
759
|
+
return {
|
|
760
|
+
cacheDir: path.join(stateDir, ".cache"),
|
|
761
|
+
...(opts.noCache !== undefined ? { noCache: opts.noCache } : {}),
|
|
762
|
+
};
|
|
763
|
+
}
|
|
764
|
+
function parseArgs(argv) {
|
|
765
|
+
const flags = {};
|
|
766
|
+
const positionals = [];
|
|
767
|
+
let command;
|
|
768
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
769
|
+
const token = argv[index];
|
|
770
|
+
if (token === undefined)
|
|
771
|
+
continue;
|
|
772
|
+
if (!command && !token.startsWith("-")) {
|
|
773
|
+
command = token;
|
|
774
|
+
continue;
|
|
775
|
+
}
|
|
776
|
+
if (token === "-v") {
|
|
777
|
+
flags.verbose = true;
|
|
778
|
+
continue;
|
|
779
|
+
}
|
|
780
|
+
if (token.startsWith("--")) {
|
|
781
|
+
const [rawKey, inlineValue] = token.slice(2).split("=", 2);
|
|
782
|
+
if (!rawKey)
|
|
783
|
+
continue;
|
|
784
|
+
if (inlineValue !== undefined) {
|
|
785
|
+
flags[rawKey] = inlineValue;
|
|
786
|
+
continue;
|
|
787
|
+
}
|
|
788
|
+
const next = argv[index + 1];
|
|
789
|
+
if (!next || next.startsWith("-")) {
|
|
790
|
+
flags[rawKey] = true;
|
|
791
|
+
continue;
|
|
792
|
+
}
|
|
793
|
+
flags[rawKey] = next;
|
|
794
|
+
index += 1;
|
|
795
|
+
continue;
|
|
796
|
+
}
|
|
797
|
+
positionals.push(token);
|
|
798
|
+
}
|
|
799
|
+
return { command, positionals, flags };
|
|
800
|
+
}
|
|
801
|
+
function printHelp() {
|
|
802
|
+
output.info(`skillex — AI agent skill manager
|
|
803
|
+
|
|
804
|
+
Commands:
|
|
805
|
+
skillex init [--repo owner/repo] [--ref main]
|
|
806
|
+
skillex list [--json]
|
|
807
|
+
skillex search [query] [--compatibility claude] [--tag git]
|
|
808
|
+
skillex install <skill-id... | owner/repo[@ref]> [--trust]
|
|
809
|
+
skillex install --all
|
|
810
|
+
skillex update [skill-id...]
|
|
811
|
+
skillex remove <skill-id...> aliases: rm, uninstall
|
|
812
|
+
skillex source <add|remove|list> [...]
|
|
813
|
+
skillex sync [--adapter id] [--dry-run] [--mode copy]
|
|
814
|
+
skillex run <skill-id:command> [--yes] [--timeout 30]
|
|
815
|
+
skillex ui
|
|
816
|
+
skillex status [--json]
|
|
817
|
+
skillex doctor [--json]
|
|
818
|
+
skillex config set <key> <value>
|
|
819
|
+
skillex config get <key>
|
|
820
|
+
|
|
821
|
+
Global flags:
|
|
822
|
+
--repo <owner/repo> GitHub repository with skills (default: lgili/skillex)
|
|
823
|
+
--ref <ref> Branch, tag, or commit (default: main)
|
|
824
|
+
--adapter <id> Force adapter: ${listAdapters()
|
|
825
|
+
.map((a) => a.id)
|
|
826
|
+
.join(", ")}
|
|
827
|
+
--cwd <path> Project directory (default: current)
|
|
828
|
+
--verbose, -v Enable debug output
|
|
829
|
+
--json Machine-readable JSON output
|
|
830
|
+
--no-cache Bypass local catalog cache
|
|
831
|
+
--dry-run Preview without writing to disk
|
|
832
|
+
|
|
833
|
+
Run "skillex <command> --help" for command-specific usage.`);
|
|
834
|
+
}
|
|
835
|
+
function printTable(rows) {
|
|
836
|
+
const columns = Object.keys(rows[0]);
|
|
837
|
+
const widths = columns.map((column) => Math.max(column.length, ...rows.map((row) => String(row[column] ?? "").length)));
|
|
838
|
+
output.info(columns.map((column, index) => column.padEnd(widths[index] ?? 0)).join(" "));
|
|
839
|
+
output.info(widths.map((size) => "-".repeat(size)).join(" "));
|
|
840
|
+
for (const row of rows) {
|
|
841
|
+
output.info(columns.map((column, index) => String(row[column] ?? "").padEnd(widths[index] ?? 0)).join(" "));
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
function truncate(value, maxLength) {
|
|
845
|
+
if (!value || value.length <= maxLength)
|
|
846
|
+
return value;
|
|
847
|
+
return `${value.slice(0, maxLength - 3)}...`;
|
|
848
|
+
}
|
|
849
|
+
function parseBooleanFlag(value) {
|
|
850
|
+
if (value === undefined)
|
|
851
|
+
return undefined;
|
|
852
|
+
if (value === true)
|
|
853
|
+
return true;
|
|
854
|
+
const normalized = String(value).trim().toLowerCase();
|
|
855
|
+
if (["true", "1", "yes", "on"].includes(normalized))
|
|
856
|
+
return true;
|
|
857
|
+
if (["false", "0", "no", "off"].includes(normalized))
|
|
858
|
+
return false;
|
|
859
|
+
throw new CliError(`Invalid boolean value: ${value}`, "INVALID_BOOLEAN_FLAG");
|
|
860
|
+
}
|
|
861
|
+
function parsePositiveInt(value) {
|
|
862
|
+
if (!value)
|
|
863
|
+
return undefined;
|
|
864
|
+
const parsed = Number.parseInt(value, 10);
|
|
865
|
+
if (!Number.isFinite(parsed) || parsed <= 0) {
|
|
866
|
+
throw new CliError(`Invalid numeric value: ${value}`, "INVALID_NUMBER_FLAG");
|
|
867
|
+
}
|
|
868
|
+
return parsed;
|
|
869
|
+
}
|
|
870
|
+
function parseSyncMode(value) {
|
|
871
|
+
if (!value)
|
|
872
|
+
return undefined;
|
|
873
|
+
if (value === "copy" || value === "symlink")
|
|
874
|
+
return value;
|
|
875
|
+
throw new CliError(`Invalid sync mode: ${value}. Use "symlink" or "copy".`, "INVALID_SYNC_MODE");
|
|
876
|
+
}
|
|
877
|
+
function printAutoSyncResult(result) {
|
|
878
|
+
if (!result)
|
|
879
|
+
return;
|
|
880
|
+
const suffix = result.changed ? "" : " (no changes)";
|
|
881
|
+
output.info(`Auto-sync: ${result.sync.adapter} → ${result.sync.targetPath} [${result.syncMode}]${suffix}`);
|
|
882
|
+
}
|
|
883
|
+
function asOptionalString(value) {
|
|
884
|
+
return typeof value === "string" ? value : undefined;
|
|
885
|
+
}
|