skillshop 2.0.0 → 2.2.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.
- package/README.md +273 -100
- package/agents-content/000_communication-writing.block.md +11 -0
- package/agents-content/001_external-actions.block.md +9 -0
- package/agents-content/AGENTS.md +11 -0
- package/package.json +6 -3
- package/skills-content/architecture-scanner/SKILL.md +274 -0
- package/skills-content/architecture-scanner/files.json +10 -0
- package/skills-content/architecture-scanner/scan.sh +823 -0
- package/skills-content/change-review/SKILL.md +76 -0
- package/skills-content/change-review/agents/openai.yaml +4 -0
- package/skills-content/changelog/SKILL.md +99 -0
- package/skills-content/changeset/changeset.md +80 -0
- package/skills-content/code-buddy/SKILL.md +136 -0
- package/skills-content/design-system-catalog/SKILL.md +106 -0
- package/skills-content/design-system-catalog/extract-components.mjs +441 -0
- package/skills-content/design-system-catalog/generate-catalog.sh +255 -0
- package/skills-content/flare/SKILL.md +96 -0
- package/skills-content/grilling/SKILL.md +31 -0
- package/skills-content/install-sigmap/SKILL.md +97 -0
- package/skills-content/one-shot/SKILL.md +51 -0
- package/skills-content/release/SKILL.md +128 -0
- package/skills-content/shadow-clone/SKILL.md +228 -0
- package/skills-content/shadow-clone/extract-signatures.mjs +202 -0
- package/skills-content/shadow-clone/scan-repo.mjs +343 -0
- package/skills-content/ship/SKILL.md +95 -0
- package/skills-content/shots/SKILL.md +89 -0
- package/skills-content/shots/agents/openai.yaml +4 -0
- package/skills-content/skillshop/SKILL.md +157 -0
- package/skills-content/skillz/SKILL.md +15 -0
- package/skills-content/transcript-clean/SKILL.md +55 -0
- package/skills-content/transcript-edit/SKILL.md +80 -0
- package/skills-content/vitest/GENERATION.md +5 -0
- package/skills-content/vitest/SKILL.md +52 -0
- package/skills-content/vitest/references/advanced-environments.md +264 -0
- package/skills-content/vitest/references/advanced-projects.md +300 -0
- package/skills-content/vitest/references/advanced-type-testing.md +237 -0
- package/skills-content/vitest/references/advanced-vi.md +249 -0
- package/skills-content/vitest/references/core-cli.md +166 -0
- package/skills-content/vitest/references/core-config.md +174 -0
- package/skills-content/vitest/references/core-describe.md +193 -0
- package/skills-content/vitest/references/core-expect.md +219 -0
- package/skills-content/vitest/references/core-hooks.md +244 -0
- package/skills-content/vitest/references/core-test-api.md +233 -0
- package/skills-content/vitest/references/features-concurrency.md +250 -0
- package/skills-content/vitest/references/features-context.md +238 -0
- package/skills-content/vitest/references/features-coverage.md +207 -0
- package/skills-content/vitest/references/features-filtering.md +211 -0
- package/skills-content/vitest/references/features-mocking.md +265 -0
- package/skills-content/vitest/references/features-snapshots.md +207 -0
- package/skills-content/workleaf/SKILL.md +77 -0
- package/skills-content/worklog/SKILL.md +145 -0
- package/skills-content/worklog/references/summary-format.md +114 -0
- package/skills-content/worktree/SKILL.md +71 -0
- package/src/agents-instructions.js +256 -0
- package/src/cli.js +87 -23
- package/src/import.js +12 -7
- package/src/index.js +1 -0
- package/src/legacy-agents-content.js +36 -0
- package/src/legacy-cli.js +1822 -0
- package/src/library.js +9 -0
- package/src/specifier.js +11 -0
- package/src/user-install.js +48 -0
|
@@ -0,0 +1,1822 @@
|
|
|
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, spawnSync } from 'node:child_process';
|
|
7
|
+
import { createInterface } from 'node:readline/promises';
|
|
8
|
+
import { fileURLToPath } from 'node:url';
|
|
9
|
+
import {
|
|
10
|
+
AgentsInstructionsError,
|
|
11
|
+
applyInstructions,
|
|
12
|
+
applyLocalFragments,
|
|
13
|
+
applyProjectInstructions,
|
|
14
|
+
composeAgentsInstructions,
|
|
15
|
+
hasProjectInstructions
|
|
16
|
+
} from './agents-instructions.js';
|
|
17
|
+
|
|
18
|
+
const packageRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
19
|
+
const skillsContentRoot = path.join(packageRoot, 'skills-content');
|
|
20
|
+
const agentsContentRoot = path.join(packageRoot, 'agents-content');
|
|
21
|
+
const packageJson = JSON.parse(fs.readFileSync(path.join(packageRoot, 'package.json'), 'utf8'));
|
|
22
|
+
const lockFileName = '.skillshop-lock';
|
|
23
|
+
const legacyLockFileName = '.skillz-lock';
|
|
24
|
+
const registryFileName = 'repositories.json';
|
|
25
|
+
const configFileName = 'config.json';
|
|
26
|
+
const globalSkillsFileName = 'global-skills.json';
|
|
27
|
+
const excludeBlockStart = '# >>> skillshop managed dependencies >>>';
|
|
28
|
+
const excludeBlockEnd = '# <<< skillshop managed dependencies <<<';
|
|
29
|
+
const legacyExcludeBlockStart = '# >>> skillz managed dependencies >>>';
|
|
30
|
+
const legacyExcludeBlockEnd = '# <<< skillz managed dependencies <<<';
|
|
31
|
+
const projectTemplateDirectory = path.join('.agents', 'instructions');
|
|
32
|
+
const projectTemplateName = 'AGENTS.template.md';
|
|
33
|
+
const defaultProjectTemplate = '# Project instructions\n\n{{ blocks }}\n';
|
|
34
|
+
const internalUpgradeRefreshCommand = '__skillshop_upgrade_refresh';
|
|
35
|
+
const internalUpgradeRefreshToken = 'skillshop-managed-upgrade-v1';
|
|
36
|
+
const globalAgentsTargets = [
|
|
37
|
+
path.join('.codex', 'AGENTS.md'),
|
|
38
|
+
path.join('.agents', 'AGENTS.md'),
|
|
39
|
+
path.join('.copilot', 'copilot-instructions.md')
|
|
40
|
+
];
|
|
41
|
+
|
|
42
|
+
function lockPathFor(root) {
|
|
43
|
+
const current = path.join(root, lockFileName);
|
|
44
|
+
const legacy = path.join(root, legacyLockFileName);
|
|
45
|
+
return fs.existsSync(current) || !fs.existsSync(legacy) ? current : legacy;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function hasLock(root) {
|
|
49
|
+
return fs.existsSync(path.join(root, lockFileName)) || fs.existsSync(path.join(root, legacyLockFileName));
|
|
50
|
+
}
|
|
51
|
+
const agentOptions = [
|
|
52
|
+
{ id: 'copilot', label: 'Copilot', local: '.github/skills', global: '.copilot/skills' },
|
|
53
|
+
{ id: 'claude', label: 'Claude', local: '.claude/skills', global: '.claude/skills' },
|
|
54
|
+
{ id: 'codex', label: 'Codex', local: '.agents/skills', global: '.agents/skills' },
|
|
55
|
+
{ id: 'opencode', label: 'OpenCode', local: '.agents/skills', global: '.agents/skills' },
|
|
56
|
+
{ id: 'others', label: 'Others', local: '.agents/skills', global: '.agents/skills' }
|
|
57
|
+
];
|
|
58
|
+
|
|
59
|
+
class CliError extends Error {}
|
|
60
|
+
|
|
61
|
+
function unquote(value) {
|
|
62
|
+
const trimmed = value.trim();
|
|
63
|
+
const match = trimmed.match(/^(['"])(.*)\1$/);
|
|
64
|
+
return match ? match[2] : trimmed;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function validateSkillName(name) {
|
|
68
|
+
if (!/^[a-zA-Z0-9][a-zA-Z0-9._-]*$/.test(name)) {
|
|
69
|
+
throw new CliError(`invalid skill name: ${name}`);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function skillMetadata(name) {
|
|
74
|
+
validateSkillName(name);
|
|
75
|
+
const sourceDir = path.join(skillsContentRoot, name);
|
|
76
|
+
const manifestPath = path.join(sourceDir, 'SKILL.md');
|
|
77
|
+
|
|
78
|
+
if (!fs.existsSync(manifestPath)) {
|
|
79
|
+
throw new CliError(`unknown skill: ${name}`);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const lines = fs.readFileSync(manifestPath, 'utf8').split(/\r?\n/);
|
|
83
|
+
if (lines[0] !== '---') {
|
|
84
|
+
throw new CliError(`${name}/SKILL.md has no front matter`);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const frontMatterEnd = lines.indexOf('---', 1);
|
|
88
|
+
const frontMatter = lines.slice(1, frontMatterEnd === -1 ? lines.length : frontMatterEnd);
|
|
89
|
+
const metadataStart = frontMatter.findIndex((line) => /^metadata:\s*$/.test(line));
|
|
90
|
+
let author;
|
|
91
|
+
let version;
|
|
92
|
+
|
|
93
|
+
if (metadataStart !== -1) {
|
|
94
|
+
for (const line of frontMatter.slice(metadataStart + 1)) {
|
|
95
|
+
if (line !== '' && !/^\s+/.test(line)) break;
|
|
96
|
+
const authorMatch = line.match(/^\s+author:\s*(.+)$/);
|
|
97
|
+
const versionMatch = line.match(/^\s+version:\s*(.+)$/);
|
|
98
|
+
if (authorMatch) author = unquote(authorMatch[1]);
|
|
99
|
+
if (versionMatch) version = unquote(versionMatch[1]);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (!author || !version) {
|
|
104
|
+
throw new CliError(`${name}/SKILL.md must declare metadata.author and metadata.version`);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return { name, author, version, sourceDir };
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function availableSkills() {
|
|
111
|
+
return fs.readdirSync(skillsContentRoot, { withFileTypes: true })
|
|
112
|
+
.filter((entry) => entry.isDirectory())
|
|
113
|
+
.map((entry) => entry.name)
|
|
114
|
+
.filter((name) => fs.existsSync(path.join(skillsContentRoot, name, 'SKILL.md')))
|
|
115
|
+
.sort();
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function printHelp() {
|
|
119
|
+
console.log(`Usage:
|
|
120
|
+
skillshop init
|
|
121
|
+
skillshop config agents
|
|
122
|
+
skillshop agents init [--project <directory>]
|
|
123
|
+
skillshop agents [--project <directory>] [--check]
|
|
124
|
+
skillshop agents --global
|
|
125
|
+
skillshop <name> [--version <version>] [--global]
|
|
126
|
+
skillshop install <name> [--version <version>] [--global]
|
|
127
|
+
skillshop uninstall <name> [--global]
|
|
128
|
+
skillshop upgrade
|
|
129
|
+
skillshop update [name]
|
|
130
|
+
skillshop update --push
|
|
131
|
+
skillshop migrate [root ...]
|
|
132
|
+
skillshop list
|
|
133
|
+
|
|
134
|
+
Install, uninstall, and update skills from the local registry.
|
|
135
|
+
|
|
136
|
+
Agent instructions:
|
|
137
|
+
skillshop agents init creates a neutral .agents/instructions/AGENTS.template.md and renders it into a separate managed region in the project-root AGENTS.md. Existing text and prior managed guidance are preserved. Edit the template and sibling *.block.md sources, not the generated region.
|
|
138
|
+
Plain skillshop agents keeps the bounded bundled behavior until project sources exist; afterward it renders project sources. Missing sources in an initialized project are errors.
|
|
139
|
+
skillshop agents --check reports whether generated output is current and never writes. skillshop agents --global retains user-level instruction updates.
|
|
140
|
+
The default boundary is the nearest Git/worktree root, or the current directory outside Git. --project selects another directory relative to the current working directory. Nested projects need an explicit selection.
|
|
141
|
+
Source, block, template, and output paths containing symlink components, including dangling links, are rejected.
|
|
142
|
+
Templates accept {{ include "name.block.md" }}, {{ include 'name.block.md' }}, legacy {{ fragments["name.block.md"] }}, and {{ blocks }} for unreferenced blocks. Inserted block text is literal.
|
|
143
|
+
|
|
144
|
+
Upgrade:
|
|
145
|
+
skillshop upgrade updates the npm installation that supplied the CLI, then refreshes recorded Skillshop-managed copies with the newly installed CLI. skillshop init is optional and upgrade does not generate AGENTS.md files.
|
|
146
|
+
|
|
147
|
+
Options:
|
|
148
|
+
-g, --global Use configured system skill directories without a project lockfile
|
|
149
|
+
--project <directory> Select the agents boundary relative to the current directory
|
|
150
|
+
--check Check project-generated instructions without writing
|
|
151
|
+
--push Update every registered repository
|
|
152
|
+
-v, --version <version> Require an exact skill version
|
|
153
|
+
-h, --help Show this help
|
|
154
|
+
|
|
155
|
+
Conveniences:
|
|
156
|
+
skillshop i <name> Alias for install
|
|
157
|
+
skillshop add <name> Alias for install
|
|
158
|
+
skillshop rm <name> Alias for uninstall
|
|
159
|
+
skillshop <name>@<version> Install an exact version
|
|
160
|
+
|
|
161
|
+
Available skills:
|
|
162
|
+
${availableSkills().join('\n ')}`);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function skillshopHome() {
|
|
166
|
+
if (process.env.SKILLSHOP_HOME) return process.env.SKILLSHOP_HOME;
|
|
167
|
+
if (process.env.SKILLZ_HOME) return process.env.SKILLZ_HOME;
|
|
168
|
+
const current = path.join(os.homedir(), '.skillshop');
|
|
169
|
+
const legacy = path.join(os.homedir(), '.skillz');
|
|
170
|
+
return fs.existsSync(current) || !fs.existsSync(legacy) ? current : legacy;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function configPath() {
|
|
174
|
+
return path.join(skillshopHome(), configFileName);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function readConfig() {
|
|
178
|
+
const filePath = configPath();
|
|
179
|
+
if (!fs.existsSync(filePath)) return null;
|
|
180
|
+
|
|
181
|
+
let config;
|
|
182
|
+
try {
|
|
183
|
+
config = JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
184
|
+
} catch (error) {
|
|
185
|
+
throw new CliError(`cannot parse ${filePath}: ${error.message}`);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const knownAgents = new Set(agentOptions.map(({ id }) => id));
|
|
189
|
+
if (
|
|
190
|
+
config?.configVersion !== 1 ||
|
|
191
|
+
!Array.isArray(config.agents) ||
|
|
192
|
+
config.agents.length === 0 ||
|
|
193
|
+
config.agents.some((agent) => typeof agent !== 'string' || !knownAgents.has(agent))
|
|
194
|
+
) {
|
|
195
|
+
throw new CliError(`${filePath} is not a supported skillshop config`);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
return config;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function writeConfig(agents) {
|
|
202
|
+
const filePath = configPath();
|
|
203
|
+
const config = { configVersion: 1, agents: [...new Set(agents)] };
|
|
204
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
205
|
+
const temporaryPath = `${filePath}.tmp-${process.pid}`;
|
|
206
|
+
fs.writeFileSync(temporaryPath, `${JSON.stringify(config, null, 2)}\n`);
|
|
207
|
+
fs.renameSync(temporaryPath, filePath);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function configuredAgents(config) {
|
|
211
|
+
return config.agents.map((id) => agentOptions.find((option) => option.id === id));
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function configuredSkillDirectories(config, scope, root = null) {
|
|
215
|
+
const base = scope === 'local' ? root : os.homedir();
|
|
216
|
+
return uniqueDestinations(configuredAgents(config).map((agent) => path.join(base, agent[scope])));
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
async function selectAgents() {
|
|
220
|
+
const terminal = createInterface({ input: process.stdin, output: process.stdout });
|
|
221
|
+
console.log('Which agents do you use? Select one or more:');
|
|
222
|
+
for (const [index, agent] of agentOptions.entries()) {
|
|
223
|
+
console.log(` ${index + 1}. ${agent.label}`);
|
|
224
|
+
}
|
|
225
|
+
console.log('You can change this later with `skillshop config agents`.');
|
|
226
|
+
|
|
227
|
+
try {
|
|
228
|
+
while (true) {
|
|
229
|
+
if (process.stdin.readableEnded) throw new CliError('agent selection requires an answer');
|
|
230
|
+
let onEnd;
|
|
231
|
+
const inputEnded = new Promise((_, reject) => {
|
|
232
|
+
onEnd = () => reject(new CliError('agent selection requires an answer'));
|
|
233
|
+
process.stdin.once('end', onEnd);
|
|
234
|
+
});
|
|
235
|
+
let answer;
|
|
236
|
+
try {
|
|
237
|
+
answer = await Promise.race([
|
|
238
|
+
terminal.question('Enter numbers separated by commas: '),
|
|
239
|
+
inputEnded
|
|
240
|
+
]);
|
|
241
|
+
} finally {
|
|
242
|
+
process.stdin.off('end', onEnd);
|
|
243
|
+
}
|
|
244
|
+
const indexes = answer.split(',').map((value) => Number(value.trim()) - 1);
|
|
245
|
+
if (
|
|
246
|
+
indexes.length > 0 &&
|
|
247
|
+
indexes.every((index) => Number.isInteger(index) && index >= 0 && index < agentOptions.length)
|
|
248
|
+
) {
|
|
249
|
+
return [...new Set(indexes.map((index) => agentOptions[index].id))];
|
|
250
|
+
}
|
|
251
|
+
console.log(`Choose one or more numbers from 1 to ${agentOptions.length}.`);
|
|
252
|
+
}
|
|
253
|
+
} finally {
|
|
254
|
+
terminal.close();
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
async function configureAgents() {
|
|
259
|
+
const agents = await selectAgents();
|
|
260
|
+
writeConfig(agents);
|
|
261
|
+
const labels = configuredAgents({ agents }).map(({ label }) => label);
|
|
262
|
+
console.log(`Configured agents: ${labels.join(', ')}`);
|
|
263
|
+
console.log(`Wrote ${configPath()}`);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
async function initialize() {
|
|
267
|
+
const config = readConfig();
|
|
268
|
+
if (!config) {
|
|
269
|
+
await configureAgents();
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
console.log(`Agents already configured: ${configuredAgents(config).map(({ label }) => label).join(', ')}`);
|
|
274
|
+
console.log('Change this setting with `skillshop config agents`.');
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function projectRoot(startDir, { required = true } = {}) {
|
|
278
|
+
let current = path.resolve(startDir);
|
|
279
|
+
|
|
280
|
+
while (true) {
|
|
281
|
+
if (fs.existsSync(path.join(current, '.git'))) return current;
|
|
282
|
+
|
|
283
|
+
const parent = path.dirname(current);
|
|
284
|
+
if (parent === current) {
|
|
285
|
+
if (!required) return null;
|
|
286
|
+
throw new CliError(`${path.resolve(startDir)} is not inside a Git repository`);
|
|
287
|
+
}
|
|
288
|
+
current = parent;
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
function directoriesToRoot(startDir, rootDir) {
|
|
293
|
+
const directories = [];
|
|
294
|
+
let current = path.resolve(startDir);
|
|
295
|
+
|
|
296
|
+
while (true) {
|
|
297
|
+
directories.push(current);
|
|
298
|
+
if (current === rootDir) return directories;
|
|
299
|
+
|
|
300
|
+
const parent = path.dirname(current);
|
|
301
|
+
if (parent === current) return [rootDir];
|
|
302
|
+
current = parent;
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
function findLockRoot(cwd, options) {
|
|
307
|
+
const root = projectRoot(cwd, options);
|
|
308
|
+
if (!root) return null;
|
|
309
|
+
return directoriesToRoot(cwd, root)
|
|
310
|
+
.find((directory) => hasLock(directory)) ?? null;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
function resolveAgentDir(cwd) {
|
|
314
|
+
const root = projectRoot(cwd);
|
|
315
|
+
const directories = directoriesToRoot(cwd, root);
|
|
316
|
+
|
|
317
|
+
for (const agentDirName of ['.agents', '.github', '.codex']) {
|
|
318
|
+
for (const directory of directories) {
|
|
319
|
+
const candidate = path.join(directory, agentDirName);
|
|
320
|
+
if (fs.existsSync(candidate) && fs.statSync(candidate).isDirectory()) {
|
|
321
|
+
return candidate;
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
return path.join(root, '.agents');
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
function emptyLock() {
|
|
330
|
+
return { lockfileVersion: 2, registry: 'local', skills: {} };
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
function readLock(root) {
|
|
334
|
+
const lockPath = lockPathFor(root);
|
|
335
|
+
if (!fs.existsSync(lockPath)) return emptyLock();
|
|
336
|
+
|
|
337
|
+
let lock;
|
|
338
|
+
try {
|
|
339
|
+
lock = JSON.parse(fs.readFileSync(lockPath, 'utf8'));
|
|
340
|
+
} catch (error) {
|
|
341
|
+
throw new CliError(`cannot parse ${lockPath}: ${error.message}`);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
if (![1, 2].includes(lock?.lockfileVersion) || lock.registry !== 'local' || !lock.skills || Array.isArray(lock.skills)) {
|
|
345
|
+
throw new CliError(`${lockPath} is not a supported skillshop lockfile`);
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
return lock;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
function relativePath(root, target) {
|
|
352
|
+
return path.relative(root, target).split(path.sep).join('/');
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
function lockedTargets(root, name, entry) {
|
|
356
|
+
const paths = Array.isArray(entry?.paths) ? entry.paths : [entry?.path];
|
|
357
|
+
if (paths.length === 0 || paths.some((targetPath) => typeof targetPath !== 'string')) {
|
|
358
|
+
throw new CliError(`lock entry for ${name} has no install paths`);
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
return uniqueDestinations(paths.map((targetPath) => {
|
|
362
|
+
const target = path.resolve(root, targetPath);
|
|
363
|
+
const relative = path.relative(root, target);
|
|
364
|
+
if (relative === '' || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative)) {
|
|
365
|
+
throw new CliError(`lock entry for ${name} points outside the project`);
|
|
366
|
+
}
|
|
367
|
+
if (path.basename(target) !== name || path.basename(path.dirname(target)) !== 'skills') {
|
|
368
|
+
throw new CliError(`lock entry for ${name} is not a skills/${name} directory`);
|
|
369
|
+
}
|
|
370
|
+
assertLocalDestination(root, target, `lock entry for ${name}`);
|
|
371
|
+
return target;
|
|
372
|
+
}));
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
function inferredSkillDirectories(root, lock, cwd) {
|
|
376
|
+
const preferredName = lock.skills.skillshop ? 'skillshop' : lock.skills.skillz ? 'skillz' : Object.keys(lock.skills).sort()[0];
|
|
377
|
+
if (!preferredName) return [path.join(resolveAgentDir(cwd), 'skills')];
|
|
378
|
+
return lockedTargets(root, preferredName, lock.skills[preferredName]).map((target) => path.dirname(target));
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
function localSkillDirectories(root, lock, cwd) {
|
|
382
|
+
const config = readConfig();
|
|
383
|
+
const directories = config
|
|
384
|
+
? configuredSkillDirectories(config, 'local', root)
|
|
385
|
+
: inferredSkillDirectories(root, lock, cwd);
|
|
386
|
+
for (const directory of directories) assertLocalDestination(root, directory, 'agent skill directory');
|
|
387
|
+
return directories;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
function writeLock(root, lock) {
|
|
391
|
+
const skills = Object.fromEntries(
|
|
392
|
+
Object.entries(lock.skills)
|
|
393
|
+
.sort(([left], [right]) => left.localeCompare(right))
|
|
394
|
+
.map(([name, entry]) => [name, {
|
|
395
|
+
version: entry.version,
|
|
396
|
+
author: entry.author,
|
|
397
|
+
paths: lockedTargets(root, name, entry).map((target) => relativePath(root, target)).sort()
|
|
398
|
+
}])
|
|
399
|
+
);
|
|
400
|
+
const stableLock = { lockfileVersion: 2, registry: 'local', skills };
|
|
401
|
+
const lockPath = path.join(root, lockFileName);
|
|
402
|
+
const contents = `${JSON.stringify(stableLock, null, 2)}\n`;
|
|
403
|
+
if (fs.existsSync(lockPath) && fs.readFileSync(lockPath, 'utf8') === contents) return;
|
|
404
|
+
const temporaryPath = `${lockPath}.tmp-${process.pid}`;
|
|
405
|
+
fs.writeFileSync(temporaryPath, contents);
|
|
406
|
+
fs.renameSync(temporaryPath, lockPath);
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
function registryPath() {
|
|
410
|
+
return path.join(skillshopHome(), registryFileName);
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
function readRegistry() {
|
|
414
|
+
const filePath = registryPath();
|
|
415
|
+
if (!fs.existsSync(filePath)) return { registryVersion: 1, repositories: [] };
|
|
416
|
+
|
|
417
|
+
let registry;
|
|
418
|
+
try {
|
|
419
|
+
registry = JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
420
|
+
} catch (error) {
|
|
421
|
+
throw new CliError(`cannot parse ${filePath}: ${error.message}`);
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
if (
|
|
425
|
+
registry?.registryVersion !== 1 ||
|
|
426
|
+
!Array.isArray(registry.repositories) ||
|
|
427
|
+
registry.repositories.some((repository) => typeof repository !== 'string' || !path.isAbsolute(repository))
|
|
428
|
+
) {
|
|
429
|
+
throw new CliError(`${filePath} is not a supported skillshop repository registry`);
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
return registry;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
function canonicalPath(target) {
|
|
436
|
+
try {
|
|
437
|
+
return fs.realpathSync.native(target);
|
|
438
|
+
} catch {
|
|
439
|
+
return path.resolve(target);
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
function canonicalDestination(target) {
|
|
444
|
+
let existing = path.resolve(target);
|
|
445
|
+
const missing = [];
|
|
446
|
+
while (!fs.existsSync(existing)) {
|
|
447
|
+
const parent = path.dirname(existing);
|
|
448
|
+
if (parent === existing) return path.resolve(target);
|
|
449
|
+
missing.unshift(path.basename(existing));
|
|
450
|
+
existing = parent;
|
|
451
|
+
}
|
|
452
|
+
return path.join(canonicalPath(existing), ...missing);
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
function assertLocalDestination(root, target, label) {
|
|
456
|
+
const relative = path.relative(canonicalDestination(root), canonicalDestination(target));
|
|
457
|
+
if (relative === '' || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative)) {
|
|
458
|
+
throw new CliError(`${label} resolves outside the project`);
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
function uniqueDestinations(targets) {
|
|
463
|
+
const seen = new Set();
|
|
464
|
+
return targets.filter((target) => {
|
|
465
|
+
const destination = canonicalDestination(target);
|
|
466
|
+
if (seen.has(destination)) return false;
|
|
467
|
+
seen.add(destination);
|
|
468
|
+
return true;
|
|
469
|
+
});
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
function staleTargets(previousTargets, targetDirs) {
|
|
473
|
+
const destinations = new Set(targetDirs.map(canonicalDestination));
|
|
474
|
+
return previousTargets.filter((target) => !destinations.has(canonicalDestination(target)));
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
function assertTargetsAvailable(name, targetDirs, previousTargets) {
|
|
478
|
+
const managedDestinations = new Set(previousTargets.map(canonicalDestination));
|
|
479
|
+
for (const targetDir of targetDirs) {
|
|
480
|
+
if (fs.existsSync(targetDir) && !managedDestinations.has(canonicalDestination(targetDir))) {
|
|
481
|
+
throw new CliError(`${targetDir} exists and is not managed as ${name} in ${lockFileName}`);
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
function writeRegistry(repositories) {
|
|
487
|
+
const filePath = registryPath();
|
|
488
|
+
const stableRegistry = {
|
|
489
|
+
registryVersion: 1,
|
|
490
|
+
repositories: [...new Set(repositories.map(canonicalPath))].sort()
|
|
491
|
+
};
|
|
492
|
+
const contents = `${JSON.stringify(stableRegistry, null, 2)}\n`;
|
|
493
|
+
if (fs.existsSync(filePath) && fs.readFileSync(filePath, 'utf8') === contents) return;
|
|
494
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
495
|
+
const temporaryPath = `${filePath}.tmp-${process.pid}`;
|
|
496
|
+
fs.writeFileSync(temporaryPath, contents);
|
|
497
|
+
fs.renameSync(temporaryPath, filePath);
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
function registerRepository(root) {
|
|
501
|
+
const registry = readRegistry();
|
|
502
|
+
writeRegistry([...registry.repositories, root]);
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
function gitOutput(root, args) {
|
|
506
|
+
try {
|
|
507
|
+
return execFileSync('git', args, { cwd: root, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] }).trim();
|
|
508
|
+
} catch (error) {
|
|
509
|
+
const detail = error.stderr?.trim();
|
|
510
|
+
throw new CliError(detail || `cannot inspect Git repository at ${root}`);
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
function managedGitPaths(root, lock) {
|
|
515
|
+
const gitRoot = canonicalPath(gitOutput(root, ['rev-parse', '--show-toplevel']));
|
|
516
|
+
const scope = relativePath(gitRoot, canonicalPath(root));
|
|
517
|
+
const scopedPath = (target) => relativePath(gitRoot, path.join(root, target));
|
|
518
|
+
const paths = [scopedPath(lockFileName)];
|
|
519
|
+
if (fs.existsSync(path.join(root, legacyLockFileName))) paths.push(scopedPath(legacyLockFileName));
|
|
520
|
+
|
|
521
|
+
for (const [name, entry] of Object.entries(lock.skills)) {
|
|
522
|
+
for (const target of lockedTargets(root, name, entry)) {
|
|
523
|
+
paths.push(scopedPath(relativePath(root, target)));
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
if (scope.startsWith('../') || path.isAbsolute(scope)) {
|
|
528
|
+
throw new CliError(`${root} is outside its Git repository`);
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
return { gitRoot, paths: [...new Set(paths)].sort() };
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
function gitExcludePattern(relative) {
|
|
535
|
+
const escaped = relative.replaceAll('\\', '\\\\').replace(/([ !#*?\[\]])/g, '\\$1');
|
|
536
|
+
return `/${escaped}${[lockFileName, legacyLockFileName].includes(path.posix.basename(relative)) ? '' : '/'}`;
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
function syncGitExclude(root, lock) {
|
|
540
|
+
const { paths } = managedGitPaths(root, lock);
|
|
541
|
+
const excludePathOutput = gitOutput(root, ['rev-parse', '--git-path', 'info/exclude']);
|
|
542
|
+
const excludePath = path.isAbsolute(excludePathOutput)
|
|
543
|
+
? excludePathOutput
|
|
544
|
+
: path.resolve(root, excludePathOutput);
|
|
545
|
+
const existing = fs.existsSync(excludePath) ? fs.readFileSync(excludePath, 'utf8') : '';
|
|
546
|
+
const lines = existing.replace(/\r\n/g, '\n').split('\n');
|
|
547
|
+
const retained = [];
|
|
548
|
+
let insideBlock = false;
|
|
549
|
+
|
|
550
|
+
for (const line of lines) {
|
|
551
|
+
if (line === excludeBlockStart || line === legacyExcludeBlockStart) {
|
|
552
|
+
insideBlock = true;
|
|
553
|
+
continue;
|
|
554
|
+
}
|
|
555
|
+
if (line === excludeBlockEnd || line === legacyExcludeBlockEnd) {
|
|
556
|
+
insideBlock = false;
|
|
557
|
+
continue;
|
|
558
|
+
}
|
|
559
|
+
if (!insideBlock) retained.push(line);
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
while (retained.at(-1) === '') retained.pop();
|
|
563
|
+
const block = [excludeBlockStart, ...paths.map(gitExcludePattern), excludeBlockEnd];
|
|
564
|
+
const contents = `${[...retained, ...(retained.length ? [''] : []), ...block].join('\n')}\n`;
|
|
565
|
+
fs.mkdirSync(path.dirname(excludePath), { recursive: true });
|
|
566
|
+
if (contents !== existing) fs.writeFileSync(excludePath, contents);
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
function untrackManagedPaths(root, lock) {
|
|
570
|
+
const { gitRoot, paths } = managedGitPaths(root, lock);
|
|
571
|
+
try {
|
|
572
|
+
execFileSync(
|
|
573
|
+
'git',
|
|
574
|
+
['rm', '-r', '--cached', '-f', '--ignore-unmatch', '--', ...paths],
|
|
575
|
+
{ cwd: gitRoot, stdio: 'ignore' }
|
|
576
|
+
);
|
|
577
|
+
} catch {
|
|
578
|
+
throw new CliError(`cannot remove managed skill dependencies from the Git index at ${gitRoot}`);
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
function maintainRepository(root, lock, { register = true, untrack = false } = {}) {
|
|
583
|
+
if (untrack) untrackManagedPaths(root, lock);
|
|
584
|
+
syncGitExclude(root, lock);
|
|
585
|
+
if (register) registerRepository(root);
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
function manifestParts(contents, label) {
|
|
589
|
+
const newline = contents.includes('\r\n') ? '\r\n' : '\n';
|
|
590
|
+
if (!contents.startsWith(`---${newline}`)) return { lines: [], body: contents, newline };
|
|
591
|
+
const end = contents.indexOf(`${newline}---`, 3);
|
|
592
|
+
if (end === -1 || !['', '\n', '\r'].includes(contents[end + newline.length + 3] ?? '')) {
|
|
593
|
+
throw new CliError(`${label} has unterminated front matter`);
|
|
594
|
+
}
|
|
595
|
+
return {
|
|
596
|
+
lines: contents.slice(3 + newline.length, end).split(newline),
|
|
597
|
+
body: contents.slice(end + newline.length + 3),
|
|
598
|
+
newline
|
|
599
|
+
};
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
function repairInstalledManifest(name, entry, manifestPath) {
|
|
603
|
+
const contents = fs.readFileSync(manifestPath, 'utf8');
|
|
604
|
+
const { lines, body, newline } = manifestParts(contents, manifestPath);
|
|
605
|
+
const sourcePath = path.join(skillsContentRoot, name, 'SKILL.md');
|
|
606
|
+
const sourceLines = fs.existsSync(sourcePath)
|
|
607
|
+
? manifestParts(fs.readFileSync(sourcePath, 'utf8'), sourcePath).lines
|
|
608
|
+
: [];
|
|
609
|
+
const sourceField = (key) => sourceLines.find((line) => line.startsWith(`${key}:`))?.slice(key.length + 1).trim();
|
|
610
|
+
const setField = (key, value) => {
|
|
611
|
+
const index = lines.findIndex((line) => line.startsWith(`${key}:`));
|
|
612
|
+
const replacement = `${key}: ${value}`;
|
|
613
|
+
if (index === -1) lines.push(replacement);
|
|
614
|
+
else if (unquote(lines[index].slice(key.length + 1)) !== unquote(value)) lines[index] = replacement;
|
|
615
|
+
};
|
|
616
|
+
setField('name', JSON.stringify(name));
|
|
617
|
+
const description = lines.find((line) => line.startsWith('description:'))?.slice(12).trim();
|
|
618
|
+
if (!description || ['""', "''", 'null', '~'].includes(description)) {
|
|
619
|
+
const fallback = sourceField('description');
|
|
620
|
+
if (!fallback) throw new CliError(`cannot restore description for ${manifestPath}`);
|
|
621
|
+
setField('description', fallback);
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
let metadataIndex = lines.findIndex((line) => /^metadata:/.test(line));
|
|
625
|
+
if (metadataIndex === -1) {
|
|
626
|
+
metadataIndex = lines.length;
|
|
627
|
+
lines.push('metadata:');
|
|
628
|
+
} else if (!/^metadata:\s*(?:#.*)?$/.test(lines[metadataIndex])) {
|
|
629
|
+
throw new CliError(`${manifestPath} must use a metadata block mapping`);
|
|
630
|
+
}
|
|
631
|
+
let metadataEnd = metadataIndex + 1;
|
|
632
|
+
while (metadataEnd < lines.length && /^(\s|$|#)/.test(lines[metadataEnd])) metadataEnd += 1;
|
|
633
|
+
const indent = lines.slice(metadataIndex + 1, metadataEnd)
|
|
634
|
+
.find((line) => /^\s+\S/.test(line))?.match(/^\s+/)[0] ?? ' ';
|
|
635
|
+
const metadata = {};
|
|
636
|
+
for (const key of ['author', 'version']) {
|
|
637
|
+
const fieldPrefix = `${indent}${key}:`;
|
|
638
|
+
const index = lines.findIndex((line, i) => i > metadataIndex && i < metadataEnd && line.startsWith(fieldPrefix));
|
|
639
|
+
const installed = index === -1 ? null : unquote(lines[index].slice(fieldPrefix.length));
|
|
640
|
+
const source = sourceLines.find((line) => new RegExp(`^\\s+${key}:`).test(line));
|
|
641
|
+
const value = typeof entry[key] === 'string' && entry[key].trim()
|
|
642
|
+
? entry[key]
|
|
643
|
+
: installed && !['null', '~'].includes(installed) ? installed
|
|
644
|
+
: source ? unquote(source.slice(source.indexOf(':') + 1)) : null;
|
|
645
|
+
if (!value) throw new CliError(`cannot restore metadata.${key} for ${manifestPath}`);
|
|
646
|
+
metadata[key] = value;
|
|
647
|
+
const replacement = `${fieldPrefix} ${JSON.stringify(value)}`;
|
|
648
|
+
if (index === -1) {
|
|
649
|
+
lines.splice(metadataEnd, 0, replacement);
|
|
650
|
+
metadataEnd += 1;
|
|
651
|
+
} else if (installed !== value) lines[index] = replacement;
|
|
652
|
+
}
|
|
653
|
+
const separator = body && !body.startsWith(newline) ? newline + newline : '';
|
|
654
|
+
return { contents: `---${newline}${lines.join(newline)}${newline}---${separator}${body}`, metadata };
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
function readGlobalSkills() {
|
|
658
|
+
const filePath = path.join(skillshopHome(), globalSkillsFileName);
|
|
659
|
+
if (!fs.existsSync(filePath)) return emptyLock();
|
|
660
|
+
let lock;
|
|
661
|
+
try {
|
|
662
|
+
lock = JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
663
|
+
} catch (error) {
|
|
664
|
+
throw new CliError(`cannot parse ${filePath}: ${error.message}`);
|
|
665
|
+
}
|
|
666
|
+
if (lock?.lockfileVersion !== 2 || lock.registry !== 'local' || !lock.skills || Array.isArray(lock.skills)) {
|
|
667
|
+
throw new CliError(`${filePath} is not a supported skillshop global manifest`);
|
|
668
|
+
}
|
|
669
|
+
return lock;
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
function globalTargets(name, entry) {
|
|
673
|
+
validateSkillName(name);
|
|
674
|
+
if (!Array.isArray(entry?.paths) || !entry.paths.length) {
|
|
675
|
+
throw new CliError(`global entry for ${name} has no install paths`);
|
|
676
|
+
}
|
|
677
|
+
return uniqueDestinations(entry.paths.map((target) => {
|
|
678
|
+
if (typeof target !== 'string' || !path.isAbsolute(target) ||
|
|
679
|
+
path.basename(target) !== name || path.basename(path.dirname(target)) !== 'skills') {
|
|
680
|
+
throw new CliError(`global entry for ${name} is not an absolute skills/${name} directory`);
|
|
681
|
+
}
|
|
682
|
+
assertLocalDestination(os.homedir(), target, `global entry for ${name}`);
|
|
683
|
+
return target;
|
|
684
|
+
}));
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
function writeGlobalSkills(lock) {
|
|
688
|
+
const skills = Object.fromEntries(Object.entries(lock.skills).sort(([left], [right]) => left.localeCompare(right))
|
|
689
|
+
.map(([name, entry]) => {
|
|
690
|
+
const paths = globalTargets(name, entry).sort();
|
|
691
|
+
return [name, { paths, copies: Object.fromEntries(paths.map((target) => [target, entry.copies?.[target] ?? {}])) }];
|
|
692
|
+
}));
|
|
693
|
+
const filePath = path.join(skillshopHome(), globalSkillsFileName);
|
|
694
|
+
const contents = `${JSON.stringify({ ...emptyLock(), skills }, null, 2)}\n`;
|
|
695
|
+
if (fs.existsSync(filePath) && fs.readFileSync(filePath, 'utf8') === contents) return;
|
|
696
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
697
|
+
const temporaryPath = `${filePath}.tmp-${process.pid}`;
|
|
698
|
+
fs.writeFileSync(temporaryPath, contents);
|
|
699
|
+
fs.renameSync(temporaryPath, filePath);
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
function repairTrackedSkills(root, lock) {
|
|
703
|
+
const repairs = [];
|
|
704
|
+
for (const [name, entry] of Object.entries(lock.skills)) {
|
|
705
|
+
validateSkillName(name);
|
|
706
|
+
const targets = (root ? lockedTargets(root, name, entry) : globalTargets(name, entry))
|
|
707
|
+
.filter((target) => fs.existsSync(path.join(target, 'SKILL.md')));
|
|
708
|
+
if (!targets.length) {
|
|
709
|
+
delete lock.skills[name];
|
|
710
|
+
continue;
|
|
711
|
+
}
|
|
712
|
+
for (const target of targets) {
|
|
713
|
+
const manifestPath = path.join(target, 'SKILL.md');
|
|
714
|
+
assertLocalDestination(root ?? os.homedir(), manifestPath, `manifest for ${name}`);
|
|
715
|
+
const installation = root ? entry : entry.copies?.[target] ?? {};
|
|
716
|
+
const repair = repairInstalledManifest(name, installation, manifestPath);
|
|
717
|
+
if (root) Object.assign(entry, repair.metadata);
|
|
718
|
+
else {
|
|
719
|
+
entry.copies ??= {};
|
|
720
|
+
entry.copies[target] = repair.metadata;
|
|
721
|
+
}
|
|
722
|
+
repairs.push({ manifestPath, contents: repair.contents });
|
|
723
|
+
}
|
|
724
|
+
entry.paths = targets.map((target) => root ? relativePath(root, target) : target);
|
|
725
|
+
}
|
|
726
|
+
for (const { manifestPath, contents } of repairs) {
|
|
727
|
+
if (fs.readFileSync(manifestPath, 'utf8') !== contents) fs.writeFileSync(manifestPath, contents);
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
function reconcileRepository(root, { register = true } = {}) {
|
|
732
|
+
const lock = readLock(root);
|
|
733
|
+
repairTrackedSkills(root, lock);
|
|
734
|
+
writeLock(root, lock);
|
|
735
|
+
maintainRepository(root, lock, { register });
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
function reconcileCurrentRepository() {
|
|
739
|
+
if (fs.existsSync(path.join(skillshopHome(), globalSkillsFileName))) {
|
|
740
|
+
const globalSkills = readGlobalSkills();
|
|
741
|
+
repairTrackedSkills(null, globalSkills);
|
|
742
|
+
writeGlobalSkills(globalSkills);
|
|
743
|
+
}
|
|
744
|
+
const root = findLockRoot(process.cwd(), { required: false });
|
|
745
|
+
if (root) reconcileRepository(root, { register: false });
|
|
746
|
+
const registryExists = fs.existsSync(registryPath());
|
|
747
|
+
if (!root && !registryExists) return;
|
|
748
|
+
const registry = readRegistry();
|
|
749
|
+
const repositories = registry.repositories.filter((directory) => hasLock(directory));
|
|
750
|
+
if (root) repositories.push(root);
|
|
751
|
+
writeRegistry(repositories);
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
function replaceSkillDirectory(sourceDir, targetDir) {
|
|
755
|
+
const temporaryDir = `${targetDir}.skillshop-tmp-${process.pid}`;
|
|
756
|
+
fs.mkdirSync(path.dirname(targetDir), { recursive: true });
|
|
757
|
+
fs.rmSync(temporaryDir, { recursive: true, force: true });
|
|
758
|
+
fs.cpSync(sourceDir, temporaryDir, { recursive: true, force: true });
|
|
759
|
+
fs.rmSync(targetDir, { recursive: true, force: true });
|
|
760
|
+
fs.renameSync(temporaryDir, targetDir);
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
function installAt(name, requestedVersion, targetDir) {
|
|
764
|
+
const metadata = skillMetadata(name);
|
|
765
|
+
if (requestedVersion && requestedVersion !== metadata.version) {
|
|
766
|
+
throw new CliError(
|
|
767
|
+
`${name}@${requestedVersion} is unavailable; local registry provides ${name}@${metadata.version}`
|
|
768
|
+
);
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
const updating = fs.existsSync(targetDir);
|
|
772
|
+
replaceSkillDirectory(metadata.sourceDir, targetDir);
|
|
773
|
+
return { ...metadata, targetDir, updating };
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
function lockEntry(root, results) {
|
|
777
|
+
const [result] = results;
|
|
778
|
+
return {
|
|
779
|
+
version: result.version,
|
|
780
|
+
author: result.author,
|
|
781
|
+
paths: results.map(({ targetDir }) => relativePath(root, targetDir)).sort()
|
|
782
|
+
};
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
function installGlobal(name, requestedVersion) {
|
|
786
|
+
const config = readConfig();
|
|
787
|
+
const skillDirectories = config
|
|
788
|
+
? configuredSkillDirectories(config, 'global')
|
|
789
|
+
: [path.join(os.homedir(), '.agents', 'skills')];
|
|
790
|
+
|
|
791
|
+
const globalSkills = readGlobalSkills();
|
|
792
|
+
for (const skillDirectory of skillDirectories) {
|
|
793
|
+
const targetDir = path.join(skillDirectory, name);
|
|
794
|
+
assertLocalDestination(os.homedir(), targetDir, `global installation for ${name}`);
|
|
795
|
+
const result = installAt(name, requestedVersion, targetDir);
|
|
796
|
+
const entry = globalSkills.skills[name] ?? { paths: [], copies: {} };
|
|
797
|
+
entry.paths = [...new Set([...entry.paths, targetDir])];
|
|
798
|
+
entry.copies[targetDir] = { author: result.author, version: result.version };
|
|
799
|
+
globalSkills.skills[name] = entry;
|
|
800
|
+
writeGlobalSkills(globalSkills);
|
|
801
|
+
console.log(`${result.updating ? 'Updated' : 'Installed'} ${name}@${result.version} at ${targetDir}`);
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
function instructionProjectRoot(cwd, selectedProject) {
|
|
806
|
+
if (selectedProject) {
|
|
807
|
+
const selected = path.resolve(cwd, selectedProject);
|
|
808
|
+
let stats;
|
|
809
|
+
try {
|
|
810
|
+
stats = fs.statSync(selected);
|
|
811
|
+
} catch {
|
|
812
|
+
throw new CliError(`project directory does not exist: ${selected}`);
|
|
813
|
+
}
|
|
814
|
+
if (!stats.isDirectory()) throw new CliError(`project path is not a directory: ${selected}`);
|
|
815
|
+
return canonicalPath(selected);
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
return canonicalPath(projectRoot(cwd, { required: false }) ?? cwd);
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
function isWithinDirectory(root, target) {
|
|
822
|
+
const relative = path.relative(root, target);
|
|
823
|
+
return relative === '' || (!relative.startsWith(`..${path.sep}`) && relative !== '..' && !path.isAbsolute(relative));
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
function nearestAgentsFile(cwd, root) {
|
|
827
|
+
const start = canonicalPath(cwd);
|
|
828
|
+
let directory = isWithinDirectory(root, start) ? start : root;
|
|
829
|
+
while (isWithinDirectory(root, directory)) {
|
|
830
|
+
const candidate = path.join(directory, 'AGENTS.md');
|
|
831
|
+
if (fs.existsSync(candidate)) return candidate;
|
|
832
|
+
if (directory === root) break;
|
|
833
|
+
directory = path.dirname(directory);
|
|
834
|
+
}
|
|
835
|
+
return path.join(root, 'AGENTS.md');
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
function assertLocalInstructionSources(root, contentDirectory) {
|
|
839
|
+
assertNoInstructionSymlink(root, contentDirectory, 'local instruction source directory');
|
|
840
|
+
if (!fs.existsSync(contentDirectory)) return;
|
|
841
|
+
for (const entry of fs.readdirSync(contentDirectory, { withFileTypes: true })) {
|
|
842
|
+
if (entry.name.endsWith('.block.md')) {
|
|
843
|
+
const sourcePath = path.join(contentDirectory, entry.name);
|
|
844
|
+
const label = `local instruction source ${entry.name}`;
|
|
845
|
+
assertLocalDestination(root, sourcePath, label);
|
|
846
|
+
assertNoInstructionSymlink(root, sourcePath, label);
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
function assertNoInstructionSymlink(root, target, label) {
|
|
852
|
+
const boundary = path.resolve(root);
|
|
853
|
+
const absoluteTarget = path.resolve(target);
|
|
854
|
+
const relative = path.relative(boundary, absoluteTarget);
|
|
855
|
+
if (relative === '..' || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative)) {
|
|
856
|
+
throw new CliError(`${label} resolves outside the project`);
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
let current = boundary;
|
|
860
|
+
for (const segment of relative.split(path.sep).filter(Boolean)) {
|
|
861
|
+
current = path.join(current, segment);
|
|
862
|
+
try {
|
|
863
|
+
if (fs.lstatSync(current).isSymbolicLink()) {
|
|
864
|
+
throw new CliError(`${label} contains a symlink path component`);
|
|
865
|
+
}
|
|
866
|
+
} catch (error) {
|
|
867
|
+
if (error instanceof CliError) throw error;
|
|
868
|
+
if (error.code === 'ENOENT') break;
|
|
869
|
+
throw error;
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
function installAgents(cwd, globalInstall, selectedProject) {
|
|
875
|
+
const composed = composeAgentsInstructions(agentsContentRoot);
|
|
876
|
+
const targetPaths = [];
|
|
877
|
+
const root = globalInstall ? null : instructionProjectRoot(cwd, selectedProject);
|
|
878
|
+
if (globalInstall) {
|
|
879
|
+
targetPaths.push(...globalAgentsTargets.map((target) => path.join(os.homedir(), target)));
|
|
880
|
+
} else {
|
|
881
|
+
const targetPath = nearestAgentsFile(cwd, root);
|
|
882
|
+
assertLocalDestination(root, targetPath, 'local agents instructions');
|
|
883
|
+
assertNoInstructionSymlink(root, targetPath, 'local agents instructions');
|
|
884
|
+
assertLocalInstructionSources(root, path.dirname(targetPath));
|
|
885
|
+
targetPaths.push(targetPath);
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
// Plan every target before writing any of them, so a rejected file leaves all files intact.
|
|
889
|
+
const planned = targetPaths.map((targetPath) => {
|
|
890
|
+
const existing = fs.existsSync(targetPath) ? fs.readFileSync(targetPath, 'utf8') : '';
|
|
891
|
+
const bundled = applyInstructions(existing, composed.content, packageJson.version);
|
|
892
|
+
const local = applyLocalFragments(bundled.text, path.dirname(targetPath));
|
|
893
|
+
const action = existing === local.text ? 'unchanged' : bundled.action === 'installed' || local.action === 'installed' ? 'installed' : 'refreshed';
|
|
894
|
+
return { targetPath, action, text: local.text };
|
|
895
|
+
});
|
|
896
|
+
|
|
897
|
+
for (const { targetPath, action, text } of planned) {
|
|
898
|
+
if (action === 'unchanged') {
|
|
899
|
+
console.log(`Agents instructions already installed at ${targetPath}`);
|
|
900
|
+
continue;
|
|
901
|
+
}
|
|
902
|
+
fs.mkdirSync(path.dirname(targetPath), { recursive: true });
|
|
903
|
+
fs.writeFileSync(targetPath, text);
|
|
904
|
+
console.log(
|
|
905
|
+
action === 'refreshed'
|
|
906
|
+
? `Refreshed agents instructions at ${targetPath}`
|
|
907
|
+
: `Installed agents instructions at ${targetPath}`
|
|
908
|
+
);
|
|
909
|
+
}
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
function projectInstructionPaths(cwd, selectedProject) {
|
|
913
|
+
const root = instructionProjectRoot(cwd, selectedProject);
|
|
914
|
+
const sourceDirectory = path.join(root, projectTemplateDirectory);
|
|
915
|
+
const templatePath = path.join(sourceDirectory, projectTemplateName);
|
|
916
|
+
const outputPath = path.join(root, 'AGENTS.md');
|
|
917
|
+
const paths = [
|
|
918
|
+
[sourceDirectory, 'project instruction source directory'],
|
|
919
|
+
[templatePath, 'project instruction template'],
|
|
920
|
+
[outputPath, 'project agents output']
|
|
921
|
+
];
|
|
922
|
+
for (const [target, label] of paths) {
|
|
923
|
+
assertLocalDestination(root, target, label);
|
|
924
|
+
assertNoInstructionSymlink(root, target, label);
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
if (fs.existsSync(sourceDirectory) && !fs.statSync(sourceDirectory).isDirectory()) {
|
|
928
|
+
throw new CliError(`project instruction source path is not a directory: ${sourceDirectory}`);
|
|
929
|
+
}
|
|
930
|
+
if (fs.existsSync(templatePath) && !fs.statSync(templatePath).isFile()) {
|
|
931
|
+
throw new CliError(`project instruction template is not a file: ${templatePath}`);
|
|
932
|
+
}
|
|
933
|
+
if (fs.existsSync(outputPath) && !fs.statSync(outputPath).isFile()) {
|
|
934
|
+
throw new CliError(`project agents output is not a file: ${outputPath}`);
|
|
935
|
+
}
|
|
936
|
+
if (fs.existsSync(sourceDirectory)) {
|
|
937
|
+
for (const entry of fs.readdirSync(sourceDirectory, { withFileTypes: true })) {
|
|
938
|
+
if (!entry.name.endsWith('.block.md')) continue;
|
|
939
|
+
const sourcePath = path.join(sourceDirectory, entry.name);
|
|
940
|
+
const label = `project instruction source ${entry.name}`;
|
|
941
|
+
assertLocalDestination(root, sourcePath, label);
|
|
942
|
+
assertNoInstructionSymlink(root, sourcePath, label);
|
|
943
|
+
}
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
return { root, sourceDirectory, templatePath, outputPath };
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
function renderProjectAgents(cwd, selectedProject, { initialize = false, check = false } = {}) {
|
|
950
|
+
let paths = projectInstructionPaths(cwd, selectedProject);
|
|
951
|
+
if (initialize) {
|
|
952
|
+
fs.mkdirSync(paths.sourceDirectory, { recursive: true });
|
|
953
|
+
if (!fs.existsSync(paths.templatePath)) {
|
|
954
|
+
fs.writeFileSync(paths.templatePath, defaultProjectTemplate);
|
|
955
|
+
}
|
|
956
|
+
paths = projectInstructionPaths(cwd, selectedProject);
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
if (!fs.existsSync(paths.templatePath)) {
|
|
960
|
+
throw new CliError(`project instruction template is missing; run "skillshop agents init" first: ${paths.templatePath}`);
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
const composed = composeAgentsInstructions(paths.sourceDirectory, projectTemplateName).content;
|
|
964
|
+
const existing = fs.existsSync(paths.outputPath) ? fs.readFileSync(paths.outputPath, 'utf8') : '';
|
|
965
|
+
const planned = applyProjectInstructions(existing, composed, packageJson.version);
|
|
966
|
+
|
|
967
|
+
if (check) {
|
|
968
|
+
if (fs.existsSync(paths.outputPath) && planned.text === existing) {
|
|
969
|
+
console.log(`Project agents instructions are current at ${paths.outputPath}`);
|
|
970
|
+
return;
|
|
971
|
+
}
|
|
972
|
+
console.error(
|
|
973
|
+
`Project agents instructions are ${fs.existsSync(paths.outputPath) ? 'stale' : 'missing'} at ${paths.outputPath}`
|
|
974
|
+
);
|
|
975
|
+
process.exitCode = 1;
|
|
976
|
+
return;
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
if (planned.text === existing) {
|
|
980
|
+
console.log(`Project agents instructions already current at ${paths.outputPath}`);
|
|
981
|
+
return;
|
|
982
|
+
}
|
|
983
|
+
fs.writeFileSync(paths.outputPath, planned.text);
|
|
984
|
+
console.log(`${planned.action === 'refreshed' ? 'Refreshed' : 'Rendered'} project agents instructions at ${paths.outputPath}`);
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
function projectAgentsConfigured(cwd, selectedProject) {
|
|
988
|
+
const root = instructionProjectRoot(cwd, selectedProject);
|
|
989
|
+
const sourceDirectory = path.join(root, projectTemplateDirectory);
|
|
990
|
+
const outputPath = path.join(root, 'AGENTS.md');
|
|
991
|
+
for (const candidate of [sourceDirectory, outputPath]) {
|
|
992
|
+
try {
|
|
993
|
+
const stat = fs.lstatSync(candidate);
|
|
994
|
+
if (candidate === sourceDirectory) return true;
|
|
995
|
+
if (stat.isFile() && hasProjectInstructions(fs.readFileSync(candidate, 'utf8'))) return true;
|
|
996
|
+
} catch (error) {
|
|
997
|
+
if (error.code !== 'ENOENT') throw error;
|
|
998
|
+
}
|
|
999
|
+
}
|
|
1000
|
+
return false;
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
function installLocal(name, requestedVersion, cwd) {
|
|
1004
|
+
const root = findLockRoot(cwd) ?? projectRoot(cwd);
|
|
1005
|
+
const lock = readLock(root);
|
|
1006
|
+
const config = readConfig();
|
|
1007
|
+
const skillDirectories = localSkillDirectories(root, lock, cwd);
|
|
1008
|
+
const names = [...new Set([...Object.keys(lock.skills), name, 'skillshop'])].sort();
|
|
1009
|
+
const installs = names.map((skillName) => {
|
|
1010
|
+
const metadata = skillMetadata(skillName);
|
|
1011
|
+
const targetDirs = config || !lock.skills[skillName]
|
|
1012
|
+
? skillDirectories.map((directory) => path.join(directory, skillName))
|
|
1013
|
+
: lockedTargets(root, skillName, lock.skills[skillName]);
|
|
1014
|
+
const previousTargets = lock.skills[skillName]
|
|
1015
|
+
? lockedTargets(root, skillName, lock.skills[skillName])
|
|
1016
|
+
: [];
|
|
1017
|
+
return { metadata, targetDirs, previousTargets };
|
|
1018
|
+
});
|
|
1019
|
+
const requestedMetadata = installs.find(({ metadata }) => metadata.name === name).metadata;
|
|
1020
|
+
if (requestedVersion && requestedVersion !== requestedMetadata.version) {
|
|
1021
|
+
throw new CliError(
|
|
1022
|
+
`${name}@${requestedVersion} is unavailable; local registry provides ${name}@${requestedMetadata.version}`
|
|
1023
|
+
);
|
|
1024
|
+
}
|
|
1025
|
+
for (const { metadata, targetDirs, previousTargets } of installs) {
|
|
1026
|
+
assertTargetsAvailable(metadata.name, targetDirs, previousTargets);
|
|
1027
|
+
}
|
|
1028
|
+
|
|
1029
|
+
for (const { metadata, targetDirs, previousTargets } of installs) {
|
|
1030
|
+
const version = metadata.name === name ? requestedVersion : null;
|
|
1031
|
+
const results = targetDirs.map((targetDir) => installAt(metadata.name, version, targetDir));
|
|
1032
|
+
for (const staleTarget of staleTargets(previousTargets, targetDirs)) {
|
|
1033
|
+
fs.rmSync(staleTarget, { recursive: true, force: true });
|
|
1034
|
+
}
|
|
1035
|
+
lock.skills[metadata.name] = lockEntry(root, results);
|
|
1036
|
+
for (const result of results) {
|
|
1037
|
+
console.log(
|
|
1038
|
+
`${result.updating ? 'Updated' : 'Installed'} ${result.name}@${result.version} at ${result.targetDir}`
|
|
1039
|
+
);
|
|
1040
|
+
}
|
|
1041
|
+
}
|
|
1042
|
+
|
|
1043
|
+
writeLock(root, lock);
|
|
1044
|
+
maintainRepository(root, lock);
|
|
1045
|
+
console.log(`Wrote ${path.join(root, lockFileName)}`);
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1048
|
+
function uninstallGlobal(name) {
|
|
1049
|
+
const config = readConfig();
|
|
1050
|
+
const skillDirectories = config
|
|
1051
|
+
? configuredSkillDirectories(config, 'global')
|
|
1052
|
+
: [path.join(os.homedir(), '.agents', 'skills')];
|
|
1053
|
+
const targets = skillDirectories.map((directory) => path.join(directory, name));
|
|
1054
|
+
const installedTargets = targets.filter((target) => fs.existsSync(target));
|
|
1055
|
+
if (installedTargets.length === 0) {
|
|
1056
|
+
throw new CliError(`${name} is not installed globally in the configured skill directories`);
|
|
1057
|
+
}
|
|
1058
|
+
|
|
1059
|
+
for (const targetDir of installedTargets) {
|
|
1060
|
+
fs.rmSync(targetDir, { recursive: true });
|
|
1061
|
+
console.log(`Uninstalled ${name} from ${targetDir}`);
|
|
1062
|
+
}
|
|
1063
|
+
const globalSkills = readGlobalSkills();
|
|
1064
|
+
if (globalSkills.skills[name]) {
|
|
1065
|
+
const remaining = staleTargets(globalTargets(name, globalSkills.skills[name]), installedTargets);
|
|
1066
|
+
if (remaining.length) globalSkills.skills[name].paths = remaining;
|
|
1067
|
+
else delete globalSkills.skills[name];
|
|
1068
|
+
writeGlobalSkills(globalSkills);
|
|
1069
|
+
}
|
|
1070
|
+
}
|
|
1071
|
+
|
|
1072
|
+
function uninstallLocal(name, cwd) {
|
|
1073
|
+
const root = findLockRoot(cwd);
|
|
1074
|
+
if (!root) throw new CliError(`no ${lockFileName} found in this project`);
|
|
1075
|
+
|
|
1076
|
+
const lock = readLock(root);
|
|
1077
|
+
if (!lock.skills[name]) {
|
|
1078
|
+
throw new CliError(`${name} is not listed in ${path.join(root, lockFileName)}`);
|
|
1079
|
+
}
|
|
1080
|
+
|
|
1081
|
+
const targetDirs = lockedTargets(root, name, lock.skills[name]);
|
|
1082
|
+
for (const targetDir of targetDirs) fs.rmSync(targetDir, { recursive: true, force: true });
|
|
1083
|
+
delete lock.skills[name];
|
|
1084
|
+
writeLock(root, lock);
|
|
1085
|
+
maintainRepository(root, lock);
|
|
1086
|
+
|
|
1087
|
+
for (const targetDir of targetDirs) console.log(`Uninstalled ${name} from ${targetDir}`);
|
|
1088
|
+
console.log(`Updated ${path.join(root, lockFileName)}`);
|
|
1089
|
+
}
|
|
1090
|
+
|
|
1091
|
+
function updateLocal(requestedName, cwd, options = {}) {
|
|
1092
|
+
const root = findLockRoot(cwd);
|
|
1093
|
+
if (!root) throw new CliError(`no ${lockFileName} found in this project`);
|
|
1094
|
+
|
|
1095
|
+
const lock = readLock(root);
|
|
1096
|
+
if (requestedName && !lock.skills[requestedName]) {
|
|
1097
|
+
throw new CliError(`${requestedName} is not listed in ${path.join(root, lockFileName)}`);
|
|
1098
|
+
}
|
|
1099
|
+
|
|
1100
|
+
const names = requestedName ? [requestedName] : Object.keys(lock.skills);
|
|
1101
|
+
const config = readConfig();
|
|
1102
|
+
const skillDirectories = localSkillDirectories(root, lock, cwd);
|
|
1103
|
+
|
|
1104
|
+
const updates = [...new Set(names)].sort().map((name) => {
|
|
1105
|
+
const metadata = skillMetadata(name);
|
|
1106
|
+
const targetDirs = config
|
|
1107
|
+
? skillDirectories.map((directory) => path.join(directory, name))
|
|
1108
|
+
: lockedTargets(root, name, lock.skills[name]);
|
|
1109
|
+
const previousTargets = lockedTargets(root, name, lock.skills[name]);
|
|
1110
|
+
return { metadata, targetDirs, previousTargets };
|
|
1111
|
+
});
|
|
1112
|
+
for (const { metadata, targetDirs, previousTargets } of updates) {
|
|
1113
|
+
assertTargetsAvailable(metadata.name, targetDirs, previousTargets);
|
|
1114
|
+
}
|
|
1115
|
+
|
|
1116
|
+
for (const { metadata, targetDirs, previousTargets } of updates) {
|
|
1117
|
+
const results = targetDirs.map((targetDir) => installAt(metadata.name, null, targetDir));
|
|
1118
|
+
for (const staleTarget of staleTargets(previousTargets, targetDirs)) {
|
|
1119
|
+
fs.rmSync(staleTarget, { recursive: true, force: true });
|
|
1120
|
+
}
|
|
1121
|
+
lock.skills[metadata.name] = lockEntry(root, results);
|
|
1122
|
+
for (const result of results) {
|
|
1123
|
+
console.log(`Updated ${result.name}@${result.version} at ${result.targetDir}`);
|
|
1124
|
+
}
|
|
1125
|
+
}
|
|
1126
|
+
|
|
1127
|
+
writeLock(root, lock);
|
|
1128
|
+
maintainRepository(root, lock, options);
|
|
1129
|
+
console.log(`Updated ${path.join(root, lockFileName)}`);
|
|
1130
|
+
}
|
|
1131
|
+
|
|
1132
|
+
function updateAllRepositories() {
|
|
1133
|
+
const registry = readRegistry();
|
|
1134
|
+
const retained = [];
|
|
1135
|
+
const failures = [];
|
|
1136
|
+
|
|
1137
|
+
for (const root of registry.repositories) {
|
|
1138
|
+
if (!hasLock(root)) {
|
|
1139
|
+
console.log(`Removed missing repository ${root}`);
|
|
1140
|
+
continue;
|
|
1141
|
+
}
|
|
1142
|
+
|
|
1143
|
+
retained.push(root);
|
|
1144
|
+
console.log(`Updating ${root}`);
|
|
1145
|
+
try {
|
|
1146
|
+
reconcileRepository(root, { register: false });
|
|
1147
|
+
updateLocal(null, root, { register: false });
|
|
1148
|
+
} catch (error) {
|
|
1149
|
+
if (!(error instanceof CliError)) throw error;
|
|
1150
|
+
failures.push(`${root}: ${error.message}`);
|
|
1151
|
+
console.error(`skillshop: ${root}: ${error.message}`);
|
|
1152
|
+
}
|
|
1153
|
+
}
|
|
1154
|
+
|
|
1155
|
+
writeRegistry(retained);
|
|
1156
|
+
if (failures.length) {
|
|
1157
|
+
throw new CliError(`failed to update ${failures.length} registered ${failures.length === 1 ? 'repository' : 'repositories'}`);
|
|
1158
|
+
}
|
|
1159
|
+
console.log(`Updated ${retained.length} registered ${retained.length === 1 ? 'repository' : 'repositories'}`);
|
|
1160
|
+
}
|
|
1161
|
+
|
|
1162
|
+
function assertNoManagedSymlink(base, target, label) {
|
|
1163
|
+
const boundary = path.resolve(base);
|
|
1164
|
+
const absoluteTarget = path.resolve(target);
|
|
1165
|
+
const relative = path.relative(boundary, absoluteTarget);
|
|
1166
|
+
if (relative === '..' || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative)) {
|
|
1167
|
+
throw new CliError(`${label} resolves outside its managed root`);
|
|
1168
|
+
}
|
|
1169
|
+
|
|
1170
|
+
let current = boundary;
|
|
1171
|
+
for (const segment of relative.split(path.sep).filter(Boolean)) {
|
|
1172
|
+
current = path.join(current, segment);
|
|
1173
|
+
try {
|
|
1174
|
+
if (fs.lstatSync(current).isSymbolicLink()) {
|
|
1175
|
+
throw new CliError(`${label} contains a symlink path component`);
|
|
1176
|
+
}
|
|
1177
|
+
} catch (error) {
|
|
1178
|
+
if (error instanceof CliError) throw error;
|
|
1179
|
+
if (error.code === 'ENOENT') break;
|
|
1180
|
+
throw error;
|
|
1181
|
+
}
|
|
1182
|
+
}
|
|
1183
|
+
}
|
|
1184
|
+
|
|
1185
|
+
function replaceManagedSkillDirectory(sourceDir, targetDir) {
|
|
1186
|
+
const temporaryDir = `${targetDir}.skillshop-upgrade-tmp-${process.pid}`;
|
|
1187
|
+
const backupDir = `${targetDir}.skillshop-upgrade-backup-${process.pid}`;
|
|
1188
|
+
fs.rmSync(temporaryDir, { recursive: true, force: true });
|
|
1189
|
+
fs.rmSync(backupDir, { recursive: true, force: true });
|
|
1190
|
+
fs.mkdirSync(path.dirname(targetDir), { recursive: true });
|
|
1191
|
+
fs.cpSync(sourceDir, temporaryDir, { recursive: true, force: true });
|
|
1192
|
+
|
|
1193
|
+
const hadTarget = fs.existsSync(targetDir);
|
|
1194
|
+
if (hadTarget) fs.renameSync(targetDir, backupDir);
|
|
1195
|
+
try {
|
|
1196
|
+
fs.renameSync(temporaryDir, targetDir);
|
|
1197
|
+
fs.rmSync(backupDir, { recursive: true, force: true });
|
|
1198
|
+
} catch (error) {
|
|
1199
|
+
if (hadTarget && !fs.existsSync(targetDir) && fs.existsSync(backupDir)) {
|
|
1200
|
+
fs.renameSync(backupDir, targetDir);
|
|
1201
|
+
}
|
|
1202
|
+
throw error;
|
|
1203
|
+
} finally {
|
|
1204
|
+
fs.rmSync(temporaryDir, { recursive: true, force: true });
|
|
1205
|
+
}
|
|
1206
|
+
}
|
|
1207
|
+
|
|
1208
|
+
function upgradeSummary() {
|
|
1209
|
+
return { updated: 0, unchanged: 0, missing: 0, failed: [] };
|
|
1210
|
+
}
|
|
1211
|
+
|
|
1212
|
+
function validateManagedCopy(root, target, name) {
|
|
1213
|
+
assertNoManagedSymlink(root, target, `managed ${name} copy`);
|
|
1214
|
+
let targetStats;
|
|
1215
|
+
try {
|
|
1216
|
+
targetStats = fs.lstatSync(target);
|
|
1217
|
+
} catch (error) {
|
|
1218
|
+
if (error.code === 'ENOENT') return false;
|
|
1219
|
+
throw error;
|
|
1220
|
+
}
|
|
1221
|
+
if (!targetStats.isDirectory()) throw new CliError(`${target} is not a managed skill directory`);
|
|
1222
|
+
|
|
1223
|
+
const manifestPath = path.join(target, 'SKILL.md');
|
|
1224
|
+
assertNoManagedSymlink(root, manifestPath, `managed ${name} manifest`);
|
|
1225
|
+
let manifestStats;
|
|
1226
|
+
try {
|
|
1227
|
+
manifestStats = fs.lstatSync(manifestPath);
|
|
1228
|
+
} catch (error) {
|
|
1229
|
+
if (error.code === 'ENOENT') throw new CliError(`${manifestPath} is missing from a recorded skill copy`);
|
|
1230
|
+
throw error;
|
|
1231
|
+
}
|
|
1232
|
+
if (!manifestStats.isFile()) throw new CliError(`${manifestPath} is not a regular skill manifest`);
|
|
1233
|
+
return true;
|
|
1234
|
+
}
|
|
1235
|
+
|
|
1236
|
+
function refreshManagedLock(root, lock, { global = false, summary }) {
|
|
1237
|
+
let changed = false;
|
|
1238
|
+
for (const name of Object.keys(lock.skills).sort()) {
|
|
1239
|
+
const entry = lock.skills[name];
|
|
1240
|
+
try {
|
|
1241
|
+
const metadata = skillMetadata(name);
|
|
1242
|
+
const targets = global ? globalTargets(name, entry) : lockedTargets(root, name, entry);
|
|
1243
|
+
const base = global ? os.homedir() : root;
|
|
1244
|
+
const existingTargets = [];
|
|
1245
|
+
for (const target of targets) {
|
|
1246
|
+
if (validateManagedCopy(base, target, name)) existingTargets.push(target);
|
|
1247
|
+
else summary.missing += 1;
|
|
1248
|
+
}
|
|
1249
|
+
|
|
1250
|
+
if (!existingTargets.length) {
|
|
1251
|
+
delete lock.skills[name];
|
|
1252
|
+
changed = true;
|
|
1253
|
+
continue;
|
|
1254
|
+
}
|
|
1255
|
+
|
|
1256
|
+
const successfulTargets = [];
|
|
1257
|
+
let skillFailed = false;
|
|
1258
|
+
for (const target of existingTargets) {
|
|
1259
|
+
const recordedVersion = global ? entry.copies?.[target]?.version : entry.version;
|
|
1260
|
+
if (recordedVersion === metadata.version) {
|
|
1261
|
+
summary.unchanged += 1;
|
|
1262
|
+
successfulTargets.push(target);
|
|
1263
|
+
continue;
|
|
1264
|
+
}
|
|
1265
|
+
try {
|
|
1266
|
+
replaceManagedSkillDirectory(metadata.sourceDir, target);
|
|
1267
|
+
summary.updated += 1;
|
|
1268
|
+
successfulTargets.push(target);
|
|
1269
|
+
} catch (error) {
|
|
1270
|
+
summary.failed.push(`${name} at ${target}: ${error.message}`);
|
|
1271
|
+
skillFailed = true;
|
|
1272
|
+
}
|
|
1273
|
+
}
|
|
1274
|
+
|
|
1275
|
+
if (global) {
|
|
1276
|
+
const keptTargets = skillFailed ? targets : successfulTargets;
|
|
1277
|
+
entry.paths = keptTargets;
|
|
1278
|
+
entry.copies ??= {};
|
|
1279
|
+
for (const target of successfulTargets) {
|
|
1280
|
+
const previous = entry.copies[target] ?? {};
|
|
1281
|
+
entry.copies[target] = { ...previous, author: metadata.author, version: metadata.version };
|
|
1282
|
+
}
|
|
1283
|
+
if (!skillFailed) changed = true;
|
|
1284
|
+
if (!entry.paths.length) {
|
|
1285
|
+
delete lock.skills[name];
|
|
1286
|
+
changed = true;
|
|
1287
|
+
}
|
|
1288
|
+
} else if (!skillFailed) {
|
|
1289
|
+
entry.paths = successfulTargets.map((target) => relativePath(root, target)).sort();
|
|
1290
|
+
entry.author = metadata.author;
|
|
1291
|
+
entry.version = metadata.version;
|
|
1292
|
+
changed = true;
|
|
1293
|
+
} else if (existingTargets.length !== targets.length) {
|
|
1294
|
+
entry.paths = existingTargets.map((target) => relativePath(root, target)).sort();
|
|
1295
|
+
changed = true;
|
|
1296
|
+
}
|
|
1297
|
+
} catch (error) {
|
|
1298
|
+
summary.failed.push(`${name}: ${error.message}`);
|
|
1299
|
+
}
|
|
1300
|
+
}
|
|
1301
|
+
|
|
1302
|
+
if (changed) {
|
|
1303
|
+
try {
|
|
1304
|
+
if (global) writeGlobalSkills(lock);
|
|
1305
|
+
else writeLock(root, lock);
|
|
1306
|
+
} catch (error) {
|
|
1307
|
+
summary.failed.push(`could not save ${global ? 'global skill manifest' : path.join(root, lockFileName)}: ${error.message}`);
|
|
1308
|
+
}
|
|
1309
|
+
}
|
|
1310
|
+
}
|
|
1311
|
+
|
|
1312
|
+
function refreshManagedCopies(scope) {
|
|
1313
|
+
const summary = upgradeSummary();
|
|
1314
|
+
if (scope === 'local') {
|
|
1315
|
+
const owner = localPackageOwner();
|
|
1316
|
+
if (!owner) {
|
|
1317
|
+
summary.failed.push('fresh Skillshop package is no longer attached to a local npm dependency');
|
|
1318
|
+
} else {
|
|
1319
|
+
try {
|
|
1320
|
+
const lock = readLock(owner.root);
|
|
1321
|
+
refreshManagedLock(owner.root, lock, { summary });
|
|
1322
|
+
} catch (error) {
|
|
1323
|
+
summary.failed.push(`${owner.root}: ${error.message}`);
|
|
1324
|
+
}
|
|
1325
|
+
}
|
|
1326
|
+
} else if (scope === 'global') {
|
|
1327
|
+
try {
|
|
1328
|
+
const globalLock = readGlobalSkills();
|
|
1329
|
+
refreshManagedLock(null, globalLock, { global: true, summary });
|
|
1330
|
+
} catch (error) {
|
|
1331
|
+
summary.failed.push(`global managed copies: ${error.message}`);
|
|
1332
|
+
}
|
|
1333
|
+
|
|
1334
|
+
try {
|
|
1335
|
+
const registry = readRegistry();
|
|
1336
|
+
for (const root of registry.repositories) {
|
|
1337
|
+
if (!hasLock(root)) continue;
|
|
1338
|
+
try {
|
|
1339
|
+
const lock = readLock(root);
|
|
1340
|
+
refreshManagedLock(root, lock, { summary });
|
|
1341
|
+
} catch (error) {
|
|
1342
|
+
summary.failed.push(`${root}: ${error.message}`);
|
|
1343
|
+
}
|
|
1344
|
+
}
|
|
1345
|
+
} catch (error) {
|
|
1346
|
+
summary.failed.push(`registered project copies: ${error.message}`);
|
|
1347
|
+
}
|
|
1348
|
+
} else {
|
|
1349
|
+
summary.failed.push(`unsupported managed-copy scope: ${scope}`);
|
|
1350
|
+
}
|
|
1351
|
+
|
|
1352
|
+
console.log(
|
|
1353
|
+
`Managed-copy refresh summary: ${summary.updated} updated, ${summary.unchanged} unchanged, ${summary.missing} missing paths pruned, ${summary.failed.length} failed.`
|
|
1354
|
+
);
|
|
1355
|
+
if (summary.failed.length) {
|
|
1356
|
+
for (const failure of summary.failed) console.error(`skillshop: upgrade refresh: ${failure}`);
|
|
1357
|
+
console.error('skillshop: npm upgrade succeeded, but managed-copy refresh was incomplete');
|
|
1358
|
+
process.exitCode = 1;
|
|
1359
|
+
} else {
|
|
1360
|
+
console.log(`Refreshed Skillshop-managed copies for ${scope} scope.`);
|
|
1361
|
+
}
|
|
1362
|
+
}
|
|
1363
|
+
|
|
1364
|
+
const migrationSkipDirectories = new Set([
|
|
1365
|
+
'.git',
|
|
1366
|
+
'.skillshop',
|
|
1367
|
+
'.skillz',
|
|
1368
|
+
'.Trash',
|
|
1369
|
+
'Library',
|
|
1370
|
+
'node_modules'
|
|
1371
|
+
]);
|
|
1372
|
+
|
|
1373
|
+
function discoverLockRoots(startPath, roots) {
|
|
1374
|
+
let stat;
|
|
1375
|
+
try {
|
|
1376
|
+
stat = fs.statSync(startPath);
|
|
1377
|
+
} catch {
|
|
1378
|
+
return;
|
|
1379
|
+
}
|
|
1380
|
+
|
|
1381
|
+
if (stat.isFile()) {
|
|
1382
|
+
if ([lockFileName, legacyLockFileName].includes(path.basename(startPath))) roots.add(canonicalPath(path.dirname(startPath)));
|
|
1383
|
+
return;
|
|
1384
|
+
}
|
|
1385
|
+
if (!stat.isDirectory()) return;
|
|
1386
|
+
|
|
1387
|
+
if (hasLock(startPath)) roots.add(canonicalPath(startPath));
|
|
1388
|
+
|
|
1389
|
+
let entries;
|
|
1390
|
+
try {
|
|
1391
|
+
entries = fs.readdirSync(startPath, { withFileTypes: true });
|
|
1392
|
+
} catch {
|
|
1393
|
+
return;
|
|
1394
|
+
}
|
|
1395
|
+
|
|
1396
|
+
for (const entry of entries) {
|
|
1397
|
+
if (!entry.isDirectory() || entry.isSymbolicLink() || migrationSkipDirectories.has(entry.name)) continue;
|
|
1398
|
+
discoverLockRoots(path.join(startPath, entry.name), roots);
|
|
1399
|
+
}
|
|
1400
|
+
}
|
|
1401
|
+
|
|
1402
|
+
function migrateRepositories(requestedRoots, cwd) {
|
|
1403
|
+
const registry = readRegistry();
|
|
1404
|
+
const roots = new Set(registry.repositories);
|
|
1405
|
+
const searchRoots = requestedRoots.length
|
|
1406
|
+
? requestedRoots.map((root) => path.resolve(cwd, root))
|
|
1407
|
+
: [os.homedir()];
|
|
1408
|
+
|
|
1409
|
+
for (const searchRoot of searchRoots) discoverLockRoots(searchRoot, roots);
|
|
1410
|
+
|
|
1411
|
+
const migrated = [];
|
|
1412
|
+
const failures = [];
|
|
1413
|
+
for (const root of [...roots].sort()) {
|
|
1414
|
+
if (!hasLock(root)) continue;
|
|
1415
|
+
|
|
1416
|
+
try {
|
|
1417
|
+
reconcileRepository(root, { register: false });
|
|
1418
|
+
const lock = readLock(root);
|
|
1419
|
+
maintainRepository(root, lock, { register: false, untrack: true });
|
|
1420
|
+
migrated.push(root);
|
|
1421
|
+
console.log(`Migrated ${root}`);
|
|
1422
|
+
} catch (error) {
|
|
1423
|
+
if (!(error instanceof CliError)) throw error;
|
|
1424
|
+
failures.push(`${root}: ${error.message}`);
|
|
1425
|
+
console.error(`skillshop: ${root}: ${error.message}`);
|
|
1426
|
+
}
|
|
1427
|
+
}
|
|
1428
|
+
|
|
1429
|
+
writeRegistry(migrated);
|
|
1430
|
+
if (failures.length) {
|
|
1431
|
+
throw new CliError(`failed to migrate ${failures.length} ${failures.length === 1 ? 'repository' : 'repositories'}`);
|
|
1432
|
+
}
|
|
1433
|
+
console.log(`Migrated ${migrated.length} ${migrated.length === 1 ? 'repository' : 'repositories'}`);
|
|
1434
|
+
}
|
|
1435
|
+
|
|
1436
|
+
function parseSkillSpec(spec, requestedVersion) {
|
|
1437
|
+
const separator = spec.lastIndexOf('@');
|
|
1438
|
+
if (separator <= 0) return { name: spec, version: requestedVersion };
|
|
1439
|
+
|
|
1440
|
+
const name = spec.slice(0, separator);
|
|
1441
|
+
const inlineVersion = spec.slice(separator + 1);
|
|
1442
|
+
if (!inlineVersion) throw new CliError(`missing version in ${spec}`);
|
|
1443
|
+
if (requestedVersion && requestedVersion !== inlineVersion) {
|
|
1444
|
+
throw new CliError(`conflicting versions requested for ${name}`);
|
|
1445
|
+
}
|
|
1446
|
+
return { name, version: inlineVersion };
|
|
1447
|
+
}
|
|
1448
|
+
|
|
1449
|
+
function parseArgs(argv) {
|
|
1450
|
+
const positional = [];
|
|
1451
|
+
let globalInstall = false;
|
|
1452
|
+
let pushUpdate = false;
|
|
1453
|
+
const agentsTargets = [];
|
|
1454
|
+
let requestedVersion = null;
|
|
1455
|
+
let selectedProject = null;
|
|
1456
|
+
let checkProject = false;
|
|
1457
|
+
|
|
1458
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
1459
|
+
const argument = argv[index];
|
|
1460
|
+
if (argument === '--global' || argument === '-g') {
|
|
1461
|
+
globalInstall = true;
|
|
1462
|
+
} else if (argument === '--push') {
|
|
1463
|
+
pushUpdate = true;
|
|
1464
|
+
} else if (argument === '--check') {
|
|
1465
|
+
checkProject = true;
|
|
1466
|
+
} else if (argument === '--project') {
|
|
1467
|
+
selectedProject = argv[index + 1];
|
|
1468
|
+
if (!selectedProject) throw new CliError('--project requires a directory');
|
|
1469
|
+
index += 1;
|
|
1470
|
+
} else if (argument.startsWith('--project=')) {
|
|
1471
|
+
selectedProject = argument.slice('--project='.length);
|
|
1472
|
+
if (!selectedProject) throw new CliError('--project requires a directory');
|
|
1473
|
+
} else if (argument === '--codex' || argument === '--copilot') {
|
|
1474
|
+
throw new CliError(`${argument} is no longer supported; use agents --global`);
|
|
1475
|
+
} else if (argument === '--version' || argument === '-v') {
|
|
1476
|
+
requestedVersion = argv[index + 1];
|
|
1477
|
+
if (!requestedVersion) throw new CliError(`${argument} requires a version`);
|
|
1478
|
+
index += 1;
|
|
1479
|
+
} else if (argument.startsWith('--version=')) {
|
|
1480
|
+
requestedVersion = argument.slice('--version='.length);
|
|
1481
|
+
if (!requestedVersion) throw new CliError('--version requires a version');
|
|
1482
|
+
} else if (argument.startsWith('-')) {
|
|
1483
|
+
throw new CliError(`unknown option: ${argument}`);
|
|
1484
|
+
} else {
|
|
1485
|
+
positional.push(argument);
|
|
1486
|
+
}
|
|
1487
|
+
}
|
|
1488
|
+
|
|
1489
|
+
return {
|
|
1490
|
+
positional,
|
|
1491
|
+
globalInstall,
|
|
1492
|
+
pushUpdate,
|
|
1493
|
+
requestedVersion,
|
|
1494
|
+
selectedProject,
|
|
1495
|
+
checkProject,
|
|
1496
|
+
agentsTargets: [...new Set(agentsTargets)]
|
|
1497
|
+
};
|
|
1498
|
+
}
|
|
1499
|
+
|
|
1500
|
+
function listSkills() {
|
|
1501
|
+
for (const name of availableSkills()) {
|
|
1502
|
+
const metadata = skillMetadata(name);
|
|
1503
|
+
console.log(`${name}@${metadata.version} — ${metadata.author}`);
|
|
1504
|
+
}
|
|
1505
|
+
}
|
|
1506
|
+
|
|
1507
|
+
function packageDependencySection(manifest) {
|
|
1508
|
+
for (const section of ['dependencies', 'devDependencies', 'optionalDependencies', 'peerDependencies']) {
|
|
1509
|
+
if (Object.hasOwn(manifest?.[section] ?? {}, 'skillshop')) return section;
|
|
1510
|
+
}
|
|
1511
|
+
return null;
|
|
1512
|
+
}
|
|
1513
|
+
|
|
1514
|
+
function localPackageOwner() {
|
|
1515
|
+
const installedPackageRoot = canonicalPath(packageRoot);
|
|
1516
|
+
let current = path.dirname(installedPackageRoot);
|
|
1517
|
+
while (true) {
|
|
1518
|
+
const manifestPath = path.join(current, 'package.json');
|
|
1519
|
+
if (fs.existsSync(manifestPath)) {
|
|
1520
|
+
try {
|
|
1521
|
+
const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
|
|
1522
|
+
const section = packageDependencySection(manifest);
|
|
1523
|
+
const declaredInstall = path.join(current, 'node_modules', 'skillshop');
|
|
1524
|
+
if (section && fs.existsSync(declaredInstall) && canonicalPath(declaredInstall) === installedPackageRoot) {
|
|
1525
|
+
return { root: current, section };
|
|
1526
|
+
}
|
|
1527
|
+
} catch (error) {
|
|
1528
|
+
if (error instanceof SyntaxError) throw new CliError(`cannot parse ${manifestPath}: ${error.message}`);
|
|
1529
|
+
throw error;
|
|
1530
|
+
}
|
|
1531
|
+
}
|
|
1532
|
+
|
|
1533
|
+
const parent = path.dirname(current);
|
|
1534
|
+
if (parent === current) return null;
|
|
1535
|
+
current = parent;
|
|
1536
|
+
}
|
|
1537
|
+
}
|
|
1538
|
+
|
|
1539
|
+
function npmGlobalRoot() {
|
|
1540
|
+
try {
|
|
1541
|
+
return path.resolve(execFileSync('npm', ['root', '--global'], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] }).trim());
|
|
1542
|
+
} catch (error) {
|
|
1543
|
+
const detail = error.stderr?.toString().trim();
|
|
1544
|
+
throw new CliError(`cannot determine the active npm global package directory${detail ? `: ${detail}` : ''}`);
|
|
1545
|
+
}
|
|
1546
|
+
}
|
|
1547
|
+
|
|
1548
|
+
function npmPrefixFromRoot(globalRoot) {
|
|
1549
|
+
const parent = path.dirname(globalRoot);
|
|
1550
|
+
return path.basename(parent) === 'lib' ? path.dirname(parent) : parent;
|
|
1551
|
+
}
|
|
1552
|
+
|
|
1553
|
+
function resolveUpgradeScope() {
|
|
1554
|
+
const localOwner = localPackageOwner();
|
|
1555
|
+
if (localOwner) return { type: 'local', root: localOwner.root, dependencySection: localOwner.section, packageRoot };
|
|
1556
|
+
|
|
1557
|
+
const insideNodeModules = canonicalPath(packageRoot).split(path.sep).includes('node_modules');
|
|
1558
|
+
const activeGlobalRoot = npmGlobalRoot();
|
|
1559
|
+
if (canonicalPath(path.join(activeGlobalRoot, 'skillshop')) === canonicalPath(packageRoot)) {
|
|
1560
|
+
return { type: 'global', prefix: npmPrefixFromRoot(activeGlobalRoot), packageRoot };
|
|
1561
|
+
}
|
|
1562
|
+
if (insideNodeModules) {
|
|
1563
|
+
throw new CliError(
|
|
1564
|
+
`cannot match this installed Skillshop package to the active npm global prefix (${activeGlobalRoot}); no upgrade was run`
|
|
1565
|
+
);
|
|
1566
|
+
}
|
|
1567
|
+
|
|
1568
|
+
// Preserve the historical source-checkout behavior: install the package into
|
|
1569
|
+
// the active npm global prefix without editing the checkout's package files.
|
|
1570
|
+
return { type: 'global-checkout', packageRoot };
|
|
1571
|
+
}
|
|
1572
|
+
|
|
1573
|
+
function runUpgradeInstall(scope) {
|
|
1574
|
+
const args = scope.type === 'local'
|
|
1575
|
+
? ['install', `--save-${scope.dependencySection === 'dependencies' ? 'prod' : scope.dependencySection === 'devDependencies' ? 'dev' : scope.dependencySection === 'optionalDependencies' ? 'optional' : 'peer'}`, 'skillshop@latest']
|
|
1576
|
+
: scope.type === 'global'
|
|
1577
|
+
? ['install', '--global', '--prefix', scope.prefix, 'skillshop@latest']
|
|
1578
|
+
: ['install', '--global', 'skillshop@latest'];
|
|
1579
|
+
try {
|
|
1580
|
+
execFileSync('npm', args, {
|
|
1581
|
+
cwd: scope.type === 'local' ? scope.root : process.cwd(),
|
|
1582
|
+
stdio: 'inherit'
|
|
1583
|
+
});
|
|
1584
|
+
} catch (error) {
|
|
1585
|
+
const status = Number.isInteger(error.status) ? ` (exit ${error.status})` : '';
|
|
1586
|
+
throw new CliError(`npm upgrade failed${status}; managed copies were not refreshed`);
|
|
1587
|
+
}
|
|
1588
|
+
}
|
|
1589
|
+
|
|
1590
|
+
function freshUpgradePackageRoot(scope) {
|
|
1591
|
+
if (scope.type !== 'global-checkout') return scope.packageRoot;
|
|
1592
|
+
return path.join(npmGlobalRoot(), 'skillshop');
|
|
1593
|
+
}
|
|
1594
|
+
|
|
1595
|
+
function upgrade() {
|
|
1596
|
+
const scope = resolveUpgradeScope();
|
|
1597
|
+
const scopeLabel = scope.type === 'local'
|
|
1598
|
+
? `project dependency in ${scope.root}`
|
|
1599
|
+
: scope.type === 'global'
|
|
1600
|
+
? `global installation at ${scope.packageRoot}`
|
|
1601
|
+
: 'global installation (source-checkout compatibility mode)';
|
|
1602
|
+
console.log(`Upgrading Skillshop ${scopeLabel}...`);
|
|
1603
|
+
runUpgradeInstall(scope);
|
|
1604
|
+
|
|
1605
|
+
let freshCliPath;
|
|
1606
|
+
try {
|
|
1607
|
+
freshCliPath = path.join(freshUpgradePackageRoot(scope), 'src', 'cli.js');
|
|
1608
|
+
if (!fs.statSync(freshCliPath).isFile()) throw new Error('not a file');
|
|
1609
|
+
} catch {
|
|
1610
|
+
throw new CliError('npm upgrade succeeded, but the newly installed Skillshop CLI could not be found for managed-copy refresh');
|
|
1611
|
+
}
|
|
1612
|
+
|
|
1613
|
+
const child = spawnSync(process.execPath, [freshCliPath, internalUpgradeRefreshCommand], {
|
|
1614
|
+
cwd: scope.type === 'local' ? scope.root : process.cwd(),
|
|
1615
|
+
env: {
|
|
1616
|
+
...process.env,
|
|
1617
|
+
SKILLSHOP_INTERNAL_UPGRADE_TOKEN: internalUpgradeRefreshToken,
|
|
1618
|
+
SKILLSHOP_INTERNAL_UPGRADE_SCOPE: scope.type === 'local' ? 'local' : 'global'
|
|
1619
|
+
},
|
|
1620
|
+
stdio: 'inherit'
|
|
1621
|
+
});
|
|
1622
|
+
if (child.error) {
|
|
1623
|
+
throw new CliError(`npm upgrade succeeded, but fresh Skillshop managed-copy refresh could not start: ${child.error.message}`);
|
|
1624
|
+
}
|
|
1625
|
+
if (child.status !== 0) {
|
|
1626
|
+
const status = child.status === null ? 'without an exit status' : `with exit ${child.status}`;
|
|
1627
|
+
throw new CliError(`npm upgrade succeeded, but fresh Skillshop managed-copy refresh failed ${status}`);
|
|
1628
|
+
}
|
|
1629
|
+
}
|
|
1630
|
+
|
|
1631
|
+
async function main(argv) {
|
|
1632
|
+
if (argv.length === 0 || argv.includes('--help') || argv.includes('-h')) {
|
|
1633
|
+
printHelp();
|
|
1634
|
+
return;
|
|
1635
|
+
}
|
|
1636
|
+
if (argv.length === 1 && (argv[0] === '--version' || argv[0] === '-v')) {
|
|
1637
|
+
console.log(packageJson.version);
|
|
1638
|
+
return;
|
|
1639
|
+
}
|
|
1640
|
+
|
|
1641
|
+
const { positional, globalInstall, pushUpdate, requestedVersion, selectedProject, checkProject, agentsTargets } = parseArgs(argv);
|
|
1642
|
+
const command = positional[0];
|
|
1643
|
+
|
|
1644
|
+
if ((selectedProject || checkProject) && command !== 'agents') {
|
|
1645
|
+
throw new CliError('--project and --check are only valid with agents');
|
|
1646
|
+
}
|
|
1647
|
+
|
|
1648
|
+
if (command === internalUpgradeRefreshCommand) {
|
|
1649
|
+
if (
|
|
1650
|
+
process.env.SKILLSHOP_INTERNAL_UPGRADE_TOKEN !== internalUpgradeRefreshToken ||
|
|
1651
|
+
positional.length !== 1 ||
|
|
1652
|
+
requestedVersion || globalInstall || pushUpdate || selectedProject || checkProject || agentsTargets.length
|
|
1653
|
+
) {
|
|
1654
|
+
throw new CliError('invalid internal upgrade continuation');
|
|
1655
|
+
}
|
|
1656
|
+
refreshManagedCopies(process.env.SKILLSHOP_INTERNAL_UPGRADE_SCOPE);
|
|
1657
|
+
return;
|
|
1658
|
+
}
|
|
1659
|
+
|
|
1660
|
+
if (command === 'init') {
|
|
1661
|
+
if (positional.length !== 1 || requestedVersion || globalInstall || pushUpdate || agentsTargets.length) {
|
|
1662
|
+
throw new CliError('init does not accept arguments or options');
|
|
1663
|
+
}
|
|
1664
|
+
await initialize();
|
|
1665
|
+
return;
|
|
1666
|
+
}
|
|
1667
|
+
|
|
1668
|
+
if (command === 'config') {
|
|
1669
|
+
if (positional.length !== 2 || positional[1] !== 'agents' || requestedVersion || globalInstall || pushUpdate || agentsTargets.length) {
|
|
1670
|
+
throw new CliError('config expects `agents` and does not accept options');
|
|
1671
|
+
}
|
|
1672
|
+
await configureAgents();
|
|
1673
|
+
return;
|
|
1674
|
+
}
|
|
1675
|
+
|
|
1676
|
+
if (command === 'list' || command === 'ls') {
|
|
1677
|
+
if (positional.length !== 1 || requestedVersion || globalInstall || pushUpdate || agentsTargets.length) {
|
|
1678
|
+
throw new CliError('list does not accept skill or install options');
|
|
1679
|
+
}
|
|
1680
|
+
listSkills();
|
|
1681
|
+
return;
|
|
1682
|
+
}
|
|
1683
|
+
|
|
1684
|
+
if (command === 'upgrade') {
|
|
1685
|
+
if (positional.length !== 1 || requestedVersion || globalInstall || pushUpdate || agentsTargets.length) {
|
|
1686
|
+
throw new CliError('upgrade does not accept arguments or options');
|
|
1687
|
+
}
|
|
1688
|
+
upgrade();
|
|
1689
|
+
return;
|
|
1690
|
+
}
|
|
1691
|
+
|
|
1692
|
+
if (command === 'update' || command === 'up') {
|
|
1693
|
+
if (agentsTargets.length) throw new CliError('--codex and --copilot are only valid with agents');
|
|
1694
|
+
if (globalInstall) throw new CliError('global updates are not tracked; reinstall the global skill by name');
|
|
1695
|
+
if (requestedVersion) throw new CliError('update uses the current registry versions and does not accept --version');
|
|
1696
|
+
if (positional.length > 2) throw new CliError('update accepts at most one locked skill name');
|
|
1697
|
+
if (pushUpdate && positional.length !== 1) {
|
|
1698
|
+
throw new CliError('push update does not accept a skill name');
|
|
1699
|
+
}
|
|
1700
|
+
if (pushUpdate) updateAllRepositories();
|
|
1701
|
+
else updateLocal(positional[1] ?? null, process.cwd());
|
|
1702
|
+
return;
|
|
1703
|
+
}
|
|
1704
|
+
|
|
1705
|
+
if (command === 'migrate') {
|
|
1706
|
+
if (requestedVersion || globalInstall || pushUpdate || agentsTargets.length) {
|
|
1707
|
+
throw new CliError('migrate does not accept install or update options');
|
|
1708
|
+
}
|
|
1709
|
+
migrateRepositories(positional.slice(1), process.cwd());
|
|
1710
|
+
return;
|
|
1711
|
+
}
|
|
1712
|
+
|
|
1713
|
+
if (command === 'agents') {
|
|
1714
|
+
const initializeProject = positional.length === 2 && positional[1] === 'init';
|
|
1715
|
+
if (
|
|
1716
|
+
positional.length > 2 ||
|
|
1717
|
+
(positional.length === 2 && !initializeProject) ||
|
|
1718
|
+
requestedVersion ||
|
|
1719
|
+
pushUpdate ||
|
|
1720
|
+
agentsTargets.length
|
|
1721
|
+
) {
|
|
1722
|
+
throw new CliError('agents does not accept skill or update options');
|
|
1723
|
+
}
|
|
1724
|
+
if (globalInstall) {
|
|
1725
|
+
if (initializeProject || selectedProject || checkProject) {
|
|
1726
|
+
throw new CliError('--global cannot be combined with project agents options');
|
|
1727
|
+
}
|
|
1728
|
+
installAgents(process.cwd(), true);
|
|
1729
|
+
return;
|
|
1730
|
+
}
|
|
1731
|
+
if (initializeProject && checkProject) throw new CliError('agents init does not accept --check');
|
|
1732
|
+
if (initializeProject || checkProject || projectAgentsConfigured(process.cwd(), selectedProject)) {
|
|
1733
|
+
renderProjectAgents(process.cwd(), selectedProject, {
|
|
1734
|
+
initialize: initializeProject,
|
|
1735
|
+
check: checkProject
|
|
1736
|
+
});
|
|
1737
|
+
} else {
|
|
1738
|
+
installAgents(process.cwd(), false, selectedProject);
|
|
1739
|
+
}
|
|
1740
|
+
return;
|
|
1741
|
+
}
|
|
1742
|
+
|
|
1743
|
+
const uninstallAliases = new Set(['uninstall', 'remove', 'rm']);
|
|
1744
|
+
if (uninstallAliases.has(command)) {
|
|
1745
|
+
if (agentsTargets.length) throw new CliError('--codex and --copilot are only valid with agents');
|
|
1746
|
+
if (requestedVersion) throw new CliError('uninstall does not accept --version');
|
|
1747
|
+
if (pushUpdate) throw new CliError('--push is only valid with update');
|
|
1748
|
+
if (positional.length !== 2) throw new CliError('uninstall expects exactly one skill name');
|
|
1749
|
+
|
|
1750
|
+
const name = positional[1];
|
|
1751
|
+
validateSkillName(name);
|
|
1752
|
+
if (globalInstall) uninstallGlobal(name);
|
|
1753
|
+
else uninstallLocal(name, process.cwd());
|
|
1754
|
+
return;
|
|
1755
|
+
}
|
|
1756
|
+
|
|
1757
|
+
const installAliases = new Set(['install', 'i', 'add']);
|
|
1758
|
+
if (agentsTargets.length) throw new CliError('--codex and --copilot are only valid with agents');
|
|
1759
|
+
const spec = installAliases.has(command) ? positional[1] : command;
|
|
1760
|
+
const expectedLength = installAliases.has(command) ? 2 : 1;
|
|
1761
|
+
if (!spec || positional.length !== expectedLength) {
|
|
1762
|
+
throw new CliError('expected exactly one skill name');
|
|
1763
|
+
}
|
|
1764
|
+
if (pushUpdate) throw new CliError('--push is only valid with update');
|
|
1765
|
+
|
|
1766
|
+
const { name, version } = parseSkillSpec(spec, requestedVersion);
|
|
1767
|
+
validateSkillName(name);
|
|
1768
|
+
if (globalInstall) installGlobal(name, version);
|
|
1769
|
+
else installLocal(name, version, process.cwd());
|
|
1770
|
+
}
|
|
1771
|
+
|
|
1772
|
+
function isIsolatedAgentsInvocation(argv) {
|
|
1773
|
+
// Preserve the top-level help/version routing before looking for a command.
|
|
1774
|
+
// main() handles help anywhere and the standalone version form without parsing.
|
|
1775
|
+
if (argv[0] === '--help' || argv[0] === '-h') return false;
|
|
1776
|
+
if (argv.length === 1 && (argv[0] === '--version' || argv[0] === '-v')) return false;
|
|
1777
|
+
|
|
1778
|
+
// parseArgs accepts project options on either side of the command. Ignore help
|
|
1779
|
+
// flags here because main() handles them before parsing, then classify using
|
|
1780
|
+
// the same positional-command rules as execution.
|
|
1781
|
+
try {
|
|
1782
|
+
const args = argv.filter((argument) => argument !== '--help' && argument !== '-h');
|
|
1783
|
+
return parseArgs(args).positional[0] === 'agents';
|
|
1784
|
+
} catch {
|
|
1785
|
+
return false;
|
|
1786
|
+
}
|
|
1787
|
+
}
|
|
1788
|
+
|
|
1789
|
+
function isUpgradeFlowInvocation(argv) {
|
|
1790
|
+
if (argv.includes('--help') || argv.includes('-h')) return false;
|
|
1791
|
+
if (
|
|
1792
|
+
argv[0] === internalUpgradeRefreshCommand &&
|
|
1793
|
+
process.env.SKILLSHOP_INTERNAL_UPGRADE_TOKEN === internalUpgradeRefreshToken
|
|
1794
|
+
) return true;
|
|
1795
|
+
try {
|
|
1796
|
+
return parseArgs(argv).positional[0] === 'upgrade';
|
|
1797
|
+
} catch {
|
|
1798
|
+
return false;
|
|
1799
|
+
}
|
|
1800
|
+
}
|
|
1801
|
+
|
|
1802
|
+
export async function runLegacyCli(argv) {
|
|
1803
|
+
const isolatedAgentsCommand = isIsolatedAgentsInvocation(argv);
|
|
1804
|
+
const upgradeFlow = isUpgradeFlowInvocation(argv);
|
|
1805
|
+
const skipLegacyReconciliation = isolatedAgentsCommand || upgradeFlow;
|
|
1806
|
+
try {
|
|
1807
|
+
if (!skipLegacyReconciliation) reconcileCurrentRepository();
|
|
1808
|
+
try {
|
|
1809
|
+
await main(argv);
|
|
1810
|
+
} finally {
|
|
1811
|
+
if (!skipLegacyReconciliation) reconcileCurrentRepository();
|
|
1812
|
+
}
|
|
1813
|
+
} catch (error) {
|
|
1814
|
+
if (!(error instanceof CliError) && !(error instanceof AgentsInstructionsError)) throw error;
|
|
1815
|
+
console.error(`skillshop: ${error.message}`);
|
|
1816
|
+
process.exitCode = 1;
|
|
1817
|
+
}
|
|
1818
|
+
}
|
|
1819
|
+
|
|
1820
|
+
export function reconcileLegacyState() {
|
|
1821
|
+
reconcileCurrentRepository();
|
|
1822
|
+
}
|