skills-package-manager 0.6.2 → 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 CHANGED
@@ -1,42 +1,59 @@
1
- # spm
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
- spm --help
9
- spm --version
10
- spm add <specifier> [--skill <name>]
11
- spm install
12
- spm update [skill...]
13
- spm init [--yes]
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
- - `spm` with no command shows top-level help
17
- - `spm --help` prints top-level help
18
- - `spm --version` prints the package version
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
- ### `spm add`
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
- spm add owner/repo
27
- spm add https://github.com/owner/repo
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
- spm add owner/repo --skill find-skills
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
- spm add https://github.com/owner/repo.git#path:/skills/my-skill
34
- spm add link:./local-source/skills/my-skill
35
- spm add file:./skills-package.tgz#path:/skills/my-skill
36
- spm add npm:@scope/skills-package#path:/skills/my-skill
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 `spm add`, the newly added skills are resolved, materialized into `installDir`, and linked to each configured `linkTarget` immediately.
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
- ### `spm init`
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
- spm init
74
+ npx skills-package-manager init
58
75
 
59
76
  # Non-interactive — write the default manifest immediately
60
- spm init --yes
77
+ npx skills-package-manager init --yes
61
78
  ```
62
79
 
63
80
  Behavior:
64
81
 
65
- - `spm init` prompts for `installDir` and `linkTargets`, then writes `skills.json`
66
- - `spm init --yes` skips prompts and writes the default manifest
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 `spm init --yes`:
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
- ### `spm install`
97
+ ### `npx skills-package-manager install`
81
98
 
82
99
  Install all skills declared in `skills.json`:
83
100
 
84
101
  ```bash
85
- spm install
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`, `spm install` also installs the bundled `skills-package-manager-cli` skill so users get guidance for `skills.json`, `skills-lock.yaml`, and the `spm` workflow. This helper skill is not written to `skills-lock.yaml`.
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
- ### `spm update`
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
- spm update
97
- spm update find-skills rspress-custom-theme
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 (spm, skills)
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.2"
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
- async function scanForSkills(baseDir, subDir) {
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((e)=>e.isDirectory() && !SKIP_DIRS.has(e.name));
1070
- const checks = dirs.map(async (entry)=>{
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 relativePath = subDir ? `/${subDir}/${entry.name}` : `/${entry.name}`;
1074
- return parseSkillDir(fullPath, relativePath);
1359
+ const parsed = await parseSkillDir(fullPath, `/${relativePath}`);
1360
+ return parsed ? [
1361
+ parsed
1362
+ ] : [];
1075
1363
  }
1076
- return null;
1077
- });
1078
- const results = await Promise.all(checks);
1079
- for (const r of results)if (r) skills.push(r);
1080
- } catch {}
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
- return discoverSkillsInDirs(baseDir, [
1145
- 'skills',
1146
- '.agents/skills',
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 buildGitHubSpecifier(owner, repo, skillPath) {
1636
- return `https://github.com/${owner}/${repo}.git#path:${skillPath}`;
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
- async function addSingleSkill(cwd, specifier) {
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 { cwd, specifier, skill } = options;
1674
- const shorthand = parseOwnerRepo(specifier);
1675
- const githubUrl = shorthand ? null : parseGitHubUrl(specifier);
1676
- const parsed = shorthand ?? githubUrl;
1677
- if (parsed) {
1678
- const { owner, repo } = parsed;
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
- if (skill) {
1683
- spinner.start(`Cloning ${source}...`);
1684
- const skills = await listRepoSkills(owner, repo);
1685
- spinner.stop(`Found ${picocolors.green(String(skills.length))} skill${1 !== skills.length ? 's' : ''}`);
1686
- const found = skills.find((s)=>s.name === skill);
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: source,
1704
- message: `No skills found in ${source}`
2282
+ skillName: skill ?? sourceLabel,
2283
+ message: `No valid skills found in ${sourceLabel}`
1705
2284
  });
1706
2285
  }
1707
- spinner.stop(`Found ${picocolors.green(String(skills.length))} skill${1 !== skills.length ? 's' : ''}`);
1708
- const selected = await promptSkillSelection(skills);
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 s of selected){
1711
- const gitSpecifier = buildGitHubSpecifier(owner, repo, s.path);
1712
- const result = await addSingleSkill(cwd, gitSpecifier);
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 1 === results.length ? results[0] : results;
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "skills-package-manager",
3
- "version": "0.6.2",
3
+ "version": "0.6.3",
4
4
  "type": "module",
5
5
  "repository": {
6
6
  "type": "git",
@@ -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 `spm` commands but do not yet know how they fit together.
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
@@ -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",