skillshop 2.0.0 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. package/README.md +217 -104
  2. package/agents-content/000_communication-writing.block.md +11 -0
  3. package/agents-content/001_external-actions.block.md +9 -0
  4. package/agents-content/AGENTS.md +11 -0
  5. package/package.json +6 -3
  6. package/skills-content/architecture-scanner/SKILL.md +274 -0
  7. package/skills-content/architecture-scanner/files.json +10 -0
  8. package/skills-content/architecture-scanner/scan.sh +823 -0
  9. package/skills-content/change-review/SKILL.md +76 -0
  10. package/skills-content/change-review/agents/openai.yaml +4 -0
  11. package/skills-content/changelog/SKILL.md +99 -0
  12. package/skills-content/changeset/changeset.md +80 -0
  13. package/skills-content/code-buddy/SKILL.md +136 -0
  14. package/skills-content/design-system-catalog/SKILL.md +106 -0
  15. package/skills-content/design-system-catalog/extract-components.mjs +441 -0
  16. package/skills-content/design-system-catalog/generate-catalog.sh +255 -0
  17. package/skills-content/flare/SKILL.md +96 -0
  18. package/skills-content/grilling/SKILL.md +31 -0
  19. package/skills-content/install-sigmap/SKILL.md +97 -0
  20. package/skills-content/one-shot/SKILL.md +51 -0
  21. package/skills-content/release/SKILL.md +128 -0
  22. package/skills-content/shadow-clone/SKILL.md +228 -0
  23. package/skills-content/shadow-clone/extract-signatures.mjs +202 -0
  24. package/skills-content/shadow-clone/scan-repo.mjs +343 -0
  25. package/skills-content/ship/SKILL.md +95 -0
  26. package/skills-content/shots/SKILL.md +89 -0
  27. package/skills-content/shots/agents/openai.yaml +4 -0
  28. package/skills-content/skillshop/SKILL.md +104 -0
  29. package/skills-content/skillz/SKILL.md +15 -0
  30. package/skills-content/transcript-clean/SKILL.md +55 -0
  31. package/skills-content/transcript-edit/SKILL.md +80 -0
  32. package/skills-content/vitest/GENERATION.md +5 -0
  33. package/skills-content/vitest/SKILL.md +52 -0
  34. package/skills-content/vitest/references/advanced-environments.md +264 -0
  35. package/skills-content/vitest/references/advanced-projects.md +300 -0
  36. package/skills-content/vitest/references/advanced-type-testing.md +237 -0
  37. package/skills-content/vitest/references/advanced-vi.md +249 -0
  38. package/skills-content/vitest/references/core-cli.md +166 -0
  39. package/skills-content/vitest/references/core-config.md +174 -0
  40. package/skills-content/vitest/references/core-describe.md +193 -0
  41. package/skills-content/vitest/references/core-expect.md +219 -0
  42. package/skills-content/vitest/references/core-hooks.md +244 -0
  43. package/skills-content/vitest/references/core-test-api.md +233 -0
  44. package/skills-content/vitest/references/features-concurrency.md +250 -0
  45. package/skills-content/vitest/references/features-context.md +238 -0
  46. package/skills-content/vitest/references/features-coverage.md +207 -0
  47. package/skills-content/vitest/references/features-filtering.md +211 -0
  48. package/skills-content/vitest/references/features-mocking.md +265 -0
  49. package/skills-content/vitest/references/features-snapshots.md +207 -0
  50. package/skills-content/workleaf/SKILL.md +77 -0
  51. package/skills-content/worklog/SKILL.md +145 -0
  52. package/skills-content/worklog/references/summary-format.md +114 -0
  53. package/skills-content/worktree/SKILL.md +71 -0
  54. package/src/agents-instructions.js +219 -0
  55. package/src/cli.js +87 -23
  56. package/src/import.js +12 -7
  57. package/src/index.js +1 -0
  58. package/src/legacy-agents-content.js +36 -0
  59. package/src/legacy-cli.js +1234 -0
  60. package/src/library.js +9 -0
  61. package/src/specifier.js +11 -0
  62. package/src/user-install.js +48 -0
