@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.
- package/README.md +51 -6
- package/bin/skills-hub +583 -69
- 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:
|
|
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
|
|
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
|
-
-
|
|
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`
|
|
107
|
-
| `skills-hub
|
|
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
|
|
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
|
-
|
|
47
|
-
let
|
|
51
|
+
async function main() {
|
|
52
|
+
let command = null;
|
|
53
|
+
let commandIndex = -1;
|
|
48
54
|
|
|
49
|
-
for (let i = 0; i < args.length; i++) {
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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
|
-
|
|
69
|
-
|
|
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
|
-
|
|
74
|
-
|
|
75
|
-
|
|
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
|
-
|
|
80
|
-
|
|
81
|
-
|
|
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
|
-
|
|
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
|
-
|
|
91
|
-
|
|
92
|
-
|
|
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> [
|
|
101
|
-
list
|
|
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
|
|
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
|
|
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
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
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
|
|
921
|
-
const
|
|
922
|
-
|
|
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
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
936
|
-
|
|
937
|
-
|
|
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
|
-
|
|
940
|
-
|
|
941
|
-
|
|
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
|
|
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(
|
|
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.
|
|
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",
|