@skillshub-labs/cli 0.1.17

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/bin/skills-hub ADDED
@@ -0,0 +1,1611 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ const fsPromises = require('fs/promises');
5
+ const fse = require('fs-extra');
6
+ const path = require('path');
7
+ const os = require('os');
8
+ const simpleGit = require('simple-git');
9
+ const matter = require('gray-matter');
10
+
11
+ const args = process.argv.slice(2);
12
+ const commandAliases = new Map([
13
+ ['ls', 'list'],
14
+ ['rm', 'remove'],
15
+ ]);
16
+ const commands = new Set(['import', 'list', 'ls', 'remove', 'rm', 'sync', 'provider', 'kit']);
17
+ const flagsWithValues = new Set([
18
+ '-b',
19
+ '--branch',
20
+ '--target',
21
+ '-t',
22
+ '--app',
23
+ '--id',
24
+ '--name',
25
+ '--note',
26
+ '--website',
27
+ '--account-name',
28
+ '--vendor-key',
29
+ '--base-url',
30
+ '--api-key',
31
+ '--apps',
32
+ '--claude-model',
33
+ '--codex-model',
34
+ '--gemini-model',
35
+ '--config-json',
36
+ '--config-file',
37
+ '--policy-id',
38
+ '--loadout-id',
39
+ '--description',
40
+ '--skills',
41
+ '--project',
42
+ '--agent',
43
+ '-a',
44
+ '--mode',
45
+ '--content',
46
+ '--content-file',
47
+ ]);
48
+ let providerCorePromise = null;
49
+ let kitServicePromise = null;
50
+
51
+ async function main() {
52
+ let command = null;
53
+ let commandIndex = -1;
54
+
55
+ for (let i = 0; i < args.length; i++) {
56
+ const arg = args[i];
57
+ if (arg.startsWith('-')) {
58
+ // Check if it's a flag=value style
59
+ if (arg.includes('=')) continue;
60
+
61
+ // Check if it's a flag that takes a value next
62
+ if (flagsWithValues.has(arg)) {
63
+ i++; // Skip the next argument (the value)
64
+ }
65
+ } else {
66
+ // Found the first non-flag argument, closest thing to a command
67
+ command = arg;
68
+ commandIndex = i;
69
+ break;
70
+ }
71
+ }
72
+
73
+ if (args.includes('--help') || args.includes('-h')) {
74
+ printHelp();
75
+ process.exit(0);
76
+ }
77
+
78
+ if (args.includes('--version') || args.includes('-v') || args.includes('-V')) {
79
+ const pkg = require(path.join(__dirname, '..', 'package.json'));
80
+ console.log(pkg.version);
81
+ process.exit(0);
82
+ }
83
+
84
+ if (command && !commands.has(command)) {
85
+ console.error(`Unknown command: ${command}`);
86
+ printHelp();
87
+ process.exit(1);
88
+ }
89
+
90
+ if (!command) {
91
+ printHelp();
92
+ process.exit(1);
93
+ }
94
+
95
+ const normalizedCommand = commandAliases.get(command) || command;
96
+ const commandArgs = args.slice(commandIndex + 1);
97
+ await runCommand(normalizedCommand, commandArgs);
98
+ }
99
+
100
+ function printHelp() {
101
+ console.log(`Usage:
102
+ skills-hub <command> [options]
103
+
104
+ Commands:
105
+ import <url> [options] Import a skill from a git repository
106
+ list [options] List installed skills (alias: ls)
107
+ remove [skills...] [options] Remove installed skills (alias: rm)
108
+ sync --all | --target <name> Sync hub skills to agent targets
109
+ provider <subcommand> Manage provider profiles and switching
110
+ kit <subcommand> Manage AGENTS templates, loadouts, and kits
111
+
112
+ Import options:
113
+ -b, --branch <branch> Use branch when importing from Git
114
+ -l, --list List installable skills in remote source only
115
+ -g, --global Install to global scope (default: current project)
116
+ -a, --agent <name> Install to target agent(s), repeatable or comma-separated
117
+ --copy Copy files instead of symbolic links (default: symlink)
118
+ -y, --yes Overwrite conflicting destinations without prompt
119
+
120
+ List options:
121
+ -g, --global Show global installation scope
122
+ -a, --agent <name> Filter by target agent(s)
123
+ --hub Show skills in hub storage instead of installation view
124
+
125
+ Remove options:
126
+ -g, --global Remove from global installation scope
127
+ -a, --agent <name> Remove only from target agent(s)
128
+ --all Remove all installed skills in selected scope
129
+ --hub Remove from hub storage only
130
+
131
+ Provider subcommands:
132
+ provider list [--app <app>] List providers
133
+ provider add --app <app> --name <name> --config-json <json>
134
+ provider add --app <app> --name <name> --config-file <path>
135
+ provider update --id <id> [--name <name>] [--config-json <json>|--config-file <path>]
136
+ provider switch --app <app> --id <providerId> Switch current provider
137
+ provider delete --id <providerId> Delete a provider
138
+ provider restore --app <app> Restore latest live backup
139
+ provider capture --app <app> --name <name> Capture current live config as provider
140
+ provider universal-list List universal providers
141
+ provider universal-add --name <name> --base-url <url> --api-key <key> [--apps claude,codex,gemini]
142
+ provider universal-apply --id <universalProviderId> Apply universal provider to enabled apps
143
+ provider universal-delete --id <universalProviderId> Delete universal provider
144
+
145
+ Kit subcommands:
146
+ kit policy-list
147
+ kit policy-add --name <name> [--description <text>] (--content <text> | --content-file <path>)
148
+ kit policy-update --id <id> [--name <name>] [--description <text>] [--content <text>|--content-file <path>]
149
+ kit policy-delete --id <id>
150
+ kit loadout-list
151
+ kit loadout-add --name <name> --skills <path1,path2,...> [--description <text>] [--mode copy|link]
152
+ kit loadout-update --id <id> [--name <name>] [--description <text>] [--skills <path1,path2,...>] [--mode copy|link]
153
+ kit loadout-delete --id <id>
154
+ kit list
155
+ kit add --name <name> --policy-id <id> --loadout-id <id> [--description <text>]
156
+ kit update --id <id> [--name <name>] [--policy-id <id>] [--loadout-id <id>] [--description <text>]
157
+ kit delete --id <id>
158
+ kit apply --id <id> --project <path> --agent <name> [--mode copy|link] [--overwrite-agents-md]
159
+
160
+ -v, --version Show version number
161
+ -h, --help Show this help message
162
+ `);
163
+ }
164
+
165
+ function readArgValue(argv, flag, shortFlag) {
166
+ const index = argv.indexOf(flag);
167
+ if (index !== -1 && index + 1 < argv.length) {
168
+ return argv[index + 1];
169
+ }
170
+
171
+ if (shortFlag) {
172
+ const shortIndex = argv.indexOf(shortFlag);
173
+ if (shortIndex !== -1 && shortIndex + 1 < argv.length) {
174
+ return argv[shortIndex + 1];
175
+ }
176
+ }
177
+
178
+ const withEq = argv.find((arg) => arg.startsWith(`${flag}=`));
179
+ if (withEq) {
180
+ return withEq.split('=').slice(1).join('=');
181
+ }
182
+
183
+ return null;
184
+ }
185
+
186
+ function expandHome(inputPath) {
187
+ if (!inputPath) return inputPath;
188
+ if (inputPath.startsWith('~')) {
189
+ return path.join(os.homedir(), inputPath.slice(1));
190
+ }
191
+ return inputPath;
192
+ }
193
+
194
+ function normalizeRepoWebUrl(url) {
195
+ return url.trim().replace(/\/$/, '').replace(/\.git$/, '');
196
+ }
197
+
198
+ function parseSkillImportUrl(url, preferredBranch) {
199
+ const input = url.trim();
200
+ if (!input) {
201
+ throw new Error('Missing URL for import.');
202
+ }
203
+
204
+ let repoWebUrl = '';
205
+ let branch = preferredBranch ? preferredBranch.trim() : '';
206
+ let subdir = '';
207
+
208
+ if (input.includes('/tree/')) {
209
+ const [base, restRaw = ''] = input.split('/tree/', 2);
210
+ repoWebUrl = normalizeRepoWebUrl(base);
211
+
212
+ const rest = restRaw.replace(/^\/+/, '');
213
+ if (rest) {
214
+ const parts = rest.split('/').filter(Boolean);
215
+ if (!branch && parts.length > 0) {
216
+ branch = parts[0];
217
+ }
218
+ if (parts.length > 1) {
219
+ subdir = parts.slice(1).join('/');
220
+ }
221
+ }
222
+ } else {
223
+ repoWebUrl = normalizeRepoWebUrl(input);
224
+ }
225
+
226
+ const skillName = subdir ? path.basename(subdir) : path.basename(repoWebUrl);
227
+ if (!repoWebUrl || !skillName) {
228
+ throw new Error('Invalid skill import URL.');
229
+ }
230
+
231
+ return {
232
+ repoUrl: `${repoWebUrl}.git`,
233
+ repoWebUrl,
234
+ branch: branch || undefined,
235
+ subdir,
236
+ skillName,
237
+ };
238
+ }
239
+
240
+ function buildGitSourceUrl(repoWebUrl, branch, subdir) {
241
+ const normalized = normalizeRepoWebUrl(repoWebUrl);
242
+ if (!branch) {
243
+ return normalized;
244
+ }
245
+ if (subdir) {
246
+ return `${normalized}/tree/${branch}/${subdir}`;
247
+ }
248
+ return `${normalized}/tree/${branch}`;
249
+ }
250
+
251
+ async function attachSkillImportMetadata(skillDirPath, metadata) {
252
+ const skillMdPath = path.join(skillDirPath, 'SKILL.md');
253
+ if (!await fse.pathExists(skillMdPath)) {
254
+ return;
255
+ }
256
+
257
+ const rawContent = await fsPromises.readFile(skillMdPath, 'utf-8');
258
+ const parsed = matter(rawContent);
259
+ const { source_branch: _ignoredSourceBranch, ...restFrontmatter } = parsed.data || {};
260
+ const nextFrontmatter = {
261
+ ...restFrontmatter,
262
+ source_repo: metadata.sourceRepo,
263
+ source_url: metadata.sourceUrl,
264
+ source_subdir: metadata.sourceSubdir || '/',
265
+ source_last_updated: metadata.sourceLastUpdated,
266
+ imported_at: metadata.importedAt,
267
+ };
268
+
269
+ const nextRawContent = matter.stringify(parsed.content, nextFrontmatter);
270
+ await fsPromises.writeFile(skillMdPath, nextRawContent, 'utf-8');
271
+ }
272
+
273
+ async function runCommand(commandName, commandArgs) {
274
+ switch (commandName) {
275
+ case 'import':
276
+ await handleImport(commandArgs);
277
+ return;
278
+ case 'list':
279
+ await handleList(commandArgs);
280
+ return;
281
+ case 'remove':
282
+ await handleRemove(commandArgs);
283
+ return;
284
+ case 'sync':
285
+ await handleSync(commandArgs);
286
+ return;
287
+ case 'provider':
288
+ await handleProvider(commandArgs);
289
+ return;
290
+ case 'kit':
291
+ await handleKit(commandArgs);
292
+ return;
293
+ default:
294
+ console.error(`Unknown command: ${commandName}`);
295
+ process.exit(1);
296
+ }
297
+ }
298
+
299
+ function loadProviderCore() {
300
+ if (!providerCorePromise) {
301
+ providerCorePromise = import('../lib/core/provider-core.mjs');
302
+ }
303
+ return providerCorePromise;
304
+ }
305
+
306
+ function loadKitService() {
307
+ if (!kitServicePromise) {
308
+ kitServicePromise = import('../lib/services/kit-service.mjs');
309
+ }
310
+ return kitServicePromise;
311
+ }
312
+
313
+ function parseJsonArg(jsonText) {
314
+ try {
315
+ const parsed = JSON.parse(jsonText);
316
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
317
+ throw new Error('must be a JSON object');
318
+ }
319
+ return parsed;
320
+ } catch (error) {
321
+ throw new Error(`Invalid JSON config: ${error.message || error}`);
322
+ }
323
+ }
324
+
325
+ async function readProviderConfigFromArgs(commandArgs) {
326
+ const configJson = readArgValue(commandArgs, '--config-json');
327
+ const configFile = readArgValue(commandArgs, '--config-file');
328
+
329
+ if (configJson && configFile) {
330
+ throw new Error('Use either --config-json or --config-file, not both.');
331
+ }
332
+
333
+ if (configJson) {
334
+ return parseJsonArg(configJson);
335
+ }
336
+
337
+ if (configFile) {
338
+ const resolved = path.resolve(configFile);
339
+ const raw = await fsPromises.readFile(resolved, 'utf-8');
340
+ return parseJsonArg(raw);
341
+ }
342
+
343
+ return undefined;
344
+ }
345
+
346
+ function readProviderProfileArgs(commandArgs, defaultKind = 'api') {
347
+ const note = readArgValue(commandArgs, '--note');
348
+ const website = readArgValue(commandArgs, '--website');
349
+ const accountName = readArgValue(commandArgs, '--account-name');
350
+ const vendorKey = readArgValue(commandArgs, '--vendor-key');
351
+
352
+ return {
353
+ kind: defaultKind,
354
+ note: note || undefined,
355
+ website: website || undefined,
356
+ accountName: accountName || undefined,
357
+ vendorKey: vendorKey || undefined,
358
+ };
359
+ }
360
+
361
+ function hasFlag(argv, flag) {
362
+ return argv.includes(flag);
363
+ }
364
+
365
+ function parseCsvValues(rawValue) {
366
+ if (!rawValue) return [];
367
+ return rawValue
368
+ .split(',')
369
+ .map((value) => value.trim())
370
+ .filter(Boolean);
371
+ }
372
+
373
+ function appendUniqueValues(target, values) {
374
+ for (const value of values) {
375
+ if (!target.includes(value)) {
376
+ target.push(value);
377
+ }
378
+ }
379
+ }
380
+
381
+ function parseAgentOptionValues(commandArgs, optionName = 'option') {
382
+ const agents = [];
383
+
384
+ for (let i = 0; i < commandArgs.length; i++) {
385
+ const arg = commandArgs[i];
386
+ if (arg === '--agent' || arg === '-a') {
387
+ const next = commandArgs[i + 1];
388
+ if (!next || next.startsWith('-')) {
389
+ throw new Error(`${optionName} requires a value for --agent/-a.`);
390
+ }
391
+ appendUniqueValues(agents, parseCsvValues(next));
392
+ i++;
393
+ continue;
394
+ }
395
+
396
+ if (arg.startsWith('--agent=')) {
397
+ const values = parseCsvValues(arg.split('=').slice(1).join('='));
398
+ if (values.length === 0) {
399
+ throw new Error(`${optionName} requires a value for --agent.`);
400
+ }
401
+ appendUniqueValues(agents, values);
402
+ continue;
403
+ }
404
+
405
+ if (arg.startsWith('-a=')) {
406
+ const values = parseCsvValues(arg.split('=').slice(1).join('='));
407
+ if (values.length === 0) {
408
+ throw new Error(`${optionName} requires a value for -a.`);
409
+ }
410
+ appendUniqueValues(agents, values);
411
+ }
412
+ }
413
+
414
+ return agents;
415
+ }
416
+
417
+ function parseScopeArgs(commandArgs, commandName, extraBooleanFlags = new Set()) {
418
+ const parsed = {
419
+ useGlobal: false,
420
+ useHub: false,
421
+ agents: [],
422
+ positional: [],
423
+ };
424
+
425
+ for (let i = 0; i < commandArgs.length; i++) {
426
+ const arg = commandArgs[i];
427
+
428
+ if (arg === '--global' || arg === '-g') {
429
+ parsed.useGlobal = true;
430
+ continue;
431
+ }
432
+
433
+ if (arg === '--hub') {
434
+ parsed.useHub = true;
435
+ continue;
436
+ }
437
+
438
+ if (arg === '--agent' || arg === '-a') {
439
+ const next = commandArgs[i + 1];
440
+ if (!next || next.startsWith('-')) {
441
+ throw new Error(`${commandName}: missing value for --agent/-a.`);
442
+ }
443
+ appendUniqueValues(parsed.agents, parseCsvValues(next));
444
+ i++;
445
+ continue;
446
+ }
447
+
448
+ if (arg.startsWith('--agent=')) {
449
+ const values = parseCsvValues(arg.split('=').slice(1).join('='));
450
+ if (values.length === 0) {
451
+ throw new Error(`${commandName}: missing value for --agent.`);
452
+ }
453
+ appendUniqueValues(parsed.agents, values);
454
+ continue;
455
+ }
456
+
457
+ if (arg.startsWith('-a=')) {
458
+ const values = parseCsvValues(arg.split('=').slice(1).join('='));
459
+ if (values.length === 0) {
460
+ throw new Error(`${commandName}: missing value for -a.`);
461
+ }
462
+ appendUniqueValues(parsed.agents, values);
463
+ continue;
464
+ }
465
+
466
+ if (extraBooleanFlags.has(arg)) {
467
+ continue;
468
+ }
469
+
470
+ if (arg.startsWith('-')) {
471
+ throw new Error(`${commandName}: unknown option ${arg}`);
472
+ }
473
+
474
+ parsed.positional.push(arg);
475
+ }
476
+
477
+ if (parsed.useHub && (parsed.useGlobal || parsed.agents.length > 0)) {
478
+ throw new Error(`${commandName}: --hub cannot be combined with --global or --agent.`);
479
+ }
480
+
481
+ return parsed;
482
+ }
483
+
484
+ function resolveAgentScopePath(agent, useGlobal, cwdPath = process.cwd()) {
485
+ const configuredPath = useGlobal ? agent.globalPath : agent.projectPath;
486
+ const expandedPath = expandHome(configuredPath);
487
+
488
+ if (useGlobal || path.isAbsolute(expandedPath)) {
489
+ return expandedPath;
490
+ }
491
+
492
+ return path.join(cwdPath, expandedPath);
493
+ }
494
+
495
+ function matchAgentsBySelectors(agents, selectors) {
496
+ const normalizedSelectors = selectors.map((value) => value.trim().toLowerCase()).filter(Boolean);
497
+
498
+ if (normalizedSelectors.length === 0) {
499
+ return agents.filter((agent) => agent.enabled);
500
+ }
501
+
502
+ const matched = [];
503
+ for (const selector of normalizedSelectors) {
504
+ const candidates = agents.filter((agent) => {
505
+ const lowerName = String(agent.name || '').toLowerCase();
506
+ return lowerName === selector || lowerName.includes(selector);
507
+ });
508
+
509
+ for (const candidate of candidates) {
510
+ if (!matched.some((item) => item.name === candidate.name)) {
511
+ matched.push(candidate);
512
+ }
513
+ }
514
+ }
515
+
516
+ return matched;
517
+ }
518
+
519
+ async function readSkillNamesUnderRoot(rootPath) {
520
+ if (!await fse.pathExists(rootPath)) {
521
+ return [];
522
+ }
523
+
524
+ const entries = await fsPromises.readdir(rootPath);
525
+ const skills = [];
526
+ for (const entry of entries) {
527
+ const fullPath = path.join(rootPath, entry);
528
+ const stat = await fse.stat(fullPath).catch(() => null);
529
+ if (!stat) continue;
530
+ if (stat.isDirectory()) {
531
+ skills.push(entry);
532
+ }
533
+ }
534
+
535
+ return skills.sort((a, b) => a.localeCompare(b));
536
+ }
537
+
538
+ function isInteractiveTerminal() {
539
+ return Boolean(process.stdin.isTTY && process.stdout.isTTY);
540
+ }
541
+
542
+ async function promptYesNo(message) {
543
+ const readline = require('readline');
544
+ return new Promise((resolve) => {
545
+ const rl = readline.createInterface({
546
+ input: process.stdin,
547
+ output: process.stdout,
548
+ });
549
+
550
+ rl.question(`${message} [y/N]: `, (answer) => {
551
+ rl.close();
552
+ const normalized = String(answer || '').trim().toLowerCase();
553
+ resolve(normalized === 'y' || normalized === 'yes');
554
+ });
555
+ });
556
+ }
557
+
558
+ async function syncSkillToTarget(sourcePath, destPath, syncMode) {
559
+ await fse.ensureDir(path.dirname(destPath));
560
+ await fse.remove(destPath);
561
+
562
+ if (syncMode === 'link') {
563
+ await fse.ensureSymlink(sourcePath, destPath);
564
+ return;
565
+ }
566
+
567
+ await fse.copy(sourcePath, destPath, { overwrite: true });
568
+ }
569
+
570
+ function parseImportArgs(commandArgs) {
571
+ const parsed = {
572
+ url: '',
573
+ branch: readArgValue(commandArgs, '--branch', '-b') || undefined,
574
+ listOnly: hasFlag(commandArgs, '--list') || hasFlag(commandArgs, '-l'),
575
+ useGlobal: hasFlag(commandArgs, '--global') || hasFlag(commandArgs, '-g'),
576
+ copyMode: hasFlag(commandArgs, '--copy'),
577
+ autoYes: hasFlag(commandArgs, '--yes') || hasFlag(commandArgs, '-y'),
578
+ agents: parseAgentOptionValues(commandArgs, 'import'),
579
+ };
580
+
581
+ const positional = [];
582
+ for (let i = 0; i < commandArgs.length; i++) {
583
+ const arg = commandArgs[i];
584
+ if (arg === '--branch' || arg === '-b' || arg === '--agent' || arg === '-a') {
585
+ i++;
586
+ continue;
587
+ }
588
+ if (arg.startsWith('--branch=')) continue;
589
+ if (arg.startsWith('--agent=')) continue;
590
+ if (arg.startsWith('-a=')) continue;
591
+ if (arg === '--list' || arg === '-l' || arg === '--global' || arg === '-g' || arg === '--copy' || arg === '--yes' || arg === '-y') {
592
+ continue;
593
+ }
594
+ if (arg.startsWith('-')) {
595
+ throw new Error(`Unknown import option: ${arg}`);
596
+ }
597
+ positional.push(arg);
598
+ }
599
+
600
+ if (positional.length === 0) {
601
+ throw new Error('Error: Missing URL for import.');
602
+ }
603
+
604
+ if (positional.length > 1) {
605
+ throw new Error(`Unexpected extra import arguments: ${positional.slice(1).join(' ')}`);
606
+ }
607
+
608
+ parsed.url = positional[0];
609
+ return parsed;
610
+ }
611
+
612
+ function normalizeKitMode(modeRaw) {
613
+ if (!modeRaw) return 'copy';
614
+ const normalized = String(modeRaw).trim().toLowerCase();
615
+ if (normalized === 'copy' || normalized === 'link') {
616
+ return normalized;
617
+ }
618
+ throw new Error(`Invalid --mode value: ${modeRaw}. Use copy or link.`);
619
+ }
620
+
621
+ function parseSkillsArg(skillsRaw) {
622
+ if (!skillsRaw || !skillsRaw.trim()) {
623
+ throw new Error('Missing --skills value.');
624
+ }
625
+
626
+ const skillPaths = skillsRaw
627
+ .split(',')
628
+ .map((entry) => entry.trim())
629
+ .filter(Boolean);
630
+
631
+ if (skillPaths.length === 0) {
632
+ throw new Error('At least one skill path is required in --skills.');
633
+ }
634
+
635
+ return Array.from(new Set(skillPaths));
636
+ }
637
+
638
+ async function readContentFromArgs(commandArgs) {
639
+ const content = readArgValue(commandArgs, '--content');
640
+ const contentFile = readArgValue(commandArgs, '--content-file');
641
+
642
+ if (content && contentFile) {
643
+ throw new Error('Use either --content or --content-file, not both.');
644
+ }
645
+
646
+ if (contentFile) {
647
+ const resolved = path.resolve(contentFile);
648
+ return fsPromises.readFile(resolved, 'utf-8');
649
+ }
650
+
651
+ if (content) {
652
+ return content;
653
+ }
654
+
655
+ return undefined;
656
+ }
657
+
658
+ function readUniversalApps(commandArgs) {
659
+ const raw = readArgValue(commandArgs, '--apps');
660
+ if (!raw) {
661
+ return {
662
+ claude: true,
663
+ codex: true,
664
+ gemini: true,
665
+ };
666
+ }
667
+
668
+ const set = new Set(
669
+ raw
670
+ .split(',')
671
+ .map((item) => item.trim().toLowerCase())
672
+ .filter(Boolean)
673
+ );
674
+
675
+ return {
676
+ claude: set.has('claude'),
677
+ codex: set.has('codex'),
678
+ gemini: set.has('gemini'),
679
+ };
680
+ }
681
+
682
+ function readUniversalModels(commandArgs) {
683
+ const claudeModel = readArgValue(commandArgs, '--claude-model');
684
+ const codexModel = readArgValue(commandArgs, '--codex-model');
685
+ const geminiModel = readArgValue(commandArgs, '--gemini-model');
686
+
687
+ return {
688
+ claude: { model: claudeModel || undefined },
689
+ codex: { model: codexModel || undefined },
690
+ gemini: { model: geminiModel || undefined },
691
+ };
692
+ }
693
+
694
+ function printProviderRow(provider) {
695
+ const currentLabel = provider.isCurrent ? ' (current)' : '';
696
+ console.log(`- ${provider.id} | ${provider.appType} | ${provider.name}${currentLabel}`);
697
+ }
698
+
699
+ function printUniversalProviderRow(provider) {
700
+ const enabledApps = ['claude', 'codex', 'gemini'].filter((app) => provider.apps?.[app]).join(',');
701
+ console.log(`- ${provider.id} | ${provider.name} | ${provider.baseUrl} | apps=${enabledApps || 'none'}`);
702
+ }
703
+
704
+ function printKitPolicyRow(policy) {
705
+ console.log(`- ${policy.id} | ${policy.name}${policy.description ? ` | ${policy.description}` : ''}`);
706
+ }
707
+
708
+ function printKitLoadoutRow(loadout) {
709
+ console.log(`- ${loadout.id} | ${loadout.name} | skills=${loadout.items.length}`);
710
+ }
711
+
712
+ function printKitRow(kit) {
713
+ console.log(`- ${kit.id} | ${kit.name} | policy=${kit.policyId} | loadout=${kit.loadoutId}`);
714
+ }
715
+
716
+ async function handleKit(commandArgs) {
717
+ const subcommand = commandArgs[0];
718
+ if (!subcommand) {
719
+ throw new Error('Missing kit subcommand. Try: kit policy-list|policy-add|loadout-list|add|apply');
720
+ }
721
+
722
+ const service = await loadKitService();
723
+
724
+ switch (subcommand) {
725
+ case 'policy-list': {
726
+ const policies = service.listKitPolicies();
727
+ if (policies.length === 0) {
728
+ console.log('No AGENTS.md templates found.');
729
+ return;
730
+ }
731
+ policies.forEach(printKitPolicyRow);
732
+ return;
733
+ }
734
+ case 'policy-add': {
735
+ const rest = commandArgs.slice(1);
736
+ const name = readArgValue(rest, '--name');
737
+ const description = readArgValue(rest, '--description');
738
+ const content = await readContentFromArgs(rest);
739
+
740
+ if (!name || !content) {
741
+ throw new Error('kit policy-add requires --name and one of --content / --content-file');
742
+ }
743
+
744
+ const policy = service.addKitPolicy({
745
+ name,
746
+ description: description || undefined,
747
+ content,
748
+ });
749
+ console.log(`Policy created: ${policy.id} (${policy.name})`);
750
+ return;
751
+ }
752
+ case 'policy-update': {
753
+ const rest = commandArgs.slice(1);
754
+ const id = readArgValue(rest, '--id');
755
+ const name = readArgValue(rest, '--name');
756
+ const description = readArgValue(rest, '--description');
757
+ const content = await readContentFromArgs(rest);
758
+
759
+ if (!id) {
760
+ throw new Error('kit policy-update requires --id');
761
+ }
762
+
763
+ const policy = service.updateKitPolicy({
764
+ id,
765
+ name: name || undefined,
766
+ description: description || undefined,
767
+ content: content === undefined ? undefined : content,
768
+ });
769
+ console.log(`Policy updated: ${policy.id} (${policy.name})`);
770
+ return;
771
+ }
772
+ case 'policy-delete': {
773
+ const rest = commandArgs.slice(1);
774
+ const id = readArgValue(rest, '--id');
775
+ if (!id) {
776
+ throw new Error('kit policy-delete requires --id');
777
+ }
778
+ const deleted = service.deleteKitPolicy(id);
779
+ console.log(deleted ? `Deleted policy: ${id}` : `Policy not found: ${id}`);
780
+ return;
781
+ }
782
+ case 'loadout-list': {
783
+ const loadouts = service.listKitLoadouts();
784
+ if (loadouts.length === 0) {
785
+ console.log('No skills packages found.');
786
+ return;
787
+ }
788
+ loadouts.forEach(printKitLoadoutRow);
789
+ return;
790
+ }
791
+ case 'loadout-add': {
792
+ const rest = commandArgs.slice(1);
793
+ const name = readArgValue(rest, '--name');
794
+ const description = readArgValue(rest, '--description');
795
+ const skillsRaw = readArgValue(rest, '--skills');
796
+ const mode = normalizeKitMode(readArgValue(rest, '--mode'));
797
+
798
+ if (!name || !skillsRaw) {
799
+ throw new Error('kit loadout-add requires --name and --skills');
800
+ }
801
+
802
+ const skillPaths = parseSkillsArg(skillsRaw);
803
+ const loadout = service.addKitLoadout({
804
+ name,
805
+ description: description || undefined,
806
+ items: skillPaths.map((skillPath, index) => ({
807
+ skillPath,
808
+ mode,
809
+ sortOrder: index,
810
+ })),
811
+ });
812
+ console.log(`Loadout created: ${loadout.id} (${loadout.name})`);
813
+ return;
814
+ }
815
+ case 'loadout-update': {
816
+ const rest = commandArgs.slice(1);
817
+ const id = readArgValue(rest, '--id');
818
+ const name = readArgValue(rest, '--name');
819
+ const description = readArgValue(rest, '--description');
820
+ const skillsRaw = readArgValue(rest, '--skills');
821
+ const mode = normalizeKitMode(readArgValue(rest, '--mode'));
822
+
823
+ if (!id) {
824
+ throw new Error('kit loadout-update requires --id');
825
+ }
826
+
827
+ const items = skillsRaw
828
+ ? parseSkillsArg(skillsRaw).map((skillPath, index) => ({
829
+ skillPath,
830
+ mode,
831
+ sortOrder: index,
832
+ }))
833
+ : undefined;
834
+
835
+ const loadout = service.updateKitLoadout({
836
+ id,
837
+ name: name || undefined,
838
+ description: description || undefined,
839
+ items,
840
+ });
841
+ console.log(`Loadout updated: ${loadout.id} (${loadout.name})`);
842
+ return;
843
+ }
844
+ case 'loadout-delete': {
845
+ const rest = commandArgs.slice(1);
846
+ const id = readArgValue(rest, '--id');
847
+ if (!id) {
848
+ throw new Error('kit loadout-delete requires --id');
849
+ }
850
+ const deleted = service.deleteKitLoadout(id);
851
+ console.log(deleted ? `Deleted loadout: ${id}` : `Loadout not found: ${id}`);
852
+ return;
853
+ }
854
+ case 'list': {
855
+ const kits = service.listKits();
856
+ if (kits.length === 0) {
857
+ console.log('No kits found.');
858
+ return;
859
+ }
860
+ kits.forEach(printKitRow);
861
+ return;
862
+ }
863
+ case 'add': {
864
+ const rest = commandArgs.slice(1);
865
+ const name = readArgValue(rest, '--name');
866
+ const description = readArgValue(rest, '--description');
867
+ const policyId = readArgValue(rest, '--policy-id');
868
+ const loadoutId = readArgValue(rest, '--loadout-id');
869
+
870
+ if (!name || !policyId || !loadoutId) {
871
+ throw new Error('kit add requires --name --policy-id --loadout-id');
872
+ }
873
+
874
+ const kit = service.addKit({
875
+ name,
876
+ description: description || undefined,
877
+ policyId,
878
+ loadoutId,
879
+ });
880
+ console.log(`Kit created: ${kit.id} (${kit.name})`);
881
+ return;
882
+ }
883
+ case 'update': {
884
+ const rest = commandArgs.slice(1);
885
+ const id = readArgValue(rest, '--id');
886
+ const name = readArgValue(rest, '--name');
887
+ const description = readArgValue(rest, '--description');
888
+ const policyId = readArgValue(rest, '--policy-id');
889
+ const loadoutId = readArgValue(rest, '--loadout-id');
890
+
891
+ if (!id) {
892
+ throw new Error('kit update requires --id');
893
+ }
894
+
895
+ const kit = service.updateKit({
896
+ id,
897
+ name: name || undefined,
898
+ description: description || undefined,
899
+ policyId: policyId || undefined,
900
+ loadoutId: loadoutId || undefined,
901
+ });
902
+ console.log(`Kit updated: ${kit.id} (${kit.name})`);
903
+ return;
904
+ }
905
+ case 'delete': {
906
+ const rest = commandArgs.slice(1);
907
+ const id = readArgValue(rest, '--id');
908
+ if (!id) {
909
+ throw new Error('kit delete requires --id');
910
+ }
911
+ const deleted = service.deleteKit(id);
912
+ console.log(deleted ? `Deleted kit: ${id}` : `Kit not found: ${id}`);
913
+ return;
914
+ }
915
+ case 'apply': {
916
+ const rest = commandArgs.slice(1);
917
+ const kitId = readArgValue(rest, '--id');
918
+ const projectPath = readArgValue(rest, '--project');
919
+ const agentName = readArgValue(rest, '--agent');
920
+ const mode = normalizeKitMode(readArgValue(rest, '--mode'));
921
+ const overwriteAgentsMd = hasFlag(rest, '--overwrite-agents-md');
922
+
923
+ if (!kitId || !projectPath || !agentName) {
924
+ throw new Error('kit apply requires --id --project --agent');
925
+ }
926
+
927
+ const result = await service.applyKit({
928
+ kitId,
929
+ projectPath,
930
+ agentName,
931
+ mode,
932
+ overwriteAgentsMd,
933
+ });
934
+
935
+ const successCount = result.loadoutResults.filter((item) => item.status === 'success').length;
936
+ console.log(
937
+ `Kit applied: ${result.kitId} -> ${result.projectPath} (${result.agentName}), ` +
938
+ `skills=${successCount}, policy=${result.policyPath}`
939
+ );
940
+ return;
941
+ }
942
+ default:
943
+ throw new Error(`Unknown kit subcommand: ${subcommand}`);
944
+ }
945
+ }
946
+
947
+ async function handleProvider(commandArgs) {
948
+ const subcommand = commandArgs[0];
949
+ if (!subcommand) {
950
+ throw new Error('Missing provider subcommand. Try: provider list|add|update|switch|delete|restore|capture|universal-list|universal-add|universal-apply|universal-delete');
951
+ }
952
+
953
+ const core = await loadProviderCore();
954
+
955
+ switch (subcommand) {
956
+ case 'list': {
957
+ const appType = readArgValue(commandArgs.slice(1), '--app');
958
+ const providers = core.maskProviders(core.listProviders(appType || undefined));
959
+ if (providers.length === 0) {
960
+ console.log('No providers found.');
961
+ return;
962
+ }
963
+ providers.forEach(printProviderRow);
964
+ return;
965
+ }
966
+ case 'add': {
967
+ const rest = commandArgs.slice(1);
968
+ const appType = readArgValue(rest, '--app');
969
+ const name = readArgValue(rest, '--name');
970
+ if (!appType || !name) {
971
+ throw new Error('provider add requires --app and --name');
972
+ }
973
+ const config = await readProviderConfigFromArgs(rest);
974
+ if (!config) {
975
+ throw new Error('provider add requires --config-json or --config-file');
976
+ }
977
+ const profile = readProviderProfileArgs(rest, 'api');
978
+ const provider = core.addProvider({
979
+ appType,
980
+ name,
981
+ config: {
982
+ ...config,
983
+ _profile: {
984
+ ...(config._profile || {}),
985
+ ...profile,
986
+ },
987
+ },
988
+ });
989
+ console.log(`Provider created: ${provider.id} (${provider.appType} / ${provider.name})`);
990
+ return;
991
+ }
992
+ case 'update': {
993
+ const rest = commandArgs.slice(1);
994
+ const id = readArgValue(rest, '--id');
995
+ const name = readArgValue(rest, '--name');
996
+ if (!id) {
997
+ throw new Error('provider update requires --id');
998
+ }
999
+ const config = await readProviderConfigFromArgs(rest);
1000
+ if (name === null && config === undefined) {
1001
+ throw new Error('provider update requires at least one of --name / --config-json / --config-file');
1002
+ }
1003
+ const provider = core.updateProvider({ id, name: name || undefined, config });
1004
+ console.log(`Provider updated: ${provider.id} (${provider.name})`);
1005
+ return;
1006
+ }
1007
+ case 'switch': {
1008
+ const rest = commandArgs.slice(1);
1009
+ const appType = readArgValue(rest, '--app');
1010
+ const id = readArgValue(rest, '--id');
1011
+ if (!appType || !id) {
1012
+ throw new Error('provider switch requires --app and --id');
1013
+ }
1014
+ const result = await core.switchProvider({ appType, providerId: id });
1015
+ console.log(
1016
+ `Switched ${result.appType}: ${result.switchedFrom || 'none'} -> ${result.switchedTo} (backup #${result.backupId})`
1017
+ );
1018
+ return;
1019
+ }
1020
+ case 'delete': {
1021
+ const rest = commandArgs.slice(1);
1022
+ const id = readArgValue(rest, '--id');
1023
+ if (!id) {
1024
+ throw new Error('provider delete requires --id');
1025
+ }
1026
+ const deleted = core.deleteProvider(id);
1027
+ console.log(deleted ? `Deleted provider: ${id}` : `Provider not found: ${id}`);
1028
+ return;
1029
+ }
1030
+ case 'restore': {
1031
+ const rest = commandArgs.slice(1);
1032
+ const appType = readArgValue(rest, '--app');
1033
+ if (!appType) {
1034
+ throw new Error('provider restore requires --app');
1035
+ }
1036
+ const restored = await core.restoreBackup(appType);
1037
+ console.log(`Restored ${appType} live config from backup #${restored.id}.`);
1038
+ return;
1039
+ }
1040
+ case 'capture': {
1041
+ const rest = commandArgs.slice(1);
1042
+ const appType = readArgValue(rest, '--app');
1043
+ const name = readArgValue(rest, '--name');
1044
+ if (!appType || !name) {
1045
+ throw new Error('provider capture requires --app and --name');
1046
+ }
1047
+ const profile = readProviderProfileArgs(rest, 'official');
1048
+ const provider = await core.captureProviderFromLive({
1049
+ appType,
1050
+ name,
1051
+ profile,
1052
+ });
1053
+ console.log(`Provider captured from live config: ${provider.id} (${provider.appType} / ${provider.name})`);
1054
+ return;
1055
+ }
1056
+ case 'universal-list': {
1057
+ const universalProviders = core.listUniversalProviders();
1058
+ if (universalProviders.length === 0) {
1059
+ console.log('No universal providers found.');
1060
+ return;
1061
+ }
1062
+ universalProviders.forEach(printUniversalProviderRow);
1063
+ return;
1064
+ }
1065
+ case 'universal-add': {
1066
+ const rest = commandArgs.slice(1);
1067
+ const name = readArgValue(rest, '--name');
1068
+ const baseUrl = readArgValue(rest, '--base-url');
1069
+ const apiKey = readArgValue(rest, '--api-key');
1070
+ const websiteUrl = readArgValue(rest, '--website');
1071
+ const notes = readArgValue(rest, '--note');
1072
+
1073
+ if (!name || !baseUrl || !apiKey) {
1074
+ throw new Error('provider universal-add requires --name, --base-url and --api-key');
1075
+ }
1076
+
1077
+ const universalProvider = core.addUniversalProvider({
1078
+ name,
1079
+ baseUrl,
1080
+ apiKey,
1081
+ websiteUrl: websiteUrl || undefined,
1082
+ notes: notes || undefined,
1083
+ apps: readUniversalApps(rest),
1084
+ models: readUniversalModels(rest),
1085
+ });
1086
+
1087
+ const applied = core.applyUniversalProvider({ id: universalProvider.id });
1088
+ console.log(
1089
+ `Universal provider created: ${universalProvider.id} (${universalProvider.name}), applied ${applied.length} app providers.`
1090
+ );
1091
+ return;
1092
+ }
1093
+ case 'universal-apply': {
1094
+ const rest = commandArgs.slice(1);
1095
+ const id = readArgValue(rest, '--id');
1096
+ if (!id) {
1097
+ throw new Error('provider universal-apply requires --id');
1098
+ }
1099
+ const applied = core.applyUniversalProvider({ id });
1100
+ console.log(`Universal provider applied: ${id} (${applied.length} app providers updated)`);
1101
+ return;
1102
+ }
1103
+ case 'universal-delete': {
1104
+ const rest = commandArgs.slice(1);
1105
+ const id = readArgValue(rest, '--id');
1106
+ if (!id) {
1107
+ throw new Error('provider universal-delete requires --id');
1108
+ }
1109
+ const deleted = core.deleteUniversalProvider(id);
1110
+ console.log(deleted ? `Deleted universal provider: ${id}` : `Universal provider not found: ${id}`);
1111
+ return;
1112
+ }
1113
+ default:
1114
+ throw new Error(`Unknown provider subcommand: ${subcommand}`);
1115
+ }
1116
+ }
1117
+
1118
+ async function handleHubList(hubPath) {
1119
+ console.log(`Listing skills in ${hubPath}:`);
1120
+ if (!await fse.pathExists(hubPath)) {
1121
+ console.log(' (Hub directory does not exist yet)');
1122
+ return;
1123
+ }
1124
+
1125
+ const items = await readSkillNamesUnderRoot(hubPath);
1126
+ if (items.length === 0) {
1127
+ console.log(' (Hub directory is empty)');
1128
+ return;
1129
+ }
1130
+
1131
+ for (const item of items) {
1132
+ console.log(` - ${item}`);
1133
+ }
1134
+ }
1135
+
1136
+ async function handleInstallList({ config, useGlobal, agents }) {
1137
+ const scopeName = useGlobal ? 'global' : 'project';
1138
+ const selectedAgents = matchAgentsBySelectors(config.agents, agents);
1139
+
1140
+ if (selectedAgents.length === 0) {
1141
+ console.error('No matching agents found.');
1142
+ return;
1143
+ }
1144
+
1145
+ console.log(`Listing installed skills (${scopeName} scope):`);
1146
+ for (const agent of selectedAgents) {
1147
+ const targetRoot = resolveAgentScopePath(agent, useGlobal);
1148
+ const skills = await readSkillNamesUnderRoot(targetRoot);
1149
+ const skillLabel = skills.length > 0 ? skills.join(', ') : '(none)';
1150
+ console.log(` - ${agent.name} (${targetRoot}) -> ${skillLabel}`);
1151
+ }
1152
+ }
1153
+
1154
+ async function handleList(commandArgs) {
1155
+ const parsed = parseScopeArgs(commandArgs, 'list');
1156
+ const config = await loadConfig();
1157
+ const hubPath = expandHome(config.hubPath);
1158
+
1159
+ if (parsed.positional.length > 0) {
1160
+ throw new Error(`list: unexpected arguments: ${parsed.positional.join(' ')}`);
1161
+ }
1162
+
1163
+ if (parsed.useHub) {
1164
+ await handleHubList(hubPath);
1165
+ return;
1166
+ }
1167
+
1168
+ await handleInstallList({
1169
+ config,
1170
+ useGlobal: parsed.useGlobal,
1171
+ agents: parsed.agents,
1172
+ });
1173
+ }
1174
+
1175
+ async function removeHubSkills(hubPath, skillNames, removeAll) {
1176
+ if (!await fse.pathExists(hubPath)) {
1177
+ console.log('Hub directory does not exist, nothing to remove.');
1178
+ return;
1179
+ }
1180
+
1181
+ const targetNames = removeAll ? await readSkillNamesUnderRoot(hubPath) : skillNames;
1182
+ if (targetNames.length === 0) {
1183
+ console.log('No hub skills matched for removal.');
1184
+ return;
1185
+ }
1186
+
1187
+ for (const skillName of targetNames) {
1188
+ const targetPath = path.join(hubPath, skillName);
1189
+ if (!await fse.pathExists(targetPath)) {
1190
+ console.log(` [SKIP] Hub skill not found: ${skillName}`);
1191
+ continue;
1192
+ }
1193
+ await fse.remove(targetPath);
1194
+ console.log(` [OK] Removed hub skill: ${skillName}`);
1195
+ }
1196
+ }
1197
+
1198
+ async function removeInstalledSkills({ config, useGlobal, agents, skillNames, removeAll }) {
1199
+ const selectedAgents = matchAgentsBySelectors(config.agents, agents);
1200
+ if (selectedAgents.length === 0) {
1201
+ throw new Error('No matching agents found.');
1202
+ }
1203
+
1204
+ for (const agent of selectedAgents) {
1205
+ const targetRoot = resolveAgentScopePath(agent, useGlobal);
1206
+ if (!await fse.pathExists(targetRoot)) {
1207
+ console.log(` [SKIP] ${agent.name}: target path missing (${targetRoot})`);
1208
+ continue;
1209
+ }
1210
+
1211
+ const targetNames = removeAll ? await readSkillNamesUnderRoot(targetRoot) : skillNames;
1212
+ if (targetNames.length === 0) {
1213
+ console.log(` [SKIP] ${agent.name}: no matching skills to remove`);
1214
+ continue;
1215
+ }
1216
+
1217
+ for (const skillName of targetNames) {
1218
+ const targetPath = path.join(targetRoot, skillName);
1219
+ if (!await fse.pathExists(targetPath)) {
1220
+ console.log(` [SKIP] ${agent.name}: ${skillName} not found`);
1221
+ continue;
1222
+ }
1223
+
1224
+ await fse.remove(targetPath);
1225
+ console.log(` [OK] ${agent.name}: removed ${skillName}`);
1226
+ }
1227
+ }
1228
+ }
1229
+
1230
+ async function handleRemove(commandArgs) {
1231
+ const parsed = parseScopeArgs(commandArgs, 'remove', new Set(['--all']));
1232
+ const removeAll = hasFlag(commandArgs, '--all');
1233
+ const skillNames = parsed.positional;
1234
+
1235
+ if (!removeAll && skillNames.length === 0) {
1236
+ throw new Error('remove requires at least one skill name or --all.');
1237
+ }
1238
+
1239
+ const config = await loadConfig();
1240
+ const hubPath = expandHome(config.hubPath);
1241
+
1242
+ if (parsed.useHub) {
1243
+ await removeHubSkills(hubPath, skillNames, removeAll);
1244
+ return;
1245
+ }
1246
+
1247
+ await removeInstalledSkills({
1248
+ config,
1249
+ useGlobal: parsed.useGlobal,
1250
+ agents: parsed.agents,
1251
+ skillNames,
1252
+ removeAll,
1253
+ });
1254
+ }
1255
+
1256
+ async function handleSync(commandArgs) {
1257
+ const targetName = readArgValue(commandArgs, '--target', '-t');
1258
+ const allTargets = commandArgs.includes('--all');
1259
+
1260
+ if (!targetName && !allTargets) {
1261
+ console.error('Error: specify --target <name> or --all');
1262
+ process.exit(1);
1263
+ }
1264
+
1265
+ const config = await loadConfig();
1266
+ const hubPath = expandHome(config.hubPath);
1267
+
1268
+ if (!await fse.pathExists(hubPath)) {
1269
+ console.log('Hub directory empty, nothing to sync.');
1270
+ return;
1271
+ }
1272
+
1273
+ let targetAgents = [];
1274
+ if (allTargets) {
1275
+ targetAgents = config.agents.filter((agent) => agent.enabled);
1276
+ } else {
1277
+ const normalized = targetName.toLowerCase();
1278
+ targetAgents = config.agents.filter((agent) => agent.name.toLowerCase().includes(normalized));
1279
+ }
1280
+
1281
+ if (targetAgents.length === 0) {
1282
+ console.error('No matching agents found to sync to.');
1283
+ return;
1284
+ }
1285
+
1286
+ const skills = await fsPromises.readdir(hubPath);
1287
+ console.log(`Found ${skills.length} skills in Hub. Syncing to ${targetAgents.length} agents...`);
1288
+
1289
+ for (const agent of targetAgents) {
1290
+ const destRoot = expandHome(agent.globalPath);
1291
+ console.log(`\nSyncing to Agent: ${agent.name} (${destRoot})...`);
1292
+ await fse.ensureDir(destRoot);
1293
+
1294
+ for (const skill of skills) {
1295
+ const skillSource = path.join(hubPath, skill);
1296
+ const stat = await fse.stat(skillSource);
1297
+ if (!stat.isDirectory()) continue;
1298
+
1299
+ const skillDest = path.join(destRoot, skill);
1300
+
1301
+ try {
1302
+ await fse.copy(skillSource, skillDest, { overwrite: true });
1303
+ console.log(` [OK] ${skill} -> ${skillDest}`);
1304
+ } catch (error) {
1305
+ console.error(` [ERR] Failed to sync ${skill}:`, error);
1306
+ }
1307
+ }
1308
+ }
1309
+
1310
+ console.log('\nSync complete.');
1311
+ }
1312
+
1313
+ async function collectRemoteInstallableSkills(basePath, currentPath, out, rootSkillName) {
1314
+ const skillMdPath = path.join(currentPath, 'SKILL.md');
1315
+ if (await fse.pathExists(skillMdPath)) {
1316
+ const relative = path.relative(basePath, currentPath).split(path.sep).join('/') || '.';
1317
+ out.push({
1318
+ name: relative === '.' ? rootSkillName : path.basename(currentPath),
1319
+ relativePath: relative,
1320
+ });
1321
+ return;
1322
+ }
1323
+
1324
+ const entries = await fsPromises.readdir(currentPath, { withFileTypes: true });
1325
+ for (const entry of entries) {
1326
+ if (!entry.isDirectory()) continue;
1327
+ if (entry.name === '.git' || entry.name === 'node_modules') continue;
1328
+ await collectRemoteInstallableSkills(basePath, path.join(currentPath, entry.name), out, rootSkillName);
1329
+ }
1330
+ }
1331
+
1332
+ async function listRemoteInstallableSkills(repoUrl, subdir, branch, rootSkillName) {
1333
+ const tempDir = await fsPromises.mkdtemp(path.join(os.tmpdir(), 'skills-hub-list-'));
1334
+ const git = simpleGit();
1335
+
1336
+ try {
1337
+ const cloneArgs = ['--depth', '1'];
1338
+ if (branch) {
1339
+ cloneArgs.push('--branch', branch);
1340
+ }
1341
+ await git.clone(repoUrl, tempDir, cloneArgs);
1342
+
1343
+ const localGit = simpleGit(tempDir);
1344
+ const resolvedBranch = await localGit.revparse(['--abbrev-ref', 'HEAD']).catch(() => branch || 'unknown');
1345
+
1346
+ const sourceRoot = subdir ? path.join(tempDir, subdir) : tempDir;
1347
+ if (!await fse.pathExists(sourceRoot)) {
1348
+ throw new Error(`Directory '${subdir}' not found in remote repository.`);
1349
+ }
1350
+
1351
+ const installableSkills = [];
1352
+ await collectRemoteInstallableSkills(sourceRoot, sourceRoot, installableSkills, rootSkillName);
1353
+ installableSkills.sort((a, b) => a.relativePath.localeCompare(b.relativePath));
1354
+
1355
+ return {
1356
+ resolvedBranch,
1357
+ installableSkills,
1358
+ };
1359
+ } finally {
1360
+ await fse.remove(tempDir);
1361
+ }
1362
+ }
1363
+
1364
+ async function handleImport(commandArgs) {
1365
+ const parsed = parseImportArgs(commandArgs);
1366
+ const { repoUrl, repoWebUrl, subdir, skillName, branch } = parseSkillImportUrl(parsed.url, parsed.branch);
1367
+ const config = await loadConfig();
1368
+ const hubPath = expandHome(config.hubPath);
1369
+ const hubSkillPath = path.join(hubPath, skillName);
1370
+ const installMode = parsed.useGlobal || parsed.copyMode || parsed.agents.length > 0;
1371
+ const syncMode = parsed.copyMode ? 'copy' : 'link';
1372
+
1373
+ if (parsed.listOnly) {
1374
+ const result = await listRemoteInstallableSkills(repoUrl, subdir, branch, skillName);
1375
+ console.log(`Installable skills from ${repoWebUrl} (branch: ${result.resolvedBranch}):`);
1376
+ if (result.installableSkills.length === 0) {
1377
+ console.log(' (none found)');
1378
+ return;
1379
+ }
1380
+ for (const item of result.installableSkills) {
1381
+ const pathLabel = item.relativePath === '.' ? '(root)' : item.relativePath;
1382
+ console.log(` - ${item.name} [${pathLabel}]`);
1383
+ }
1384
+ return;
1385
+ }
1386
+
1387
+ const installTargets = [];
1388
+ if (installMode) {
1389
+ const selectedAgents = matchAgentsBySelectors(config.agents, parsed.agents);
1390
+ if (selectedAgents.length === 0) {
1391
+ throw new Error('No matching agents found to install skills.');
1392
+ }
1393
+
1394
+ for (const agent of selectedAgents) {
1395
+ const targetRoot = resolveAgentScopePath(agent, parsed.useGlobal);
1396
+ installTargets.push({
1397
+ agentName: agent.name,
1398
+ targetRoot,
1399
+ skillPath: path.join(targetRoot, skillName),
1400
+ });
1401
+ }
1402
+ }
1403
+
1404
+ const conflictTargets = [];
1405
+ if (await fse.pathExists(hubSkillPath)) {
1406
+ conflictTargets.push({
1407
+ label: `Hub skill ${skillName}`,
1408
+ path: hubSkillPath,
1409
+ });
1410
+ }
1411
+ for (const target of installTargets) {
1412
+ if (await fse.pathExists(target.skillPath)) {
1413
+ conflictTargets.push({
1414
+ label: `${target.agentName} skill ${skillName}`,
1415
+ path: target.skillPath,
1416
+ });
1417
+ }
1418
+ }
1419
+
1420
+ if (conflictTargets.length > 0 && !parsed.autoYes && !isInteractiveTerminal()) {
1421
+ throw new Error(`Target already exists: ${conflictTargets[0].path}. Use -y/--yes to overwrite in non-interactive mode.`);
1422
+ }
1423
+
1424
+ if (conflictTargets.length > 0 && !parsed.autoYes && isInteractiveTerminal()) {
1425
+ for (const target of conflictTargets) {
1426
+ const confirmed = await promptYesNo(`${target.label} already exists at ${target.path}. Overwrite?`);
1427
+ if (!confirmed) {
1428
+ console.log('Import cancelled.');
1429
+ return;
1430
+ }
1431
+ }
1432
+ }
1433
+
1434
+ await fse.ensureDir(hubPath);
1435
+ if (await fse.pathExists(hubSkillPath)) {
1436
+ await fse.remove(hubSkillPath);
1437
+ }
1438
+
1439
+ console.log(` Repo: ${repoUrl}`);
1440
+ console.log(` Subdir: ${subdir || '(root)'}`);
1441
+ console.log(` Hub target: ${hubSkillPath}`);
1442
+
1443
+ const downloadResult = await downloadRemoteSkill(repoUrl, subdir, hubSkillPath, branch);
1444
+ const sourceUrl = buildGitSourceUrl(repoWebUrl, downloadResult.resolvedBranch, subdir);
1445
+ await attachSkillImportMetadata(hubSkillPath, {
1446
+ sourceRepo: repoWebUrl,
1447
+ sourceUrl,
1448
+ sourceSubdir: subdir,
1449
+ sourceLastUpdated: downloadResult.lastUpdatedAt,
1450
+ importedAt: new Date().toISOString(),
1451
+ });
1452
+
1453
+ console.log(`Successfully imported ${skillName} to Hub from ${repoWebUrl}.`);
1454
+ console.log(`Source last updated: ${downloadResult.lastUpdatedAt}`);
1455
+
1456
+ if (!installMode) {
1457
+ return;
1458
+ }
1459
+
1460
+ console.log(`Installing '${skillName}' to ${installTargets.length} agent target(s) (${syncMode})...`);
1461
+ for (const target of installTargets) {
1462
+ await syncSkillToTarget(hubSkillPath, target.skillPath, syncMode);
1463
+ console.log(` [OK] ${target.agentName}: ${target.skillPath}`);
1464
+ }
1465
+ }
1466
+
1467
+ const CONFIG_PATH = path.join(os.homedir(), '.skills-hub', 'config.json');
1468
+
1469
+ const DEFAULT_AGENTS = [
1470
+ { name: 'Antigravity', globalPath: path.join(os.homedir(), '.gemini/antigravity/skills'), projectPath: '.agent/skills', enabled: true, isCustom: false },
1471
+ { name: 'Claude Code', globalPath: path.join(os.homedir(), '.claude/skills'), projectPath: '.claude/skills', enabled: true, isCustom: false },
1472
+ { name: 'Cursor', globalPath: path.join(os.homedir(), '.cursor/skills'), projectPath: '.cursor/skills', enabled: true, isCustom: false },
1473
+ { name: 'OpenCode', globalPath: path.join(os.homedir(), '.config/opencode/skill'), projectPath: '.opencode/skill', enabled: false, isCustom: false },
1474
+ { name: 'Codex', globalPath: path.join(os.homedir(), '.codex/skills'), projectPath: '.codex/skills', enabled: false, isCustom: false },
1475
+ { name: 'Amp', globalPath: path.join(os.homedir(), '.config/agents/skills'), projectPath: '.agents/skills', enabled: false, isCustom: false },
1476
+ { name: 'Kilo Code', globalPath: path.join(os.homedir(), '.kilocode/skills'), projectPath: '.kilocode/skills', enabled: false, isCustom: false },
1477
+ { name: 'Roo Code', globalPath: path.join(os.homedir(), '.roo/skills'), projectPath: '.roo/skills', enabled: false, isCustom: false },
1478
+ { name: 'Goose', globalPath: path.join(os.homedir(), '.config/goose/skills'), projectPath: '.goose/skills', enabled: false, isCustom: false },
1479
+ { name: 'Gemini CLI', globalPath: path.join(os.homedir(), '.gemini/skills'), projectPath: '.gemini/skills', enabled: false, isCustom: false },
1480
+ { name: 'GitHub Copilot', globalPath: path.join(os.homedir(), '.copilot/skills'), projectPath: '.github/skills', enabled: false, isCustom: false },
1481
+ { name: 'Clawdbot', globalPath: path.join(os.homedir(), '.clawdbot/skills'), projectPath: 'skills', enabled: false, isCustom: false },
1482
+ { name: 'Droid', globalPath: path.join(os.homedir(), '.factory/skills'), projectPath: '.factory/skills', enabled: false, isCustom: false },
1483
+ { name: 'Windsurf', globalPath: path.join(os.homedir(), '.codeium/windsurf/skills'), projectPath: '.windsurf/skills', enabled: false, isCustom: false },
1484
+ { name: 'Trae', globalPath: path.join(os.homedir(), '.trae/skills'), projectPath: '.trae/skills', enabled: false, isCustom: false },
1485
+ { name: 'Qoder', globalPath: path.join(os.homedir(), '.qoder/skills'), projectPath: '.qoder/skills', enabled: false, isCustom: false },
1486
+ ];
1487
+
1488
+ const DEFAULT_CONFIG = {
1489
+ hubPath: path.join(os.homedir(), 'skills-hub'),
1490
+ projects: [],
1491
+ scanRoots: [path.join(os.homedir(), 'workspace')],
1492
+ agents: DEFAULT_AGENTS,
1493
+ };
1494
+
1495
+ async function loadConfig() {
1496
+ try {
1497
+ const content = await fsPromises.readFile(CONFIG_PATH, 'utf-8');
1498
+ if (!content.trim()) return DEFAULT_CONFIG;
1499
+ const userConfig = JSON.parse(content);
1500
+
1501
+ const mergedAgents = [...DEFAULT_AGENTS];
1502
+ const userAgents = userConfig.agents || [];
1503
+
1504
+ mergedAgents.forEach((agent, index) => {
1505
+ const userAgent = userAgents.find((ua) => ua.name === agent.name);
1506
+ if (userAgent) {
1507
+ mergedAgents[index] = {
1508
+ ...agent,
1509
+ enabled: userAgent.enabled,
1510
+ projectPath: userAgent.projectPath,
1511
+ globalPath: userAgent.globalPath,
1512
+ };
1513
+ }
1514
+ });
1515
+
1516
+ const customAgents = userAgents.filter((ua) => ua.isCustom);
1517
+
1518
+ return {
1519
+ ...DEFAULT_CONFIG,
1520
+ ...userConfig,
1521
+ agents: [...mergedAgents, ...customAgents],
1522
+ };
1523
+ } catch {
1524
+ return DEFAULT_CONFIG;
1525
+ }
1526
+ }
1527
+
1528
+ async function resolveDefaultBranch(git) {
1529
+ try {
1530
+ const result = await git.listRemote(['--symref', 'origin', 'HEAD']);
1531
+ const match = result.match(/ref:\s+refs\/heads\/([^\s]+)\s+HEAD/);
1532
+ return match ? match[1] : null;
1533
+ } catch {
1534
+ return null;
1535
+ }
1536
+ }
1537
+
1538
+ async function downloadRemoteSkill(repoUrl, subdir, destPath, branch) {
1539
+ const tempDir = await fsPromises.mkdtemp(path.join(os.tmpdir(), 'skills-hub-import-'));
1540
+ const git = simpleGit(tempDir);
1541
+
1542
+ try {
1543
+ await git.init();
1544
+ await git.addRemote('origin', repoUrl);
1545
+ await git.addConfig('core.sparseCheckout', 'true');
1546
+
1547
+ if (subdir) {
1548
+ await fsPromises.writeFile(path.join(tempDir, '.git/info/sparse-checkout'), `${subdir}\n`, 'utf-8');
1549
+ } else {
1550
+ await fsPromises.writeFile(path.join(tempDir, '.git/info/sparse-checkout'), '*\n', 'utf-8');
1551
+ }
1552
+
1553
+ const requestedBranch = branch ? branch.trim() : '';
1554
+ const defaultBranch = requestedBranch ? null : await resolveDefaultBranch(git);
1555
+ const branchesToTry = requestedBranch
1556
+ ? [requestedBranch]
1557
+ : defaultBranch
1558
+ ? [defaultBranch]
1559
+ : ['main', 'master'];
1560
+
1561
+ let pulledBranch = '';
1562
+ let lastError;
1563
+ for (const candidate of branchesToTry) {
1564
+ try {
1565
+ await git.pull('origin', candidate, { '--depth': 1 });
1566
+ pulledBranch = candidate;
1567
+ break;
1568
+ } catch (error) {
1569
+ lastError = error;
1570
+ }
1571
+ }
1572
+
1573
+ if (!pulledBranch) {
1574
+ throw lastError instanceof Error
1575
+ ? lastError
1576
+ : new Error('Failed to resolve default branch for remote repository.');
1577
+ }
1578
+
1579
+ const logArgs = ['log', '-1', '--format=%cI'];
1580
+ if (subdir) {
1581
+ logArgs.push('--', subdir);
1582
+ }
1583
+ let lastUpdatedAt = new Date().toISOString();
1584
+ try {
1585
+ const lastUpdatedAtRaw = await git.raw(logArgs);
1586
+ lastUpdatedAt = lastUpdatedAtRaw.trim() || lastUpdatedAt;
1587
+ } catch {
1588
+ // Keep fallback timestamp to avoid import failure on metadata lookup issues.
1589
+ }
1590
+
1591
+ const sourceContentPath = subdir ? path.join(tempDir, subdir) : tempDir;
1592
+
1593
+ if (!await fse.pathExists(sourceContentPath)) {
1594
+ throw new Error(`Directory '${subdir}' not found in remote repository.`);
1595
+ }
1596
+
1597
+ await fse.ensureDir(path.dirname(destPath));
1598
+ await fse.copy(sourceContentPath, destPath);
1599
+ return {
1600
+ resolvedBranch: pulledBranch,
1601
+ lastUpdatedAt,
1602
+ };
1603
+ } finally {
1604
+ await fse.remove(tempDir);
1605
+ }
1606
+ }
1607
+
1608
+ main().catch((error) => {
1609
+ console.error(`Command failed: ${error.message || error}`);
1610
+ process.exit(1);
1611
+ });