skills-package-manager 0.6.1 → 0.6.3
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 +49 -32
- package/dist/index.js +689 -88
- package/package.json +1 -1
- package/skills/skills-package-manager-cli/SKILL.md +1 -1
- package/skills.schema.json +0 -11
package/README.md
CHANGED
|
@@ -1,42 +1,59 @@
|
|
|
1
|
-
#
|
|
1
|
+
# skills-package-manager
|
|
2
2
|
|
|
3
3
|
Core library and CLI for managing agent skills.
|
|
4
4
|
|
|
5
5
|
## CLI Usage
|
|
6
6
|
|
|
7
|
+
For one-off usage, `npx skills-package-manager add ...` is the low-friction migration path for teams already familiar with `npx skills add ...`.
|
|
8
|
+
|
|
7
9
|
```bash
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
10
|
+
npx skills-package-manager --help
|
|
11
|
+
npx skills-package-manager --version
|
|
12
|
+
npx skills-package-manager add <specifier> [--skill <name>]
|
|
13
|
+
npx skills-package-manager install
|
|
14
|
+
npx skills-package-manager update [skill...]
|
|
15
|
+
npx skills-package-manager init [--yes]
|
|
14
16
|
```
|
|
15
17
|
|
|
16
|
-
- `
|
|
17
|
-
- `
|
|
18
|
-
- `
|
|
18
|
+
- `npx skills-package-manager` with no command shows top-level help
|
|
19
|
+
- `npx skills-package-manager --help` prints top-level help
|
|
20
|
+
- `npx skills-package-manager --version` prints the package version
|
|
19
21
|
|
|
20
|
-
### `
|
|
22
|
+
### `npx skills-package-manager add`
|
|
21
23
|
|
|
22
24
|
Add skills to your project.
|
|
23
25
|
|
|
26
|
+
For teams already familiar with `npx skills add ...`, the headline migration message is:
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
npx skills add owner/repo
|
|
30
|
+
# becomes
|
|
31
|
+
npx skills-package-manager add owner/repo
|
|
32
|
+
```
|
|
33
|
+
|
|
24
34
|
```bash
|
|
25
35
|
# Interactive — clone repo, discover skills, select via multiselect prompt
|
|
26
|
-
|
|
27
|
-
|
|
36
|
+
npx skills-package-manager add owner/repo
|
|
37
|
+
npx skills-package-manager add https://github.com/owner/repo
|
|
28
38
|
|
|
29
39
|
# Non-interactive — add a specific skill by name
|
|
30
|
-
|
|
40
|
+
npx skills-package-manager add owner/repo --skill find-skills
|
|
41
|
+
npx skills-package-manager add owner/repo@find-skills
|
|
42
|
+
npx skills-package-manager add owner/repo#main@find-skills
|
|
43
|
+
|
|
44
|
+
# Direct repo subpath
|
|
45
|
+
npx skills-package-manager add owner/repo/skills/my-skill
|
|
46
|
+
npx skills-package-manager add https://github.com/owner/repo/tree/main/skills/my-skill#main
|
|
31
47
|
|
|
32
48
|
# Direct specifier — skip discovery
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
49
|
+
npx skills-package-manager add https://github.com/owner/repo.git#path:/skills/my-skill
|
|
50
|
+
npx skills-package-manager add link:./local-source/skills/my-skill
|
|
51
|
+
npx skills-package-manager add ./local-source
|
|
52
|
+
npx skills-package-manager add file:./skills-package.tgz#path:/skills/my-skill
|
|
53
|
+
npx skills-package-manager add npm:@scope/skills-package#path:/skills/my-skill
|
|
37
54
|
```
|
|
38
55
|
|
|
39
|
-
After `
|
|
56
|
+
After `npx skills-package-manager add`, the newly added skills are resolved, materialized into `installDir`, and linked to each configured `linkTarget` immediately.
|
|
40
57
|
|
|
41
58
|
#### How it works
|
|
42
59
|
|
|
@@ -48,25 +65,25 @@ When given `owner/repo` or a GitHub URL:
|
|
|
48
65
|
4. Writes selected skills to `skills.json` and resolves `skills-lock.yaml`
|
|
49
66
|
5. Cleans up the temp directory
|
|
50
67
|
|
|
51
|
-
### `
|
|
68
|
+
### `npx skills-package-manager init`
|
|
52
69
|
|
|
53
70
|
Create a new `skills.json` manifest in the current directory.
|
|
54
71
|
|
|
55
72
|
```bash
|
|
56
73
|
# Interactive — prompt for installDir and linkTargets
|
|
57
|
-
|
|
74
|
+
npx skills-package-manager init
|
|
58
75
|
|
|
59
76
|
# Non-interactive — write the default manifest immediately
|
|
60
|
-
|
|
77
|
+
npx skills-package-manager init --yes
|
|
61
78
|
```
|
|
62
79
|
|
|
63
80
|
Behavior:
|
|
64
81
|
|
|
65
|
-
- `
|
|
66
|
-
- `
|
|
82
|
+
- `npx skills-package-manager init` prompts for `installDir` and `linkTargets`, then writes `skills.json`
|
|
83
|
+
- `npx skills-package-manager init --yes` skips prompts and writes the default manifest
|
|
67
84
|
- If `skills.json` already exists, the command fails and does not overwrite it
|
|
68
85
|
|
|
69
|
-
Default `skills.json` written by `
|
|
86
|
+
Default `skills.json` written by `npx skills-package-manager init --yes`:
|
|
70
87
|
|
|
71
88
|
```json
|
|
72
89
|
{
|
|
@@ -77,24 +94,24 @@ Default `skills.json` written by `spm init --yes`:
|
|
|
77
94
|
}
|
|
78
95
|
```
|
|
79
96
|
|
|
80
|
-
### `
|
|
97
|
+
### `npx skills-package-manager install`
|
|
81
98
|
|
|
82
99
|
Install all skills declared in `skills.json`:
|
|
83
100
|
|
|
84
101
|
```bash
|
|
85
|
-
|
|
102
|
+
npx skills-package-manager install
|
|
86
103
|
```
|
|
87
104
|
|
|
88
105
|
This resolves each skill from its specifier, materializes it into `installDir` (default `.agents/skills/`), and creates symlinks for each `linkTarget`.
|
|
89
|
-
When `selfSkill` is `true`, `
|
|
106
|
+
When `selfSkill` is `true`, `npx skills-package-manager install` also installs the bundled `skills-package-manager-cli` skill so users get guidance for `skills.json`, `skills-lock.yaml`, and `npx skills-package-manager` commands. This helper skill is not written to `skills-lock.yaml`.
|
|
90
107
|
|
|
91
|
-
### `
|
|
108
|
+
### `npx skills-package-manager update`
|
|
92
109
|
|
|
93
110
|
Refresh resolvable skills declared in `skills.json` without changing the manifest:
|
|
94
111
|
|
|
95
112
|
```bash
|
|
96
|
-
|
|
97
|
-
|
|
113
|
+
npx skills-package-manager update
|
|
114
|
+
npx skills-package-manager update find-skills rspress-custom-theme
|
|
98
115
|
```
|
|
99
116
|
|
|
100
117
|
Behavior:
|
|
@@ -153,7 +170,7 @@ link: link:<path-to-skill-dir>
|
|
|
153
170
|
|
|
154
171
|
```
|
|
155
172
|
src/
|
|
156
|
-
├── bin/ # CLI entry points
|
|
173
|
+
├── bin/ # CLI entry points
|
|
157
174
|
├── cli/ # CLI runner and interactive prompts
|
|
158
175
|
├── commands/ # add, install command implementations
|
|
159
176
|
├── config/ # skills.json / skills-lock.yaml read/write
|
package/dist/index.js
CHANGED
|
@@ -1,21 +1,302 @@
|
|
|
1
1
|
import { cac } from "cac";
|
|
2
|
+
import { accessSync, createReadStream, existsSync } from "node:fs";
|
|
3
|
+
import node_path, { basename, join } from "node:path";
|
|
2
4
|
import picocolors from "picocolors";
|
|
5
|
+
import { homedir, tmpdir } from "node:os";
|
|
3
6
|
import { access, cp, lstat, mkdir, mkdtemp, readFile, readdir, readlink, rm, stat as promises_stat, symlink, writeFile } from "node:fs/promises";
|
|
4
|
-
import node_path, { basename, join } from "node:path";
|
|
5
7
|
import yaml from "yaml";
|
|
6
8
|
import { z } from "zod";
|
|
7
9
|
import { execFile } from "node:child_process";
|
|
8
|
-
import { homedir, tmpdir } from "node:os";
|
|
9
10
|
import { promisify } from "node:util";
|
|
10
11
|
import { createHash } from "node:crypto";
|
|
11
12
|
import semver from "semver";
|
|
12
|
-
import { accessSync, createReadStream } from "node:fs";
|
|
13
13
|
import { fileURLToPath } from "node:url";
|
|
14
14
|
import { x } from "tar";
|
|
15
15
|
import * as __rspack_external__clack_prompts_3cae1695 from "@clack/prompts";
|
|
16
16
|
var package_namespaceObject = {
|
|
17
|
-
rE: "0.6.
|
|
17
|
+
rE: "0.6.3"
|
|
18
18
|
};
|
|
19
|
+
function getHomeDir() {
|
|
20
|
+
return homedir();
|
|
21
|
+
}
|
|
22
|
+
function getConfigHome() {
|
|
23
|
+
return process.env.XDG_CONFIG_HOME?.trim() || node_path.join(getHomeDir(), '.config');
|
|
24
|
+
}
|
|
25
|
+
function getCodexHome() {
|
|
26
|
+
return process.env.CODEX_HOME?.trim() || node_path.join(getHomeDir(), '.codex');
|
|
27
|
+
}
|
|
28
|
+
function getClaudeHome() {
|
|
29
|
+
return process.env.CLAUDE_CONFIG_DIR?.trim() || node_path.join(getHomeDir(), '.claude');
|
|
30
|
+
}
|
|
31
|
+
function getOpenClawGlobalSkillsDir(homeDir = getHomeDir()) {
|
|
32
|
+
if (existsSync(node_path.join(homeDir, '.openclaw'))) return node_path.join(homeDir, '.openclaw/skills');
|
|
33
|
+
if (existsSync(node_path.join(homeDir, '.clawdbot'))) return node_path.join(homeDir, '.clawdbot/skills');
|
|
34
|
+
if (existsSync(node_path.join(homeDir, '.moltbot'))) return node_path.join(homeDir, '.moltbot/skills');
|
|
35
|
+
return node_path.join(homeDir, '.openclaw/skills');
|
|
36
|
+
}
|
|
37
|
+
const COMPATIBLE_ADD_AGENTS = {
|
|
38
|
+
adal: {
|
|
39
|
+
displayName: 'Adal',
|
|
40
|
+
projectPath: '.adal/skills',
|
|
41
|
+
globalPath: ()=>node_path.join(getHomeDir(), '.adal/skills')
|
|
42
|
+
},
|
|
43
|
+
amp: {
|
|
44
|
+
displayName: 'Amp',
|
|
45
|
+
projectPath: '.agents/skills',
|
|
46
|
+
globalPath: ()=>node_path.join(getConfigHome(), 'agents/skills')
|
|
47
|
+
},
|
|
48
|
+
antigravity: {
|
|
49
|
+
displayName: 'Antigravity',
|
|
50
|
+
projectPath: '.agents/skills',
|
|
51
|
+
globalPath: ()=>node_path.join(getHomeDir(), '.gemini/antigravity/skills')
|
|
52
|
+
},
|
|
53
|
+
augment: {
|
|
54
|
+
displayName: 'Augment',
|
|
55
|
+
projectPath: '.augment/skills',
|
|
56
|
+
globalPath: ()=>node_path.join(getHomeDir(), '.augment/skills')
|
|
57
|
+
},
|
|
58
|
+
bob: {
|
|
59
|
+
displayName: 'IBM Bob',
|
|
60
|
+
projectPath: '.bob/skills',
|
|
61
|
+
globalPath: ()=>node_path.join(getHomeDir(), '.bob/skills')
|
|
62
|
+
},
|
|
63
|
+
'claude-code': {
|
|
64
|
+
displayName: 'Claude Code',
|
|
65
|
+
projectPath: '.claude/skills',
|
|
66
|
+
globalPath: ()=>node_path.join(getClaudeHome(), 'skills')
|
|
67
|
+
},
|
|
68
|
+
cline: {
|
|
69
|
+
displayName: 'Cline',
|
|
70
|
+
projectPath: '.agents/skills',
|
|
71
|
+
globalPath: ()=>node_path.join(getHomeDir(), '.agents/skills')
|
|
72
|
+
},
|
|
73
|
+
codebuddy: {
|
|
74
|
+
displayName: 'CodeBuddy',
|
|
75
|
+
projectPath: '.codebuddy/skills',
|
|
76
|
+
globalPath: ()=>node_path.join(getHomeDir(), '.codebuddy/skills')
|
|
77
|
+
},
|
|
78
|
+
codex: {
|
|
79
|
+
displayName: 'Codex',
|
|
80
|
+
projectPath: '.agents/skills',
|
|
81
|
+
globalPath: ()=>node_path.join(getCodexHome(), 'skills')
|
|
82
|
+
},
|
|
83
|
+
'command-code': {
|
|
84
|
+
displayName: 'Command Code',
|
|
85
|
+
projectPath: '.commandcode/skills',
|
|
86
|
+
globalPath: ()=>node_path.join(getHomeDir(), '.commandcode/skills')
|
|
87
|
+
},
|
|
88
|
+
continue: {
|
|
89
|
+
displayName: 'Continue',
|
|
90
|
+
projectPath: '.continue/skills',
|
|
91
|
+
globalPath: ()=>node_path.join(getHomeDir(), '.continue/skills')
|
|
92
|
+
},
|
|
93
|
+
cortex: {
|
|
94
|
+
displayName: 'Cortex Code',
|
|
95
|
+
projectPath: '.cortex/skills',
|
|
96
|
+
globalPath: ()=>node_path.join(getHomeDir(), '.snowflake/cortex/skills')
|
|
97
|
+
},
|
|
98
|
+
crush: {
|
|
99
|
+
displayName: 'Crush',
|
|
100
|
+
projectPath: '.crush/skills',
|
|
101
|
+
globalPath: ()=>node_path.join(getHomeDir(), '.config/crush/skills')
|
|
102
|
+
},
|
|
103
|
+
cursor: {
|
|
104
|
+
displayName: 'Cursor',
|
|
105
|
+
projectPath: '.agents/skills',
|
|
106
|
+
globalPath: ()=>node_path.join(getHomeDir(), '.cursor/skills')
|
|
107
|
+
},
|
|
108
|
+
deepagents: {
|
|
109
|
+
displayName: 'Deep Agents',
|
|
110
|
+
projectPath: '.agents/skills',
|
|
111
|
+
globalPath: ()=>node_path.join(getHomeDir(), '.deepagents/agent/skills')
|
|
112
|
+
},
|
|
113
|
+
droid: {
|
|
114
|
+
displayName: 'Droid',
|
|
115
|
+
projectPath: '.factory/skills',
|
|
116
|
+
globalPath: ()=>node_path.join(getHomeDir(), '.factory/skills')
|
|
117
|
+
},
|
|
118
|
+
firebender: {
|
|
119
|
+
displayName: 'Firebender',
|
|
120
|
+
projectPath: '.agents/skills',
|
|
121
|
+
globalPath: ()=>node_path.join(getHomeDir(), '.firebender/skills')
|
|
122
|
+
},
|
|
123
|
+
'gemini-cli': {
|
|
124
|
+
displayName: 'Gemini CLI',
|
|
125
|
+
projectPath: '.agents/skills',
|
|
126
|
+
globalPath: ()=>node_path.join(getHomeDir(), '.gemini/skills')
|
|
127
|
+
},
|
|
128
|
+
'github-copilot': {
|
|
129
|
+
displayName: 'GitHub Copilot',
|
|
130
|
+
projectPath: '.agents/skills',
|
|
131
|
+
globalPath: ()=>node_path.join(getHomeDir(), '.copilot/skills')
|
|
132
|
+
},
|
|
133
|
+
goose: {
|
|
134
|
+
displayName: 'Goose',
|
|
135
|
+
projectPath: '.goose/skills',
|
|
136
|
+
globalPath: ()=>node_path.join(getConfigHome(), 'goose/skills')
|
|
137
|
+
},
|
|
138
|
+
'iflow-cli': {
|
|
139
|
+
displayName: 'iFlow CLI',
|
|
140
|
+
projectPath: '.iflow/skills',
|
|
141
|
+
globalPath: ()=>node_path.join(getHomeDir(), '.iflow/skills')
|
|
142
|
+
},
|
|
143
|
+
junie: {
|
|
144
|
+
displayName: 'Junie',
|
|
145
|
+
projectPath: '.junie/skills',
|
|
146
|
+
globalPath: ()=>node_path.join(getHomeDir(), '.junie/skills')
|
|
147
|
+
},
|
|
148
|
+
kilo: {
|
|
149
|
+
displayName: 'Kilo Code',
|
|
150
|
+
projectPath: '.kilocode/skills',
|
|
151
|
+
globalPath: ()=>node_path.join(getHomeDir(), '.kilocode/skills')
|
|
152
|
+
},
|
|
153
|
+
'kimi-cli': {
|
|
154
|
+
displayName: 'Kimi Code CLI',
|
|
155
|
+
projectPath: '.agents/skills',
|
|
156
|
+
globalPath: ()=>node_path.join(getHomeDir(), '.config/agents/skills')
|
|
157
|
+
},
|
|
158
|
+
'kiro-cli': {
|
|
159
|
+
displayName: 'Kiro CLI',
|
|
160
|
+
projectPath: '.kiro/skills',
|
|
161
|
+
globalPath: ()=>node_path.join(getHomeDir(), '.kiro/skills')
|
|
162
|
+
},
|
|
163
|
+
kode: {
|
|
164
|
+
displayName: 'Kode',
|
|
165
|
+
projectPath: '.kode/skills',
|
|
166
|
+
globalPath: ()=>node_path.join(getHomeDir(), '.kode/skills')
|
|
167
|
+
},
|
|
168
|
+
mcpjam: {
|
|
169
|
+
displayName: 'MCPJam',
|
|
170
|
+
projectPath: '.mcpjam/skills',
|
|
171
|
+
globalPath: ()=>node_path.join(getHomeDir(), '.mcpjam/skills')
|
|
172
|
+
},
|
|
173
|
+
'mistral-vibe': {
|
|
174
|
+
displayName: 'Mistral Vibe',
|
|
175
|
+
projectPath: '.vibe/skills',
|
|
176
|
+
globalPath: ()=>node_path.join(getHomeDir(), '.vibe/skills')
|
|
177
|
+
},
|
|
178
|
+
mux: {
|
|
179
|
+
displayName: 'Mux',
|
|
180
|
+
projectPath: '.mux/skills',
|
|
181
|
+
globalPath: ()=>node_path.join(getHomeDir(), '.mux/skills')
|
|
182
|
+
},
|
|
183
|
+
neovate: {
|
|
184
|
+
displayName: 'Neovate',
|
|
185
|
+
projectPath: '.neovate/skills',
|
|
186
|
+
globalPath: ()=>node_path.join(getHomeDir(), '.neovate/skills')
|
|
187
|
+
},
|
|
188
|
+
opencode: {
|
|
189
|
+
displayName: 'OpenCode',
|
|
190
|
+
projectPath: '.agents/skills',
|
|
191
|
+
globalPath: ()=>node_path.join(getConfigHome(), 'opencode/skills')
|
|
192
|
+
},
|
|
193
|
+
openclaw: {
|
|
194
|
+
displayName: 'OpenClaw',
|
|
195
|
+
projectPath: 'skills',
|
|
196
|
+
globalPath: ()=>getOpenClawGlobalSkillsDir()
|
|
197
|
+
},
|
|
198
|
+
openhands: {
|
|
199
|
+
displayName: 'OpenHands',
|
|
200
|
+
projectPath: '.openhands/skills',
|
|
201
|
+
globalPath: ()=>node_path.join(getHomeDir(), '.openhands/skills')
|
|
202
|
+
},
|
|
203
|
+
pi: {
|
|
204
|
+
displayName: 'PI',
|
|
205
|
+
projectPath: '.pi/skills',
|
|
206
|
+
globalPath: ()=>node_path.join(getHomeDir(), '.pi/agent/skills')
|
|
207
|
+
},
|
|
208
|
+
pochi: {
|
|
209
|
+
displayName: 'Pochi',
|
|
210
|
+
projectPath: '.pochi/skills',
|
|
211
|
+
globalPath: ()=>node_path.join(getHomeDir(), '.pochi/skills')
|
|
212
|
+
},
|
|
213
|
+
qoder: {
|
|
214
|
+
displayName: 'Qoder',
|
|
215
|
+
projectPath: '.qoder/skills',
|
|
216
|
+
globalPath: ()=>node_path.join(getHomeDir(), '.qoder/skills')
|
|
217
|
+
},
|
|
218
|
+
'qwen-code': {
|
|
219
|
+
displayName: 'Qwen Code',
|
|
220
|
+
projectPath: '.qwen/skills',
|
|
221
|
+
globalPath: ()=>node_path.join(getHomeDir(), '.qwen/skills')
|
|
222
|
+
},
|
|
223
|
+
replit: {
|
|
224
|
+
displayName: 'Replit',
|
|
225
|
+
projectPath: '.agents/skills',
|
|
226
|
+
globalPath: ()=>node_path.join(getConfigHome(), 'agents/skills')
|
|
227
|
+
},
|
|
228
|
+
roo: {
|
|
229
|
+
displayName: 'Roo',
|
|
230
|
+
projectPath: '.roo/skills',
|
|
231
|
+
globalPath: ()=>node_path.join(getHomeDir(), '.roo/skills')
|
|
232
|
+
},
|
|
233
|
+
trae: {
|
|
234
|
+
displayName: 'Trae',
|
|
235
|
+
projectPath: '.trae/skills',
|
|
236
|
+
globalPath: ()=>node_path.join(getHomeDir(), '.trae/skills')
|
|
237
|
+
},
|
|
238
|
+
'trae-cn': {
|
|
239
|
+
displayName: 'Trae CN',
|
|
240
|
+
projectPath: '.trae/skills',
|
|
241
|
+
globalPath: ()=>node_path.join(getHomeDir(), '.trae-cn/skills')
|
|
242
|
+
},
|
|
243
|
+
universal: {
|
|
244
|
+
displayName: 'Universal',
|
|
245
|
+
projectPath: '.agents/skills',
|
|
246
|
+
globalPath: ()=>node_path.join(getConfigHome(), 'agents/skills')
|
|
247
|
+
},
|
|
248
|
+
warp: {
|
|
249
|
+
displayName: 'Warp',
|
|
250
|
+
projectPath: '.agents/skills',
|
|
251
|
+
globalPath: ()=>node_path.join(getHomeDir(), '.agents/skills')
|
|
252
|
+
},
|
|
253
|
+
windsurf: {
|
|
254
|
+
displayName: 'Windsurf',
|
|
255
|
+
projectPath: '.windsurf/skills',
|
|
256
|
+
globalPath: ()=>node_path.join(getHomeDir(), '.codeium/windsurf/skills')
|
|
257
|
+
},
|
|
258
|
+
zencoder: {
|
|
259
|
+
displayName: 'Zencoder',
|
|
260
|
+
projectPath: '.zencoder/skills',
|
|
261
|
+
globalPath: ()=>node_path.join(getHomeDir(), '.zencoder/skills')
|
|
262
|
+
}
|
|
263
|
+
};
|
|
264
|
+
function dedupePreserveOrder(values) {
|
|
265
|
+
return [
|
|
266
|
+
...new Set(values)
|
|
267
|
+
];
|
|
268
|
+
}
|
|
269
|
+
function listCompatibleAddAgentNames() {
|
|
270
|
+
return Object.keys(COMPATIBLE_ADD_AGENTS).sort();
|
|
271
|
+
}
|
|
272
|
+
function getCompatibleAddAgent(name) {
|
|
273
|
+
return COMPATIBLE_ADD_AGENTS[name] ?? null;
|
|
274
|
+
}
|
|
275
|
+
function resolveCompatibleAddAgentTargets(agentNames, options) {
|
|
276
|
+
if (!agentNames || 0 === agentNames.length) return {
|
|
277
|
+
invalidAgents: [],
|
|
278
|
+
linkTargets: []
|
|
279
|
+
};
|
|
280
|
+
const expandedAgentNames = agentNames.includes('*') ? listCompatibleAddAgentNames() : agentNames;
|
|
281
|
+
const linkTargets = [];
|
|
282
|
+
const invalidAgents = [];
|
|
283
|
+
for (const agentName of dedupePreserveOrder(expandedAgentNames)){
|
|
284
|
+
const target = getCompatibleAddAgent(agentName);
|
|
285
|
+
if (!target) {
|
|
286
|
+
invalidAgents.push(agentName);
|
|
287
|
+
continue;
|
|
288
|
+
}
|
|
289
|
+
const nextTarget = options.global ? target.globalPath() : target.projectPath;
|
|
290
|
+
if ('.agents/skills' !== nextTarget || '.agents/skills' !== options.installDir) linkTargets.push(nextTarget);
|
|
291
|
+
}
|
|
292
|
+
return {
|
|
293
|
+
invalidAgents,
|
|
294
|
+
linkTargets: dedupePreserveOrder(linkTargets)
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
function getSkillsPackageManagerHome() {
|
|
298
|
+
return process.env.SKILLS_PACKAGE_MANAGER_HOME?.trim() || node_path.join(getHomeDir(), '.skills-package-manager');
|
|
299
|
+
}
|
|
19
300
|
const UNIVERSAL_AGENT_NAMES = [
|
|
20
301
|
'Amp',
|
|
21
302
|
'Antigravity',
|
|
@@ -437,9 +718,6 @@ const skillsManifestSchema = z.object({
|
|
|
437
718
|
installDir: z.string().default('.agents/skills').describe('Directory where skills will be installed'),
|
|
438
719
|
linkTargets: z.array(z.string()).default([]).describe('Directories where skill symlinks will be created'),
|
|
439
720
|
selfSkill: z.boolean().optional().describe('Whether this project is itself a skill'),
|
|
440
|
-
pnpmPlugin: z.object({
|
|
441
|
-
removePnpmfileChecksum: z.boolean().optional().describe('Temporarily remove pnpmfileChecksum from pnpm lockfiles in pnpm-plugin-skills afterAllResolved')
|
|
442
|
-
}).optional().describe('pnpm-plugin-skills specific compatibility settings'),
|
|
443
721
|
skills: z.record(z.string(), z.string()).default({}).describe('Map of skill names to their specifiers')
|
|
444
722
|
}).strict();
|
|
445
723
|
async function readSkillsManifest(rootDir) {
|
|
@@ -1005,7 +1283,6 @@ async function writeSkillsManifest(rootDir, manifest) {
|
|
|
1005
1283
|
skills: manifest.skills
|
|
1006
1284
|
};
|
|
1007
1285
|
if (void 0 !== manifest.selfSkill) nextManifest.selfSkill = manifest.selfSkill;
|
|
1008
|
-
if (void 0 !== manifest.pnpmPlugin) nextManifest.pnpmPlugin = manifest.pnpmPlugin;
|
|
1009
1286
|
try {
|
|
1010
1287
|
await writeFile(filePath, `${JSON.stringify(nextManifest, null, 2)}\n`, 'utf8');
|
|
1011
1288
|
} catch (error) {
|
|
@@ -1023,6 +1300,11 @@ const SKIP_DIRS = new Set([
|
|
|
1023
1300
|
'build',
|
|
1024
1301
|
'__pycache__'
|
|
1025
1302
|
]);
|
|
1303
|
+
const ALLOWED_HIDDEN_DIRS = new Set([
|
|
1304
|
+
'.agents',
|
|
1305
|
+
'.claude',
|
|
1306
|
+
'.github'
|
|
1307
|
+
]);
|
|
1026
1308
|
function parseSkillFrontmatter(content) {
|
|
1027
1309
|
const fmMatch = content.match(/^---\n([\s\S]*?)\n---/);
|
|
1028
1310
|
if (!fmMatch) return {
|
|
@@ -1059,43 +1341,32 @@ async function parseSkillDir(dir, relativePath) {
|
|
|
1059
1341
|
return null;
|
|
1060
1342
|
}
|
|
1061
1343
|
}
|
|
1062
|
-
|
|
1344
|
+
function shouldSkipDir(name) {
|
|
1345
|
+
if (SKIP_DIRS.has(name)) return true;
|
|
1346
|
+
return name.startsWith('.') && !ALLOWED_HIDDEN_DIRS.has(name);
|
|
1347
|
+
}
|
|
1348
|
+
async function scanForSkillsRecursive(baseDir, subDir = '') {
|
|
1063
1349
|
const searchDir = subDir ? join(baseDir, subDir) : baseDir;
|
|
1064
|
-
const skills = [];
|
|
1065
1350
|
try {
|
|
1066
1351
|
const entries = await readdir(searchDir, {
|
|
1067
1352
|
withFileTypes: true
|
|
1068
1353
|
});
|
|
1069
|
-
const dirs = entries.filter((
|
|
1070
|
-
const
|
|
1354
|
+
const dirs = entries.filter((entry)=>entry.isDirectory() && !shouldSkipDir(entry.name));
|
|
1355
|
+
const results = await Promise.all(dirs.map(async (entry)=>{
|
|
1356
|
+
const relativePath = subDir ? `${subDir}/${entry.name}` : entry.name;
|
|
1071
1357
|
const fullPath = join(searchDir, entry.name);
|
|
1072
1358
|
if (await hasSkillMd(fullPath)) {
|
|
1073
|
-
const
|
|
1074
|
-
return
|
|
1359
|
+
const parsed = await parseSkillDir(fullPath, `/${relativePath}`);
|
|
1360
|
+
return parsed ? [
|
|
1361
|
+
parsed
|
|
1362
|
+
] : [];
|
|
1075
1363
|
}
|
|
1076
|
-
return
|
|
1077
|
-
});
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
return skills;
|
|
1082
|
-
}
|
|
1083
|
-
async function discoverSkillsInDirs(baseDir, commonDirs, options) {
|
|
1084
|
-
if (options?.includeRoot ?? true) {
|
|
1085
|
-
const rootSkills = await scanForSkills(baseDir, '');
|
|
1086
|
-
if (rootSkills.length > 0) {
|
|
1087
|
-
rootSkills.sort((a, b)=>a.name.localeCompare(b.name));
|
|
1088
|
-
return rootSkills;
|
|
1089
|
-
}
|
|
1090
|
-
}
|
|
1091
|
-
for (const dir of commonDirs){
|
|
1092
|
-
const skills = await scanForSkills(baseDir, dir);
|
|
1093
|
-
if (skills.length > 0) {
|
|
1094
|
-
skills.sort((a, b)=>a.name.localeCompare(b.name));
|
|
1095
|
-
return skills;
|
|
1096
|
-
}
|
|
1364
|
+
return scanForSkillsRecursive(baseDir, relativePath);
|
|
1365
|
+
}));
|
|
1366
|
+
return results.flat();
|
|
1367
|
+
} catch {
|
|
1368
|
+
return [];
|
|
1097
1369
|
}
|
|
1098
|
-
return [];
|
|
1099
1370
|
}
|
|
1100
1371
|
async function cloneAndDiscover(gitUrl, ref) {
|
|
1101
1372
|
const tempDir = await mkdtemp(join(tmpdir(), 'skills-pm-discover-'));
|
|
@@ -1141,12 +1412,9 @@ async function cloneAndDiscover(gitUrl, ref) {
|
|
|
1141
1412
|
}
|
|
1142
1413
|
}
|
|
1143
1414
|
async function discoverSkillsInDir(baseDir) {
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
'.claude/skills',
|
|
1148
|
-
'.github/skills'
|
|
1149
|
-
]);
|
|
1415
|
+
const skills = await scanForSkillsRecursive(baseDir);
|
|
1416
|
+
skills.sort((a, b)=>a.path.localeCompare(b.path) || a.name.localeCompare(b.name));
|
|
1417
|
+
return skills;
|
|
1150
1418
|
}
|
|
1151
1419
|
async function listRepoSkills(owner, repo, ref) {
|
|
1152
1420
|
const gitUrl = `https://github.com/${owner}/${repo}.git`;
|
|
@@ -1159,7 +1427,7 @@ function parseOwnerRepo(input) {
|
|
|
1159
1427
|
if (!match) return null;
|
|
1160
1428
|
return {
|
|
1161
1429
|
owner: match[1],
|
|
1162
|
-
repo: match[2]
|
|
1430
|
+
repo: match[2].replace(/\.git$/, '')
|
|
1163
1431
|
};
|
|
1164
1432
|
}
|
|
1165
1433
|
function parseGitHubUrl(input) {
|
|
@@ -1249,7 +1517,6 @@ function normalizeSkillsManifest(manifest) {
|
|
|
1249
1517
|
installDir: manifest.installDir ?? '.agents/skills',
|
|
1250
1518
|
linkTargets: manifest.linkTargets ?? [],
|
|
1251
1519
|
selfSkill: manifest.selfSkill ?? false,
|
|
1252
|
-
pnpmPlugin: manifest.pnpmPlugin,
|
|
1253
1520
|
skills: manifest.skills ?? {}
|
|
1254
1521
|
};
|
|
1255
1522
|
}
|
|
@@ -1305,9 +1572,12 @@ async function writeInstallState(rootDir, installDir, value) {
|
|
|
1305
1572
|
const filePath = node_path.join(dirPath, INSTALL_STATE_FILE);
|
|
1306
1573
|
await writeJson(filePath, value);
|
|
1307
1574
|
}
|
|
1575
|
+
function resolveTargetPath(rootDir, targetPath) {
|
|
1576
|
+
return node_path.isAbsolute(targetPath) ? targetPath : node_path.join(rootDir, targetPath);
|
|
1577
|
+
}
|
|
1308
1578
|
async function linkSkill(rootDir, installDir, linkTarget, skillName) {
|
|
1309
1579
|
const absoluteTarget = node_path.join(rootDir, installDir, skillName);
|
|
1310
|
-
const absoluteLink = node_path.join(rootDir, linkTarget, skillName);
|
|
1580
|
+
const absoluteLink = node_path.join(resolveTargetPath(rootDir, linkTarget), skillName);
|
|
1311
1581
|
await ensureDir(node_path.dirname(absoluteLink));
|
|
1312
1582
|
await replaceSymlink(absoluteTarget, absoluteLink);
|
|
1313
1583
|
}
|
|
@@ -1452,6 +1722,9 @@ async function materializePackedSkill(rootDir, skillName, tarballPath, sourcePat
|
|
|
1452
1722
|
}).catch(()=>{});
|
|
1453
1723
|
}
|
|
1454
1724
|
}
|
|
1725
|
+
function pruneManagedSkills_resolveTargetPath(rootDir, targetPath) {
|
|
1726
|
+
return node_path.isAbsolute(targetPath) ? targetPath : node_path.join(rootDir, targetPath);
|
|
1727
|
+
}
|
|
1455
1728
|
async function isManagedSkillDir(dirPath) {
|
|
1456
1729
|
try {
|
|
1457
1730
|
const marker = JSON.parse(await readFile(node_path.join(dirPath, '.skills-pm.json'), 'utf8'));
|
|
@@ -1475,7 +1748,7 @@ async function pruneManagedSkills(rootDir, installDir, linkTargets, wantedSkillN
|
|
|
1475
1748
|
force: true
|
|
1476
1749
|
});
|
|
1477
1750
|
for (const linkTarget of linkTargets){
|
|
1478
|
-
const linkPath = node_path.join(rootDir, linkTarget, entry);
|
|
1751
|
+
const linkPath = node_path.join(pruneManagedSkills_resolveTargetPath(rootDir, linkTarget), entry);
|
|
1479
1752
|
try {
|
|
1480
1753
|
const stat = await lstat(linkPath);
|
|
1481
1754
|
if (stat.isSymbolicLink() || stat.isDirectory() || stat.isFile()) await rm(linkPath, {
|
|
@@ -1632,10 +1905,277 @@ async function installSkills(rootDir, options) {
|
|
|
1632
1905
|
installed: Object.keys(runtimeLock.skills)
|
|
1633
1906
|
};
|
|
1634
1907
|
}
|
|
1635
|
-
function
|
|
1636
|
-
return
|
|
1908
|
+
function buildGitSpecifier(repoUrl, skillPath, ref) {
|
|
1909
|
+
return ref ? `${repoUrl}#${ref}&path:${skillPath}` : `${repoUrl}#path:${skillPath}`;
|
|
1910
|
+
}
|
|
1911
|
+
function buildLinkSpecifier(sourceRoot, skillPath) {
|
|
1912
|
+
const absoluteSkillPath = node_path.join(sourceRoot, skillPath.replace(/^\//, ''));
|
|
1913
|
+
return normalizeLinkSource(`link:${absoluteSkillPath}`);
|
|
1914
|
+
}
|
|
1915
|
+
function isDirectSkillSpecifier(specifier) {
|
|
1916
|
+
return specifier.startsWith('link:') || specifier.startsWith('file:') || specifier.startsWith('npm:') || specifier.includes('#path:') || specifier.includes('&path:');
|
|
1917
|
+
}
|
|
1918
|
+
function isLocalPathSpecifier(specifier) {
|
|
1919
|
+
return node_path.isAbsolute(specifier) || specifier.startsWith('./') || specifier.startsWith('../') || '.' === specifier || '..' === specifier || /^[a-zA-Z]:[/\\]/.test(specifier);
|
|
1920
|
+
}
|
|
1921
|
+
function sanitizeSourceSubpath(subpath) {
|
|
1922
|
+
const normalizedSubpath = subpath.replace(/\\/g, '/').replace(/^\/+/, '').replace(/\/+$/, '');
|
|
1923
|
+
if (!normalizedSubpath) throw new ParseError({
|
|
1924
|
+
code: codes_ErrorCode.INVALID_SPECIFIER,
|
|
1925
|
+
message: 'Invalid add source: subpath cannot be empty',
|
|
1926
|
+
content: subpath
|
|
1927
|
+
});
|
|
1928
|
+
if (normalizedSubpath.split('/').some((segment)=>'..' === segment)) throw new ParseError({
|
|
1929
|
+
code: codes_ErrorCode.INVALID_SPECIFIER,
|
|
1930
|
+
message: `Invalid add source: unsafe subpath "${subpath}"`,
|
|
1931
|
+
content: subpath
|
|
1932
|
+
});
|
|
1933
|
+
return normalizedSubpath;
|
|
1934
|
+
}
|
|
1935
|
+
function formatSourceWithRef(source, ref) {
|
|
1936
|
+
return ref ? `${source}#${ref}` : source;
|
|
1937
|
+
}
|
|
1938
|
+
function parseTreeUrlSuffix(provider, input, treeSuffix, ref) {
|
|
1939
|
+
const normalizedTreeSuffix = treeSuffix.replace(/\/+$/, '');
|
|
1940
|
+
if (ref) {
|
|
1941
|
+
if (normalizedTreeSuffix === ref) return {
|
|
1942
|
+
ref
|
|
1943
|
+
};
|
|
1944
|
+
if (normalizedTreeSuffix.startsWith(`${ref}/`)) return {
|
|
1945
|
+
ref,
|
|
1946
|
+
subpath: sanitizeSourceSubpath(normalizedTreeSuffix.slice(ref.length + 1))
|
|
1947
|
+
};
|
|
1948
|
+
throw new ParseError({
|
|
1949
|
+
code: codes_ErrorCode.INVALID_SPECIFIER,
|
|
1950
|
+
message: `${provider} tree URL does not match explicit ref "${ref}": ${input}`,
|
|
1951
|
+
content: input
|
|
1952
|
+
});
|
|
1953
|
+
}
|
|
1954
|
+
if (normalizedTreeSuffix.includes('/')) throw new ParseError({
|
|
1955
|
+
code: codes_ErrorCode.INVALID_SPECIFIER,
|
|
1956
|
+
message: 'GitHub' === provider ? `Ambiguous GitHub tree URL: ${input}. If the ref contains "/", specify it explicitly with "#<ref>" instead.` : `Ambiguous GitLab tree URL: ${input}. GitLab refs can contain slashes, so provide the ref explicitly via #<ref>.`,
|
|
1957
|
+
content: input
|
|
1958
|
+
});
|
|
1959
|
+
return {
|
|
1960
|
+
ref: normalizedTreeSuffix
|
|
1961
|
+
};
|
|
1962
|
+
}
|
|
1963
|
+
function parseGitHubTreeSource(input, ref) {
|
|
1964
|
+
const treeMatch = input.match(/^https?:\/\/github\.com\/([^/]+)\/([^/]+?)(?:\.git)?\/tree\/(.+?)\/?$/);
|
|
1965
|
+
if (!treeMatch) return null;
|
|
1966
|
+
const [, owner, repo, treeSuffix] = treeMatch;
|
|
1967
|
+
const cleanRepo = repo.replace(/\.git$/, '');
|
|
1968
|
+
const parsedTree = parseTreeUrlSuffix('GitHub', input, treeSuffix, ref);
|
|
1969
|
+
return {
|
|
1970
|
+
type: 'repo',
|
|
1971
|
+
cloneUrl: `https://github.com/${owner}/${cleanRepo}.git`,
|
|
1972
|
+
displaySource: `${owner}/${cleanRepo}`,
|
|
1973
|
+
ref: parsedTree.ref,
|
|
1974
|
+
...parsedTree.subpath ? {
|
|
1975
|
+
subpath: parsedTree.subpath
|
|
1976
|
+
} : {}
|
|
1977
|
+
};
|
|
1978
|
+
}
|
|
1979
|
+
function parseGitLabSource(input, ref) {
|
|
1980
|
+
const treeMatch = input.match(/^(https?):\/\/([^/]+)\/(.+?)\/-\/tree\/(.+?)\/?$/);
|
|
1981
|
+
if (treeMatch) {
|
|
1982
|
+
const [, protocol, hostname, repoPath, treeSuffix] = treeMatch;
|
|
1983
|
+
if ('github.com' === hostname) return null;
|
|
1984
|
+
const cleanRepoPath = repoPath.replace(/\.git$/, '');
|
|
1985
|
+
const parsedTree = parseTreeUrlSuffix('GitLab', input, treeSuffix, ref);
|
|
1986
|
+
return {
|
|
1987
|
+
type: 'repo',
|
|
1988
|
+
cloneUrl: `${protocol}://${hostname}/${cleanRepoPath}.git`,
|
|
1989
|
+
displaySource: cleanRepoPath,
|
|
1990
|
+
ref: parsedTree.ref,
|
|
1991
|
+
...parsedTree.subpath ? {
|
|
1992
|
+
subpath: parsedTree.subpath
|
|
1993
|
+
} : {}
|
|
1994
|
+
};
|
|
1995
|
+
}
|
|
1996
|
+
const gitlabRepoMatch = input.match(/^https?:\/\/gitlab\.com\/(.+?)(?:\.git)?\/?$/);
|
|
1997
|
+
if (!gitlabRepoMatch) return null;
|
|
1998
|
+
const repoPath = gitlabRepoMatch[1];
|
|
1999
|
+
if (!repoPath.includes('/')) return null;
|
|
2000
|
+
const cleanRepoPath = repoPath.replace(/\.git$/, '');
|
|
2001
|
+
return {
|
|
2002
|
+
type: 'repo',
|
|
2003
|
+
cloneUrl: `https://gitlab.com/${cleanRepoPath}.git`,
|
|
2004
|
+
displaySource: cleanRepoPath,
|
|
2005
|
+
...ref ? {
|
|
2006
|
+
ref
|
|
2007
|
+
} : {}
|
|
2008
|
+
};
|
|
2009
|
+
}
|
|
2010
|
+
function parseGitHubShorthandSource(input, ref) {
|
|
2011
|
+
const match = input.match(/^([^/]+)\/([^/]+)(?:\/(.+?))?\/?$/);
|
|
2012
|
+
if (!match || input.includes(':') || input.startsWith('.') || input.startsWith('/')) return null;
|
|
2013
|
+
const [, owner, repo, subpath] = match;
|
|
2014
|
+
const cleanRepo = repo.replace(/\.git$/, '');
|
|
2015
|
+
return {
|
|
2016
|
+
type: 'repo',
|
|
2017
|
+
cloneUrl: `https://github.com/${owner}/${cleanRepo}.git`,
|
|
2018
|
+
displaySource: `${owner}/${cleanRepo}`,
|
|
2019
|
+
...ref ? {
|
|
2020
|
+
ref
|
|
2021
|
+
} : {},
|
|
2022
|
+
...subpath ? {
|
|
2023
|
+
subpath: sanitizeSourceSubpath(subpath)
|
|
2024
|
+
} : {}
|
|
2025
|
+
};
|
|
1637
2026
|
}
|
|
1638
|
-
|
|
2027
|
+
function parseGenericGitSource(input, ref) {
|
|
2028
|
+
if (!/^https?:\/\/.+\.git\/?$/i.test(input) && !/^git@[^:]+:.+\.git$/.test(input)) return null;
|
|
2029
|
+
return {
|
|
2030
|
+
type: 'repo',
|
|
2031
|
+
cloneUrl: input.replace(/\/$/, ''),
|
|
2032
|
+
displaySource: input.replace(/\/$/, ''),
|
|
2033
|
+
...ref ? {
|
|
2034
|
+
ref
|
|
2035
|
+
} : {}
|
|
2036
|
+
};
|
|
2037
|
+
}
|
|
2038
|
+
function parseAddSourceBase(input, ref) {
|
|
2039
|
+
if (isLocalPathSpecifier(input)) {
|
|
2040
|
+
const resolvedPath = node_path.resolve(input);
|
|
2041
|
+
const skillDocPath = node_path.join(resolvedPath, 'SKILL.md');
|
|
2042
|
+
if (existsSync(skillDocPath)) return {
|
|
2043
|
+
type: 'local',
|
|
2044
|
+
localPath: node_path.dirname(resolvedPath),
|
|
2045
|
+
displaySource: input,
|
|
2046
|
+
subpath: node_path.basename(resolvedPath)
|
|
2047
|
+
};
|
|
2048
|
+
return {
|
|
2049
|
+
type: 'local',
|
|
2050
|
+
localPath: resolvedPath,
|
|
2051
|
+
displaySource: input
|
|
2052
|
+
};
|
|
2053
|
+
}
|
|
2054
|
+
const githubPrefixMatch = input.match(/^github:(.+)$/);
|
|
2055
|
+
if (githubPrefixMatch) return parseAddSourceBase(githubPrefixMatch[1], ref);
|
|
2056
|
+
const gitlabPrefixMatch = input.match(/^gitlab:(.+)$/);
|
|
2057
|
+
if (gitlabPrefixMatch) {
|
|
2058
|
+
const repoPath = gitlabPrefixMatch[1].replace(/^\/+/, '').replace(/\/+$/, '');
|
|
2059
|
+
if (repoPath.split('/').length < 2) return null;
|
|
2060
|
+
return {
|
|
2061
|
+
type: 'repo',
|
|
2062
|
+
cloneUrl: `https://gitlab.com/${repoPath.replace(/\.git$/, '')}.git`,
|
|
2063
|
+
displaySource: repoPath.replace(/\.git$/, ''),
|
|
2064
|
+
...ref ? {
|
|
2065
|
+
ref
|
|
2066
|
+
} : {}
|
|
2067
|
+
};
|
|
2068
|
+
}
|
|
2069
|
+
const githubTreeSource = parseGitHubTreeSource(input, ref);
|
|
2070
|
+
if (githubTreeSource) return githubTreeSource;
|
|
2071
|
+
const githubRepo = parseGitHubUrl(input);
|
|
2072
|
+
if (githubRepo) return {
|
|
2073
|
+
type: 'repo',
|
|
2074
|
+
cloneUrl: `https://github.com/${githubRepo.owner}/${githubRepo.repo}.git`,
|
|
2075
|
+
displaySource: `${githubRepo.owner}/${githubRepo.repo}`,
|
|
2076
|
+
...ref ? {
|
|
2077
|
+
ref
|
|
2078
|
+
} : {}
|
|
2079
|
+
};
|
|
2080
|
+
const gitlabSource = parseGitLabSource(input, ref);
|
|
2081
|
+
if (gitlabSource) return gitlabSource;
|
|
2082
|
+
const githubShorthand = parseGitHubShorthandSource(input, ref);
|
|
2083
|
+
if (githubShorthand) return githubShorthand;
|
|
2084
|
+
return parseGenericGitSource(input, ref);
|
|
2085
|
+
}
|
|
2086
|
+
function extractAddSource(input) {
|
|
2087
|
+
if (isDirectSkillSpecifier(input)) return {
|
|
2088
|
+
source: input
|
|
2089
|
+
};
|
|
2090
|
+
let source = input;
|
|
2091
|
+
let ref;
|
|
2092
|
+
let skill;
|
|
2093
|
+
const hashIndex = input.indexOf('#');
|
|
2094
|
+
if (hashIndex >= 0) {
|
|
2095
|
+
source = input.slice(0, hashIndex);
|
|
2096
|
+
const fragment = input.slice(hashIndex + 1);
|
|
2097
|
+
const skillSeparatorIndex = fragment.indexOf('@');
|
|
2098
|
+
if (skillSeparatorIndex >= 0) {
|
|
2099
|
+
ref = fragment.slice(0, skillSeparatorIndex) || void 0;
|
|
2100
|
+
skill = fragment.slice(skillSeparatorIndex + 1) || void 0;
|
|
2101
|
+
} else ref = fragment || void 0;
|
|
2102
|
+
}
|
|
2103
|
+
if (!skill) {
|
|
2104
|
+
const atIndex = source.lastIndexOf('@');
|
|
2105
|
+
if (atIndex > 0 && atIndex < source.length - 1) {
|
|
2106
|
+
const nextSource = source.slice(0, atIndex);
|
|
2107
|
+
const nextSkill = source.slice(atIndex + 1);
|
|
2108
|
+
if (parseAddSourceBase(nextSource, ref)?.type === 'repo') {
|
|
2109
|
+
source = nextSource;
|
|
2110
|
+
skill = nextSkill;
|
|
2111
|
+
}
|
|
2112
|
+
}
|
|
2113
|
+
}
|
|
2114
|
+
return {
|
|
2115
|
+
source,
|
|
2116
|
+
ref,
|
|
2117
|
+
skill
|
|
2118
|
+
};
|
|
2119
|
+
}
|
|
2120
|
+
function parseRepoSkillSpecifier(input) {
|
|
2121
|
+
const extracted = extractAddSource(input);
|
|
2122
|
+
if (!extracted.skill) return null;
|
|
2123
|
+
return {
|
|
2124
|
+
specifier: formatSourceWithRef(extracted.source, extracted.ref),
|
|
2125
|
+
skill: extracted.skill
|
|
2126
|
+
};
|
|
2127
|
+
}
|
|
2128
|
+
function normalizeAddCommandInput(specifier, skill) {
|
|
2129
|
+
const parsedRepoSkill = parseRepoSkillSpecifier(specifier);
|
|
2130
|
+
if (!parsedRepoSkill) return {
|
|
2131
|
+
specifier,
|
|
2132
|
+
skill
|
|
2133
|
+
};
|
|
2134
|
+
return {
|
|
2135
|
+
specifier: parsedRepoSkill.specifier,
|
|
2136
|
+
skill: skill ?? parsedRepoSkill.skill
|
|
2137
|
+
};
|
|
2138
|
+
}
|
|
2139
|
+
function parseAddSourceSpecifier(specifier) {
|
|
2140
|
+
if (isDirectSkillSpecifier(specifier)) return null;
|
|
2141
|
+
const extracted = extractAddSource(specifier);
|
|
2142
|
+
return parseAddSourceBase(extracted.source, extracted.ref);
|
|
2143
|
+
}
|
|
2144
|
+
function normalizeRequestedSkill(requestedSkill) {
|
|
2145
|
+
return requestedSkill.replace(/^\/+/, '').replace(/\/+$/, '');
|
|
2146
|
+
}
|
|
2147
|
+
function findRequestedSkill(skills, requestedSkill) {
|
|
2148
|
+
const normalizedRequestedSkill = normalizeRequestedSkill(requestedSkill);
|
|
2149
|
+
return skills.find((candidate)=>candidate.name === requestedSkill || normalizeRequestedSkill(candidate.path) === normalizedRequestedSkill) ?? null;
|
|
2150
|
+
}
|
|
2151
|
+
function formatAvailableSkills(skills) {
|
|
2152
|
+
const preview = skills.slice(0, 10).map((candidate)=>`${candidate.name} (${candidate.path})`).join(', ');
|
|
2153
|
+
if (skills.length <= 10) return preview;
|
|
2154
|
+
return `${preview}, ...`;
|
|
2155
|
+
}
|
|
2156
|
+
function filterSkillsBySubpath(skills, subpath) {
|
|
2157
|
+
if (!subpath) return skills;
|
|
2158
|
+
const normalizedSubpath = normalizeRequestedSkill(subpath);
|
|
2159
|
+
return skills.filter((candidate)=>{
|
|
2160
|
+
const candidatePath = normalizeRequestedSkill(candidate.path);
|
|
2161
|
+
return candidatePath === normalizedSubpath || candidatePath.startsWith(`${normalizedSubpath}/`);
|
|
2162
|
+
});
|
|
2163
|
+
}
|
|
2164
|
+
async function discoverSkillsFromSource(source) {
|
|
2165
|
+
if ('local' === source.type) {
|
|
2166
|
+
if (!existsSync(source.localPath)) throw new ParseError({
|
|
2167
|
+
code: codes_ErrorCode.INVALID_SPECIFIER,
|
|
2168
|
+
message: `Local path does not exist: ${source.localPath}`,
|
|
2169
|
+
content: source.displaySource
|
|
2170
|
+
});
|
|
2171
|
+
const skills = await discoverSkillsInDir(source.localPath);
|
|
2172
|
+
return filterSkillsBySubpath(skills, source.subpath);
|
|
2173
|
+
}
|
|
2174
|
+
const { skills, cleanup } = await cloneAndDiscover(source.cloneUrl, source.ref);
|
|
2175
|
+
await cleanup();
|
|
2176
|
+
return filterSkillsBySubpath(skills, source.subpath);
|
|
2177
|
+
}
|
|
2178
|
+
async function addSingleSkill(cwd, specifier, manifestDefaults) {
|
|
1639
2179
|
let normalized;
|
|
1640
2180
|
try {
|
|
1641
2181
|
normalized = normalizeSpecifier(specifier);
|
|
@@ -1648,11 +2188,16 @@ async function addSingleSkill(cwd, specifier) {
|
|
|
1648
2188
|
cause: error
|
|
1649
2189
|
});
|
|
1650
2190
|
}
|
|
2191
|
+
await ensureDir(cwd);
|
|
1651
2192
|
const existingManifest = await readSkillsManifest(cwd) ?? {
|
|
1652
|
-
installDir: '.agents/skills',
|
|
1653
|
-
linkTargets: [],
|
|
2193
|
+
installDir: manifestDefaults?.installDir ?? '.agents/skills',
|
|
2194
|
+
linkTargets: manifestDefaults?.linkTargets ?? [],
|
|
1654
2195
|
skills: {}
|
|
1655
2196
|
};
|
|
2197
|
+
if (manifestDefaults) {
|
|
2198
|
+
existingManifest.installDir = manifestDefaults.installDir;
|
|
2199
|
+
existingManifest.linkTargets = manifestDefaults.linkTargets;
|
|
2200
|
+
}
|
|
1656
2201
|
const existing = existingManifest.skills[normalized.skillName];
|
|
1657
2202
|
if (existing && existing !== normalized.normalized) throw new SkillError({
|
|
1658
2203
|
code: codes_ErrorCode.SKILL_EXISTS,
|
|
@@ -1669,57 +2214,107 @@ async function addSingleSkill(cwd, specifier) {
|
|
|
1669
2214
|
specifier: normalized.normalized
|
|
1670
2215
|
};
|
|
1671
2216
|
}
|
|
2217
|
+
function normalizeStringArray(values) {
|
|
2218
|
+
if (void 0 === values) return;
|
|
2219
|
+
const arrayValues = Array.isArray(values) ? values : [
|
|
2220
|
+
values
|
|
2221
|
+
];
|
|
2222
|
+
const normalizedValues = arrayValues.map((value)=>value.trim()).filter(Boolean);
|
|
2223
|
+
return normalizedValues.length > 0 ? normalizedValues : void 0;
|
|
2224
|
+
}
|
|
2225
|
+
function mergeUnique(existing, next) {
|
|
2226
|
+
return [
|
|
2227
|
+
...new Set([
|
|
2228
|
+
...existing ?? [],
|
|
2229
|
+
...next ?? []
|
|
2230
|
+
])
|
|
2231
|
+
];
|
|
2232
|
+
}
|
|
2233
|
+
async function resolveAddManifestContext(options) {
|
|
2234
|
+
const targetCwd = options.global ? getSkillsPackageManagerHome() : options.cwd;
|
|
2235
|
+
const existingManifest = await readSkillsManifest(targetCwd);
|
|
2236
|
+
const installDir = existingManifest?.installDir ?? '.agents/skills';
|
|
2237
|
+
const requestedAgents = normalizeStringArray(options.agent);
|
|
2238
|
+
if (requestedAgents) {
|
|
2239
|
+
const resolvedTargets = resolveCompatibleAddAgentTargets(requestedAgents, {
|
|
2240
|
+
global: true === options.global,
|
|
2241
|
+
installDir
|
|
2242
|
+
});
|
|
2243
|
+
if (resolvedTargets.invalidAgents.length > 0) throw new ParseError({
|
|
2244
|
+
code: codes_ErrorCode.INVALID_SPECIFIER,
|
|
2245
|
+
message: `Invalid agents: ${resolvedTargets.invalidAgents.join(', ')}. Valid agents: ${listCompatibleAddAgentNames().join(', ')}`,
|
|
2246
|
+
content: requestedAgents.join(', ')
|
|
2247
|
+
});
|
|
2248
|
+
return {
|
|
2249
|
+
cwd: targetCwd,
|
|
2250
|
+
installDir,
|
|
2251
|
+
linkTargets: mergeUnique(existingManifest?.linkTargets, resolvedTargets.linkTargets)
|
|
2252
|
+
};
|
|
2253
|
+
}
|
|
2254
|
+
if (options.global && !(existingManifest?.linkTargets && existingManifest.linkTargets.length > 0)) throw new ParseError({
|
|
2255
|
+
code: codes_ErrorCode.INVALID_SPECIFIER,
|
|
2256
|
+
message: 'Global add requires at least one --agent on first use so skills-package-manager knows which global agent directories to link into',
|
|
2257
|
+
content: options.specifier
|
|
2258
|
+
});
|
|
2259
|
+
return {
|
|
2260
|
+
cwd: targetCwd,
|
|
2261
|
+
installDir,
|
|
2262
|
+
linkTargets: existingManifest?.linkTargets ?? []
|
|
2263
|
+
};
|
|
2264
|
+
}
|
|
1672
2265
|
async function addCommand(options) {
|
|
1673
|
-
const
|
|
1674
|
-
const
|
|
1675
|
-
const
|
|
1676
|
-
const
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
const source = `${owner}/${repo}`;
|
|
2266
|
+
const manifestContext = await resolveAddManifestContext(options);
|
|
2267
|
+
const { cwd } = manifestContext;
|
|
2268
|
+
const normalizedInput = normalizeAddCommandInput(options.specifier, options.skill);
|
|
2269
|
+
const { specifier, skill } = normalizedInput;
|
|
2270
|
+
const parsedSource = parseAddSourceSpecifier(specifier);
|
|
2271
|
+
if (parsedSource) {
|
|
1680
2272
|
__rspack_external__clack_prompts_3cae1695.intro(picocolors.bgCyan(picocolors.black(' spm ')));
|
|
1681
2273
|
const spinner = __rspack_external__clack_prompts_3cae1695.spinner();
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
const skillPath = found?.path ?? `/${skill}`;
|
|
1688
|
-
const gitSpecifier = buildGitHubSpecifier(owner, repo, skillPath);
|
|
1689
|
-
const result = await addSingleSkill(cwd, gitSpecifier);
|
|
1690
|
-
spinner.start('Installing skills...');
|
|
1691
|
-
await installSkills(cwd);
|
|
1692
|
-
spinner.stop('Installed skills');
|
|
1693
|
-
__rspack_external__clack_prompts_3cae1695.outro(`Added ${picocolors.cyan(result.skillName)}`);
|
|
1694
|
-
return result;
|
|
1695
|
-
}
|
|
1696
|
-
spinner.start(`Cloning ${source}...`);
|
|
1697
|
-
const skills = await listRepoSkills(owner, repo);
|
|
1698
|
-
if (0 === skills.length) {
|
|
2274
|
+
const sourceLabel = parsedSource.displaySource;
|
|
2275
|
+
if ('repo' === parsedSource.type) spinner.start(`Cloning ${sourceLabel}...`);
|
|
2276
|
+
else spinner.start(`Scanning ${sourceLabel}...`);
|
|
2277
|
+
const discoveredSkills = await discoverSkillsFromSource(parsedSource);
|
|
2278
|
+
if (0 === discoveredSkills.length) {
|
|
1699
2279
|
spinner.stop(picocolors.red('No skills found'));
|
|
1700
|
-
__rspack_external__clack_prompts_3cae1695.outro(picocolors.red(`No valid skills found in ${source}`));
|
|
1701
2280
|
throw new SkillError({
|
|
1702
2281
|
code: codes_ErrorCode.SKILL_NOT_FOUND,
|
|
1703
|
-
skillName:
|
|
1704
|
-
message: `No skills found in ${
|
|
2282
|
+
skillName: skill ?? sourceLabel,
|
|
2283
|
+
message: `No valid skills found in ${sourceLabel}`
|
|
1705
2284
|
});
|
|
1706
2285
|
}
|
|
1707
|
-
spinner.stop(`Found ${picocolors.green(String(
|
|
1708
|
-
|
|
2286
|
+
spinner.stop(`Found ${picocolors.green(String(discoveredSkills.length))} skill${1 !== discoveredSkills.length ? 's' : ''}`);
|
|
2287
|
+
let selectedSkills;
|
|
2288
|
+
if ('*' === skill) selectedSkills = discoveredSkills;
|
|
2289
|
+
else if (skill) {
|
|
2290
|
+
const found = findRequestedSkill(discoveredSkills, skill);
|
|
2291
|
+
if (!found) throw new SkillError({
|
|
2292
|
+
code: codes_ErrorCode.SKILL_NOT_FOUND,
|
|
2293
|
+
skillName: skill,
|
|
2294
|
+
message: `Skill ${skill} not found in ${sourceLabel}. Available skills: ${formatAvailableSkills(discoveredSkills)}`
|
|
2295
|
+
});
|
|
2296
|
+
selectedSkills = [
|
|
2297
|
+
found
|
|
2298
|
+
];
|
|
2299
|
+
} else selectedSkills = options.yes ? discoveredSkills : await promptSkillSelection(discoveredSkills);
|
|
1709
2300
|
const results = [];
|
|
1710
|
-
for (const
|
|
1711
|
-
const
|
|
1712
|
-
const result = await addSingleSkill(cwd,
|
|
2301
|
+
for (const selectedSkill of selectedSkills){
|
|
2302
|
+
const nextSpecifier = 'repo' === parsedSource.type ? buildGitSpecifier(parsedSource.cloneUrl, selectedSkill.path, parsedSource.ref) : buildLinkSpecifier(parsedSource.localPath, selectedSkill.path);
|
|
2303
|
+
const result = await addSingleSkill(cwd, nextSpecifier, manifestContext);
|
|
1713
2304
|
results.push(result);
|
|
1714
|
-
__rspack_external__clack_prompts_3cae1695.log.success(`Added ${picocolors.cyan(result.skillName)}`);
|
|
2305
|
+
if (selectedSkills.length > 1) __rspack_external__clack_prompts_3cae1695.log.success(`Added ${picocolors.cyan(result.skillName)}`);
|
|
1715
2306
|
}
|
|
1716
2307
|
spinner.start('Installing skills...');
|
|
1717
2308
|
await installSkills(cwd);
|
|
1718
2309
|
spinner.stop('Installed skills');
|
|
2310
|
+
if (1 === results.length) {
|
|
2311
|
+
__rspack_external__clack_prompts_3cae1695.outro(`Added ${picocolors.cyan(results[0].skillName)}`);
|
|
2312
|
+
return results[0];
|
|
2313
|
+
}
|
|
1719
2314
|
__rspack_external__clack_prompts_3cae1695.outro('Done');
|
|
1720
|
-
return
|
|
2315
|
+
return results;
|
|
1721
2316
|
}
|
|
1722
|
-
const result = await addSingleSkill(cwd, specifier);
|
|
2317
|
+
const result = await addSingleSkill(cwd, specifier, manifestContext);
|
|
1723
2318
|
const spinner = __rspack_external__clack_prompts_3cae1695.spinner();
|
|
1724
2319
|
spinner.start('Installing skills...');
|
|
1725
2320
|
await installSkills(cwd);
|
|
@@ -2040,13 +2635,19 @@ async function runCli(argv, context = {}) {
|
|
|
2040
2635
|
cli.help();
|
|
2041
2636
|
cli.version(packageVersion);
|
|
2042
2637
|
cli.showVersionOnExit = false;
|
|
2043
|
-
cli.command('add [...positionals]').option('--skill <name>', 'Select a skill').action(async (positionals = [], options)=>{
|
|
2638
|
+
cli.command('add [...positionals]').option('-a, --agent <name>', 'Target agent').option('-g, --global', 'Install into the global skills workspace').option('--skill <name>', 'Select a skill').option('-y, --yes', 'Skip prompts and select defaults').action(async (positionals = [], options)=>{
|
|
2044
2639
|
const specifier = positionals[0];
|
|
2045
2640
|
if (!specifier) throw new Error('Missing required specifier');
|
|
2641
|
+
const agent = Array.isArray(options.agent) ? options.agent : options.agent ? [
|
|
2642
|
+
options.agent
|
|
2643
|
+
] : void 0;
|
|
2046
2644
|
return handlers.addCommand({
|
|
2047
2645
|
cwd,
|
|
2048
2646
|
specifier,
|
|
2049
|
-
skill: options.skill
|
|
2647
|
+
skill: options.skill,
|
|
2648
|
+
global: options.global,
|
|
2649
|
+
yes: options.yes,
|
|
2650
|
+
agent
|
|
2050
2651
|
});
|
|
2051
2652
|
});
|
|
2052
2653
|
cli.command('install [...args]').option('--frozen-lockfile', 'Fail if lockfile is out of sync').action(async (_args, options)=>handlers.installCommand({
|
package/package.json
CHANGED
|
@@ -19,7 +19,7 @@ Use this skill for repositories that already use `skills-package-manager`, or wh
|
|
|
19
19
|
## What `selfSkill` Means
|
|
20
20
|
|
|
21
21
|
- `selfSkill: true` adds the bundled `skills-package-manager-cli` skill during install.
|
|
22
|
-
- It is meant to help users who see `skills.json`, `skills-lock.yaml`, and `
|
|
22
|
+
- It is meant to help users who see `skills.json`, `skills-lock.yaml`, and `npx skills-package-manager` commands but do not yet know how they fit together.
|
|
23
23
|
- The bundled skill is injected automatically. It should not be added manually under `skills` unless there is a very specific reason.
|
|
24
24
|
|
|
25
25
|
## Command Guide
|
package/skills.schema.json
CHANGED
|
@@ -23,17 +23,6 @@
|
|
|
23
23
|
"description": "Whether this project is itself a skill",
|
|
24
24
|
"type": "boolean"
|
|
25
25
|
},
|
|
26
|
-
"pnpmPlugin": {
|
|
27
|
-
"description": "pnpm-plugin-skills specific compatibility settings",
|
|
28
|
-
"type": "object",
|
|
29
|
-
"properties": {
|
|
30
|
-
"removePnpmfileChecksum": {
|
|
31
|
-
"description": "Temporarily remove pnpmfileChecksum from pnpm lockfiles in pnpm-plugin-skills afterAllResolved",
|
|
32
|
-
"type": "boolean"
|
|
33
|
-
}
|
|
34
|
-
},
|
|
35
|
-
"additionalProperties": false
|
|
36
|
-
},
|
|
37
26
|
"skills": {
|
|
38
27
|
"default": {},
|
|
39
28
|
"description": "Map of skill names to their specifiers",
|