@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,414 @@
|
|
|
1
|
+
import { cpSync, existsSync, mkdirSync, readFileSync, readdirSync, renameSync, rmSync, watch, writeFileSync } from 'node:fs';
|
|
2
|
+
import { dirname, join, resolve } from 'node:path';
|
|
3
|
+
import { createRequire } from 'node:module';
|
|
4
|
+
import {
|
|
5
|
+
findPackageDirByName,
|
|
6
|
+
normalizeRepoPath,
|
|
7
|
+
parseSkillFrontmatterFile,
|
|
8
|
+
readPackageMetadata,
|
|
9
|
+
syncSkillDependencies,
|
|
10
|
+
} from './skills.js';
|
|
11
|
+
import { findRepoRoot } from './context.js';
|
|
12
|
+
import { AgentpackError, EXIT_CODES, NotFoundError, ValidationError } from '../utils/errors.js';
|
|
13
|
+
|
|
14
|
+
const require = createRequire(import.meta.url);
|
|
15
|
+
|
|
16
|
+
function resolvePluginDir(repoRoot, target) {
|
|
17
|
+
const absoluteTarget = resolve(repoRoot, target);
|
|
18
|
+
if (!existsSync(absoluteTarget)) {
|
|
19
|
+
throw new NotFoundError('plugin not found', {
|
|
20
|
+
code: 'plugin_not_found',
|
|
21
|
+
suggestion: `Target: ${target}`,
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return absoluteTarget;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function resolvePackageDir(repoRoot, pluginDir, packageName) {
|
|
29
|
+
const localDir = findPackageDirByName(repoRoot, packageName);
|
|
30
|
+
if (localDir) {
|
|
31
|
+
return { packageDir: localDir, source: 'repo' };
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
try {
|
|
35
|
+
const packageJsonPath = require.resolve(`${packageName}/package.json`, {
|
|
36
|
+
paths: [pluginDir],
|
|
37
|
+
});
|
|
38
|
+
return { packageDir: dirname(packageJsonPath), source: 'node_modules' };
|
|
39
|
+
} catch {
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function collectPluginLocalSkills(pluginDir) {
|
|
45
|
+
const skillsRoot = join(pluginDir, 'skills');
|
|
46
|
+
if (!existsSync(skillsRoot)) return [];
|
|
47
|
+
|
|
48
|
+
const entries = readdirSync(skillsRoot, { withFileTypes: true });
|
|
49
|
+
const skills = [];
|
|
50
|
+
|
|
51
|
+
for (const entry of entries) {
|
|
52
|
+
if (!entry.isDirectory()) continue;
|
|
53
|
+
const skillFile = join(skillsRoot, entry.name, 'SKILL.md');
|
|
54
|
+
if (!existsSync(skillFile)) continue;
|
|
55
|
+
|
|
56
|
+
const metadata = parseSkillFrontmatterFile(skillFile);
|
|
57
|
+
skills.push({
|
|
58
|
+
localName: entry.name,
|
|
59
|
+
name: metadata.name,
|
|
60
|
+
skillFile,
|
|
61
|
+
requires: metadata.requires,
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return skills.sort((a, b) => a.localName.localeCompare(b.localName));
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function collectPluginLocalSkillDirs(pluginDir) {
|
|
69
|
+
const skillsRoot = join(pluginDir, 'skills');
|
|
70
|
+
if (!existsSync(skillsRoot)) return [];
|
|
71
|
+
|
|
72
|
+
return readdirSync(skillsRoot, { withFileTypes: true })
|
|
73
|
+
.filter((entry) => entry.isDirectory() && existsSync(join(skillsRoot, entry.name, 'SKILL.md')))
|
|
74
|
+
.map((entry) => join(skillsRoot, entry.name))
|
|
75
|
+
.sort();
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function resolveBundleClosure(repoRoot, pluginDir, directRequires) {
|
|
79
|
+
const seen = new Set();
|
|
80
|
+
const queue = [...directRequires];
|
|
81
|
+
const bundled = [];
|
|
82
|
+
const unresolved = [];
|
|
83
|
+
|
|
84
|
+
while (queue.length > 0) {
|
|
85
|
+
const packageName = queue.shift();
|
|
86
|
+
if (seen.has(packageName)) continue;
|
|
87
|
+
seen.add(packageName);
|
|
88
|
+
|
|
89
|
+
const resolvedPackage = resolvePackageDir(repoRoot, pluginDir, packageName);
|
|
90
|
+
if (!resolvedPackage) {
|
|
91
|
+
unresolved.push(packageName);
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const skillFile = join(resolvedPackage.packageDir, 'SKILL.md');
|
|
96
|
+
if (!existsSync(skillFile)) {
|
|
97
|
+
unresolved.push(packageName);
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const metadata = parseSkillFrontmatterFile(skillFile);
|
|
102
|
+
const packageMetadata = readPackageMetadata(resolvedPackage.packageDir);
|
|
103
|
+
|
|
104
|
+
bundled.push({
|
|
105
|
+
packageName,
|
|
106
|
+
packageVersion: packageMetadata.packageVersion,
|
|
107
|
+
skillName: metadata.name,
|
|
108
|
+
skillFile,
|
|
109
|
+
packageDir: resolvedPackage.packageDir,
|
|
110
|
+
source: resolvedPackage.source,
|
|
111
|
+
requires: metadata.requires,
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
for (const requirement of metadata.requires) {
|
|
115
|
+
if (!seen.has(requirement)) queue.push(requirement);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
bundled.sort((a, b) => a.packageName.localeCompare(b.packageName));
|
|
120
|
+
unresolved.sort();
|
|
121
|
+
|
|
122
|
+
return { bundled, unresolved };
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function stagePluginRuntimeFiles(pluginDir, stageDir) {
|
|
126
|
+
mkdirSync(stageDir, { recursive: true });
|
|
127
|
+
for (const entry of readdirSync(pluginDir, { withFileTypes: true })) {
|
|
128
|
+
if (entry.name === 'node_modules' || entry.name === '.git' || entry.name === '.agentpack') continue;
|
|
129
|
+
cpSync(join(pluginDir, entry.name), join(stageDir, entry.name), { recursive: true });
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function watchDirectoryTree(rootDir, onChange) {
|
|
134
|
+
const watchers = new Map();
|
|
135
|
+
|
|
136
|
+
const watchDir = (dirPath) => {
|
|
137
|
+
if (watchers.has(dirPath) || !existsSync(dirPath)) return;
|
|
138
|
+
|
|
139
|
+
let entries = [];
|
|
140
|
+
try {
|
|
141
|
+
entries = readdirSync(dirPath, { withFileTypes: true });
|
|
142
|
+
} catch {
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const watcher = watch(dirPath, (_eventType, filename) => {
|
|
147
|
+
if (filename) {
|
|
148
|
+
const changedPath = join(dirPath, String(filename));
|
|
149
|
+
if (existsSync(changedPath)) {
|
|
150
|
+
watchDir(changedPath);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
onChange();
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
watchers.set(dirPath, watcher);
|
|
158
|
+
|
|
159
|
+
for (const entry of entries) {
|
|
160
|
+
if (!entry.isDirectory()) continue;
|
|
161
|
+
watchDir(join(dirPath, entry.name));
|
|
162
|
+
}
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
watchDir(rootDir);
|
|
166
|
+
|
|
167
|
+
return {
|
|
168
|
+
close() {
|
|
169
|
+
for (const watcher of watchers.values()) watcher.close();
|
|
170
|
+
watchers.clear();
|
|
171
|
+
},
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export function buildPlugin(target, {
|
|
176
|
+
cwd = process.cwd(),
|
|
177
|
+
clean = false,
|
|
178
|
+
} = {}) {
|
|
179
|
+
const repoRoot = findRepoRoot(cwd);
|
|
180
|
+
const pluginDir = resolvePluginDir(repoRoot, target);
|
|
181
|
+
|
|
182
|
+
for (const localSkillDir of collectPluginLocalSkillDirs(pluginDir)) {
|
|
183
|
+
if (existsSync(join(localSkillDir, 'package.json'))) {
|
|
184
|
+
syncSkillDependencies(localSkillDir);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const validation = validatePluginBundle(target, { cwd });
|
|
189
|
+
if (!validation.valid) {
|
|
190
|
+
throw new AgentpackError(validation.issues.map((issue) => issue.message).join('; '), {
|
|
191
|
+
code: validation.issues[0]?.code || 'plugin_build_failed',
|
|
192
|
+
exitCode: EXIT_CODES.GENERAL,
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const inspection = inspectPluginBundle(target, { cwd });
|
|
197
|
+
const outputPath = join(repoRoot, '.agentpack', 'dist', 'plugins', inspection.pluginName);
|
|
198
|
+
const stagePath = join(repoRoot, '.agentpack', 'dist', 'plugins', `.tmp-${inspection.pluginName}-${Date.now()}`);
|
|
199
|
+
|
|
200
|
+
if (clean) {
|
|
201
|
+
rmSync(outputPath, { recursive: true, force: true });
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
rmSync(stagePath, { recursive: true, force: true });
|
|
205
|
+
|
|
206
|
+
try {
|
|
207
|
+
stagePluginRuntimeFiles(pluginDir, stagePath);
|
|
208
|
+
|
|
209
|
+
const vendoredSkills = [];
|
|
210
|
+
for (const entry of [...inspection.directPackages, ...inspection.transitivePackages]) {
|
|
211
|
+
const vendoredDir = join(stagePath, 'skills', entry.skillName);
|
|
212
|
+
mkdirSync(vendoredDir, { recursive: true });
|
|
213
|
+
cpSync(join(repoRoot, entry.skillFile), join(vendoredDir, 'SKILL.md'));
|
|
214
|
+
vendoredSkills.push(entry.skillName);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
const bundledManifestPath = join(stagePath, '.claude-plugin', 'bundled-skills.json');
|
|
218
|
+
writeFileSync(
|
|
219
|
+
bundledManifestPath,
|
|
220
|
+
JSON.stringify({
|
|
221
|
+
pluginName: inspection.pluginName,
|
|
222
|
+
packages: [...inspection.directPackages, ...inspection.transitivePackages],
|
|
223
|
+
}, null, 2) + '\n'
|
|
224
|
+
);
|
|
225
|
+
|
|
226
|
+
rmSync(outputPath, { recursive: true, force: true });
|
|
227
|
+
renameSync(stagePath, outputPath);
|
|
228
|
+
|
|
229
|
+
return {
|
|
230
|
+
pluginName: inspection.pluginName,
|
|
231
|
+
outputPath: normalizeRepoPath(repoRoot, outputPath),
|
|
232
|
+
localSkills: inspection.localSkills.map((skill) => skill.localName),
|
|
233
|
+
vendoredSkills: [...new Set(vendoredSkills)].sort(),
|
|
234
|
+
success: true,
|
|
235
|
+
};
|
|
236
|
+
} catch (error) {
|
|
237
|
+
rmSync(stagePath, { recursive: true, force: true });
|
|
238
|
+
throw error;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
export function startPluginDev(target, {
|
|
243
|
+
cwd = process.cwd(),
|
|
244
|
+
clean = false,
|
|
245
|
+
onBuild = () => {},
|
|
246
|
+
onRebuild = () => {},
|
|
247
|
+
} = {}) {
|
|
248
|
+
const repoRoot = findRepoRoot(cwd);
|
|
249
|
+
const pluginDir = resolvePluginDir(repoRoot, target);
|
|
250
|
+
const initialResult = buildPlugin(target, { cwd, clean });
|
|
251
|
+
onBuild(initialResult);
|
|
252
|
+
|
|
253
|
+
let timer = null;
|
|
254
|
+
let closed = false;
|
|
255
|
+
|
|
256
|
+
const watcher = watchDirectoryTree(pluginDir, () => {
|
|
257
|
+
if (closed) return;
|
|
258
|
+
clearTimeout(timer);
|
|
259
|
+
timer = setTimeout(() => {
|
|
260
|
+
try {
|
|
261
|
+
const rebuildResult = buildPlugin(target, { cwd });
|
|
262
|
+
onRebuild(rebuildResult);
|
|
263
|
+
} catch (error) {
|
|
264
|
+
onRebuild({ error });
|
|
265
|
+
}
|
|
266
|
+
}, 100);
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
return {
|
|
270
|
+
initialResult,
|
|
271
|
+
close() {
|
|
272
|
+
if (closed) return;
|
|
273
|
+
closed = true;
|
|
274
|
+
clearTimeout(timer);
|
|
275
|
+
watcher.close();
|
|
276
|
+
},
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
export function inspectPluginBundle(target, { cwd = process.cwd() } = {}) {
|
|
281
|
+
const repoRoot = findRepoRoot(cwd);
|
|
282
|
+
const pluginDir = resolvePluginDir(repoRoot, target);
|
|
283
|
+
const pluginManifestPath = join(pluginDir, '.claude-plugin', 'plugin.json');
|
|
284
|
+
const pluginManifest = existsSync(pluginManifestPath)
|
|
285
|
+
? JSON.parse(readFileSync(pluginManifestPath, 'utf-8'))
|
|
286
|
+
: null;
|
|
287
|
+
|
|
288
|
+
const packageMetadata = readPackageMetadata(pluginDir);
|
|
289
|
+
if (!packageMetadata.packageName || !packageMetadata.packageVersion) {
|
|
290
|
+
throw new ValidationError('plugin package.json missing name or version', {
|
|
291
|
+
code: 'missing_plugin_package_metadata',
|
|
292
|
+
suggestion: normalizeRepoPath(repoRoot, join(pluginDir, 'package.json')),
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
if (!pluginManifest) {
|
|
297
|
+
throw new ValidationError('plugin missing .claude-plugin/plugin.json', {
|
|
298
|
+
code: 'missing_plugin_manifest',
|
|
299
|
+
suggestion: normalizeRepoPath(repoRoot, join(pluginDir, '.claude-plugin', 'plugin.json')),
|
|
300
|
+
});
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
const localSkills = collectPluginLocalSkills(pluginDir);
|
|
304
|
+
const directRequires = [...new Set(localSkills.flatMap((skill) => skill.requires))].sort();
|
|
305
|
+
const { bundled, unresolved } = resolveBundleClosure(repoRoot, pluginDir, directRequires);
|
|
306
|
+
const directPackageSet = new Set(directRequires);
|
|
307
|
+
|
|
308
|
+
return {
|
|
309
|
+
pluginName: pluginManifest.name || packageMetadata.packageName,
|
|
310
|
+
packageName: packageMetadata.packageName,
|
|
311
|
+
packageVersion: packageMetadata.packageVersion,
|
|
312
|
+
pluginPath: normalizeRepoPath(repoRoot, pluginDir),
|
|
313
|
+
pluginManifestPath: normalizeRepoPath(repoRoot, pluginManifestPath),
|
|
314
|
+
localSkills: localSkills.map((skill) => ({
|
|
315
|
+
localName: skill.localName,
|
|
316
|
+
name: skill.name,
|
|
317
|
+
skillFile: normalizeRepoPath(repoRoot, skill.skillFile),
|
|
318
|
+
requires: skill.requires,
|
|
319
|
+
})),
|
|
320
|
+
directPackages: bundled
|
|
321
|
+
.filter((entry) => directPackageSet.has(entry.packageName))
|
|
322
|
+
.map((entry) => ({
|
|
323
|
+
packageName: entry.packageName,
|
|
324
|
+
packageVersion: entry.packageVersion,
|
|
325
|
+
skillName: entry.skillName,
|
|
326
|
+
skillFile: normalizeRepoPath(repoRoot, entry.skillFile),
|
|
327
|
+
source: entry.source,
|
|
328
|
+
})),
|
|
329
|
+
transitivePackages: bundled
|
|
330
|
+
.filter((entry) => !directPackageSet.has(entry.packageName))
|
|
331
|
+
.map((entry) => ({
|
|
332
|
+
packageName: entry.packageName,
|
|
333
|
+
packageVersion: entry.packageVersion,
|
|
334
|
+
skillName: entry.skillName,
|
|
335
|
+
skillFile: normalizeRepoPath(repoRoot, entry.skillFile),
|
|
336
|
+
source: entry.source,
|
|
337
|
+
})),
|
|
338
|
+
unresolvedPackages: unresolved,
|
|
339
|
+
bundleManifestPath: normalizeRepoPath(repoRoot, join(pluginDir, '.claude-plugin', 'bundled-skills.json')),
|
|
340
|
+
};
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
export function validatePluginBundle(target, { cwd = process.cwd() } = {}) {
|
|
344
|
+
const repoRoot = findRepoRoot(cwd);
|
|
345
|
+
const pluginDir = resolvePluginDir(repoRoot, target);
|
|
346
|
+
const result = inspectPluginBundle(target, { cwd });
|
|
347
|
+
const packageMetadata = readPackageMetadata(pluginDir);
|
|
348
|
+
const issues = [];
|
|
349
|
+
const localSkillNames = new Set(result.localSkills.map((skill) => skill.localName));
|
|
350
|
+
const bundledSkillNames = new Map();
|
|
351
|
+
const coveredPackages = new Set();
|
|
352
|
+
|
|
353
|
+
for (const packageName of result.unresolvedPackages) {
|
|
354
|
+
if (coveredPackages.has(packageName)) continue;
|
|
355
|
+
issues.push({
|
|
356
|
+
code: 'unresolved_bundle_dependency',
|
|
357
|
+
packageName,
|
|
358
|
+
message: `${packageName} is required by a local plugin skill but was not resolvable from the repo or node_modules. Are you sure you ran npm install?`,
|
|
359
|
+
});
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
for (const localSkill of result.localSkills) {
|
|
363
|
+
for (const packageName of localSkill.requires) {
|
|
364
|
+
const declared = packageMetadata.devDependencies[packageName];
|
|
365
|
+
const resolvedPackage = resolvePackageDir(repoRoot, pluginDir, packageName);
|
|
366
|
+
if (!declared || !resolvedPackage) {
|
|
367
|
+
coveredPackages.add(packageName);
|
|
368
|
+
issues.push({
|
|
369
|
+
code: 'missing_bundle_input',
|
|
370
|
+
packageName,
|
|
371
|
+
skillFile: localSkill.skillFile,
|
|
372
|
+
message: `${packageName} is required by local plugin skill ${localSkill.localName} but is missing from package.json devDependencies or not installed. Are you sure you ran npm install?`,
|
|
373
|
+
});
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
for (const entry of [...result.directPackages, ...result.transitivePackages]) {
|
|
379
|
+
if (localSkillNames.has(entry.skillName)) {
|
|
380
|
+
issues.push({
|
|
381
|
+
code: 'bundled_skill_name_collision',
|
|
382
|
+
packageName: entry.packageName,
|
|
383
|
+
skillName: entry.skillName,
|
|
384
|
+
message: `Bundled skill ${entry.skillName} collides with an existing local plugin skill`,
|
|
385
|
+
});
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
if (bundledSkillNames.has(entry.skillName)) {
|
|
389
|
+
issues.push({
|
|
390
|
+
code: 'bundled_skill_name_collision',
|
|
391
|
+
packageName: entry.packageName,
|
|
392
|
+
skillName: entry.skillName,
|
|
393
|
+
message: `Bundled skill ${entry.skillName} collides with ${bundledSkillNames.get(entry.skillName)}`,
|
|
394
|
+
});
|
|
395
|
+
} else {
|
|
396
|
+
bundledSkillNames.set(entry.skillName, entry.packageName);
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
return {
|
|
401
|
+
pluginName: result.pluginName,
|
|
402
|
+
packageName: result.packageName,
|
|
403
|
+
packageVersion: result.packageVersion,
|
|
404
|
+
pluginPath: result.pluginPath,
|
|
405
|
+
valid: issues.length === 0,
|
|
406
|
+
issueCount: issues.length,
|
|
407
|
+
localSkillCount: result.localSkills.length,
|
|
408
|
+
directPackageCount: result.directPackages.length,
|
|
409
|
+
transitivePackageCount: result.transitivePackages.length,
|
|
410
|
+
issues,
|
|
411
|
+
bundledPackages: [...result.directPackages, ...result.transitivePackages],
|
|
412
|
+
bundleManifestPath: result.bundleManifestPath,
|
|
413
|
+
};
|
|
414
|
+
}
|