@potatodog1669/skills-hub 0.1.13 → 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.
Files changed (3) hide show
  1. package/README.md +51 -6
  2. package/bin/skills-hub +583 -69
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -39,7 +39,7 @@ Skills Hub supports synchronization with a wide range of AI agents, including An
39
39
  - Rust toolchain (`rustup`) for Desktop (Tauri) source build
40
40
  - Tauri platform prerequisites for your OS: [Tauri v2 prerequisites](https://v2.tauri.app/start/prerequisites/)
41
41
 
42
- ### Option A: Homebrew (macOS/Linux)
42
+ ### Option A: Homebrew CLI (macOS/Linux)
43
43
 
44
44
  ```bash
45
45
  brew tap PotatoDog1669/skillshub
@@ -54,7 +54,21 @@ brew update
54
54
  brew upgrade skills-hub
55
55
  ```
56
56
 
57
- ### Option B: CLI via npm
57
+ ### Option B: Homebrew Desktop App (macOS)
58
+
59
+ ```bash
60
+ brew tap PotatoDog1669/skillshub
61
+ brew install --cask skills-hub
62
+ ```
63
+
64
+ Upgrade:
65
+
66
+ ```bash
67
+ brew update
68
+ brew upgrade --cask skills-hub
69
+ ```
70
+
71
+ ### Option C: CLI via npm
58
72
 
59
73
  Install globally:
60
74
 
@@ -75,7 +89,7 @@ Upgrade:
75
89
  npm i -g @potatodog1669/skills-hub@latest
76
90
  ```
77
91
 
78
- ### Option C: Build Desktop App from Source
92
+ ### Option D: Build Desktop App from Source
79
93
 
80
94
  ```bash
81
95
  git clone https://github.com/PotatoDog1669/skills-hub.git
@@ -97,14 +111,17 @@ Output directory:
97
111
 
98
112
  - Latest releases: [GitHub Releases](https://github.com/PotatoDog1669/skills-hub/releases)
99
113
  - Current releases include changelog + source archives (`zipball` / `tarball`).
100
- - If desktop installer assets are attached to a release, prefer those assets for end users.
114
+ - Desktop release assets include Homebrew cask-ready DMGs:
115
+ - `skills-hub_X.Y.Z_macos_aarch64.dmg`
116
+ - `skills-hub_X.Y.Z_macos_x64.dmg`
101
117
 
102
118
  ## CLI Command Overview
103
119
 
104
120
  | Command | Description |
105
121
  | :---------------------------------------- | :------------------------------------------------------------------------------ |
106
- | `skills-hub list` | List all skills in your Central Hub (`~/skills-hub`) |
107
- | `skills-hub import <url>` | Import a skill from GitHub (supports branch: `--branch main`) |
122
+ | `skills-hub list` / `skills-hub ls` | List installed skills (project scope by default; supports `--global`, `--hub`) |
123
+ | `skills-hub remove` / `skills-hub rm` | Remove installed skills (supports `--all`, `--global`, `--hub`, `--agent`) |
124
+ | `skills-hub import <url>` | Import to Hub (supports `--branch`, install mode: `-a/-g/--copy`, and `--list`) |
108
125
  | `skills-hub sync --all` | Sync Hub skills to all enabled agents (Antigravity, Claude, Cursor, etc.) |
109
126
  | `skills-hub sync --target <name>` | Sync to a specific agent (e.g., `--target claude` syncs to `~/.claude/skills/`) |
110
127
  | `skills-hub provider list` | List provider profiles (`claude`, `codex`, `gemini`) |
@@ -119,6 +136,34 @@ Output directory:
119
136
  | `skills-hub kit loadout-*` | Manage skill packages (`loadout-list/add/update/delete`) |
120
137
  | `skills-hub kit add/update/delete/apply` | Compose Kit and apply it to target project + agent |
121
138
 
139
+ ### Import/List/Remove Quick Examples
140
+
141
+ ```bash
142
+ # Import to Hub only (backward compatible)
143
+ skills-hub import https://github.com/owner/repo
144
+
145
+ # List installable skills from remote source only
146
+ skills-hub import https://github.com/owner/repo --list
147
+
148
+ # Import + install to Codex in current project (default mode: symlink)
149
+ skills-hub import https://github.com/owner/repo -a codex
150
+
151
+ # Install globally and copy files instead of symlinks
152
+ skills-hub import https://github.com/owner/repo -g -a codex --copy
153
+
154
+ # Overwrite conflicts without prompt
155
+ skills-hub import https://github.com/owner/repo -y
156
+
157
+ # View global installation or Hub inventory
158
+ skills-hub ls --global
159
+ skills-hub list --hub
160
+
161
+ # Remove one installed skill or remove all in selected scope
162
+ skills-hub rm my-skill -a codex
163
+ skills-hub remove --all -g -a codex
164
+ skills-hub remove my-skill --hub
165
+ ```
166
+
122
167
  ### Development
123
168
 
124
169
  For contributors who want to modify the source code:
package/bin/skills-hub CHANGED
@@ -9,7 +9,11 @@ const simpleGit = require('simple-git');
9
9
  const matter = require('gray-matter');
10
10
 
11
11
  const args = process.argv.slice(2);
12
- const commands = new Set(['import', 'list', 'sync', 'provider', 'kit']);
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']);
13
17
  const flagsWithValues = new Set([
14
18
  '-b',
15
19
  '--branch',
@@ -36,6 +40,7 @@ const flagsWithValues = new Set([
36
40
  '--skills',
37
41
  '--project',
38
42
  '--agent',
43
+ '-a',
39
44
  '--mode',
40
45
  '--content',
41
46
  '--content-file',
@@ -43,53 +48,53 @@ const flagsWithValues = new Set([
43
48
  let providerCorePromise = null;
44
49
  let kitServicePromise = null;
45
50
 
46
- let command = null;
47
- let commandIndex = -1;
51
+ async function main() {
52
+ let command = null;
53
+ let commandIndex = -1;
48
54
 
49
- for (let i = 0; i < args.length; i++) {
50
- const arg = args[i];
51
- if (arg.startsWith('-')) {
52
- // Check if it's a flag=value style
53
- if (arg.includes('=')) continue;
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;
54
60
 
55
- // Check if it's a flag that takes a value next
56
- if (flagsWithValues.has(arg)) {
57
- i++; // Skip the next argument (the value)
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;
58
70
  }
59
- } else {
60
- // Found the first non-flag argument, closest thing to a command
61
- command = arg;
62
- commandIndex = i;
63
- break;
64
71
  }
65
- }
66
72
 
67
- if (args.includes('--help') || args.includes('-h')) {
68
- printHelp();
69
- process.exit(0);
70
- }
73
+ if (args.includes('--help') || args.includes('-h')) {
74
+ printHelp();
75
+ process.exit(0);
76
+ }
71
77
 
72
- if (args.includes('--version') || args.includes('-v') || args.includes('-V')) {
73
- const pkg = require(path.join(__dirname, '..', 'package.json'));
74
- console.log(pkg.version);
75
- process.exit(0);
76
- }
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
+ }
77
83
 
78
- if (command && !commands.has(command)) {
79
- console.error(`Unknown command: ${command}`);
80
- printHelp();
81
- process.exit(1);
82
- }
84
+ if (command && !commands.has(command)) {
85
+ console.error(`Unknown command: ${command}`);
86
+ printHelp();
87
+ process.exit(1);
88
+ }
83
89
 
84
- if (command) {
85
- const commandArgs = args.slice(commandIndex + 1);
86
- runCommand(command, commandArgs).catch((error) => {
87
- console.error(`Command failed: ${error.message || error}`);
90
+ if (!command) {
91
+ printHelp();
88
92
  process.exit(1);
89
- });
90
- } else {
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);
93
98
  }
94
99
 
95
100
  function printHelp() {
@@ -97,12 +102,32 @@ function printHelp() {
97
102
  skills-hub <command> [options]
98
103
 
99
104
  Commands:
100
- import <url> [--branch <branch>] Import a skill from a git repository
101
- list List skills in the hub directory
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)
102
108
  sync --all | --target <name> Sync hub skills to agent targets
103
109
  provider <subcommand> Manage provider profiles and switching
104
110
  kit <subcommand> Manage AGENTS templates, loadouts, and kits
105
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
+
106
131
  Provider subcommands:
107
132
  provider list [--app <app>] List providers
108
133
  provider add --app <app> --name <name> --config-json <json>
@@ -251,7 +276,10 @@ async function runCommand(commandName, commandArgs) {
251
276
  await handleImport(commandArgs);
252
277
  return;
253
278
  case 'list':
254
- await handleList();
279
+ await handleList(commandArgs);
280
+ return;
281
+ case 'remove':
282
+ await handleRemove(commandArgs);
255
283
  return;
256
284
  case 'sync':
257
285
  await handleSync(commandArgs);
@@ -334,6 +362,253 @@ function hasFlag(argv, flag) {
334
362
  return argv.includes(flag);
335
363
  }
336
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
+
337
612
  function normalizeKitMode(modeRaw) {
338
613
  if (!modeRaw) return 'copy';
339
614
  const normalized = String(modeRaw).trim().toLowerCase();
@@ -840,24 +1115,142 @@ async function handleProvider(commandArgs) {
840
1115
  }
841
1116
  }
842
1117
 
843
- async function handleList() {
844
- const config = await loadConfig();
845
- const hubPath = expandHome(config.hubPath);
846
-
1118
+ async function handleHubList(hubPath) {
847
1119
  console.log(`Listing skills in ${hubPath}:`);
848
1120
  if (!await fse.pathExists(hubPath)) {
849
1121
  console.log(' (Hub directory does not exist yet)');
850
1122
  return;
851
1123
  }
852
1124
 
853
- const items = await fsPromises.readdir(hubPath);
1125
+ const items = await readSkillNamesUnderRoot(hubPath);
1126
+ if (items.length === 0) {
1127
+ console.log(' (Hub directory is empty)');
1128
+ return;
1129
+ }
1130
+
854
1131
  for (const item of items) {
855
- const fullPath = path.join(hubPath, item);
856
- const stat = await fse.stat(fullPath);
857
- if (stat.isDirectory()) {
858
- console.log(` - ${item}`);
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;
859
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.');
860
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
+ });
861
1254
  }
862
1255
 
863
1256
  async function handleSync(commandArgs) {
@@ -917,33 +1310,139 @@ async function handleSync(commandArgs) {
917
1310
  console.log('\nSync complete.');
918
1311
  }
919
1312
 
920
- async function handleImport(commandArgs) {
921
- const branchArg = readArgValue(commandArgs, '--branch', '-b');
922
- const url = commandArgs.find((arg) => !arg.startsWith('-'));
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
+ }
923
1323
 
924
- if (!url) {
925
- console.error('Error: Missing URL for import.');
926
- console.log('Usage: skills-hub import <url> [--branch <branch>]');
927
- process.exit(1);
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);
928
1361
  }
1362
+ }
929
1363
 
930
- const { repoUrl, repoWebUrl, subdir, skillName, branch } = parseSkillImportUrl(url, branchArg);
1364
+ async function handleImport(commandArgs) {
1365
+ const parsed = parseImportArgs(commandArgs);
1366
+ const { repoUrl, repoWebUrl, subdir, skillName, branch } = parseSkillImportUrl(parsed.url, parsed.branch);
931
1367
  const config = await loadConfig();
932
1368
  const hubPath = expandHome(config.hubPath);
933
- const destPath = path.join(hubPath, skillName);
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
+ }
934
1386
 
935
- console.log(` Repo: ${repoUrl}`);
936
- console.log(` Subdir: ${subdir || '(root)'}`);
937
- console.log(` Target: ${destPath}`);
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
+ }
938
1393
 
939
- if (await fse.pathExists(destPath)) {
940
- console.error(`Error: Skill '${skillName}' already exists at ${destPath}`);
941
- process.exit(1);
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
+ }
942
1402
  }
943
1403
 
944
- const downloadResult = await downloadRemoteSkill(repoUrl, subdir, destPath, branch);
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);
945
1444
  const sourceUrl = buildGitSourceUrl(repoWebUrl, downloadResult.resolvedBranch, subdir);
946
- await attachSkillImportMetadata(destPath, {
1445
+ await attachSkillImportMetadata(hubSkillPath, {
947
1446
  sourceRepo: repoWebUrl,
948
1447
  sourceUrl,
949
1448
  sourceSubdir: subdir,
@@ -951,8 +1450,18 @@ async function handleImport(commandArgs) {
951
1450
  importedAt: new Date().toISOString(),
952
1451
  });
953
1452
 
954
- console.log(`Successfully imported ${skillName} from ${repoWebUrl}!`);
1453
+ console.log(`Successfully imported ${skillName} to Hub from ${repoWebUrl}.`);
955
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
+ }
956
1465
  }
957
1466
 
958
1467
  const CONFIG_PATH = path.join(os.homedir(), '.skills-hub', 'config.json');
@@ -1095,3 +1604,8 @@ async function downloadRemoteSkill(repoUrl, subdir, destPath, branch) {
1095
1604
  await fse.remove(tempDir);
1096
1605
  }
1097
1606
  }
1607
+
1608
+ main().catch((error) => {
1609
+ console.error(`Command failed: ${error.message || error}`);
1610
+ process.exit(1);
1611
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@potatodog1669/skills-hub",
3
- "version": "0.1.13",
3
+ "version": "0.1.17",
4
4
  "description": "Unify your AI toolbox. A local hub to visualize, manage, and sync skills across Antigravity, Claude, Cursor, Trae, and other AI agents.",
5
5
  "keywords": [
6
6
  "skills",