@@ -0,0 +1,1234 @@
1
+ #!/usr/bin/env node
2
+
3
+ import fs from 'node:fs';
4
+ import os from 'node:os';
5
+ import path from 'node:path';
6
+ import { execFileSync } from 'node:child_process';
7
+ import { createInterface } from 'node:readline/promises';
8
+ import { fileURLToPath } from 'node:url';
9
+ import { AgentsInstructionsError, applyInstructions, applyLocalFragments, composeAgentsInstructions } from './agents-instructions.js';
10
+
11
+ const packageRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
12
+ const skillsContentRoot = path.join(packageRoot, 'skills-content');
13
+ const agentsContentRoot = path.join(packageRoot, 'agents-content');
14
+ const packageJson = JSON.parse(fs.readFileSync(path.join(packageRoot, 'package.json'), 'utf8'));
15
+ const lockFileName = '.skillshop-lock';
16
+ const legacyLockFileName = '.skillz-lock';
17
+ const registryFileName = 'repositories.json';
18
+ const configFileName = 'config.json';
19
+ const globalSkillsFileName = 'global-skills.json';
20
+ const excludeBlockStart = '# >>> skillshop managed dependencies >>>';
21
+ const excludeBlockEnd = '# <<< skillshop managed dependencies <<<';
22
+ const legacyExcludeBlockStart = '# >>> skillz managed dependencies >>>';
23
+ const legacyExcludeBlockEnd = '# <<< skillz managed dependencies <<<';
24
+ const globalAgentsTargets = [
25
+ path.join('.codex', 'AGENTS.md'),
26
+ path.join('.agents', 'AGENTS.md'),
27
+ path.join('.copilot', 'copilot-instructions.md')
28
+ ];
29
+
30
+ function lockPathFor(root) {
31
+ const current = path.join(root, lockFileName);
32
+ const legacy = path.join(root, legacyLockFileName);
33
+ return fs.existsSync(current) || !fs.existsSync(legacy) ? current : legacy;
34
+ }
35
+
36
+ function hasLock(root) {
37
+ return fs.existsSync(path.join(root, lockFileName)) || fs.existsSync(path.join(root, legacyLockFileName));
38
+ }
39
+ const agentOptions = [
40
+ { id: 'copilot', label: 'Copilot', local: '.github/skills', global: '.copilot/skills' },
41
+ { id: 'claude', label: 'Claude', local: '.claude/skills', global: '.claude/skills' },
42
+ { id: 'codex', label: 'Codex', local: '.agents/skills', global: '.agents/skills' },
43
+ { id: 'opencode', label: 'OpenCode', local: '.agents/skills', global: '.agents/skills' },
44
+ { id: 'others', label: 'Others', local: '.agents/skills', global: '.agents/skills' }
45
+ ];
46
+
47
+ class CliError extends Error {}
48
+
49
+ function unquote(value) {
50
+ const trimmed = value.trim();
51
+ const match = trimmed.match(/^(['"])(.*)\1$/);
52
+ return match ? match[2] : trimmed;
53
+ }
54
+
55
+ function validateSkillName(name) {
56
+ if (!/^[a-zA-Z0-9][a-zA-Z0-9._-]*$/.test(name)) {
57
+ throw new CliError(`invalid skill name: ${name}`);
58
+ }
59
+ }
60
+
61
+ function skillMetadata(name) {
62
+ validateSkillName(name);
63
+ const sourceDir = path.join(skillsContentRoot, name);
64
+ const manifestPath = path.join(sourceDir, 'SKILL.md');
65
+
66
+ if (!fs.existsSync(manifestPath)) {
67
+ throw new CliError(`unknown skill: ${name}`);
68
+ }
69
+
70
+ const lines = fs.readFileSync(manifestPath, 'utf8').split(/\r?\n/);
71
+ if (lines[0] !== '---') {
72
+ throw new CliError(`${name}/SKILL.md has no front matter`);
73
+ }
74
+
75
+ const frontMatterEnd = lines.indexOf('---', 1);
76
+ const frontMatter = lines.slice(1, frontMatterEnd === -1 ? lines.length : frontMatterEnd);
77
+ const metadataStart = frontMatter.findIndex((line) => /^metadata:\s*$/.test(line));
78
+ let author;
79
+ let version;
80
+
81
+ if (metadataStart !== -1) {
82
+ for (const line of frontMatter.slice(metadataStart + 1)) {
83
+ if (line !== '' && !/^\s+/.test(line)) break;
84
+ const authorMatch = line.match(/^\s+author:\s*(.+)$/);
85
+ const versionMatch = line.match(/^\s+version:\s*(.+)$/);
86
+ if (authorMatch) author = unquote(authorMatch[1]);
87
+ if (versionMatch) version = unquote(versionMatch[1]);
88
+ }
89
+ }
90
+
91
+ if (!author || !version) {
92
+ throw new CliError(`${name}/SKILL.md must declare metadata.author and metadata.version`);
93
+ }
94
+
95
+ return { name, author, version, sourceDir };
96
+ }
97
+
98
+ function availableSkills() {
99
+ return fs.readdirSync(skillsContentRoot, { withFileTypes: true })
100
+ .filter((entry) => entry.isDirectory())
101
+ .map((entry) => entry.name)
102
+ .filter((name) => fs.existsSync(path.join(skillsContentRoot, name, 'SKILL.md')))
103
+ .sort();
104
+ }
105
+
106
+ function printHelp() {
107
+ console.log(`Usage:
108
+ skillshop init
109
+ skillshop config agents
110
+ skillshop agents [--global]
111
+ skillshop <name> [--version <version>] [--global]
112
+ skillshop install <name> [--version <version>] [--global]
113
+ skillshop uninstall <name> [--global]
114
+ skillshop upgrade
115
+ skillshop update [name]
116
+ skillshop update --push
117
+ skillshop migrate [root ...]
118
+ skillshop list
119
+
120
+ Install, uninstall, and update skills from the local registry.
121
+
122
+ Options:
123
+ -g, --global Use configured system skill directories without a project lockfile
124
+ --push Update every registered repository
125
+ -v, --version <version> Require an exact skill version
126
+ -h, --help Show this help
127
+
128
+ Conveniences:
129
+ skillshop i <name> Alias for install
130
+ skillshop add <name> Alias for install
131
+ skillshop rm <name> Alias for uninstall
132
+ skillshop <name>@<version> Install an exact version
133
+
134
+ Available skills:
135
+ ${availableSkills().join('\n ')}`);
136
+ }
137
+
138
+ function skillshopHome() {
139
+ if (process.env.SKILLSHOP_HOME) return process.env.SKILLSHOP_HOME;
140
+ if (process.env.SKILLZ_HOME) return process.env.SKILLZ_HOME;
141
+ const current = path.join(os.homedir(), '.skillshop');
142
+ const legacy = path.join(os.homedir(), '.skillz');
143
+ return fs.existsSync(current) || !fs.existsSync(legacy) ? current : legacy;
144
+ }
145
+
146
+ function configPath() {
147
+ return path.join(skillshopHome(), configFileName);
148
+ }
149
+
150
+ function readConfig() {
151
+ const filePath = configPath();
152
+ if (!fs.existsSync(filePath)) return null;
153
+
154
+ let config;
155
+ try {
156
+ config = JSON.parse(fs.readFileSync(filePath, 'utf8'));
157
+ } catch (error) {
158
+ throw new CliError(`cannot parse ${filePath}: ${error.message}`);
159
+ }
160
+
161
+ const knownAgents = new Set(agentOptions.map(({ id }) => id));
162
+ if (
163
+ config?.configVersion !== 1 ||
164
+ !Array.isArray(config.agents) ||
165
+ config.agents.length === 0 ||
166
+ config.agents.some((agent) => typeof agent !== 'string' || !knownAgents.has(agent))
167
+ ) {
168
+ throw new CliError(`${filePath} is not a supported skillshop config`);
169
+ }
170
+
171
+ return config;
172
+ }
173
+
174
+ function writeConfig(agents) {
175
+ const filePath = configPath();
176
+ const config = { configVersion: 1, agents: [...new Set(agents)] };
177
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
178
+ const temporaryPath = `${filePath}.tmp-${process.pid}`;
179
+ fs.writeFileSync(temporaryPath, `${JSON.stringify(config, null, 2)}\n`);
180
+ fs.renameSync(temporaryPath, filePath);
181
+ }
182
+
183
+ function configuredAgents(config) {
184
+ return config.agents.map((id) => agentOptions.find((option) => option.id === id));
185
+ }
186
+
187
+ function configuredSkillDirectories(config, scope, root = null) {
188
+ const base = scope === 'local' ? root : os.homedir();
189
+ return uniqueDestinations(configuredAgents(config).map((agent) => path.join(base, agent[scope])));
190
+ }
191
+
192
+ async function selectAgents() {
193
+ const terminal = createInterface({ input: process.stdin, output: process.stdout });
194
+ console.log('Which agents do you use? Select one or more:');
195
+ for (const [index, agent] of agentOptions.entries()) {
196
+ console.log(` ${index + 1}. ${agent.label}`);
197
+ }
198
+ console.log('You can change this later with `skillshop config agents`.');
199
+
200
+ try {
201
+ while (true) {
202
+ if (process.stdin.readableEnded) throw new CliError('agent selection requires an answer');
203
+ let onEnd;
204
+ const inputEnded = new Promise((_, reject) => {
205
+ onEnd = () => reject(new CliError('agent selection requires an answer'));
206
+ process.stdin.once('end', onEnd);
207
+ });
208
+ let answer;
209
+ try {
210
+ answer = await Promise.race([
211
+ terminal.question('Enter numbers separated by commas: '),
212
+ inputEnded
213
+ ]);
214
+ } finally {
215
+ process.stdin.off('end', onEnd);
216
+ }
217
+ const indexes = answer.split(',').map((value) => Number(value.trim()) - 1);
218
+ if (
219
+ indexes.length > 0 &&
220
+ indexes.every((index) => Number.isInteger(index) && index >= 0 && index < agentOptions.length)
221
+ ) {
222
+ return [...new Set(indexes.map((index) => agentOptions[index].id))];
223
+ }
224
+ console.log(`Choose one or more numbers from 1 to ${agentOptions.length}.`);
225
+ }
226
+ } finally {
227
+ terminal.close();
228
+ }
229
+ }
230
+
231
+ async function configureAgents() {
232
+ const agents = await selectAgents();
233
+ writeConfig(agents);
234
+ const labels = configuredAgents({ agents }).map(({ label }) => label);
235
+ console.log(`Configured agents: ${labels.join(', ')}`);
236
+ console.log(`Wrote ${configPath()}`);
237
+ }
238
+
239
+ async function initialize() {
240
+ const config = readConfig();
241
+ if (!config) {
242
+ await configureAgents();
243
+ return;
244
+ }
245
+
246
+ console.log(`Agents already configured: ${configuredAgents(config).map(({ label }) => label).join(', ')}`);
247
+ console.log('Change this setting with `skillshop config agents`.');
248
+ }
249
+
250
+ function projectRoot(startDir, { required = true } = {}) {
251
+ let current = path.resolve(startDir);
252
+
253
+ while (true) {
254
+ if (fs.existsSync(path.join(current, '.git'))) return current;
255
+
256
+ const parent = path.dirname(current);
257
+ if (parent === current) {
258
+ if (!required) return null;
259
+ throw new CliError(`${path.resolve(startDir)} is not inside a Git repository`);
260
+ }
261
+ current = parent;
262
+ }
263
+ }
264
+
265
+ function directoriesToRoot(startDir, rootDir) {
266
+ const directories = [];
267
+ let current = path.resolve(startDir);
268
+
269
+ while (true) {
270
+ directories.push(current);
271
+ if (current === rootDir) return directories;
272
+
273
+ const parent = path.dirname(current);
274
+ if (parent === current) return [rootDir];
275
+ current = parent;
276
+ }
277
+ }
278
+
279
+ function findLockRoot(cwd, options) {
280
+ const root = projectRoot(cwd, options);
281
+ if (!root) return null;
282
+ return directoriesToRoot(cwd, root)
283
+ .find((directory) => hasLock(directory)) ?? null;
284
+ }
285
+
286
+ function resolveAgentDir(cwd) {
287
+ const root = projectRoot(cwd);
288
+ const directories = directoriesToRoot(cwd, root);
289
+
290
+ for (const agentDirName of ['.agents', '.github', '.codex']) {
291
+ for (const directory of directories) {
292
+ const candidate = path.join(directory, agentDirName);
293
+ if (fs.existsSync(candidate) && fs.statSync(candidate).isDirectory()) {
294
+ return candidate;
295
+ }
296
+ }
297
+ }
298
+
299
+ return path.join(root, '.agents');
300
+ }
301
+
302
+ function emptyLock() {
303
+ return { lockfileVersion: 2, registry: 'local', skills: {} };
304
+ }
305
+
306
+ function readLock(root) {
307
+ const lockPath = lockPathFor(root);
308
+ if (!fs.existsSync(lockPath)) return emptyLock();
309
+
310
+ let lock;
311
+ try {
312
+ lock = JSON.parse(fs.readFileSync(lockPath, 'utf8'));
313
+ } catch (error) {
314
+ throw new CliError(`cannot parse ${lockPath}: ${error.message}`);
315
+ }
316
+
317
+ if (![1, 2].includes(lock?.lockfileVersion) || lock.registry !== 'local' || !lock.skills || Array.isArray(lock.skills)) {
318
+ throw new CliError(`${lockPath} is not a supported skillshop lockfile`);
319
+ }
320
+
321
+ return lock;
322
+ }
323
+
324
+ function relativePath(root, target) {
325
+ return path.relative(root, target).split(path.sep).join('/');
326
+ }
327
+
328
+ function lockedTargets(root, name, entry) {
329
+ const paths = Array.isArray(entry?.paths) ? entry.paths : [entry?.path];
330
+ if (paths.length === 0 || paths.some((targetPath) => typeof targetPath !== 'string')) {
331
+ throw new CliError(`lock entry for ${name} has no install paths`);
332
+ }
333
+
334
+ return uniqueDestinations(paths.map((targetPath) => {
335
+ const target = path.resolve(root, targetPath);
336
+ const relative = path.relative(root, target);
337
+ if (relative === '' || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative)) {
338
+ throw new CliError(`lock entry for ${name} points outside the project`);
339
+ }
340
+ if (path.basename(target) !== name || path.basename(path.dirname(target)) !== 'skills') {
341
+ throw new CliError(`lock entry for ${name} is not a skills/${name} directory`);
342
+ }
343
+ assertLocalDestination(root, target, `lock entry for ${name}`);
344
+ return target;
345
+ }));
346
+ }
347
+
348
+ function inferredSkillDirectories(root, lock, cwd) {
349
+ const preferredName = lock.skills.skillshop ? 'skillshop' : lock.skills.skillz ? 'skillz' : Object.keys(lock.skills).sort()[0];
350
+ if (!preferredName) return [path.join(resolveAgentDir(cwd), 'skills')];
351
+ return lockedTargets(root, preferredName, lock.skills[preferredName]).map((target) => path.dirname(target));
352
+ }
353
+
354
+ function localSkillDirectories(root, lock, cwd) {
355
+ const config = readConfig();
356
+ const directories = config
357
+ ? configuredSkillDirectories(config, 'local', root)
358
+ : inferredSkillDirectories(root, lock, cwd);
359
+ for (const directory of directories) assertLocalDestination(root, directory, 'agent skill directory');
360
+ return directories;
361
+ }
362
+
363
+ function writeLock(root, lock) {
364
+ const skills = Object.fromEntries(
365
+ Object.entries(lock.skills)
366
+ .sort(([left], [right]) => left.localeCompare(right))
367
+ .map(([name, entry]) => [name, {
368
+ version: entry.version,
369
+ author: entry.author,
370
+ paths: lockedTargets(root, name, entry).map((target) => relativePath(root, target)).sort()
371
+ }])
372
+ );
373
+ const stableLock = { lockfileVersion: 2, registry: 'local', skills };
374
+ const lockPath = path.join(root, lockFileName);
375
+ const contents = `${JSON.stringify(stableLock, null, 2)}\n`;
376
+ if (fs.existsSync(lockPath) && fs.readFileSync(lockPath, 'utf8') === contents) return;
377
+ const temporaryPath = `${lockPath}.tmp-${process.pid}`;
378
+ fs.writeFileSync(temporaryPath, contents);
379
+ fs.renameSync(temporaryPath, lockPath);
380
+ }
381
+
382
+ function registryPath() {
383
+ return path.join(skillshopHome(), registryFileName);
384
+ }
385
+
386
+ function readRegistry() {
387
+ const filePath = registryPath();
388
+ if (!fs.existsSync(filePath)) return { registryVersion: 1, repositories: [] };
389
+
390
+ let registry;
391
+ try {
392
+ registry = JSON.parse(fs.readFileSync(filePath, 'utf8'));
393
+ } catch (error) {
394
+ throw new CliError(`cannot parse ${filePath}: ${error.message}`);
395
+ }
396
+
397
+ if (
398
+ registry?.registryVersion !== 1 ||
399
+ !Array.isArray(registry.repositories) ||
400
+ registry.repositories.some((repository) => typeof repository !== 'string' || !path.isAbsolute(repository))
401
+ ) {
402
+ throw new CliError(`${filePath} is not a supported skillshop repository registry`);
403
+ }
404
+
405
+ return registry;
406
+ }
407
+
408
+ function canonicalPath(target) {
409
+ try {
410
+ return fs.realpathSync.native(target);
411
+ } catch {
412
+ return path.resolve(target);
413
+ }
414
+ }
415
+
416
+ function canonicalDestination(target) {
417
+ let existing = path.resolve(target);
418
+ const missing = [];
419
+ while (!fs.existsSync(existing)) {
420
+ const parent = path.dirname(existing);
421
+ if (parent === existing) return path.resolve(target);
422
+ missing.unshift(path.basename(existing));
423
+ existing = parent;
424
+ }
425
+ return path.join(canonicalPath(existing), ...missing);
426
+ }
427
+
428
+ function assertLocalDestination(root, target, label) {
429
+ const relative = path.relative(canonicalDestination(root), canonicalDestination(target));
430
+ if (relative === '' || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative)) {
431
+ throw new CliError(`${label} resolves outside the project`);
432
+ }
433
+ }
434
+
435
+ function uniqueDestinations(targets) {
436
+ const seen = new Set();
437
+ return targets.filter((target) => {
438
+ const destination = canonicalDestination(target);
439
+ if (seen.has(destination)) return false;
440
+ seen.add(destination);
441
+ return true;
442
+ });
443
+ }
444
+
445
+ function staleTargets(previousTargets, targetDirs) {
446
+ const destinations = new Set(targetDirs.map(canonicalDestination));
447
+ return previousTargets.filter((target) => !destinations.has(canonicalDestination(target)));
448
+ }
449
+
450
+ function assertTargetsAvailable(name, targetDirs, previousTargets) {
451
+ const managedDestinations = new Set(previousTargets.map(canonicalDestination));
452
+ for (const targetDir of targetDirs) {
453
+ if (fs.existsSync(targetDir) && !managedDestinations.has(canonicalDestination(targetDir))) {
454
+ throw new CliError(`${targetDir} exists and is not managed as ${name} in ${lockFileName}`);
455
+ }
456
+ }
457
+ }
458
+
459
+ function writeRegistry(repositories) {
460
+ const filePath = registryPath();
461
+ const stableRegistry = {
462
+ registryVersion: 1,
463
+ repositories: [...new Set(repositories.map(canonicalPath))].sort()
464
+ };
465
+ const contents = `${JSON.stringify(stableRegistry, null, 2)}\n`;
466
+ if (fs.existsSync(filePath) && fs.readFileSync(filePath, 'utf8') === contents) return;
467
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
468
+ const temporaryPath = `${filePath}.tmp-${process.pid}`;
469
+ fs.writeFileSync(temporaryPath, contents);
470
+ fs.renameSync(temporaryPath, filePath);
471
+ }
472
+
473
+ function registerRepository(root) {
474
+ const registry = readRegistry();
475
+ writeRegistry([...registry.repositories, root]);
476
+ }
477
+
478
+ function gitOutput(root, args) {
479
+ try {
480
+ return execFileSync('git', args, { cwd: root, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] }).trim();
481
+ } catch (error) {
482
+ const detail = error.stderr?.trim();
483
+ throw new CliError(detail || `cannot inspect Git repository at ${root}`);
484
+ }
485
+ }
486
+
487
+ function managedGitPaths(root, lock) {
488
+ const gitRoot = canonicalPath(gitOutput(root, ['rev-parse', '--show-toplevel']));
489
+ const scope = relativePath(gitRoot, canonicalPath(root));
490
+ const scopedPath = (target) => relativePath(gitRoot, path.join(root, target));
491
+ const paths = [scopedPath(lockFileName)];
492
+ if (fs.existsSync(path.join(root, legacyLockFileName))) paths.push(scopedPath(legacyLockFileName));
493
+
494
+ for (const [name, entry] of Object.entries(lock.skills)) {
495
+ for (const target of lockedTargets(root, name, entry)) {
496
+ paths.push(scopedPath(relativePath(root, target)));
497
+ }
498
+ }
499
+
500
+ if (scope.startsWith('../') || path.isAbsolute(scope)) {
501
+ throw new CliError(`${root} is outside its Git repository`);
502
+ }
503
+
504
+ return { gitRoot, paths: [...new Set(paths)].sort() };
505
+ }
506
+
507
+ function gitExcludePattern(relative) {
508
+ const escaped = relative.replaceAll('\\', '\\\\').replace(/([ !#*?\[\]])/g, '\\$1');
509
+ return `/${escaped}${[lockFileName, legacyLockFileName].includes(path.posix.basename(relative)) ? '' : '/'}`;
510
+ }
511
+
512
+ function syncGitExclude(root, lock) {
513
+ const { paths } = managedGitPaths(root, lock);
514
+ const excludePathOutput = gitOutput(root, ['rev-parse', '--git-path', 'info/exclude']);
515
+ const excludePath = path.isAbsolute(excludePathOutput)
516
+ ? excludePathOutput
517
+ : path.resolve(root, excludePathOutput);
518
+ const existing = fs.existsSync(excludePath) ? fs.readFileSync(excludePath, 'utf8') : '';
519
+ const lines = existing.replace(/\r\n/g, '\n').split('\n');
520
+ const retained = [];
521
+ let insideBlock = false;
522
+
523
+ for (const line of lines) {
524
+ if (line === excludeBlockStart || line === legacyExcludeBlockStart) {
525
+ insideBlock = true;
526
+ continue;
527
+ }
528
+ if (line === excludeBlockEnd || line === legacyExcludeBlockEnd) {
529
+ insideBlock = false;
530
+ continue;
531
+ }
532
+ if (!insideBlock) retained.push(line);
533
+ }
534
+
535
+ while (retained.at(-1) === '') retained.pop();
536
+ const block = [excludeBlockStart, ...paths.map(gitExcludePattern), excludeBlockEnd];
537
+ const contents = `${[...retained, ...(retained.length ? [''] : []), ...block].join('\n')}\n`;
538
+ fs.mkdirSync(path.dirname(excludePath), { recursive: true });
539
+ if (contents !== existing) fs.writeFileSync(excludePath, contents);
540
+ }
541
+
542
+ function untrackManagedPaths(root, lock) {
543
+ const { gitRoot, paths } = managedGitPaths(root, lock);
544
+ try {
545
+ execFileSync(
546
+ 'git',
547
+ ['rm', '-r', '--cached', '-f', '--ignore-unmatch', '--', ...paths],
548
+ { cwd: gitRoot, stdio: 'ignore' }
549
+ );
550
+ } catch {
551
+ throw new CliError(`cannot remove managed skill dependencies from the Git index at ${gitRoot}`);
552
+ }
553
+ }
554
+
555
+ function maintainRepository(root, lock, { register = true, untrack = false } = {}) {
556
+ if (untrack) untrackManagedPaths(root, lock);
557
+ syncGitExclude(root, lock);
558
+ if (register) registerRepository(root);
559
+ }
560
+
561
+ function manifestParts(contents, label) {
562
+ const newline = contents.includes('\r\n') ? '\r\n' : '\n';
563
+ if (!contents.startsWith(`---${newline}`)) return { lines: [], body: contents, newline };
564
+ const end = contents.indexOf(`${newline}---`, 3);
565
+ if (end === -1 || !['', '\n', '\r'].includes(contents[end + newline.length + 3] ?? '')) {
566
+ throw new CliError(`${label} has unterminated front matter`);
567
+ }
568
+ return {
569
+ lines: contents.slice(3 + newline.length, end).split(newline),
570
+ body: contents.slice(end + newline.length + 3),
571
+ newline
572
+ };
573
+ }
574
+
575
+ function repairInstalledManifest(name, entry, manifestPath) {
576
+ const contents = fs.readFileSync(manifestPath, 'utf8');
577
+ const { lines, body, newline } = manifestParts(contents, manifestPath);
578
+ const sourcePath = path.join(skillsContentRoot, name, 'SKILL.md');
579
+ const sourceLines = fs.existsSync(sourcePath)
580
+ ? manifestParts(fs.readFileSync(sourcePath, 'utf8'), sourcePath).lines
581
+ : [];
582
+ const sourceField = (key) => sourceLines.find((line) => line.startsWith(`${key}:`))?.slice(key.length + 1).trim();
583
+ const setField = (key, value) => {
584
+ const index = lines.findIndex((line) => line.startsWith(`${key}:`));
585
+ const replacement = `${key}: ${value}`;
586
+ if (index === -1) lines.push(replacement);
587
+ else if (unquote(lines[index].slice(key.length + 1)) !== unquote(value)) lines[index] = replacement;
588
+ };
589
+ setField('name', JSON.stringify(name));
590
+ const description = lines.find((line) => line.startsWith('description:'))?.slice(12).trim();
591
+ if (!description || ['""', "''", 'null', '~'].includes(description)) {
592
+ const fallback = sourceField('description');
593
+ if (!fallback) throw new CliError(`cannot restore description for ${manifestPath}`);
594
+ setField('description', fallback);
595
+ }
596
+
597
+ let metadataIndex = lines.findIndex((line) => /^metadata:/.test(line));
598
+ if (metadataIndex === -1) {
599
+ metadataIndex = lines.length;
600
+ lines.push('metadata:');
601
+ } else if (!/^metadata:\s*(?:#.*)?$/.test(lines[metadataIndex])) {
602
+ throw new CliError(`${manifestPath} must use a metadata block mapping`);
603
+ }
604
+ let metadataEnd = metadataIndex + 1;
605
+ while (metadataEnd < lines.length && /^(\s|$|#)/.test(lines[metadataEnd])) metadataEnd += 1;
606
+ const indent = lines.slice(metadataIndex + 1, metadataEnd)
607
+ .find((line) => /^\s+\S/.test(line))?.match(/^\s+/)[0] ?? ' ';
608
+ const metadata = {};
609
+ for (const key of ['author', 'version']) {
610
+ const fieldPrefix = `${indent}${key}:`;
611
+ const index = lines.findIndex((line, i) => i > metadataIndex && i < metadataEnd && line.startsWith(fieldPrefix));
612
+ const installed = index === -1 ? null : unquote(lines[index].slice(fieldPrefix.length));
613
+ const source = sourceLines.find((line) => new RegExp(`^\\s+${key}:`).test(line));
614
+ const value = typeof entry[key] === 'string' && entry[key].trim()
615
+ ? entry[key]
616
+ : installed && !['null', '~'].includes(installed) ? installed
617
+ : source ? unquote(source.slice(source.indexOf(':') + 1)) : null;
618
+ if (!value) throw new CliError(`cannot restore metadata.${key} for ${manifestPath}`);
619
+ metadata[key] = value;
620
+ const replacement = `${fieldPrefix} ${JSON.stringify(value)}`;
621
+ if (index === -1) {
622
+ lines.splice(metadataEnd, 0, replacement);
623
+ metadataEnd += 1;
624
+ } else if (installed !== value) lines[index] = replacement;
625
+ }
626
+ const separator = body && !body.startsWith(newline) ? newline + newline : '';
627
+ return { contents: `---${newline}${lines.join(newline)}${newline}---${separator}${body}`, metadata };
628
+ }
629
+
630
+ function readGlobalSkills() {
631
+ const filePath = path.join(skillshopHome(), globalSkillsFileName);
632
+ if (!fs.existsSync(filePath)) return emptyLock();
633
+ let lock;
634
+ try {
635
+ lock = JSON.parse(fs.readFileSync(filePath, 'utf8'));
636
+ } catch (error) {
637
+ throw new CliError(`cannot parse ${filePath}: ${error.message}`);
638
+ }
639
+ if (lock?.lockfileVersion !== 2 || lock.registry !== 'local' || !lock.skills || Array.isArray(lock.skills)) {
640
+ throw new CliError(`${filePath} is not a supported skillshop global manifest`);
641
+ }
642
+ return lock;
643
+ }
644
+
645
+ function globalTargets(name, entry) {
646
+ validateSkillName(name);
647
+ if (!Array.isArray(entry?.paths) || !entry.paths.length) {
648
+ throw new CliError(`global entry for ${name} has no install paths`);
649
+ }
650
+ return uniqueDestinations(entry.paths.map((target) => {
651
+ if (typeof target !== 'string' || !path.isAbsolute(target) ||
652
+ path.basename(target) !== name || path.basename(path.dirname(target)) !== 'skills') {
653
+ throw new CliError(`global entry for ${name} is not an absolute skills/${name} directory`);
654
+ }
655
+ assertLocalDestination(os.homedir(), target, `global entry for ${name}`);
656
+ return target;
657
+ }));
658
+ }
659
+
660
+ function writeGlobalSkills(lock) {
661
+ const skills = Object.fromEntries(Object.entries(lock.skills).sort(([left], [right]) => left.localeCompare(right))
662
+ .map(([name, entry]) => {
663
+ const paths = globalTargets(name, entry).sort();
664
+ return [name, { paths, copies: Object.fromEntries(paths.map((target) => [target, entry.copies?.[target] ?? {}])) }];
665
+ }));
666
+ const filePath = path.join(skillshopHome(), globalSkillsFileName);
667
+ const contents = `${JSON.stringify({ ...emptyLock(), skills }, null, 2)}\n`;
668
+ if (fs.existsSync(filePath) && fs.readFileSync(filePath, 'utf8') === contents) return;
669
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
670
+ const temporaryPath = `${filePath}.tmp-${process.pid}`;
671
+ fs.writeFileSync(temporaryPath, contents);
672
+ fs.renameSync(temporaryPath, filePath);
673
+ }
674
+
675
+ function repairTrackedSkills(root, lock) {
676
+ const repairs = [];
677
+ for (const [name, entry] of Object.entries(lock.skills)) {
678
+ validateSkillName(name);
679
+ const targets = (root ? lockedTargets(root, name, entry) : globalTargets(name, entry))
680
+ .filter((target) => fs.existsSync(path.join(target, 'SKILL.md')));
681
+ if (!targets.length) {
682
+ delete lock.skills[name];
683
+ continue;
684
+ }
685
+ for (const target of targets) {
686
+ const manifestPath = path.join(target, 'SKILL.md');
687
+ assertLocalDestination(root ?? os.homedir(), manifestPath, `manifest for ${name}`);
688
+ const installation = root ? entry : entry.copies?.[target] ?? {};
689
+ const repair = repairInstalledManifest(name, installation, manifestPath);
690
+ if (root) Object.assign(entry, repair.metadata);
691
+ else {
692
+ entry.copies ??= {};
693
+ entry.copies[target] = repair.metadata;
694
+ }
695
+ repairs.push({ manifestPath, contents: repair.contents });
696
+ }
697
+ entry.paths = targets.map((target) => root ? relativePath(root, target) : target);
698
+ }
699
+ for (const { manifestPath, contents } of repairs) {
700
+ if (fs.readFileSync(manifestPath, 'utf8') !== contents) fs.writeFileSync(manifestPath, contents);
701
+ }
702
+ }
703
+
704
+ function reconcileRepository(root, { register = true } = {}) {
705
+ const lock = readLock(root);
706
+ repairTrackedSkills(root, lock);
707
+ writeLock(root, lock);
708
+ maintainRepository(root, lock, { register });
709
+ }
710
+
711
+ function reconcileCurrentRepository() {
712
+ if (fs.existsSync(path.join(skillshopHome(), globalSkillsFileName))) {
713
+ const globalSkills = readGlobalSkills();
714
+ repairTrackedSkills(null, globalSkills);
715
+ writeGlobalSkills(globalSkills);
716
+ }
717
+ const root = findLockRoot(process.cwd(), { required: false });
718
+ if (root) reconcileRepository(root, { register: false });
719
+ const registryExists = fs.existsSync(registryPath());
720
+ if (!root && !registryExists) return;
721
+ const registry = readRegistry();
722
+ const repositories = registry.repositories.filter((directory) => hasLock(directory));
723
+ if (root) repositories.push(root);
724
+ writeRegistry(repositories);
725
+ }
726
+
727
+ function replaceSkillDirectory(sourceDir, targetDir) {
728
+ const temporaryDir = `${targetDir}.skillshop-tmp-${process.pid}`;
729
+ fs.mkdirSync(path.dirname(targetDir), { recursive: true });
730
+ fs.rmSync(temporaryDir, { recursive: true, force: true });
731
+ fs.cpSync(sourceDir, temporaryDir, { recursive: true, force: true });
732
+ fs.rmSync(targetDir, { recursive: true, force: true });
733
+ fs.renameSync(temporaryDir, targetDir);
734
+ }
735
+
736
+ function installAt(name, requestedVersion, targetDir) {
737
+ const metadata = skillMetadata(name);
738
+ if (requestedVersion && requestedVersion !== metadata.version) {
739
+ throw new CliError(
740
+ `${name}@${requestedVersion} is unavailable; local registry provides ${name}@${metadata.version}`
741
+ );
742
+ }
743
+
744
+ const updating = fs.existsSync(targetDir);
745
+ replaceSkillDirectory(metadata.sourceDir, targetDir);
746
+ return { ...metadata, targetDir, updating };
747
+ }
748
+
749
+ function lockEntry(root, results) {
750
+ const [result] = results;
751
+ return {
752
+ version: result.version,
753
+ author: result.author,
754
+ paths: results.map(({ targetDir }) => relativePath(root, targetDir)).sort()
755
+ };
756
+ }
757
+
758
+ function installGlobal(name, requestedVersion) {
759
+ const config = readConfig();
760
+ const skillDirectories = config
761
+ ? configuredSkillDirectories(config, 'global')
762
+ : [path.join(os.homedir(), '.agents', 'skills')];
763
+
764
+ const globalSkills = readGlobalSkills();
765
+ for (const skillDirectory of skillDirectories) {
766
+ const targetDir = path.join(skillDirectory, name);
767
+ assertLocalDestination(os.homedir(), targetDir, `global installation for ${name}`);
768
+ const result = installAt(name, requestedVersion, targetDir);
769
+ const entry = globalSkills.skills[name] ?? { paths: [], copies: {} };
770
+ entry.paths = [...new Set([...entry.paths, targetDir])];
771
+ entry.copies[targetDir] = { author: result.author, version: result.version };
772
+ globalSkills.skills[name] = entry;
773
+ writeGlobalSkills(globalSkills);
774
+ console.log(`${result.updating ? 'Updated' : 'Installed'} ${name}@${result.version} at ${targetDir}`);
775
+ }
776
+ }
777
+
778
+ function nearestAgentsFile(cwd) {
779
+ let directory = path.resolve(cwd);
780
+ while (true) {
781
+ const candidate = path.join(directory, 'AGENTS.md');
782
+ if (fs.existsSync(candidate)) return candidate;
783
+ const parent = path.dirname(directory);
784
+ if (parent === directory) return null;
785
+ directory = parent;
786
+ }
787
+ }
788
+
789
+ function installAgents(cwd, globalInstall) {
790
+ const composed = composeAgentsInstructions(agentsContentRoot);
791
+ const targetPaths = [];
792
+ if (globalInstall) {
793
+ targetPaths.push(...globalAgentsTargets.map((target) => path.join(os.homedir(), target)));
794
+ } else {
795
+ const targetPath = nearestAgentsFile(cwd);
796
+ if (!targetPath) {
797
+ console.log(`No AGENTS.md found at or above ${cwd}`);
798
+ return;
799
+ }
800
+ targetPaths.push(targetPath);
801
+ }
802
+
803
+ // Plan every target before writing any of them, so a rejected file leaves all files intact.
804
+ const planned = targetPaths.map((targetPath) => {
805
+ const existing = fs.existsSync(targetPath) ? fs.readFileSync(targetPath, 'utf8') : '';
806
+ const bundled = applyInstructions(existing, composed.content, packageJson.version);
807
+ const local = applyLocalFragments(bundled.text, path.dirname(targetPath));
808
+ const action = existing === local.text ? 'unchanged' : bundled.action === 'installed' || local.action === 'installed' ? 'installed' : 'refreshed';
809
+ return { targetPath, action, text: local.text };
810
+ });
811
+
812
+ for (const { targetPath, action, text } of planned) {
813
+ if (action === 'unchanged') {
814
+ console.log(`Agents instructions already installed at ${targetPath}`);
815
+ continue;
816
+ }
817
+ fs.mkdirSync(path.dirname(targetPath), { recursive: true });
818
+ fs.writeFileSync(targetPath, text);
819
+ console.log(
820
+ action === 'refreshed'
821
+ ? `Refreshed agents instructions at ${targetPath}`
822
+ : `Installed agents instructions at ${targetPath}`
823
+ );
824
+ }
825
+ }
826
+
827
+ function installLocal(name, requestedVersion, cwd) {
828
+ const root = findLockRoot(cwd) ?? projectRoot(cwd);
829
+ const lock = readLock(root);
830
+ const config = readConfig();
831
+ const skillDirectories = localSkillDirectories(root, lock, cwd);
832
+ const names = [...new Set([...Object.keys(lock.skills), name, 'skillshop'])].sort();
833
+ const installs = names.map((skillName) => {
834
+ const metadata = skillMetadata(skillName);
835
+ const targetDirs = config || !lock.skills[skillName]
836
+ ? skillDirectories.map((directory) => path.join(directory, skillName))
837
+ : lockedTargets(root, skillName, lock.skills[skillName]);
838
+ const previousTargets = lock.skills[skillName]
839
+ ? lockedTargets(root, skillName, lock.skills[skillName])
840
+ : [];
841
+ return { metadata, targetDirs, previousTargets };
842
+ });
843
+ const requestedMetadata = installs.find(({ metadata }) => metadata.name === name).metadata;
844
+ if (requestedVersion && requestedVersion !== requestedMetadata.version) {
845
+ throw new CliError(
846
+ `${name}@${requestedVersion} is unavailable; local registry provides ${name}@${requestedMetadata.version}`
847
+ );
848
+ }
849
+ for (const { metadata, targetDirs, previousTargets } of installs) {
850
+ assertTargetsAvailable(metadata.name, targetDirs, previousTargets);
851
+ }
852
+
853
+ for (const { metadata, targetDirs, previousTargets } of installs) {
854
+ const version = metadata.name === name ? requestedVersion : null;
855
+ const results = targetDirs.map((targetDir) => installAt(metadata.name, version, targetDir));
856
+ for (const staleTarget of staleTargets(previousTargets, targetDirs)) {
857
+ fs.rmSync(staleTarget, { recursive: true, force: true });
858
+ }
859
+ lock.skills[metadata.name] = lockEntry(root, results);
860
+ for (const result of results) {
861
+ console.log(
862
+ `${result.updating ? 'Updated' : 'Installed'} ${result.name}@${result.version} at ${result.targetDir}`
863
+ );
864
+ }
865
+ }
866
+
867
+ writeLock(root, lock);
868
+ maintainRepository(root, lock);
869
+ console.log(`Wrote ${path.join(root, lockFileName)}`);
870
+ }
871
+
872
+ function uninstallGlobal(name) {
873
+ const config = readConfig();
874
+ const skillDirectories = config
875
+ ? configuredSkillDirectories(config, 'global')
876
+ : [path.join(os.homedir(), '.agents', 'skills')];
877
+ const targets = skillDirectories.map((directory) => path.join(directory, name));
878
+ const installedTargets = targets.filter((target) => fs.existsSync(target));
879
+ if (installedTargets.length === 0) {
880
+ throw new CliError(`${name} is not installed globally in the configured skill directories`);
881
+ }
882
+
883
+ for (const targetDir of installedTargets) {
884
+ fs.rmSync(targetDir, { recursive: true });
885
+ console.log(`Uninstalled ${name} from ${targetDir}`);
886
+ }
887
+ const globalSkills = readGlobalSkills();
888
+ if (globalSkills.skills[name]) {
889
+ const remaining = staleTargets(globalTargets(name, globalSkills.skills[name]), installedTargets);
890
+ if (remaining.length) globalSkills.skills[name].paths = remaining;
891
+ else delete globalSkills.skills[name];
892
+ writeGlobalSkills(globalSkills);
893
+ }
894
+ }
895
+
896
+ function uninstallLocal(name, cwd) {
897
+ const root = findLockRoot(cwd);
898
+ if (!root) throw new CliError(`no ${lockFileName} found in this project`);
899
+
900
+ const lock = readLock(root);
901
+ if (!lock.skills[name]) {
902
+ throw new CliError(`${name} is not listed in ${path.join(root, lockFileName)}`);
903
+ }
904
+
905
+ const targetDirs = lockedTargets(root, name, lock.skills[name]);
906
+ for (const targetDir of targetDirs) fs.rmSync(targetDir, { recursive: true, force: true });
907
+ delete lock.skills[name];
908
+ writeLock(root, lock);
909
+ maintainRepository(root, lock);
910
+
911
+ for (const targetDir of targetDirs) console.log(`Uninstalled ${name} from ${targetDir}`);
912
+ console.log(`Updated ${path.join(root, lockFileName)}`);
913
+ }
914
+
915
+ function updateLocal(requestedName, cwd, options = {}) {
916
+ const root = findLockRoot(cwd);
917
+ if (!root) throw new CliError(`no ${lockFileName} found in this project`);
918
+
919
+ const lock = readLock(root);
920
+ if (requestedName && !lock.skills[requestedName]) {
921
+ throw new CliError(`${requestedName} is not listed in ${path.join(root, lockFileName)}`);
922
+ }
923
+
924
+ const names = requestedName ? [requestedName] : Object.keys(lock.skills);
925
+ const config = readConfig();
926
+ const skillDirectories = localSkillDirectories(root, lock, cwd);
927
+
928
+ const updates = [...new Set(names)].sort().map((name) => {
929
+ const metadata = skillMetadata(name);
930
+ const targetDirs = config
931
+ ? skillDirectories.map((directory) => path.join(directory, name))
932
+ : lockedTargets(root, name, lock.skills[name]);
933
+ const previousTargets = lockedTargets(root, name, lock.skills[name]);
934
+ return { metadata, targetDirs, previousTargets };
935
+ });
936
+ for (const { metadata, targetDirs, previousTargets } of updates) {
937
+ assertTargetsAvailable(metadata.name, targetDirs, previousTargets);
938
+ }
939
+
940
+ for (const { metadata, targetDirs, previousTargets } of updates) {
941
+ const results = targetDirs.map((targetDir) => installAt(metadata.name, null, targetDir));
942
+ for (const staleTarget of staleTargets(previousTargets, targetDirs)) {
943
+ fs.rmSync(staleTarget, { recursive: true, force: true });
944
+ }
945
+ lock.skills[metadata.name] = lockEntry(root, results);
946
+ for (const result of results) {
947
+ console.log(`Updated ${result.name}@${result.version} at ${result.targetDir}`);
948
+ }
949
+ }
950
+
951
+ writeLock(root, lock);
952
+ maintainRepository(root, lock, options);
953
+ console.log(`Updated ${path.join(root, lockFileName)}`);
954
+ }
955
+
956
+ function updateAllRepositories() {
957
+ const registry = readRegistry();
958
+ const retained = [];
959
+ const failures = [];
960
+
961
+ for (const root of registry.repositories) {
962
+ if (!hasLock(root)) {
963
+ console.log(`Removed missing repository ${root}`);
964
+ continue;
965
+ }
966
+
967
+ retained.push(root);
968
+ console.log(`Updating ${root}`);
969
+ try {
970
+ reconcileRepository(root, { register: false });
971
+ updateLocal(null, root, { register: false });
972
+ } catch (error) {
973
+ if (!(error instanceof CliError)) throw error;
974
+ failures.push(`${root}: ${error.message}`);
975
+ console.error(`skillshop: ${root}: ${error.message}`);
976
+ }
977
+ }
978
+
979
+ writeRegistry(retained);
980
+ if (failures.length) {
981
+ throw new CliError(`failed to update ${failures.length} registered ${failures.length === 1 ? 'repository' : 'repositories'}`);
982
+ }
983
+ console.log(`Updated ${retained.length} registered ${retained.length === 1 ? 'repository' : 'repositories'}`);
984
+ }
985
+
986
+ const migrationSkipDirectories = new Set([
987
+ '.git',
988
+ '.skillshop',
989
+ '.skillz',
990
+ '.Trash',
991
+ 'Library',
992
+ 'node_modules'
993
+ ]);
994
+
995
+ function discoverLockRoots(startPath, roots) {
996
+ let stat;
997
+ try {
998
+ stat = fs.statSync(startPath);
999
+ } catch {
1000
+ return;
1001
+ }
1002
+
1003
+ if (stat.isFile()) {
1004
+ if ([lockFileName, legacyLockFileName].includes(path.basename(startPath))) roots.add(canonicalPath(path.dirname(startPath)));
1005
+ return;
1006
+ }
1007
+ if (!stat.isDirectory()) return;
1008
+
1009
+ if (hasLock(startPath)) roots.add(canonicalPath(startPath));
1010
+
1011
+ let entries;
1012
+ try {
1013
+ entries = fs.readdirSync(startPath, { withFileTypes: true });
1014
+ } catch {
1015
+ return;
1016
+ }
1017
+
1018
+ for (const entry of entries) {
1019
+ if (!entry.isDirectory() || entry.isSymbolicLink() || migrationSkipDirectories.has(entry.name)) continue;
1020
+ discoverLockRoots(path.join(startPath, entry.name), roots);
1021
+ }
1022
+ }
1023
+
1024
+ function migrateRepositories(requestedRoots, cwd) {
1025
+ const registry = readRegistry();
1026
+ const roots = new Set(registry.repositories);
1027
+ const searchRoots = requestedRoots.length
1028
+ ? requestedRoots.map((root) => path.resolve(cwd, root))
1029
+ : [os.homedir()];
1030
+
1031
+ for (const searchRoot of searchRoots) discoverLockRoots(searchRoot, roots);
1032
+
1033
+ const migrated = [];
1034
+ const failures = [];
1035
+ for (const root of [...roots].sort()) {
1036
+ if (!hasLock(root)) continue;
1037
+
1038
+ try {
1039
+ reconcileRepository(root, { register: false });
1040
+ const lock = readLock(root);
1041
+ maintainRepository(root, lock, { register: false, untrack: true });
1042
+ migrated.push(root);
1043
+ console.log(`Migrated ${root}`);
1044
+ } catch (error) {
1045
+ if (!(error instanceof CliError)) throw error;
1046
+ failures.push(`${root}: ${error.message}`);
1047
+ console.error(`skillshop: ${root}: ${error.message}`);
1048
+ }
1049
+ }
1050
+
1051
+ writeRegistry(migrated);
1052
+ if (failures.length) {
1053
+ throw new CliError(`failed to migrate ${failures.length} ${failures.length === 1 ? 'repository' : 'repositories'}`);
1054
+ }
1055
+ console.log(`Migrated ${migrated.length} ${migrated.length === 1 ? 'repository' : 'repositories'}`);
1056
+ }
1057
+
1058
+ function parseSkillSpec(spec, requestedVersion) {
1059
+ const separator = spec.lastIndexOf('@');
1060
+ if (separator <= 0) return { name: spec, version: requestedVersion };
1061
+
1062
+ const name = spec.slice(0, separator);
1063
+ const inlineVersion = spec.slice(separator + 1);
1064
+ if (!inlineVersion) throw new CliError(`missing version in ${spec}`);
1065
+ if (requestedVersion && requestedVersion !== inlineVersion) {
1066
+ throw new CliError(`conflicting versions requested for ${name}`);
1067
+ }
1068
+ return { name, version: inlineVersion };
1069
+ }
1070
+
1071
+ function parseArgs(argv) {
1072
+ const positional = [];
1073
+ let globalInstall = false;
1074
+ let pushUpdate = false;
1075
+ const agentsTargets = [];
1076
+ let requestedVersion = null;
1077
+
1078
+ for (let index = 0; index < argv.length; index += 1) {
1079
+ const argument = argv[index];
1080
+ if (argument === '--global' || argument === '-g') {
1081
+ globalInstall = true;
1082
+ } else if (argument === '--push') {
1083
+ pushUpdate = true;
1084
+ } else if (argument === '--codex' || argument === '--copilot') {
1085
+ throw new CliError(`${argument} is no longer supported; use agents --global`);
1086
+ } else if (argument === '--version' || argument === '-v') {
1087
+ requestedVersion = argv[index + 1];
1088
+ if (!requestedVersion) throw new CliError(`${argument} requires a version`);
1089
+ index += 1;
1090
+ } else if (argument.startsWith('--version=')) {
1091
+ requestedVersion = argument.slice('--version='.length);
1092
+ if (!requestedVersion) throw new CliError('--version requires a version');
1093
+ } else if (argument.startsWith('-')) {
1094
+ throw new CliError(`unknown option: ${argument}`);
1095
+ } else {
1096
+ positional.push(argument);
1097
+ }
1098
+ }
1099
+
1100
+ return { positional, globalInstall, pushUpdate, requestedVersion, agentsTargets: [...new Set(agentsTargets)] };
1101
+ }
1102
+
1103
+ function listSkills() {
1104
+ for (const name of availableSkills()) {
1105
+ const metadata = skillMetadata(name);
1106
+ console.log(`${name}@${metadata.version} — ${metadata.author}`);
1107
+ }
1108
+ }
1109
+
1110
+ function upgrade() {
1111
+ execFileSync('npm', ['install', '--global', 'skillshop'], { stdio: 'inherit' });
1112
+ }
1113
+
1114
+ async function main(argv) {
1115
+ if (argv.length === 0 || argv.includes('--help') || argv.includes('-h')) {
1116
+ printHelp();
1117
+ return;
1118
+ }
1119
+ if (argv.length === 1 && (argv[0] === '--version' || argv[0] === '-v')) {
1120
+ console.log(packageJson.version);
1121
+ return;
1122
+ }
1123
+
1124
+ const { positional, globalInstall, pushUpdate, requestedVersion, agentsTargets } = parseArgs(argv);
1125
+ const command = positional[0];
1126
+
1127
+ if (command === 'init') {
1128
+ if (positional.length !== 1 || requestedVersion || globalInstall || pushUpdate || agentsTargets.length) {
1129
+ throw new CliError('init does not accept arguments or options');
1130
+ }
1131
+ await initialize();
1132
+ return;
1133
+ }
1134
+
1135
+ if (command === 'config') {
1136
+ if (positional.length !== 2 || positional[1] !== 'agents' || requestedVersion || globalInstall || pushUpdate || agentsTargets.length) {
1137
+ throw new CliError('config expects `agents` and does not accept options');
1138
+ }
1139
+ await configureAgents();
1140
+ return;
1141
+ }
1142
+
1143
+ if (command === 'list' || command === 'ls') {
1144
+ if (positional.length !== 1 || requestedVersion || globalInstall || pushUpdate || agentsTargets.length) {
1145
+ throw new CliError('list does not accept skill or install options');
1146
+ }
1147
+ listSkills();
1148
+ return;
1149
+ }
1150
+
1151
+ if (command === 'upgrade') {
1152
+ if (positional.length !== 1 || requestedVersion || globalInstall || pushUpdate || agentsTargets.length) {
1153
+ throw new CliError('upgrade does not accept arguments or options');
1154
+ }
1155
+ upgrade();
1156
+ return;
1157
+ }
1158
+
1159
+ if (command === 'update' || command === 'up') {
1160
+ if (agentsTargets.length) throw new CliError('--codex and --copilot are only valid with agents');
1161
+ if (globalInstall) throw new CliError('global updates are not tracked; reinstall the global skill by name');
1162
+ if (requestedVersion) throw new CliError('update uses the current registry versions and does not accept --version');
1163
+ if (positional.length > 2) throw new CliError('update accepts at most one locked skill name');
1164
+ if (pushUpdate && positional.length !== 1) {
1165
+ throw new CliError('push update does not accept a skill name');
1166
+ }
1167
+ if (pushUpdate) updateAllRepositories();
1168
+ else updateLocal(positional[1] ?? null, process.cwd());
1169
+ return;
1170
+ }
1171
+
1172
+ if (command === 'migrate') {
1173
+ if (requestedVersion || globalInstall || pushUpdate || agentsTargets.length) {
1174
+ throw new CliError('migrate does not accept install or update options');
1175
+ }
1176
+ migrateRepositories(positional.slice(1), process.cwd());
1177
+ return;
1178
+ }
1179
+
1180
+ if (command === 'agents') {
1181
+ if (positional.length !== 1 || requestedVersion || pushUpdate) {
1182
+ throw new CliError('agents does not accept skill or update options');
1183
+ }
1184
+ installAgents(process.cwd(), globalInstall);
1185
+ return;
1186
+ }
1187
+
1188
+ const uninstallAliases = new Set(['uninstall', 'remove', 'rm']);
1189
+ if (uninstallAliases.has(command)) {
1190
+ if (agentsTargets.length) throw new CliError('--codex and --copilot are only valid with agents');
1191
+ if (requestedVersion) throw new CliError('uninstall does not accept --version');
1192
+ if (pushUpdate) throw new CliError('--push is only valid with update');
1193
+ if (positional.length !== 2) throw new CliError('uninstall expects exactly one skill name');
1194
+
1195
+ const name = positional[1];
1196
+ validateSkillName(name);
1197
+ if (globalInstall) uninstallGlobal(name);
1198
+ else uninstallLocal(name, process.cwd());
1199
+ return;
1200
+ }
1201
+
1202
+ const installAliases = new Set(['install', 'i', 'add']);
1203
+ if (agentsTargets.length) throw new CliError('--codex and --copilot are only valid with agents');
1204
+ const spec = installAliases.has(command) ? positional[1] : command;
1205
+ const expectedLength = installAliases.has(command) ? 2 : 1;
1206
+ if (!spec || positional.length !== expectedLength) {
1207
+ throw new CliError('expected exactly one skill name');
1208
+ }
1209
+ if (pushUpdate) throw new CliError('--push is only valid with update');
1210
+
1211
+ const { name, version } = parseSkillSpec(spec, requestedVersion);
1212
+ validateSkillName(name);
1213
+ if (globalInstall) installGlobal(name, version);
1214
+ else installLocal(name, version, process.cwd());
1215
+ }
1216
+
1217
+ export async function runLegacyCli(argv) {
1218
+ try {
1219
+ reconcileCurrentRepository();
1220
+ try {
1221
+ await main(argv);
1222
+ } finally {
1223
+ reconcileCurrentRepository();
1224
+ }
1225
+ } catch (error) {
1226
+ if (!(error instanceof CliError) && !(error instanceof AgentsInstructionsError)) throw error;
1227
+ console.error(`skillshop: ${error.message}`);
1228
+ process.exitCode = 1;
1229
+ }
1230
+ }
1231
+
1232
+ export function reconcileLegacyState() {
1233
+ reconcileCurrentRepository();
1234
+ }