@alavida/agentpack 0.1.1
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 +269 -0
- package/bin/agentpack.js +5 -0
- package/package.json +54 -0
- package/skills/agentpack-cli/SKILL.md +136 -0
- package/skills/agentpack-cli/references/knowledge-as-package.md +48 -0
- package/skills/agentpack-cli/references/plugin-lifecycle.md +58 -0
- package/skills/agentpack-cli/references/skill-lifecycle.md +78 -0
- package/src/cli.js +82 -0
- package/src/commands/plugin.js +169 -0
- package/src/commands/skills.js +491 -0
- package/src/lib/context.js +167 -0
- package/src/lib/plugins.js +414 -0
- package/src/lib/skills.js +1900 -0
- package/src/utils/errors.js +67 -0
- package/src/utils/output.js +61 -0
|
@@ -0,0 +1,1900 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, readdirSync, rmSync, symlinkSync, watch, writeFileSync } from 'node:fs';
|
|
2
|
+
import { dirname, isAbsolute, join, relative, resolve } from 'node:path';
|
|
3
|
+
import { createHash } from 'node:crypto';
|
|
4
|
+
import { execFileSync } from 'node:child_process';
|
|
5
|
+
import { findAllWorkbenches, findRepoRoot, findWorkbenchContext, resolveWorkbenchFlag } from './context.js';
|
|
6
|
+
import { AgentpackError, EXIT_CODES, NetworkError, NotFoundError, ValidationError } from '../utils/errors.js';
|
|
7
|
+
|
|
8
|
+
const GITHUB_PACKAGES_REGISTRY = 'https://npm.pkg.github.com';
|
|
9
|
+
const MANAGED_PACKAGE_SCOPES = ['@alavida', '@alavida-ai'];
|
|
10
|
+
|
|
11
|
+
function isManagedPackageName(packageName) {
|
|
12
|
+
return typeof packageName === 'string'
|
|
13
|
+
&& MANAGED_PACKAGE_SCOPES.some((scope) => packageName.startsWith(`${scope}/`));
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function inferManagedScope(packageName) {
|
|
17
|
+
return MANAGED_PACKAGE_SCOPES.find((scope) => packageName?.startsWith(`${scope}/`)) || null;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function parseScalar(value) {
|
|
21
|
+
const trimmed = value.trim();
|
|
22
|
+
if (
|
|
23
|
+
(trimmed.startsWith('"') && trimmed.endsWith('"')) ||
|
|
24
|
+
(trimmed.startsWith("'") && trimmed.endsWith("'"))
|
|
25
|
+
) {
|
|
26
|
+
return trimmed.slice(1, -1);
|
|
27
|
+
}
|
|
28
|
+
return trimmed;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function foldBlockScalar(lines, startIndex, baseIndent) {
|
|
32
|
+
const values = [];
|
|
33
|
+
let index = startIndex + 1;
|
|
34
|
+
|
|
35
|
+
while (index < lines.length) {
|
|
36
|
+
const rawLine = lines[index];
|
|
37
|
+
if (!rawLine.trim()) {
|
|
38
|
+
values.push('');
|
|
39
|
+
index += 1;
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const indentMatch = rawLine.match(/^(\s*)/);
|
|
44
|
+
const indent = indentMatch ? indentMatch[1].length : 0;
|
|
45
|
+
if (indent <= baseIndent) break;
|
|
46
|
+
|
|
47
|
+
values.push(rawLine.slice(baseIndent + 2).trimEnd());
|
|
48
|
+
index += 1;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const folded = values
|
|
52
|
+
.join('\n')
|
|
53
|
+
.split('\n\n')
|
|
54
|
+
.map((chunk) => chunk.split('\n').join(' ').trim())
|
|
55
|
+
.filter((chunk, idx, arr) => chunk.length > 0 || idx < arr.length - 1)
|
|
56
|
+
.join('\n\n')
|
|
57
|
+
.trim();
|
|
58
|
+
|
|
59
|
+
return { value: folded, nextIndex: index };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function ensureContainer(target, key) {
|
|
63
|
+
if (!target[key] || typeof target[key] !== 'object' || Array.isArray(target[key])) {
|
|
64
|
+
target[key] = {};
|
|
65
|
+
}
|
|
66
|
+
return target[key];
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function parseSkillFrontmatterFile(skillFilePath) {
|
|
70
|
+
if (!existsSync(skillFilePath)) {
|
|
71
|
+
throw new NotFoundError(`skill file not found: ${skillFilePath}`, { code: 'skill_not_found' });
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const content = readFileSync(skillFilePath, 'utf-8');
|
|
75
|
+
if (!content.startsWith('---\n')) {
|
|
76
|
+
throw new ValidationError('SKILL.md missing frontmatter', { code: 'missing_frontmatter' });
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const fmEnd = content.indexOf('\n---', 4);
|
|
80
|
+
if (fmEnd === -1) {
|
|
81
|
+
throw new ValidationError('SKILL.md has unclosed frontmatter', { code: 'unclosed_frontmatter' });
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const lines = content.slice(4, fmEnd).split('\n');
|
|
85
|
+
const fields = {};
|
|
86
|
+
let activeArrayKey = null;
|
|
87
|
+
let activeArrayTarget = null;
|
|
88
|
+
let activeParentKey = null;
|
|
89
|
+
|
|
90
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
91
|
+
const rawLine = lines[index];
|
|
92
|
+
const line = rawLine.trimEnd();
|
|
93
|
+
if (!line.trim()) continue;
|
|
94
|
+
|
|
95
|
+
const listMatch = rawLine.match(/^(\s*)-\s+(.+)$/);
|
|
96
|
+
if (listMatch && activeArrayKey && activeArrayTarget) {
|
|
97
|
+
activeArrayTarget[activeArrayKey].push(parseScalar(listMatch[2]));
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const nestedKeyMatch = rawLine.match(/^\s{2}([A-Za-z][\w-]*):\s*(.*)$/);
|
|
102
|
+
if (nestedKeyMatch && activeParentKey) {
|
|
103
|
+
const [, key, value] = nestedKeyMatch;
|
|
104
|
+
const parent = ensureContainer(fields, activeParentKey);
|
|
105
|
+
if (value === '') {
|
|
106
|
+
parent[key] = [];
|
|
107
|
+
activeArrayKey = key;
|
|
108
|
+
activeArrayTarget = parent;
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
parent[key] = parseScalar(value);
|
|
113
|
+
activeArrayKey = null;
|
|
114
|
+
activeArrayTarget = null;
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const keyMatch = rawLine.match(/^([A-Za-z][\w-]*):\s*(.*)$/);
|
|
119
|
+
if (!keyMatch) continue;
|
|
120
|
+
|
|
121
|
+
const [, key, value] = keyMatch;
|
|
122
|
+
if (value === '>' || value === '|') {
|
|
123
|
+
const { value: blockValue, nextIndex } = foldBlockScalar(lines, index, 0);
|
|
124
|
+
fields[key] = blockValue;
|
|
125
|
+
activeParentKey = null;
|
|
126
|
+
activeArrayKey = null;
|
|
127
|
+
activeArrayTarget = null;
|
|
128
|
+
index = nextIndex - 1;
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (value === '') {
|
|
133
|
+
fields[key] = fields[key] && typeof fields[key] === 'object' && !Array.isArray(fields[key])
|
|
134
|
+
? fields[key]
|
|
135
|
+
: [];
|
|
136
|
+
activeParentKey = key;
|
|
137
|
+
activeArrayKey = Array.isArray(fields[key]) ? key : null;
|
|
138
|
+
activeArrayTarget = Array.isArray(fields[key]) ? fields : null;
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
fields[key] = parseScalar(value);
|
|
143
|
+
activeParentKey = null;
|
|
144
|
+
activeArrayKey = null;
|
|
145
|
+
activeArrayTarget = null;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (!fields.name) {
|
|
149
|
+
throw new ValidationError('SKILL.md frontmatter missing "name" field', { code: 'missing_name' });
|
|
150
|
+
}
|
|
151
|
+
if (!fields.description) {
|
|
152
|
+
throw new ValidationError('SKILL.md frontmatter missing "description" field', { code: 'missing_description' });
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
return {
|
|
156
|
+
name: fields.name,
|
|
157
|
+
description: fields.description,
|
|
158
|
+
sources: Array.isArray(fields.metadata?.sources)
|
|
159
|
+
? fields.metadata.sources
|
|
160
|
+
: (Array.isArray(fields.sources) ? fields.sources : []),
|
|
161
|
+
requires: Array.isArray(fields.metadata?.requires)
|
|
162
|
+
? fields.metadata.requires
|
|
163
|
+
: (Array.isArray(fields.requires) ? fields.requires : []),
|
|
164
|
+
status: typeof fields.metadata?.status === 'string' ? fields.metadata.status : null,
|
|
165
|
+
replacement: typeof fields.metadata?.replacement === 'string' ? fields.metadata.replacement : null,
|
|
166
|
+
message: typeof fields.metadata?.message === 'string' ? fields.metadata.message : null,
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function normalizeDisplayPath(repoRoot, absolutePath) {
|
|
171
|
+
return relative(repoRoot, absolutePath).split('\\').join('/');
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function ensureDir(pathValue) {
|
|
175
|
+
mkdirSync(pathValue, { recursive: true });
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function ensureSkillLink(repoRoot, baseDir, skillName, skillDir) {
|
|
179
|
+
const skillsDir = join(repoRoot, baseDir, 'skills');
|
|
180
|
+
ensureDir(skillsDir);
|
|
181
|
+
const linkPath = join(skillsDir, skillName);
|
|
182
|
+
removePathIfExists(linkPath);
|
|
183
|
+
symlinkSync(skillDir, linkPath, 'dir');
|
|
184
|
+
return normalizeDisplayPath(repoRoot, linkPath);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function resolveDevLinkedSkills(repoRoot, rootSkillDir) {
|
|
188
|
+
const queue = [rootSkillDir];
|
|
189
|
+
const seenDirs = new Set();
|
|
190
|
+
const linkedSkills = [];
|
|
191
|
+
const unresolved = new Set();
|
|
192
|
+
|
|
193
|
+
while (queue.length > 0) {
|
|
194
|
+
const skillDir = queue.shift();
|
|
195
|
+
if (seenDirs.has(skillDir)) continue;
|
|
196
|
+
seenDirs.add(skillDir);
|
|
197
|
+
|
|
198
|
+
const skillFile = join(skillDir, 'SKILL.md');
|
|
199
|
+
const metadata = parseSkillFrontmatterFile(skillFile);
|
|
200
|
+
const packageMetadata = readPackageMetadata(skillDir);
|
|
201
|
+
|
|
202
|
+
linkedSkills.push({
|
|
203
|
+
name: metadata.name,
|
|
204
|
+
skillDir,
|
|
205
|
+
requires: metadata.requires,
|
|
206
|
+
packageName: packageMetadata.packageName,
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
for (const requirement of metadata.requires) {
|
|
210
|
+
const dependencyDir = findPackageDirByName(repoRoot, requirement)
|
|
211
|
+
|| join(repoRoot, 'node_modules', ...requirement.split('/'));
|
|
212
|
+
if (!existsSync(dependencyDir)) {
|
|
213
|
+
unresolved.add(requirement);
|
|
214
|
+
continue;
|
|
215
|
+
}
|
|
216
|
+
if (!existsSync(join(dependencyDir, 'SKILL.md'))) continue;
|
|
217
|
+
if (!existsSync(join(dependencyDir, 'package.json'))) continue;
|
|
218
|
+
queue.push(dependencyDir);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
linkedSkills.sort((a, b) => a.name.localeCompare(b.name));
|
|
223
|
+
return {
|
|
224
|
+
linkedSkills,
|
|
225
|
+
unresolved: [...unresolved].sort((a, b) => a.localeCompare(b)),
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function resolveLocalPackagedSkillDir(repoRoot, target) {
|
|
230
|
+
const skillFile = resolveSkillFileTarget(repoRoot, target);
|
|
231
|
+
if (!skillFile) {
|
|
232
|
+
throw new AgentpackError(`SKILL.md not found for target: ${target}`, {
|
|
233
|
+
code: 'skill_not_found',
|
|
234
|
+
exitCode: EXIT_CODES.GENERAL,
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
const skillDir = dirname(skillFile);
|
|
239
|
+
const packageJsonPath = join(skillDir, 'package.json');
|
|
240
|
+
if (!existsSync(packageJsonPath)) {
|
|
241
|
+
throw new AgentpackError(`package.json not found: ${packageJsonPath}`, {
|
|
242
|
+
code: 'package_json_not_found',
|
|
243
|
+
exitCode: EXIT_CODES.GENERAL,
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
return { skillDir, skillFile, packageJsonPath };
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function resolveSkillFileTarget(repoRoot, target) {
|
|
251
|
+
const absoluteTarget = isAbsolute(target) ? target : resolve(repoRoot, target);
|
|
252
|
+
|
|
253
|
+
if (existsSync(absoluteTarget)) {
|
|
254
|
+
if (absoluteTarget.endsWith('SKILL.md')) return absoluteTarget;
|
|
255
|
+
const skillFile = join(absoluteTarget, 'SKILL.md');
|
|
256
|
+
if (existsSync(skillFile)) return skillFile;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
return null;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
export function findPackageDirByName(repoRoot, packageName) {
|
|
263
|
+
const stack = [repoRoot];
|
|
264
|
+
|
|
265
|
+
while (stack.length > 0) {
|
|
266
|
+
const current = stack.pop();
|
|
267
|
+
let entries = [];
|
|
268
|
+
try {
|
|
269
|
+
entries = readdirSync(current, { withFileTypes: true });
|
|
270
|
+
} catch {
|
|
271
|
+
continue;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
for (const entry of entries) {
|
|
275
|
+
if (entry.name === '.git' || entry.name === 'node_modules') continue;
|
|
276
|
+
|
|
277
|
+
const fullPath = join(current, entry.name);
|
|
278
|
+
if (entry.isDirectory()) {
|
|
279
|
+
stack.push(fullPath);
|
|
280
|
+
continue;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
if (entry.name !== 'package.json') continue;
|
|
284
|
+
|
|
285
|
+
try {
|
|
286
|
+
const pkg = JSON.parse(readFileSync(fullPath, 'utf-8'));
|
|
287
|
+
if (pkg.name === packageName) {
|
|
288
|
+
return dirname(fullPath);
|
|
289
|
+
}
|
|
290
|
+
} catch {
|
|
291
|
+
// Ignore invalid package files outside the current target set.
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
return null;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
export function readPackageMetadata(packageDir) {
|
|
300
|
+
const packageJsonPath = join(packageDir, 'package.json');
|
|
301
|
+
if (!existsSync(packageJsonPath)) {
|
|
302
|
+
return {
|
|
303
|
+
packageName: null,
|
|
304
|
+
packageVersion: null,
|
|
305
|
+
dependencies: {},
|
|
306
|
+
devDependencies: {},
|
|
307
|
+
files: null,
|
|
308
|
+
repository: null,
|
|
309
|
+
publishConfigRegistry: null,
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
const pkg = JSON.parse(readFileSync(packageJsonPath, 'utf-8'));
|
|
314
|
+
return {
|
|
315
|
+
packageName: pkg.name || null,
|
|
316
|
+
packageVersion: pkg.version || null,
|
|
317
|
+
dependencies: pkg.dependencies || {},
|
|
318
|
+
devDependencies: pkg.devDependencies || {},
|
|
319
|
+
files: Array.isArray(pkg.files) ? pkg.files : null,
|
|
320
|
+
repository: pkg.repository || null,
|
|
321
|
+
publishConfigRegistry: pkg.publishConfig?.registry || null,
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
function readPackageJson(packageDir) {
|
|
326
|
+
const packageJsonPath = join(packageDir, 'package.json');
|
|
327
|
+
if (!existsSync(packageJsonPath)) {
|
|
328
|
+
throw new NotFoundError(`package.json not found: ${packageJsonPath}`, {
|
|
329
|
+
code: 'package_json_not_found',
|
|
330
|
+
exitCode: EXIT_CODES.GENERAL,
|
|
331
|
+
});
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
return {
|
|
335
|
+
packageJsonPath,
|
|
336
|
+
packageJson: JSON.parse(readFileSync(packageJsonPath, 'utf-8')),
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
export function syncSkillDependencies(skillDir) {
|
|
341
|
+
const skillFile = join(skillDir, 'SKILL.md');
|
|
342
|
+
const metadata = parseSkillFrontmatterFile(skillFile);
|
|
343
|
+
const { packageJsonPath, packageJson } = readPackageJson(skillDir);
|
|
344
|
+
const nextDependencies = { ...(packageJson.dependencies || {}) };
|
|
345
|
+
const required = [...new Set(metadata.requires || [])].sort((a, b) => a.localeCompare(b));
|
|
346
|
+
const requiredSet = new Set(required);
|
|
347
|
+
const added = [];
|
|
348
|
+
const removed = [];
|
|
349
|
+
|
|
350
|
+
for (const packageName of required) {
|
|
351
|
+
if (!nextDependencies[packageName]) {
|
|
352
|
+
nextDependencies[packageName] = '*';
|
|
353
|
+
added.push(packageName);
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
for (const packageName of Object.keys(nextDependencies).sort((a, b) => a.localeCompare(b))) {
|
|
358
|
+
if (!isManagedPackageName(packageName)) continue;
|
|
359
|
+
if (requiredSet.has(packageName)) continue;
|
|
360
|
+
delete nextDependencies[packageName];
|
|
361
|
+
removed.push(packageName);
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
const previousSerialized = JSON.stringify(packageJson.dependencies || {});
|
|
365
|
+
const nextSerialized = JSON.stringify(nextDependencies);
|
|
366
|
+
if (previousSerialized !== nextSerialized) {
|
|
367
|
+
packageJson.dependencies = nextDependencies;
|
|
368
|
+
writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 2) + '\n');
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
return {
|
|
372
|
+
skillDir,
|
|
373
|
+
packageJsonPath,
|
|
374
|
+
added,
|
|
375
|
+
removed,
|
|
376
|
+
unchanged: added.length === 0 && removed.length === 0,
|
|
377
|
+
};
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
export function devSkill(target, {
|
|
381
|
+
cwd = process.cwd(),
|
|
382
|
+
sync = true,
|
|
383
|
+
} = {}) {
|
|
384
|
+
const repoRoot = findRepoRoot(cwd);
|
|
385
|
+
try {
|
|
386
|
+
const { skillDir } = resolveLocalPackagedSkillDir(repoRoot, target);
|
|
387
|
+
const { linkedSkills, unresolved } = resolveDevLinkedSkills(repoRoot, skillDir);
|
|
388
|
+
const rootSkill = linkedSkills.find((entry) => entry.skillDir === skillDir);
|
|
389
|
+
const synced = sync
|
|
390
|
+
? syncSkillDependencies(skillDir)
|
|
391
|
+
: {
|
|
392
|
+
skillDir,
|
|
393
|
+
packageJsonPath: join(skillDir, 'package.json'),
|
|
394
|
+
added: [],
|
|
395
|
+
removed: [],
|
|
396
|
+
unchanged: true,
|
|
397
|
+
};
|
|
398
|
+
const links = [];
|
|
399
|
+
|
|
400
|
+
for (const linkedSkill of linkedSkills) {
|
|
401
|
+
links.push(ensureSkillLink(repoRoot, '.claude', linkedSkill.name, linkedSkill.skillDir));
|
|
402
|
+
links.push(ensureSkillLink(repoRoot, '.agents', linkedSkill.name, linkedSkill.skillDir));
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
return {
|
|
406
|
+
name: rootSkill.name,
|
|
407
|
+
path: normalizeDisplayPath(repoRoot, skillDir),
|
|
408
|
+
linked: true,
|
|
409
|
+
links,
|
|
410
|
+
linkedSkills: linkedSkills.map((entry) => ({
|
|
411
|
+
name: entry.name,
|
|
412
|
+
path: normalizeDisplayPath(repoRoot, entry.skillDir),
|
|
413
|
+
packageName: entry.packageName,
|
|
414
|
+
})),
|
|
415
|
+
unresolved,
|
|
416
|
+
synced,
|
|
417
|
+
};
|
|
418
|
+
} catch (error) {
|
|
419
|
+
if (error instanceof AgentpackError && error.exitCode === EXIT_CODES.GENERAL) {
|
|
420
|
+
throw error;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
throw new AgentpackError(error.message, {
|
|
424
|
+
code: error.code || 'skill_dev_failed',
|
|
425
|
+
exitCode: EXIT_CODES.GENERAL,
|
|
426
|
+
});
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
function removeSkillLinks(repoRoot, name) {
|
|
431
|
+
const removed = [];
|
|
432
|
+
for (const pathValue of [
|
|
433
|
+
join(repoRoot, '.claude', 'skills', name),
|
|
434
|
+
join(repoRoot, '.agents', 'skills', name),
|
|
435
|
+
]) {
|
|
436
|
+
if (!existsSync(pathValue)) continue;
|
|
437
|
+
removePathIfExists(pathValue);
|
|
438
|
+
removed.push(normalizeDisplayPath(repoRoot, pathValue));
|
|
439
|
+
}
|
|
440
|
+
return removed;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
function removeSkillLinksByNames(repoRoot, names) {
|
|
444
|
+
const removed = [];
|
|
445
|
+
for (const name of names) {
|
|
446
|
+
removed.push(...removeSkillLinks(repoRoot, name));
|
|
447
|
+
}
|
|
448
|
+
return [...new Set(removed)];
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
export function startSkillDev(target, {
|
|
452
|
+
cwd = process.cwd(),
|
|
453
|
+
sync = true,
|
|
454
|
+
onStart = () => {},
|
|
455
|
+
onRebuild = () => {},
|
|
456
|
+
} = {}) {
|
|
457
|
+
const repoRoot = findRepoRoot(cwd);
|
|
458
|
+
const { skillDir } = resolveLocalPackagedSkillDir(repoRoot, target);
|
|
459
|
+
let closed = false;
|
|
460
|
+
let timer = null;
|
|
461
|
+
let currentNames = [];
|
|
462
|
+
let watcher = null;
|
|
463
|
+
|
|
464
|
+
const cleanup = () => {
|
|
465
|
+
if (closed) return { name: currentNames[0] || null, unlinked: false, removed: [] };
|
|
466
|
+
closed = true;
|
|
467
|
+
clearTimeout(timer);
|
|
468
|
+
if (watcher) watcher.close();
|
|
469
|
+
const removed = removeSkillLinksByNames(repoRoot, currentNames);
|
|
470
|
+
detachProcessCleanup();
|
|
471
|
+
return {
|
|
472
|
+
name: currentNames[0] || null,
|
|
473
|
+
unlinked: removed.length > 0,
|
|
474
|
+
removed,
|
|
475
|
+
};
|
|
476
|
+
};
|
|
477
|
+
|
|
478
|
+
const processCleanupHandlers = new Map();
|
|
479
|
+
const attachProcessCleanup = () => {
|
|
480
|
+
for (const eventName of ['exit', 'beforeExit', 'SIGINT', 'SIGTERM', 'SIGHUP']) {
|
|
481
|
+
const handler = () => {
|
|
482
|
+
cleanup();
|
|
483
|
+
};
|
|
484
|
+
processCleanupHandlers.set(eventName, handler);
|
|
485
|
+
process.once(eventName, handler);
|
|
486
|
+
}
|
|
487
|
+
};
|
|
488
|
+
|
|
489
|
+
const detachProcessCleanup = () => {
|
|
490
|
+
for (const [eventName, handler] of processCleanupHandlers.entries()) {
|
|
491
|
+
process.removeListener(eventName, handler);
|
|
492
|
+
}
|
|
493
|
+
processCleanupHandlers.clear();
|
|
494
|
+
};
|
|
495
|
+
|
|
496
|
+
const run = () => {
|
|
497
|
+
const result = devSkill(target, { cwd, sync });
|
|
498
|
+
const nextNames = result.linkedSkills.map((entry) => entry.name);
|
|
499
|
+
const staleNames = currentNames.filter((name) => !nextNames.includes(name));
|
|
500
|
+
if (staleNames.length > 0) {
|
|
501
|
+
removeSkillLinksByNames(repoRoot, staleNames);
|
|
502
|
+
}
|
|
503
|
+
currentNames = nextNames;
|
|
504
|
+
return result;
|
|
505
|
+
};
|
|
506
|
+
|
|
507
|
+
const initialResult = run();
|
|
508
|
+
onStart(initialResult);
|
|
509
|
+
|
|
510
|
+
attachProcessCleanup();
|
|
511
|
+
|
|
512
|
+
watcher = watch(skillDir, { recursive: true }, () => {
|
|
513
|
+
if (closed) return;
|
|
514
|
+
clearTimeout(timer);
|
|
515
|
+
timer = setTimeout(() => {
|
|
516
|
+
try {
|
|
517
|
+
const result = run();
|
|
518
|
+
onRebuild(result);
|
|
519
|
+
} catch (error) {
|
|
520
|
+
onRebuild({ error });
|
|
521
|
+
}
|
|
522
|
+
}, 100);
|
|
523
|
+
});
|
|
524
|
+
|
|
525
|
+
return {
|
|
526
|
+
initialResult,
|
|
527
|
+
close() {
|
|
528
|
+
return cleanup();
|
|
529
|
+
},
|
|
530
|
+
};
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
export function unlinkSkill(name, { cwd = process.cwd() } = {}) {
|
|
534
|
+
const repoRoot = findRepoRoot(cwd);
|
|
535
|
+
const existing = [
|
|
536
|
+
join(repoRoot, '.claude', 'skills', name),
|
|
537
|
+
join(repoRoot, '.agents', 'skills', name),
|
|
538
|
+
].filter((pathValue) => existsSync(pathValue));
|
|
539
|
+
|
|
540
|
+
if (existing.length === 0) {
|
|
541
|
+
throw new AgentpackError(`linked skill not found: ${name}`, {
|
|
542
|
+
code: 'linked_skill_not_found',
|
|
543
|
+
exitCode: EXIT_CODES.GENERAL,
|
|
544
|
+
});
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
const removed = removeSkillLinks(repoRoot, name);
|
|
548
|
+
|
|
549
|
+
return {
|
|
550
|
+
name,
|
|
551
|
+
unlinked: true,
|
|
552
|
+
removed,
|
|
553
|
+
};
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
export function normalizeRepoPath(repoRoot, absolutePath) {
|
|
557
|
+
return normalizeDisplayPath(repoRoot, absolutePath);
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
function buildValidateNextSteps(packageMetadata, valid) {
|
|
561
|
+
if (!valid || !packageMetadata.packageName) return [];
|
|
562
|
+
|
|
563
|
+
const steps = [
|
|
564
|
+
{
|
|
565
|
+
type: 'version',
|
|
566
|
+
command: 'npm version patch',
|
|
567
|
+
reason: 'assign the next package version after reviewed skill changes',
|
|
568
|
+
},
|
|
569
|
+
];
|
|
570
|
+
|
|
571
|
+
if (isManagedPackageName(packageMetadata.packageName)) {
|
|
572
|
+
steps.push({
|
|
573
|
+
type: 'publish',
|
|
574
|
+
command: 'npm publish',
|
|
575
|
+
registry: GITHUB_PACKAGES_REGISTRY,
|
|
576
|
+
reason: 'publish the versioned package to the private registry',
|
|
577
|
+
});
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
return steps;
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
function parseNpmRcFile(content) {
|
|
584
|
+
const config = {};
|
|
585
|
+
for (const rawLine of content.split('\n')) {
|
|
586
|
+
const line = rawLine.trim();
|
|
587
|
+
if (!line || line.startsWith('#') || line.startsWith(';')) continue;
|
|
588
|
+
const eqIndex = line.indexOf('=');
|
|
589
|
+
if (eqIndex === -1) continue;
|
|
590
|
+
const key = line.slice(0, eqIndex).trim();
|
|
591
|
+
const value = line.slice(eqIndex + 1).trim();
|
|
592
|
+
config[key] = value;
|
|
593
|
+
}
|
|
594
|
+
return config;
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
function readRepoNpmRegistryConfig(repoRoot, scope = null) {
|
|
598
|
+
const npmrcPath = join(repoRoot, '.npmrc');
|
|
599
|
+
const config = existsSync(npmrcPath)
|
|
600
|
+
? parseNpmRcFile(readFileSync(npmrcPath, 'utf-8'))
|
|
601
|
+
: {};
|
|
602
|
+
const scopes = scope ? [scope] : MANAGED_PACKAGE_SCOPES;
|
|
603
|
+
const matchedScope = scopes.find((candidate) => config[`${candidate}:registry`]) || scope || scopes[0] || null;
|
|
604
|
+
|
|
605
|
+
return {
|
|
606
|
+
npmrcPath: existsSync(npmrcPath) ? npmrcPath : null,
|
|
607
|
+
scope: matchedScope,
|
|
608
|
+
registry: matchedScope ? (config[`${matchedScope}:registry`] || null) : null,
|
|
609
|
+
authToken: config['//npm.pkg.github.com/:_authToken'] || null,
|
|
610
|
+
alwaysAuth: String(config['always-auth'] || '').toLowerCase() === 'true',
|
|
611
|
+
};
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
export function inspectRegistryConfig({
|
|
615
|
+
cwd = process.cwd(),
|
|
616
|
+
scope = null,
|
|
617
|
+
} = {}) {
|
|
618
|
+
const repoRoot = findRepoRoot(cwd);
|
|
619
|
+
const { npmrcPath, scope: resolvedScope, registry, authToken, alwaysAuth } = readRepoNpmRegistryConfig(
|
|
620
|
+
repoRoot,
|
|
621
|
+
scope
|
|
622
|
+
);
|
|
623
|
+
|
|
624
|
+
let auth = {
|
|
625
|
+
configured: false,
|
|
626
|
+
mode: 'missing',
|
|
627
|
+
key: null,
|
|
628
|
+
value: null,
|
|
629
|
+
};
|
|
630
|
+
|
|
631
|
+
if (authToken) {
|
|
632
|
+
const envMatch = authToken.match(/^\$\{([^}]+)\}$/);
|
|
633
|
+
if (envMatch) {
|
|
634
|
+
auth = {
|
|
635
|
+
configured: true,
|
|
636
|
+
mode: 'env',
|
|
637
|
+
key: envMatch[1],
|
|
638
|
+
value: null,
|
|
639
|
+
};
|
|
640
|
+
} else {
|
|
641
|
+
auth = {
|
|
642
|
+
configured: true,
|
|
643
|
+
mode: 'literal',
|
|
644
|
+
key: null,
|
|
645
|
+
value: authToken,
|
|
646
|
+
};
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
return {
|
|
651
|
+
scope: resolvedScope,
|
|
652
|
+
repoRoot,
|
|
653
|
+
npmrcPath: npmrcPath ? normalizeDisplayPath(repoRoot, npmrcPath) : null,
|
|
654
|
+
configured: Boolean(registry),
|
|
655
|
+
registry,
|
|
656
|
+
auth,
|
|
657
|
+
alwaysAuth,
|
|
658
|
+
};
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
function resolveRegistryAuthToken(rawValue) {
|
|
662
|
+
if (!rawValue) return null;
|
|
663
|
+
const envMatch = rawValue.match(/^\$\{([^}]+)\}$/);
|
|
664
|
+
if (envMatch) {
|
|
665
|
+
return process.env[envMatch[1]] || null;
|
|
666
|
+
}
|
|
667
|
+
return rawValue;
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
async function fetchRegistryLatestVersion(packageName, {
|
|
671
|
+
registry,
|
|
672
|
+
authToken,
|
|
673
|
+
} = {}) {
|
|
674
|
+
if (!registry) return null;
|
|
675
|
+
|
|
676
|
+
const base = registry.replace(/\/+$/, '');
|
|
677
|
+
const url = `${base}/${encodeURIComponent(packageName)}`;
|
|
678
|
+
const headers = { accept: 'application/json' };
|
|
679
|
+
const resolvedToken = resolveRegistryAuthToken(authToken);
|
|
680
|
+
if (resolvedToken) headers.authorization = `Bearer ${resolvedToken}`;
|
|
681
|
+
|
|
682
|
+
let response;
|
|
683
|
+
try {
|
|
684
|
+
response = await fetch(url, { headers });
|
|
685
|
+
} catch (error) {
|
|
686
|
+
throw new NetworkError(`failed to query registry for ${packageName}`, {
|
|
687
|
+
code: 'registry_lookup_failed',
|
|
688
|
+
suggestion: error instanceof Error ? error.message : String(error),
|
|
689
|
+
});
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
if (response.status === 404) return null;
|
|
693
|
+
if (!response.ok) {
|
|
694
|
+
throw new NetworkError(`registry lookup failed for ${packageName}`, {
|
|
695
|
+
code: 'registry_lookup_failed',
|
|
696
|
+
suggestion: `HTTP ${response.status}`,
|
|
697
|
+
});
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
const metadata = await response.json();
|
|
701
|
+
return metadata?.['dist-tags']?.latest || null;
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
export function inspectSkill(target, { cwd = process.cwd() } = {}) {
|
|
705
|
+
const repoRoot = findRepoRoot(cwd);
|
|
706
|
+
|
|
707
|
+
let skillFile = resolveSkillFileTarget(repoRoot, target);
|
|
708
|
+
|
|
709
|
+
if (!skillFile && target.startsWith('@')) {
|
|
710
|
+
const packageDir = findPackageDirByName(repoRoot, target);
|
|
711
|
+
if (packageDir) {
|
|
712
|
+
skillFile = join(packageDir, 'SKILL.md');
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
if (!skillFile) {
|
|
717
|
+
throw new NotFoundError('skill not found', {
|
|
718
|
+
code: 'skill_not_found',
|
|
719
|
+
suggestion: `Target: ${target}`,
|
|
720
|
+
});
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
const metadata = parseSkillFrontmatterFile(skillFile);
|
|
724
|
+
const packageMetadata = readPackageMetadata(dirname(skillFile));
|
|
725
|
+
|
|
726
|
+
return {
|
|
727
|
+
name: metadata.name,
|
|
728
|
+
description: metadata.description,
|
|
729
|
+
packageName: packageMetadata.packageName,
|
|
730
|
+
packageVersion: packageMetadata.packageVersion,
|
|
731
|
+
skillFile: normalizeDisplayPath(repoRoot, skillFile),
|
|
732
|
+
sources: metadata.sources,
|
|
733
|
+
requires: metadata.requires,
|
|
734
|
+
status: metadata.status,
|
|
735
|
+
replacement: metadata.replacement,
|
|
736
|
+
message: metadata.message,
|
|
737
|
+
};
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
function readSkillGraphNode(repoRoot, packageDir, directInstallNames = new Set()) {
|
|
741
|
+
const skillFile = join(packageDir, 'SKILL.md');
|
|
742
|
+
if (!existsSync(skillFile)) return null;
|
|
743
|
+
|
|
744
|
+
const skillMetadata = parseSkillFrontmatterFile(skillFile);
|
|
745
|
+
const packageMetadata = readPackageMetadata(packageDir);
|
|
746
|
+
if (!packageMetadata.packageName) return null;
|
|
747
|
+
|
|
748
|
+
const dependencyNames = Object.keys(packageMetadata.dependencies || {})
|
|
749
|
+
.filter((dependencyName) => {
|
|
750
|
+
const localPackageDir = findPackageDirByName(repoRoot, dependencyName);
|
|
751
|
+
if (localPackageDir && existsSync(join(localPackageDir, 'SKILL.md'))) return true;
|
|
752
|
+
const installedPackageDir = join(repoRoot, 'node_modules', ...dependencyName.split('/'));
|
|
753
|
+
return existsSync(join(installedPackageDir, 'SKILL.md'));
|
|
754
|
+
})
|
|
755
|
+
.sort((a, b) => a.localeCompare(b));
|
|
756
|
+
|
|
757
|
+
return {
|
|
758
|
+
name: skillMetadata.name,
|
|
759
|
+
packageName: packageMetadata.packageName,
|
|
760
|
+
packageVersion: packageMetadata.packageVersion,
|
|
761
|
+
skillPath: normalizeDisplayPath(repoRoot, packageDir),
|
|
762
|
+
skillFile: normalizeDisplayPath(repoRoot, skillFile),
|
|
763
|
+
direct: directInstallNames.has(packageMetadata.packageName),
|
|
764
|
+
dependencies: dependencyNames,
|
|
765
|
+
};
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
function buildAuthoredSkillGraph(repoRoot) {
|
|
769
|
+
const nodes = new Map();
|
|
770
|
+
|
|
771
|
+
for (const packageDir of listPackagedSkillDirs(repoRoot)) {
|
|
772
|
+
const node = readSkillGraphNode(repoRoot, packageDir);
|
|
773
|
+
if (!node) continue;
|
|
774
|
+
nodes.set(node.packageName, node);
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
return nodes;
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
function buildInstalledSkillGraph(repoRoot) {
|
|
781
|
+
const nodes = new Map();
|
|
782
|
+
const installState = readInstallState(repoRoot);
|
|
783
|
+
const directInstallNames = new Set(
|
|
784
|
+
Object.entries(installState.installs || {})
|
|
785
|
+
.filter(([, install]) => install.direct)
|
|
786
|
+
.map(([packageName]) => packageName)
|
|
787
|
+
);
|
|
788
|
+
|
|
789
|
+
for (const packageDir of listInstalledPackageDirs(join(repoRoot, 'node_modules'))) {
|
|
790
|
+
const node = readSkillGraphNode(repoRoot, packageDir, directInstallNames);
|
|
791
|
+
if (!node) continue;
|
|
792
|
+
nodes.set(node.packageName, node);
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
return nodes;
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
function buildReverseDependencies(nodes) {
|
|
799
|
+
const reverse = new Map();
|
|
800
|
+
for (const packageName of nodes.keys()) reverse.set(packageName, []);
|
|
801
|
+
|
|
802
|
+
for (const node of nodes.values()) {
|
|
803
|
+
for (const dependencyName of node.dependencies) {
|
|
804
|
+
if (!reverse.has(dependencyName)) continue;
|
|
805
|
+
reverse.get(dependencyName).push(node.packageName);
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
for (const values of reverse.values()) values.sort((a, b) => a.localeCompare(b));
|
|
810
|
+
return reverse;
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
function buildSkillStatusMap(repoRoot) {
|
|
814
|
+
const nodes = buildAuthoredSkillGraph(repoRoot);
|
|
815
|
+
const staleSkills = new Set(listStaleSkills({ cwd: repoRoot }).map((skill) => skill.packageName));
|
|
816
|
+
const cache = new Map();
|
|
817
|
+
|
|
818
|
+
function resolveStatus(packageName, seen = new Set()) {
|
|
819
|
+
if (cache.has(packageName)) return cache.get(packageName);
|
|
820
|
+
if (staleSkills.has(packageName)) {
|
|
821
|
+
cache.set(packageName, 'stale');
|
|
822
|
+
return 'stale';
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
if (seen.has(packageName)) return 'current';
|
|
826
|
+
seen.add(packageName);
|
|
827
|
+
|
|
828
|
+
const node = nodes.get(packageName);
|
|
829
|
+
if (!node) {
|
|
830
|
+
cache.set(packageName, null);
|
|
831
|
+
return null;
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
const dependencyStatuses = node.dependencies
|
|
835
|
+
.map((dependencyName) => resolveStatus(dependencyName, new Set(seen)))
|
|
836
|
+
.filter(Boolean);
|
|
837
|
+
|
|
838
|
+
const status = dependencyStatuses.some((value) => value === 'stale' || value === 'affected')
|
|
839
|
+
? 'affected'
|
|
840
|
+
: 'current';
|
|
841
|
+
|
|
842
|
+
cache.set(packageName, status);
|
|
843
|
+
return status;
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
for (const packageName of nodes.keys()) {
|
|
847
|
+
resolveStatus(packageName);
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
return cache;
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
function readNodeStatus(statusMap, packageName) {
|
|
854
|
+
if (!statusMap) return null;
|
|
855
|
+
return statusMap.get(packageName) || null;
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
export function inspectSkillDependencies(target, {
|
|
859
|
+
cwd = process.cwd(),
|
|
860
|
+
discoveryRoot = process.env.AGENTPACK_DISCOVERY_ROOT,
|
|
861
|
+
} = {}) {
|
|
862
|
+
const repoRoot = findRepoRoot(cwd);
|
|
863
|
+
const authoredNodes = buildAuthoredSkillGraph(repoRoot);
|
|
864
|
+
const installedNodes = buildInstalledSkillGraph(repoRoot);
|
|
865
|
+
const statusRoot = discoveryRoot ? resolve(discoveryRoot) : repoRoot;
|
|
866
|
+
const statusMap = buildSkillStatusMap(statusRoot);
|
|
867
|
+
|
|
868
|
+
const authoredTarget = authoredNodes.get(target) || null;
|
|
869
|
+
const installedTarget = installedNodes.get(target) || null;
|
|
870
|
+
|
|
871
|
+
let graph = null;
|
|
872
|
+
let nodes = null;
|
|
873
|
+
let node = null;
|
|
874
|
+
|
|
875
|
+
if (authoredTarget) {
|
|
876
|
+
graph = 'authored';
|
|
877
|
+
nodes = authoredNodes;
|
|
878
|
+
node = authoredTarget;
|
|
879
|
+
} else if (installedTarget) {
|
|
880
|
+
graph = 'installed';
|
|
881
|
+
nodes = installedNodes;
|
|
882
|
+
node = installedTarget;
|
|
883
|
+
} else {
|
|
884
|
+
const targetPath = resolveSkillFileTarget(repoRoot, target);
|
|
885
|
+
if (targetPath) {
|
|
886
|
+
const packageDir = dirname(targetPath);
|
|
887
|
+
const packageMetadata = readPackageMetadata(packageDir);
|
|
888
|
+
if (packageMetadata.packageName && authoredNodes.has(packageMetadata.packageName)) {
|
|
889
|
+
graph = 'authored';
|
|
890
|
+
nodes = authoredNodes;
|
|
891
|
+
node = authoredNodes.get(packageMetadata.packageName);
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
if (!node || !nodes || !graph) {
|
|
897
|
+
throw new NotFoundError('skill dependency graph target not found', {
|
|
898
|
+
code: 'skill_graph_target_not_found',
|
|
899
|
+
suggestion: `Target: ${target}`,
|
|
900
|
+
});
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
const reverseDependencies = buildReverseDependencies(nodes).get(node.packageName) || [];
|
|
904
|
+
|
|
905
|
+
return {
|
|
906
|
+
graph,
|
|
907
|
+
name: node.name,
|
|
908
|
+
packageName: node.packageName,
|
|
909
|
+
packageVersion: node.packageVersion,
|
|
910
|
+
skillPath: node.skillPath,
|
|
911
|
+
skillFile: node.skillFile,
|
|
912
|
+
direct: graph === 'installed' ? node.direct : null,
|
|
913
|
+
status: readNodeStatus(statusMap, node.packageName),
|
|
914
|
+
dependencies: node.dependencies.map((packageName) => {
|
|
915
|
+
const dependencyNode = nodes.get(packageName);
|
|
916
|
+
return {
|
|
917
|
+
packageName,
|
|
918
|
+
packageVersion: dependencyNode?.packageVersion || null,
|
|
919
|
+
skillPath: dependencyNode?.skillPath || null,
|
|
920
|
+
direct: graph === 'installed' ? dependencyNode?.direct || false : null,
|
|
921
|
+
status: readNodeStatus(statusMap, packageName),
|
|
922
|
+
};
|
|
923
|
+
}),
|
|
924
|
+
reverseDependencies: reverseDependencies.map((packageName) => {
|
|
925
|
+
const dependencyNode = nodes.get(packageName);
|
|
926
|
+
return {
|
|
927
|
+
packageName,
|
|
928
|
+
packageVersion: dependencyNode?.packageVersion || null,
|
|
929
|
+
skillPath: dependencyNode?.skillPath || null,
|
|
930
|
+
direct: graph === 'installed' ? dependencyNode?.direct || false : null,
|
|
931
|
+
status: readNodeStatus(statusMap, packageName),
|
|
932
|
+
};
|
|
933
|
+
}),
|
|
934
|
+
};
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
function resolvePackagedSkillTarget(repoRoot, target) {
|
|
938
|
+
let skillFile = null;
|
|
939
|
+
|
|
940
|
+
if (target) {
|
|
941
|
+
skillFile = resolveSkillFileTarget(repoRoot, target);
|
|
942
|
+
|
|
943
|
+
if (!skillFile && target.startsWith('@')) {
|
|
944
|
+
const packageDir = findPackageDirByName(repoRoot, target);
|
|
945
|
+
if (packageDir) {
|
|
946
|
+
skillFile = join(packageDir, 'SKILL.md');
|
|
947
|
+
}
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
if (!skillFile) {
|
|
951
|
+
throw new NotFoundError('skill not found', {
|
|
952
|
+
code: 'skill_not_found',
|
|
953
|
+
suggestion: `Target: ${target}`,
|
|
954
|
+
});
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
const packageDir = dirname(skillFile);
|
|
958
|
+
const packageMetadata = readPackageMetadata(packageDir);
|
|
959
|
+
if (!packageMetadata.packageName) {
|
|
960
|
+
throw new ValidationError('validate target is not a packaged skill', {
|
|
961
|
+
code: 'invalid_validate_target',
|
|
962
|
+
suggestion: `Target: ${target}`,
|
|
963
|
+
});
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
return [packageDir];
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
return listPackagedSkillDirs(repoRoot);
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
function validatePackagedSkillDir(repoRoot, packageDir) {
|
|
973
|
+
const skillFile = join(packageDir, 'SKILL.md');
|
|
974
|
+
const packageMetadata = readPackageMetadata(packageDir);
|
|
975
|
+
const issues = [];
|
|
976
|
+
let skillMetadata = null;
|
|
977
|
+
|
|
978
|
+
try {
|
|
979
|
+
skillMetadata = parseSkillFrontmatterFile(skillFile);
|
|
980
|
+
} catch (error) {
|
|
981
|
+
issues.push({
|
|
982
|
+
code: error.code || 'invalid_skill_file',
|
|
983
|
+
message: error.message,
|
|
984
|
+
});
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
if (!packageMetadata.packageName) {
|
|
988
|
+
issues.push({
|
|
989
|
+
code: 'missing_package_name',
|
|
990
|
+
message: 'package.json missing "name"',
|
|
991
|
+
});
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
if (!packageMetadata.packageVersion) {
|
|
995
|
+
issues.push({
|
|
996
|
+
code: 'missing_package_version',
|
|
997
|
+
message: 'package.json missing "version"',
|
|
998
|
+
});
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
if (packageMetadata.files && !packageMetadata.files.includes('SKILL.md')) {
|
|
1002
|
+
issues.push({
|
|
1003
|
+
code: 'skill_not_published',
|
|
1004
|
+
message: 'package.json files does not include SKILL.md',
|
|
1005
|
+
});
|
|
1006
|
+
}
|
|
1007
|
+
|
|
1008
|
+
if (isManagedPackageName(packageMetadata.packageName)) {
|
|
1009
|
+
if (!packageMetadata.repository) {
|
|
1010
|
+
issues.push({
|
|
1011
|
+
code: 'missing_repository',
|
|
1012
|
+
message: 'package.json missing repository for private registry publishing',
|
|
1013
|
+
});
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
if (packageMetadata.publishConfigRegistry !== GITHUB_PACKAGES_REGISTRY) {
|
|
1017
|
+
issues.push({
|
|
1018
|
+
code: 'invalid_publish_registry',
|
|
1019
|
+
message: `package.json publishConfig.registry must target ${GITHUB_PACKAGES_REGISTRY}`,
|
|
1020
|
+
});
|
|
1021
|
+
}
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
if (skillMetadata) {
|
|
1025
|
+
if (skillMetadata.status && !['deprecated', 'retired'].includes(skillMetadata.status)) {
|
|
1026
|
+
issues.push({
|
|
1027
|
+
code: 'invalid_skill_status',
|
|
1028
|
+
message: 'metadata.status must be "deprecated" or "retired"',
|
|
1029
|
+
});
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1032
|
+
if (skillMetadata.replacement && !skillMetadata.replacement.startsWith('@')) {
|
|
1033
|
+
issues.push({
|
|
1034
|
+
code: 'invalid_replacement',
|
|
1035
|
+
message: 'metadata.replacement must be a package name',
|
|
1036
|
+
});
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
for (const sourcePath of skillMetadata.sources) {
|
|
1040
|
+
if (!existsSync(join(repoRoot, sourcePath))) {
|
|
1041
|
+
issues.push({
|
|
1042
|
+
code: 'missing_source',
|
|
1043
|
+
message: 'declared source file does not exist',
|
|
1044
|
+
path: sourcePath,
|
|
1045
|
+
});
|
|
1046
|
+
}
|
|
1047
|
+
}
|
|
1048
|
+
|
|
1049
|
+
for (const requirement of skillMetadata.requires) {
|
|
1050
|
+
if (!packageMetadata.dependencies[requirement]) {
|
|
1051
|
+
issues.push({
|
|
1052
|
+
code: 'missing_dependency_declaration',
|
|
1053
|
+
message: 'required skill is not declared in package dependencies',
|
|
1054
|
+
dependency: requirement,
|
|
1055
|
+
});
|
|
1056
|
+
}
|
|
1057
|
+
}
|
|
1058
|
+
}
|
|
1059
|
+
|
|
1060
|
+
return {
|
|
1061
|
+
valid: issues.length === 0,
|
|
1062
|
+
name: skillMetadata?.name || null,
|
|
1063
|
+
packageName: packageMetadata.packageName,
|
|
1064
|
+
packageVersion: packageMetadata.packageVersion,
|
|
1065
|
+
skillFile: normalizeDisplayPath(repoRoot, skillFile),
|
|
1066
|
+
packagePath: normalizeDisplayPath(repoRoot, packageDir),
|
|
1067
|
+
status: skillMetadata?.status || null,
|
|
1068
|
+
replacement: skillMetadata?.replacement || null,
|
|
1069
|
+
nextSteps: buildValidateNextSteps(packageMetadata, issues.length === 0),
|
|
1070
|
+
issues,
|
|
1071
|
+
};
|
|
1072
|
+
}
|
|
1073
|
+
|
|
1074
|
+
export function validateSkills(target, { cwd = process.cwd() } = {}) {
|
|
1075
|
+
const repoRoot = findRepoRoot(cwd);
|
|
1076
|
+
const packageDirs = resolvePackagedSkillTarget(repoRoot, target);
|
|
1077
|
+
|
|
1078
|
+
for (const packageDir of packageDirs) {
|
|
1079
|
+
syncSkillDependencies(packageDir);
|
|
1080
|
+
}
|
|
1081
|
+
|
|
1082
|
+
const skills = packageDirs
|
|
1083
|
+
.map((packageDir) => validatePackagedSkillDir(repoRoot, packageDir))
|
|
1084
|
+
.sort((a, b) => (a.packageName || a.packagePath).localeCompare(b.packageName || b.packagePath));
|
|
1085
|
+
|
|
1086
|
+
const validCount = skills.filter((skill) => skill.valid).length;
|
|
1087
|
+
const invalidCount = skills.length - validCount;
|
|
1088
|
+
|
|
1089
|
+
return {
|
|
1090
|
+
valid: invalidCount === 0,
|
|
1091
|
+
count: skills.length,
|
|
1092
|
+
validCount,
|
|
1093
|
+
invalidCount,
|
|
1094
|
+
skills,
|
|
1095
|
+
};
|
|
1096
|
+
}
|
|
1097
|
+
|
|
1098
|
+
|
|
1099
|
+
function hashFile(filePath) {
|
|
1100
|
+
const digest = createHash('sha256').update(readFileSync(filePath)).digest('hex');
|
|
1101
|
+
return `sha256:${digest}`;
|
|
1102
|
+
}
|
|
1103
|
+
|
|
1104
|
+
function normalizeRelativePath(pathValue) {
|
|
1105
|
+
return pathValue.split('\\').join('/');
|
|
1106
|
+
}
|
|
1107
|
+
|
|
1108
|
+
function readBuildState(repoRoot) {
|
|
1109
|
+
const buildStatePath = join(repoRoot, '.agentpack', 'build-state.json');
|
|
1110
|
+
if (!existsSync(buildStatePath)) {
|
|
1111
|
+
return { version: 1, skills: {} };
|
|
1112
|
+
}
|
|
1113
|
+
|
|
1114
|
+
return JSON.parse(readFileSync(buildStatePath, 'utf-8'));
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1117
|
+
function readInstallState(repoRoot) {
|
|
1118
|
+
const installStatePath = join(repoRoot, '.agentpack', 'install.json');
|
|
1119
|
+
if (!existsSync(installStatePath)) {
|
|
1120
|
+
return { version: 1, installs: {} };
|
|
1121
|
+
}
|
|
1122
|
+
|
|
1123
|
+
return JSON.parse(readFileSync(installStatePath, 'utf-8'));
|
|
1124
|
+
}
|
|
1125
|
+
|
|
1126
|
+
function writeInstallState(repoRoot, state) {
|
|
1127
|
+
mkdirSync(join(repoRoot, '.agentpack'), { recursive: true });
|
|
1128
|
+
writeFileSync(join(repoRoot, '.agentpack', 'install.json'), JSON.stringify(state, null, 2) + '\n');
|
|
1129
|
+
}
|
|
1130
|
+
|
|
1131
|
+
function normalizeRequestedTarget(target, cwd = process.cwd()) {
|
|
1132
|
+
if (typeof target !== 'string') return target;
|
|
1133
|
+
if (target.startsWith('@')) return target;
|
|
1134
|
+
return normalizeRelativePath(resolve(cwd, target));
|
|
1135
|
+
}
|
|
1136
|
+
|
|
1137
|
+
function compareRecordedSources(repoRoot, record) {
|
|
1138
|
+
const changes = [];
|
|
1139
|
+
const recordedSources = record.sources || {};
|
|
1140
|
+
|
|
1141
|
+
for (const [sourcePath, sourceRecord] of Object.entries(recordedSources)) {
|
|
1142
|
+
const absoluteSourcePath = join(repoRoot, sourcePath);
|
|
1143
|
+
const currentHash = hashFile(absoluteSourcePath);
|
|
1144
|
+
const recordedHash = sourceRecord.hash;
|
|
1145
|
+
|
|
1146
|
+
if (currentHash !== recordedHash) {
|
|
1147
|
+
changes.push({
|
|
1148
|
+
path: sourcePath,
|
|
1149
|
+
recorded: recordedHash,
|
|
1150
|
+
current: currentHash,
|
|
1151
|
+
});
|
|
1152
|
+
}
|
|
1153
|
+
}
|
|
1154
|
+
|
|
1155
|
+
return changes;
|
|
1156
|
+
}
|
|
1157
|
+
|
|
1158
|
+
function listPackagedSkillDirs(repoRoot) {
|
|
1159
|
+
const stack = [repoRoot];
|
|
1160
|
+
const results = [];
|
|
1161
|
+
|
|
1162
|
+
while (stack.length > 0) {
|
|
1163
|
+
const current = stack.pop();
|
|
1164
|
+
let entries = [];
|
|
1165
|
+
try {
|
|
1166
|
+
entries = readdirSync(current, { withFileTypes: true });
|
|
1167
|
+
} catch {
|
|
1168
|
+
continue;
|
|
1169
|
+
}
|
|
1170
|
+
|
|
1171
|
+
let hasSkillFile = false;
|
|
1172
|
+
let hasPackageFile = false;
|
|
1173
|
+
|
|
1174
|
+
for (const entry of entries) {
|
|
1175
|
+
if (entry.name === '.git' || entry.name === 'node_modules' || entry.name === '.agentpack') continue;
|
|
1176
|
+
const fullPath = join(current, entry.name);
|
|
1177
|
+
|
|
1178
|
+
if (entry.isDirectory()) {
|
|
1179
|
+
stack.push(fullPath);
|
|
1180
|
+
continue;
|
|
1181
|
+
}
|
|
1182
|
+
|
|
1183
|
+
if (entry.name === 'SKILL.md') hasSkillFile = true;
|
|
1184
|
+
if (entry.name === 'package.json') hasPackageFile = true;
|
|
1185
|
+
}
|
|
1186
|
+
|
|
1187
|
+
if (hasSkillFile && hasPackageFile) {
|
|
1188
|
+
results.push(current);
|
|
1189
|
+
}
|
|
1190
|
+
}
|
|
1191
|
+
|
|
1192
|
+
return results.sort();
|
|
1193
|
+
}
|
|
1194
|
+
|
|
1195
|
+
function listAuthoredPackagedSkills(repoRoot) {
|
|
1196
|
+
return listPackagedSkillDirs(repoRoot)
|
|
1197
|
+
.map((packageDir) => {
|
|
1198
|
+
const skillFile = join(packageDir, 'SKILL.md');
|
|
1199
|
+
const metadata = parseSkillFrontmatterFile(skillFile);
|
|
1200
|
+
const packageMetadata = readPackageMetadata(packageDir);
|
|
1201
|
+
|
|
1202
|
+
if (!packageMetadata.packageName || !packageMetadata.packageVersion) {
|
|
1203
|
+
return null;
|
|
1204
|
+
}
|
|
1205
|
+
|
|
1206
|
+
return {
|
|
1207
|
+
name: metadata.name,
|
|
1208
|
+
description: metadata.description,
|
|
1209
|
+
packageName: packageMetadata.packageName,
|
|
1210
|
+
packageVersion: packageMetadata.packageVersion,
|
|
1211
|
+
skillPath: normalizeDisplayPath(repoRoot, packageDir),
|
|
1212
|
+
skillFile: normalizeDisplayPath(repoRoot, skillFile),
|
|
1213
|
+
sources: metadata.sources,
|
|
1214
|
+
requires: metadata.requires,
|
|
1215
|
+
};
|
|
1216
|
+
})
|
|
1217
|
+
.filter(Boolean)
|
|
1218
|
+
.sort((a, b) => a.packageName.localeCompare(b.packageName));
|
|
1219
|
+
}
|
|
1220
|
+
|
|
1221
|
+
export function generateSkillsCatalog({ cwd = process.cwd() } = {}) {
|
|
1222
|
+
const repoRoot = findRepoRoot(cwd);
|
|
1223
|
+
const skills = {};
|
|
1224
|
+
|
|
1225
|
+
for (const skill of listAuthoredPackagedSkills(repoRoot)) {
|
|
1226
|
+
skills[skill.packageName] = {
|
|
1227
|
+
name: skill.name,
|
|
1228
|
+
description: skill.description,
|
|
1229
|
+
path: skill.skillPath,
|
|
1230
|
+
skill_file: skill.skillFile,
|
|
1231
|
+
package_name: skill.packageName,
|
|
1232
|
+
package_version: skill.packageVersion,
|
|
1233
|
+
sources: skill.sources,
|
|
1234
|
+
requires: skill.requires,
|
|
1235
|
+
};
|
|
1236
|
+
}
|
|
1237
|
+
|
|
1238
|
+
return {
|
|
1239
|
+
version: 1,
|
|
1240
|
+
skills,
|
|
1241
|
+
};
|
|
1242
|
+
}
|
|
1243
|
+
|
|
1244
|
+
export function generateBuildState({ cwd = process.cwd() } = {}) {
|
|
1245
|
+
const repoRoot = findRepoRoot(cwd);
|
|
1246
|
+
const skills = {};
|
|
1247
|
+
|
|
1248
|
+
for (const skill of listAuthoredPackagedSkills(repoRoot)) {
|
|
1249
|
+
const sources = {};
|
|
1250
|
+
for (const sourcePath of skill.sources) {
|
|
1251
|
+
sources[sourcePath] = {
|
|
1252
|
+
hash: hashFile(join(repoRoot, sourcePath)),
|
|
1253
|
+
};
|
|
1254
|
+
}
|
|
1255
|
+
|
|
1256
|
+
skills[skill.packageName] = {
|
|
1257
|
+
package_version: skill.packageVersion,
|
|
1258
|
+
skill_path: skill.skillPath,
|
|
1259
|
+
skill_file: skill.skillFile,
|
|
1260
|
+
sources,
|
|
1261
|
+
requires: skill.requires,
|
|
1262
|
+
};
|
|
1263
|
+
}
|
|
1264
|
+
|
|
1265
|
+
return {
|
|
1266
|
+
version: 1,
|
|
1267
|
+
skills,
|
|
1268
|
+
};
|
|
1269
|
+
}
|
|
1270
|
+
|
|
1271
|
+
export function listStaleSkills({ cwd = process.cwd() } = {}) {
|
|
1272
|
+
const repoRoot = findRepoRoot(cwd);
|
|
1273
|
+
const buildState = readBuildState(repoRoot);
|
|
1274
|
+
const staleSkills = [];
|
|
1275
|
+
|
|
1276
|
+
for (const packageDir of listPackagedSkillDirs(repoRoot)) {
|
|
1277
|
+
const packageMetadata = readPackageMetadata(packageDir);
|
|
1278
|
+
if (!packageMetadata.packageName) continue;
|
|
1279
|
+
|
|
1280
|
+
const record = buildState.skills?.[packageMetadata.packageName];
|
|
1281
|
+
if (!record) continue;
|
|
1282
|
+
|
|
1283
|
+
const changedSources = compareRecordedSources(repoRoot, record);
|
|
1284
|
+
if (changedSources.length === 0) continue;
|
|
1285
|
+
|
|
1286
|
+
staleSkills.push({
|
|
1287
|
+
packageName: packageMetadata.packageName,
|
|
1288
|
+
skillPath: normalizeDisplayPath(repoRoot, packageDir),
|
|
1289
|
+
skillFile: normalizeDisplayPath(repoRoot, join(packageDir, 'SKILL.md')),
|
|
1290
|
+
changedSources,
|
|
1291
|
+
});
|
|
1292
|
+
}
|
|
1293
|
+
|
|
1294
|
+
return staleSkills.sort((a, b) => a.packageName.localeCompare(b.packageName));
|
|
1295
|
+
}
|
|
1296
|
+
|
|
1297
|
+
export function inspectStaleSkill(target, { cwd = process.cwd() } = {}) {
|
|
1298
|
+
const repoRoot = findRepoRoot(cwd);
|
|
1299
|
+
const staleSkills = listStaleSkills({ cwd });
|
|
1300
|
+
|
|
1301
|
+
let match = null;
|
|
1302
|
+
|
|
1303
|
+
if (target.startsWith('@')) {
|
|
1304
|
+
match = staleSkills.find((skill) => skill.packageName === target) || null;
|
|
1305
|
+
} else {
|
|
1306
|
+
const skillFile = resolveSkillFileTarget(repoRoot, target);
|
|
1307
|
+
if (skillFile) {
|
|
1308
|
+
const displayPath = normalizeDisplayPath(repoRoot, skillFile);
|
|
1309
|
+
match = staleSkills.find((skill) => skill.skillFile === displayPath) || null;
|
|
1310
|
+
}
|
|
1311
|
+
}
|
|
1312
|
+
|
|
1313
|
+
if (!match) {
|
|
1314
|
+
throw new NotFoundError('stale skill not found', {
|
|
1315
|
+
code: 'stale_skill_not_found',
|
|
1316
|
+
suggestion: `Target: ${target}`,
|
|
1317
|
+
});
|
|
1318
|
+
}
|
|
1319
|
+
|
|
1320
|
+
return match;
|
|
1321
|
+
}
|
|
1322
|
+
|
|
1323
|
+
function listInstalledPackageDirs(nodeModulesDir) {
|
|
1324
|
+
if (!existsSync(nodeModulesDir)) return [];
|
|
1325
|
+
|
|
1326
|
+
const packageDirs = [];
|
|
1327
|
+
const entries = readdirSync(nodeModulesDir, { withFileTypes: true });
|
|
1328
|
+
|
|
1329
|
+
for (const entry of entries) {
|
|
1330
|
+
if (!entry.isDirectory() && !entry.isSymbolicLink()) continue;
|
|
1331
|
+
|
|
1332
|
+
if (entry.name.startsWith('@')) {
|
|
1333
|
+
const scopeDir = join(nodeModulesDir, entry.name);
|
|
1334
|
+
const scopedEntries = readdirSync(scopeDir, { withFileTypes: true });
|
|
1335
|
+
for (const scopedEntry of scopedEntries) {
|
|
1336
|
+
if (!scopedEntry.isDirectory() && !scopedEntry.isSymbolicLink()) continue;
|
|
1337
|
+
packageDirs.push(join(scopeDir, scopedEntry.name));
|
|
1338
|
+
}
|
|
1339
|
+
continue;
|
|
1340
|
+
}
|
|
1341
|
+
|
|
1342
|
+
packageDirs.push(join(nodeModulesDir, entry.name));
|
|
1343
|
+
}
|
|
1344
|
+
|
|
1345
|
+
return packageDirs;
|
|
1346
|
+
}
|
|
1347
|
+
|
|
1348
|
+
function collectLocalInstallTargets(initialTarget) {
|
|
1349
|
+
const resolvedTarget = resolve(initialTarget);
|
|
1350
|
+
const queue = [resolvedTarget];
|
|
1351
|
+
const visited = new Set();
|
|
1352
|
+
const installTargets = [];
|
|
1353
|
+
|
|
1354
|
+
while (queue.length > 0) {
|
|
1355
|
+
const packageDir = queue.shift();
|
|
1356
|
+
if (visited.has(packageDir)) continue;
|
|
1357
|
+
visited.add(packageDir);
|
|
1358
|
+
installTargets.push(packageDir);
|
|
1359
|
+
|
|
1360
|
+
const packageJsonPath = join(packageDir, 'package.json');
|
|
1361
|
+
if (!existsSync(packageJsonPath)) continue;
|
|
1362
|
+
|
|
1363
|
+
const pkg = JSON.parse(readFileSync(packageJsonPath, 'utf-8'));
|
|
1364
|
+
const dependencies = pkg.dependencies || {};
|
|
1365
|
+
|
|
1366
|
+
for (const spec of Object.values(dependencies)) {
|
|
1367
|
+
if (typeof spec !== 'string' || !spec.startsWith('file:')) continue;
|
|
1368
|
+
queue.push(resolve(packageDir, spec.slice(5)));
|
|
1369
|
+
}
|
|
1370
|
+
}
|
|
1371
|
+
|
|
1372
|
+
return installTargets;
|
|
1373
|
+
}
|
|
1374
|
+
|
|
1375
|
+
function resolveNpmInstallTargets(directTargetMap) {
|
|
1376
|
+
const npmInstallTargets = [];
|
|
1377
|
+
|
|
1378
|
+
for (const requestedTarget of directTargetMap.values()) {
|
|
1379
|
+
if (typeof requestedTarget === 'string' && requestedTarget.startsWith('@')) {
|
|
1380
|
+
npmInstallTargets.push(requestedTarget);
|
|
1381
|
+
continue;
|
|
1382
|
+
}
|
|
1383
|
+
|
|
1384
|
+
npmInstallTargets.push(...collectLocalInstallTargets(requestedTarget));
|
|
1385
|
+
}
|
|
1386
|
+
|
|
1387
|
+
return [...new Set(npmInstallTargets)];
|
|
1388
|
+
}
|
|
1389
|
+
|
|
1390
|
+
function ensureSymlink(targetPath, linkPath) {
|
|
1391
|
+
rmSync(linkPath, { recursive: true, force: true });
|
|
1392
|
+
mkdirSync(dirname(linkPath), { recursive: true });
|
|
1393
|
+
symlinkSync(targetPath, linkPath, 'dir');
|
|
1394
|
+
}
|
|
1395
|
+
|
|
1396
|
+
function removePathIfExists(pathValue) {
|
|
1397
|
+
rmSync(pathValue, { recursive: true, force: true });
|
|
1398
|
+
}
|
|
1399
|
+
|
|
1400
|
+
function buildInstallRecord(repoRoot, packageDir, directTargetMap) {
|
|
1401
|
+
const packageMetadata = readPackageMetadata(packageDir);
|
|
1402
|
+
if (!packageMetadata.packageName) return null;
|
|
1403
|
+
|
|
1404
|
+
const skillMetadata = parseSkillFrontmatterFile(join(packageDir, 'SKILL.md'));
|
|
1405
|
+
const skillDirName = skillMetadata.name;
|
|
1406
|
+
const materializations = [];
|
|
1407
|
+
|
|
1408
|
+
const claudeTargetAbs = join(repoRoot, '.claude', 'skills', skillDirName);
|
|
1409
|
+
ensureSymlink(packageDir, claudeTargetAbs);
|
|
1410
|
+
materializations.push({
|
|
1411
|
+
target: normalizeRelativePath(relative(repoRoot, claudeTargetAbs)),
|
|
1412
|
+
mode: 'symlink',
|
|
1413
|
+
});
|
|
1414
|
+
|
|
1415
|
+
const agentsTargetAbs = join(repoRoot, '.agents', 'skills', skillDirName);
|
|
1416
|
+
ensureSymlink(packageDir, agentsTargetAbs);
|
|
1417
|
+
materializations.push({
|
|
1418
|
+
target: normalizeRelativePath(relative(repoRoot, agentsTargetAbs)),
|
|
1419
|
+
mode: 'symlink',
|
|
1420
|
+
});
|
|
1421
|
+
|
|
1422
|
+
return {
|
|
1423
|
+
packageName: packageMetadata.packageName,
|
|
1424
|
+
direct: directTargetMap.has(packageMetadata.packageName),
|
|
1425
|
+
requestedTarget: directTargetMap.get(packageMetadata.packageName) || null,
|
|
1426
|
+
packageVersion: packageMetadata.packageVersion,
|
|
1427
|
+
sourcePackagePath: normalizeRelativePath(relative(repoRoot, packageDir)),
|
|
1428
|
+
materializations,
|
|
1429
|
+
};
|
|
1430
|
+
}
|
|
1431
|
+
|
|
1432
|
+
function rebuildInstallState(repoRoot, directTargetMap) {
|
|
1433
|
+
const packageDirs = listInstalledPackageDirs(join(repoRoot, 'node_modules'));
|
|
1434
|
+
const installs = {};
|
|
1435
|
+
|
|
1436
|
+
for (const packageDir of packageDirs) {
|
|
1437
|
+
const skillFile = join(packageDir, 'SKILL.md');
|
|
1438
|
+
if (!existsSync(skillFile)) continue;
|
|
1439
|
+
|
|
1440
|
+
const record = buildInstallRecord(repoRoot, packageDir, directTargetMap);
|
|
1441
|
+
if (!record) continue;
|
|
1442
|
+
|
|
1443
|
+
installs[record.packageName] = {
|
|
1444
|
+
direct: record.direct,
|
|
1445
|
+
requested_target: record.requestedTarget,
|
|
1446
|
+
package_version: record.packageVersion,
|
|
1447
|
+
source_package_path: record.sourcePackagePath,
|
|
1448
|
+
materializations: record.materializations,
|
|
1449
|
+
};
|
|
1450
|
+
}
|
|
1451
|
+
|
|
1452
|
+
const state = { version: 1, installs };
|
|
1453
|
+
writeInstallState(repoRoot, state);
|
|
1454
|
+
return state;
|
|
1455
|
+
}
|
|
1456
|
+
|
|
1457
|
+
export function installSkills(targets, { cwd = process.cwd() } = {}) {
|
|
1458
|
+
const repoRoot = findRepoRoot(cwd);
|
|
1459
|
+
const previousState = readInstallState(repoRoot);
|
|
1460
|
+
const requestedTargets = Array.isArray(targets) ? targets : [targets];
|
|
1461
|
+
const directTargetMap = new Map(
|
|
1462
|
+
Object.entries(previousState.installs || {})
|
|
1463
|
+
.filter(([, install]) => install.direct && install.requested_target)
|
|
1464
|
+
.map(([packageName, install]) => [packageName, install.requested_target])
|
|
1465
|
+
);
|
|
1466
|
+
|
|
1467
|
+
for (const target of requestedTargets) {
|
|
1468
|
+
if (typeof target === 'string' && target.startsWith('@')) {
|
|
1469
|
+
directTargetMap.set(target, normalizeRequestedTarget(target, cwd));
|
|
1470
|
+
continue;
|
|
1471
|
+
}
|
|
1472
|
+
|
|
1473
|
+
const skillFile = resolveSkillFileTarget(repoRoot, target)
|
|
1474
|
+
|| (existsSync(target) ? resolveSkillFileTarget(repoRoot, target) : null);
|
|
1475
|
+
const packageDir = skillFile ? dirname(skillFile) : resolve(target);
|
|
1476
|
+
const packageMetadata = readPackageMetadata(packageDir);
|
|
1477
|
+
|
|
1478
|
+
if (!packageMetadata.packageName) {
|
|
1479
|
+
throw new NotFoundError('install target is not a packaged skill', {
|
|
1480
|
+
code: 'invalid_install_target',
|
|
1481
|
+
suggestion: `Target: ${target}`,
|
|
1482
|
+
});
|
|
1483
|
+
}
|
|
1484
|
+
|
|
1485
|
+
directTargetMap.set(packageMetadata.packageName, normalizeRequestedTarget(target, cwd));
|
|
1486
|
+
}
|
|
1487
|
+
|
|
1488
|
+
const uniqueInstallTargets = resolveNpmInstallTargets(directTargetMap);
|
|
1489
|
+
|
|
1490
|
+
execFileSync('npm', ['install', '--no-save', ...uniqueInstallTargets], {
|
|
1491
|
+
cwd: repoRoot,
|
|
1492
|
+
encoding: 'utf-8',
|
|
1493
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
1494
|
+
});
|
|
1495
|
+
|
|
1496
|
+
return rebuildInstallState(repoRoot, directTargetMap);
|
|
1497
|
+
}
|
|
1498
|
+
|
|
1499
|
+
export function inspectSkillsEnv({ cwd = process.cwd() } = {}) {
|
|
1500
|
+
const repoRoot = findRepoRoot(cwd);
|
|
1501
|
+
const state = readInstallState(repoRoot);
|
|
1502
|
+
const installs = Object.entries(state.installs || {})
|
|
1503
|
+
.sort(([a], [b]) => a.localeCompare(b))
|
|
1504
|
+
.map(([packageName, install]) => ({
|
|
1505
|
+
...readInstalledSkillLifecycle(repoRoot, install.source_package_path),
|
|
1506
|
+
packageName,
|
|
1507
|
+
direct: install.direct,
|
|
1508
|
+
packageVersion: install.package_version,
|
|
1509
|
+
sourcePackagePath: install.source_package_path,
|
|
1510
|
+
materializations: install.materializations || [],
|
|
1511
|
+
}));
|
|
1512
|
+
|
|
1513
|
+
return {
|
|
1514
|
+
repoRoot,
|
|
1515
|
+
installs,
|
|
1516
|
+
};
|
|
1517
|
+
}
|
|
1518
|
+
|
|
1519
|
+
function readInstalledSkillLifecycle(repoRoot, sourcePackagePath) {
|
|
1520
|
+
if (!sourcePackagePath) {
|
|
1521
|
+
return {
|
|
1522
|
+
requires: [],
|
|
1523
|
+
status: null,
|
|
1524
|
+
replacement: null,
|
|
1525
|
+
message: null,
|
|
1526
|
+
};
|
|
1527
|
+
}
|
|
1528
|
+
|
|
1529
|
+
const skillFile = join(repoRoot, sourcePackagePath, 'SKILL.md');
|
|
1530
|
+
if (!existsSync(skillFile)) {
|
|
1531
|
+
return {
|
|
1532
|
+
requires: [],
|
|
1533
|
+
status: null,
|
|
1534
|
+
replacement: null,
|
|
1535
|
+
message: null,
|
|
1536
|
+
};
|
|
1537
|
+
}
|
|
1538
|
+
|
|
1539
|
+
const metadata = parseSkillFrontmatterFile(skillFile);
|
|
1540
|
+
return {
|
|
1541
|
+
requires: metadata.requires,
|
|
1542
|
+
status: metadata.status,
|
|
1543
|
+
replacement: metadata.replacement,
|
|
1544
|
+
message: metadata.message,
|
|
1545
|
+
};
|
|
1546
|
+
}
|
|
1547
|
+
|
|
1548
|
+
function buildInstallCommand(packageName) {
|
|
1549
|
+
return `agentpack skills install ${packageName}`;
|
|
1550
|
+
}
|
|
1551
|
+
|
|
1552
|
+
function listLocalWorkbenchSkillRecords(repoRoot) {
|
|
1553
|
+
const records = [];
|
|
1554
|
+
|
|
1555
|
+
for (const workbench of findAllWorkbenches(repoRoot)) {
|
|
1556
|
+
const skillsDir = join(workbench.path, 'skills');
|
|
1557
|
+
if (!existsSync(skillsDir)) continue;
|
|
1558
|
+
|
|
1559
|
+
const entries = readdirSync(skillsDir, { withFileTypes: true });
|
|
1560
|
+
for (const entry of entries) {
|
|
1561
|
+
if (!entry.isDirectory()) continue;
|
|
1562
|
+
|
|
1563
|
+
const skillDir = join(skillsDir, entry.name);
|
|
1564
|
+
const skillFile = join(skillDir, 'SKILL.md');
|
|
1565
|
+
if (!existsSync(skillFile)) continue;
|
|
1566
|
+
if (existsSync(join(skillDir, 'package.json'))) continue;
|
|
1567
|
+
|
|
1568
|
+
const metadata = parseSkillFrontmatterFile(skillFile);
|
|
1569
|
+
records.push({
|
|
1570
|
+
packageName: null,
|
|
1571
|
+
name: metadata.name,
|
|
1572
|
+
skillFile: normalizeDisplayPath(repoRoot, skillFile),
|
|
1573
|
+
direct: true,
|
|
1574
|
+
requires: metadata.requires,
|
|
1575
|
+
});
|
|
1576
|
+
}
|
|
1577
|
+
}
|
|
1578
|
+
|
|
1579
|
+
return records.sort((a, b) => a.skillFile.localeCompare(b.skillFile));
|
|
1580
|
+
}
|
|
1581
|
+
|
|
1582
|
+
function buildMissingRecordForRequirements(subject, installed) {
|
|
1583
|
+
const missing = (subject.requires || [])
|
|
1584
|
+
.filter((requirement) => !installed.has(requirement))
|
|
1585
|
+
.sort((a, b) => a.localeCompare(b))
|
|
1586
|
+
.map((packageName) => ({
|
|
1587
|
+
packageName,
|
|
1588
|
+
recommendedCommand: buildInstallCommand(packageName),
|
|
1589
|
+
}));
|
|
1590
|
+
|
|
1591
|
+
if (missing.length === 0) return null;
|
|
1592
|
+
|
|
1593
|
+
return {
|
|
1594
|
+
packageName: subject.packageName || null,
|
|
1595
|
+
name: subject.name || null,
|
|
1596
|
+
skillFile: subject.skillFile || null,
|
|
1597
|
+
direct: Boolean(subject.direct),
|
|
1598
|
+
missing,
|
|
1599
|
+
};
|
|
1600
|
+
}
|
|
1601
|
+
|
|
1602
|
+
function parseSimpleSemver(version) {
|
|
1603
|
+
const match = String(version).match(/^(\d+)\.(\d+)\.(\d+)$/);
|
|
1604
|
+
if (!match) return null;
|
|
1605
|
+
return {
|
|
1606
|
+
major: Number(match[1]),
|
|
1607
|
+
minor: Number(match[2]),
|
|
1608
|
+
patch: Number(match[3]),
|
|
1609
|
+
};
|
|
1610
|
+
}
|
|
1611
|
+
|
|
1612
|
+
function compareSimpleSemver(left, right) {
|
|
1613
|
+
const a = parseSimpleSemver(left);
|
|
1614
|
+
const b = parseSimpleSemver(right);
|
|
1615
|
+
if (!a || !b) return 0;
|
|
1616
|
+
if (a.major !== b.major) return a.major - b.major;
|
|
1617
|
+
if (a.minor !== b.minor) return a.minor - b.minor;
|
|
1618
|
+
return a.patch - b.patch;
|
|
1619
|
+
}
|
|
1620
|
+
|
|
1621
|
+
function classifyUpdateType(currentVersion, availableVersion) {
|
|
1622
|
+
const current = parseSimpleSemver(currentVersion);
|
|
1623
|
+
const next = parseSimpleSemver(availableVersion);
|
|
1624
|
+
if (!current || !next) return 'unknown';
|
|
1625
|
+
if (next.major !== current.major) return 'major';
|
|
1626
|
+
if (next.minor !== current.minor) return 'minor';
|
|
1627
|
+
if (next.patch !== current.patch) return 'patch';
|
|
1628
|
+
return 'none';
|
|
1629
|
+
}
|
|
1630
|
+
|
|
1631
|
+
export function inspectMissingSkillDependencies({
|
|
1632
|
+
target = null,
|
|
1633
|
+
cwd = process.cwd(),
|
|
1634
|
+
} = {}) {
|
|
1635
|
+
const repoRoot = findRepoRoot(cwd);
|
|
1636
|
+
const env = inspectSkillsEnv({ cwd });
|
|
1637
|
+
const installed = new Set(env.installs.map((install) => install.packageName));
|
|
1638
|
+
const installedRecords = env.installs.map((install) => ({
|
|
1639
|
+
packageName: install.packageName,
|
|
1640
|
+
name: null,
|
|
1641
|
+
skillFile: install.sourcePackagePath ? `${install.sourcePackagePath}/SKILL.md` : null,
|
|
1642
|
+
direct: install.direct,
|
|
1643
|
+
requires: install.requires,
|
|
1644
|
+
}));
|
|
1645
|
+
const localWorkbenchRecords = listLocalWorkbenchSkillRecords(repoRoot);
|
|
1646
|
+
|
|
1647
|
+
let records = [...installedRecords, ...localWorkbenchRecords];
|
|
1648
|
+
if (target) {
|
|
1649
|
+
if (target.startsWith('@')) {
|
|
1650
|
+
records = installedRecords.filter((install) => install.packageName === target);
|
|
1651
|
+
} else {
|
|
1652
|
+
const skillFile = resolveSkillFileTarget(repoRoot, target);
|
|
1653
|
+
if (!skillFile) {
|
|
1654
|
+
throw new NotFoundError('skill not found', {
|
|
1655
|
+
code: 'skill_not_found',
|
|
1656
|
+
suggestion: `Target: ${target}`,
|
|
1657
|
+
});
|
|
1658
|
+
}
|
|
1659
|
+
|
|
1660
|
+
const packageMetadata = readPackageMetadata(dirname(skillFile));
|
|
1661
|
+
if (packageMetadata.packageName) {
|
|
1662
|
+
records = installedRecords.filter((install) => install.packageName === packageMetadata.packageName);
|
|
1663
|
+
} else {
|
|
1664
|
+
const metadata = parseSkillFrontmatterFile(skillFile);
|
|
1665
|
+
records = [{
|
|
1666
|
+
packageName: null,
|
|
1667
|
+
name: metadata.name,
|
|
1668
|
+
skillFile: normalizeDisplayPath(repoRoot, skillFile),
|
|
1669
|
+
direct: true,
|
|
1670
|
+
requires: metadata.requires,
|
|
1671
|
+
}];
|
|
1672
|
+
}
|
|
1673
|
+
}
|
|
1674
|
+
}
|
|
1675
|
+
|
|
1676
|
+
const skills = records
|
|
1677
|
+
.map((record) => buildMissingRecordForRequirements(record, installed))
|
|
1678
|
+
.filter(Boolean)
|
|
1679
|
+
.sort((a, b) => {
|
|
1680
|
+
const left = a.packageName || a.skillFile || a.name || '';
|
|
1681
|
+
const right = b.packageName || b.skillFile || b.name || '';
|
|
1682
|
+
return left.localeCompare(right);
|
|
1683
|
+
});
|
|
1684
|
+
|
|
1685
|
+
return {
|
|
1686
|
+
count: skills.length,
|
|
1687
|
+
skills,
|
|
1688
|
+
};
|
|
1689
|
+
}
|
|
1690
|
+
|
|
1691
|
+
export async function listOutdatedSkills({
|
|
1692
|
+
cwd = process.cwd(),
|
|
1693
|
+
discoveryRoot = process.env.AGENTPACK_DISCOVERY_ROOT,
|
|
1694
|
+
} = {}) {
|
|
1695
|
+
const repoRoot = findRepoRoot(cwd);
|
|
1696
|
+
const state = readInstallState(repoRoot);
|
|
1697
|
+
const authoredRoot = discoveryRoot ? resolve(discoveryRoot) : repoRoot;
|
|
1698
|
+
const registryConfig = readRepoNpmRegistryConfig(repoRoot);
|
|
1699
|
+
const skills = [];
|
|
1700
|
+
|
|
1701
|
+
for (const [packageName, install] of Object.entries(state.installs || {}).sort(([a], [b]) => a.localeCompare(b))) {
|
|
1702
|
+
const currentVersion = install.package_version;
|
|
1703
|
+
let availableVersion = null;
|
|
1704
|
+
let availablePackagePath = null;
|
|
1705
|
+
let source = 'none';
|
|
1706
|
+
|
|
1707
|
+
if (isManagedPackageName(packageName) && registryConfig.registry) {
|
|
1708
|
+
availableVersion = await fetchRegistryLatestVersion(packageName, registryConfig);
|
|
1709
|
+
source = availableVersion ? 'registry' : 'none';
|
|
1710
|
+
}
|
|
1711
|
+
|
|
1712
|
+
if (!availableVersion) {
|
|
1713
|
+
const availableDir = findPackageDirByName(authoredRoot, packageName);
|
|
1714
|
+
if (availableDir) {
|
|
1715
|
+
const availableMeta = readPackageMetadata(availableDir);
|
|
1716
|
+
availableVersion = availableMeta.packageVersion;
|
|
1717
|
+
availablePackagePath = normalizeRelativePath(relative(authoredRoot, availableDir));
|
|
1718
|
+
source = 'local';
|
|
1719
|
+
}
|
|
1720
|
+
}
|
|
1721
|
+
|
|
1722
|
+
if (!currentVersion || !availableVersion) continue;
|
|
1723
|
+
if (compareSimpleSemver(availableVersion, currentVersion) <= 0) continue;
|
|
1724
|
+
|
|
1725
|
+
skills.push({
|
|
1726
|
+
packageName,
|
|
1727
|
+
currentVersion,
|
|
1728
|
+
availableVersion,
|
|
1729
|
+
updateType: classifyUpdateType(currentVersion, availableVersion),
|
|
1730
|
+
currentSourcePackagePath: install.source_package_path,
|
|
1731
|
+
availablePackagePath,
|
|
1732
|
+
source,
|
|
1733
|
+
recommendedCommand: buildInstallCommand(packageName),
|
|
1734
|
+
});
|
|
1735
|
+
}
|
|
1736
|
+
|
|
1737
|
+
return {
|
|
1738
|
+
count: skills.length,
|
|
1739
|
+
skills,
|
|
1740
|
+
};
|
|
1741
|
+
}
|
|
1742
|
+
|
|
1743
|
+
export async function inspectSkillsStatus({ cwd = process.cwd() } = {}) {
|
|
1744
|
+
const env = inspectSkillsEnv({ cwd });
|
|
1745
|
+
const registry = inspectRegistryConfig({ cwd });
|
|
1746
|
+
const outdatedResult = await listOutdatedSkills({ cwd });
|
|
1747
|
+
const missingResult = inspectMissingSkillDependencies({ cwd });
|
|
1748
|
+
|
|
1749
|
+
const installedCount = env.installs.length;
|
|
1750
|
+
const directCount = env.installs.filter((install) => install.direct).length;
|
|
1751
|
+
const transitiveCount = installedCount - directCount;
|
|
1752
|
+
const outdatedCount = outdatedResult.count;
|
|
1753
|
+
const deprecated = env.installs
|
|
1754
|
+
.filter((install) => install.status === 'deprecated' || install.status === 'retired')
|
|
1755
|
+
.map((install) => ({
|
|
1756
|
+
packageName: install.packageName,
|
|
1757
|
+
status: install.status,
|
|
1758
|
+
replacement: install.replacement,
|
|
1759
|
+
message: install.message,
|
|
1760
|
+
}));
|
|
1761
|
+
const deprecatedCount = deprecated.length;
|
|
1762
|
+
const incomplete = missingResult.skills;
|
|
1763
|
+
const incompleteCount = missingResult.count;
|
|
1764
|
+
|
|
1765
|
+
let health = 'healthy';
|
|
1766
|
+
if (!registry.configured) {
|
|
1767
|
+
health = installedCount > 0 || outdatedCount > 0 ? 'attention-needed' : 'needs-config';
|
|
1768
|
+
} else if (outdatedCount > 0 || deprecatedCount > 0 || incompleteCount > 0) {
|
|
1769
|
+
health = 'attention-needed';
|
|
1770
|
+
} else if (incompleteCount > 0) {
|
|
1771
|
+
health = 'attention-needed';
|
|
1772
|
+
}
|
|
1773
|
+
|
|
1774
|
+
return {
|
|
1775
|
+
repoRoot: env.repoRoot,
|
|
1776
|
+
installedCount,
|
|
1777
|
+
directCount,
|
|
1778
|
+
transitiveCount,
|
|
1779
|
+
outdatedCount,
|
|
1780
|
+
deprecatedCount,
|
|
1781
|
+
incompleteCount,
|
|
1782
|
+
registry,
|
|
1783
|
+
outdated: outdatedResult.skills,
|
|
1784
|
+
deprecated,
|
|
1785
|
+
incomplete,
|
|
1786
|
+
installs: env.installs,
|
|
1787
|
+
health,
|
|
1788
|
+
};
|
|
1789
|
+
}
|
|
1790
|
+
|
|
1791
|
+
export function uninstallSkills(target, { cwd = process.cwd() } = {}) {
|
|
1792
|
+
const repoRoot = findRepoRoot(cwd);
|
|
1793
|
+
const previousState = readInstallState(repoRoot);
|
|
1794
|
+
|
|
1795
|
+
const nextDirectTargetMap = new Map(
|
|
1796
|
+
Object.entries(previousState.installs || {})
|
|
1797
|
+
.filter(([packageName, install]) => install.direct && packageName !== target)
|
|
1798
|
+
.map(([packageName, install]) => [packageName, install.requested_target])
|
|
1799
|
+
);
|
|
1800
|
+
|
|
1801
|
+
removePathIfExists(join(repoRoot, 'node_modules'));
|
|
1802
|
+
|
|
1803
|
+
const nextInstallTargets = resolveNpmInstallTargets(nextDirectTargetMap);
|
|
1804
|
+
if (nextInstallTargets.length > 0) {
|
|
1805
|
+
execFileSync('npm', ['install', '--no-save', ...nextInstallTargets], {
|
|
1806
|
+
cwd: repoRoot,
|
|
1807
|
+
encoding: 'utf-8',
|
|
1808
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
1809
|
+
});
|
|
1810
|
+
}
|
|
1811
|
+
|
|
1812
|
+
const nextState = rebuildInstallState(repoRoot, nextDirectTargetMap);
|
|
1813
|
+
const remainingTargets = new Set(
|
|
1814
|
+
Object.values(nextState.installs)
|
|
1815
|
+
.flatMap((install) => (install.materializations || []).map((entry) => entry.target))
|
|
1816
|
+
);
|
|
1817
|
+
|
|
1818
|
+
const removedPackages = [];
|
|
1819
|
+
|
|
1820
|
+
for (const [packageName, install] of Object.entries(previousState.installs || {})) {
|
|
1821
|
+
if (nextState.installs[packageName]) continue;
|
|
1822
|
+
removedPackages.push(packageName);
|
|
1823
|
+
|
|
1824
|
+
for (const materialization of install.materializations || []) {
|
|
1825
|
+
const absTarget = join(repoRoot, materialization.target);
|
|
1826
|
+
if (!remainingTargets.has(materialization.target)) {
|
|
1827
|
+
removePathIfExists(absTarget);
|
|
1828
|
+
}
|
|
1829
|
+
}
|
|
1830
|
+
}
|
|
1831
|
+
|
|
1832
|
+
return {
|
|
1833
|
+
version: nextState.version,
|
|
1834
|
+
installs: nextState.installs,
|
|
1835
|
+
removed: removedPackages.sort(),
|
|
1836
|
+
};
|
|
1837
|
+
}
|
|
1838
|
+
|
|
1839
|
+
function resolvePackageDirsFromWorkbench(workbench, repoRoot) {
|
|
1840
|
+
const skillsDir = join(workbench.path, 'skills');
|
|
1841
|
+
if (!existsSync(skillsDir)) {
|
|
1842
|
+
return [];
|
|
1843
|
+
}
|
|
1844
|
+
|
|
1845
|
+
const packageDirs = [];
|
|
1846
|
+
const entries = readdirSync(skillsDir, { withFileTypes: true });
|
|
1847
|
+
|
|
1848
|
+
for (const entry of entries) {
|
|
1849
|
+
if (!entry.isDirectory()) continue;
|
|
1850
|
+
|
|
1851
|
+
const skillFile = join(skillsDir, entry.name, 'SKILL.md');
|
|
1852
|
+
if (!existsSync(skillFile)) continue;
|
|
1853
|
+
|
|
1854
|
+
const metadata = parseSkillFrontmatterFile(skillFile);
|
|
1855
|
+
for (const requirement of metadata.requires) {
|
|
1856
|
+
const packageDir = findPackageDirByName(repoRoot, requirement);
|
|
1857
|
+
if (packageDir) {
|
|
1858
|
+
packageDirs.push(packageDir);
|
|
1859
|
+
}
|
|
1860
|
+
}
|
|
1861
|
+
}
|
|
1862
|
+
|
|
1863
|
+
return [...new Set(packageDirs)].sort();
|
|
1864
|
+
}
|
|
1865
|
+
|
|
1866
|
+
export function resolveInstallTargets({
|
|
1867
|
+
target,
|
|
1868
|
+
workbench: workbenchArg,
|
|
1869
|
+
cwd = process.cwd(),
|
|
1870
|
+
} = {}) {
|
|
1871
|
+
if (target) {
|
|
1872
|
+
return Array.isArray(target) ? target : [target];
|
|
1873
|
+
}
|
|
1874
|
+
|
|
1875
|
+
const repoRoot = findRepoRoot(cwd);
|
|
1876
|
+
let workbench = null;
|
|
1877
|
+
|
|
1878
|
+
if (workbenchArg) {
|
|
1879
|
+
workbench = resolveWorkbenchFlag(workbenchArg, cwd);
|
|
1880
|
+
} else {
|
|
1881
|
+
workbench = findWorkbenchContext(cwd);
|
|
1882
|
+
}
|
|
1883
|
+
|
|
1884
|
+
if (!workbench) {
|
|
1885
|
+
throw new NotFoundError('no install target provided and no workbench context found', {
|
|
1886
|
+
code: 'missing_install_target',
|
|
1887
|
+
suggestion: 'Pass a packaged skill target or use --workbench <path>',
|
|
1888
|
+
});
|
|
1889
|
+
}
|
|
1890
|
+
|
|
1891
|
+
const targets = resolvePackageDirsFromWorkbench(workbench, repoRoot);
|
|
1892
|
+
if (targets.length === 0) {
|
|
1893
|
+
throw new NotFoundError('no external skill dependencies found for workbench', {
|
|
1894
|
+
code: 'no_workbench_skill_roots',
|
|
1895
|
+
suggestion: `Workbench: ${workbench.relativePath}`,
|
|
1896
|
+
});
|
|
1897
|
+
}
|
|
1898
|
+
|
|
1899
|
+
return targets;
|
|
1900
|
+
}
|