@alavida/agentpack 0.1.1 → 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. package/README.md +16 -2
  2. package/bin/intent.js +20 -0
  3. package/package.json +11 -5
  4. package/skills/agentpack-cli/SKILL.md +16 -1
  5. package/skills/agentpack-cli/references/skill-lifecycle.md +21 -1
  6. package/skills/authoring-skillgraphs-from-knowledge/SKILL.md +148 -0
  7. package/skills/authoring-skillgraphs-from-knowledge/references/authored-metadata.md +6 -0
  8. package/skills/developing-and-testing-skills/SKILL.md +109 -0
  9. package/skills/developing-and-testing-skills/references/local-workbench.md +7 -0
  10. package/skills/getting-started-skillgraphs/SKILL.md +115 -0
  11. package/skills/getting-started-skillgraphs/references/command-routing.md +7 -0
  12. package/skills/identifying-skill-opportunities/SKILL.md +119 -0
  13. package/skills/identifying-skill-opportunities/references/capability-boundaries.md +6 -0
  14. package/skills/maintaining-skillgraph-freshness/SKILL.md +110 -0
  15. package/skills/repairing-broken-skill-or-plugin-state/SKILL.md +112 -0
  16. package/skills/repairing-broken-skill-or-plugin-state/references/diagnostic-flows.md +6 -0
  17. package/skills/shipping-production-plugins-and-packages/SKILL.md +123 -0
  18. package/skills/shipping-production-plugins-and-packages/references/plugin-delivery.md +6 -0
  19. package/src/application/plugins/build-plugin.js +5 -0
  20. package/src/application/plugins/inspect-plugin-bundle.js +5 -0
  21. package/src/application/plugins/validate-plugin-bundle.js +5 -0
  22. package/src/application/skills/build-skill-workbench-model.js +194 -0
  23. package/src/application/skills/inspect-skill.js +5 -0
  24. package/src/application/skills/list-stale-skills.js +9 -0
  25. package/src/application/skills/run-skill-workbench-action.js +23 -0
  26. package/src/application/skills/start-skill-dev-workbench.js +192 -0
  27. package/src/application/skills/validate-skills.js +5 -0
  28. package/src/cli.js +1 -1
  29. package/src/commands/plugin.js +7 -4
  30. package/src/commands/skills.js +14 -9
  31. package/src/dashboard/App.jsx +343 -0
  32. package/src/dashboard/components/Breadcrumbs.jsx +45 -0
  33. package/src/dashboard/components/ControlStrip.jsx +153 -0
  34. package/src/dashboard/components/InspectorPanel.jsx +203 -0
  35. package/src/dashboard/components/SkillGraph.jsx +567 -0
  36. package/src/dashboard/components/Tooltip.jsx +111 -0
  37. package/src/dashboard/dist/dashboard.js +26692 -0
  38. package/src/dashboard/index.html +81 -0
  39. package/src/dashboard/lib/api.js +19 -0
  40. package/src/dashboard/lib/router.js +15 -0
  41. package/src/dashboard/main.jsx +4 -0
  42. package/src/domain/plugins/load-plugin-definition.js +163 -0
  43. package/src/domain/plugins/plugin-diagnostic-error.js +18 -0
  44. package/src/domain/plugins/plugin-requirements.js +15 -0
  45. package/src/domain/skills/skill-graph.js +137 -0
  46. package/src/domain/skills/skill-model.js +187 -0
  47. package/src/domain/skills/skill-provenance.js +69 -0
  48. package/src/infrastructure/fs/build-state-repository.js +16 -0
  49. package/src/infrastructure/fs/install-state-repository.js +16 -0
  50. package/src/infrastructure/runtime/materialize-skills.js +117 -0
  51. package/src/infrastructure/runtime/open-browser.js +20 -0
  52. package/src/infrastructure/runtime/skill-dev-workbench-server.js +96 -0
  53. package/src/infrastructure/runtime/watch-skill-workbench.js +68 -0
  54. package/src/infrastructure/runtime/watch-tree.js +44 -0
  55. package/src/lib/plugins.js +46 -117
  56. package/src/lib/skills.js +141 -459
  57. package/src/utils/errors.js +33 -1
@@ -0,0 +1,69 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { readFileSync } from 'node:fs';
3
+ import { join } from 'node:path';
4
+ import {
5
+ readBuildState as readBuildStateRecord,
6
+ writeBuildState as writeBuildStateRecord,
7
+ } from '../../infrastructure/fs/build-state-repository.js';
8
+
9
+ export function hashFile(filePath) {
10
+ const digest = createHash('sha256').update(readFileSync(filePath)).digest('hex');
11
+ return `sha256:${digest}`;
12
+ }
13
+
14
+ export function readBuildState(repoRoot) {
15
+ return readBuildStateRecord(repoRoot);
16
+ }
17
+
18
+ export function writeBuildState(repoRoot, state) {
19
+ writeBuildStateRecord(repoRoot, state);
20
+ }
21
+
22
+ export function compareRecordedSources(repoRoot, record) {
23
+ const changes = [];
24
+ const recordedSources = record.sources || {};
25
+
26
+ for (const [sourcePath, sourceRecord] of Object.entries(recordedSources)) {
27
+ const absoluteSourcePath = join(repoRoot, sourcePath);
28
+ const currentHash = hashFile(absoluteSourcePath);
29
+ const recordedHash = sourceRecord.hash;
30
+
31
+ if (currentHash !== recordedHash) {
32
+ changes.push({
33
+ path: sourcePath,
34
+ recorded: recordedHash,
35
+ current: currentHash,
36
+ });
37
+ }
38
+ }
39
+
40
+ return changes;
41
+ }
42
+
43
+ export function buildStateRecordForPackageDir(repoRoot, packageDir, {
44
+ parseSkillFrontmatterFile,
45
+ readPackageMetadata,
46
+ normalizeDisplayPath,
47
+ } = {}) {
48
+ const skillFile = join(packageDir, 'SKILL.md');
49
+ const metadata = parseSkillFrontmatterFile(skillFile);
50
+ const packageMetadata = readPackageMetadata(packageDir);
51
+ const sources = {};
52
+
53
+ for (const sourcePath of metadata.sources) {
54
+ sources[sourcePath] = {
55
+ hash: hashFile(join(repoRoot, sourcePath)),
56
+ };
57
+ }
58
+
59
+ return {
60
+ packageName: packageMetadata.packageName,
61
+ record: {
62
+ package_version: packageMetadata.packageVersion,
63
+ skill_path: normalizeDisplayPath(repoRoot, packageDir),
64
+ skill_file: normalizeDisplayPath(repoRoot, skillFile),
65
+ sources,
66
+ requires: metadata.requires,
67
+ },
68
+ };
69
+ }
@@ -0,0 +1,16 @@
1
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+
4
+ export function readBuildState(repoRoot) {
5
+ const buildStatePath = join(repoRoot, '.agentpack', 'build-state.json');
6
+ if (!existsSync(buildStatePath)) {
7
+ return { version: 1, skills: {} };
8
+ }
9
+
10
+ return JSON.parse(readFileSync(buildStatePath, 'utf-8'));
11
+ }
12
+
13
+ export function writeBuildState(repoRoot, state) {
14
+ mkdirSync(join(repoRoot, '.agentpack'), { recursive: true });
15
+ writeFileSync(join(repoRoot, '.agentpack', 'build-state.json'), JSON.stringify(state, null, 2) + '\n');
16
+ }
@@ -0,0 +1,16 @@
1
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+
4
+ export function readInstallState(repoRoot) {
5
+ const installStatePath = join(repoRoot, '.agentpack', 'install.json');
6
+ if (!existsSync(installStatePath)) {
7
+ return { version: 1, installs: {} };
8
+ }
9
+
10
+ return JSON.parse(readFileSync(installStatePath, 'utf-8'));
11
+ }
12
+
13
+ export function writeInstallState(repoRoot, state) {
14
+ mkdirSync(join(repoRoot, '.agentpack'), { recursive: true });
15
+ writeFileSync(join(repoRoot, '.agentpack', 'install.json'), JSON.stringify(state, null, 2) + '\n');
16
+ }
@@ -0,0 +1,117 @@
1
+ import { existsSync, mkdirSync, rmSync, symlinkSync } from 'node:fs';
2
+ import { dirname, join, relative } from 'node:path';
3
+ import { writeInstallState } from '../fs/install-state-repository.js';
4
+
5
+ function ensureDir(pathValue) {
6
+ mkdirSync(pathValue, { recursive: true });
7
+ }
8
+
9
+ export function removePathIfExists(pathValue) {
10
+ rmSync(pathValue, { recursive: true, force: true });
11
+ }
12
+
13
+ export function ensureSkillLink(repoRoot, baseDir, skillName, skillDir, normalizeDisplayPath) {
14
+ const skillsDir = join(repoRoot, baseDir, 'skills');
15
+ ensureDir(skillsDir);
16
+ const linkPath = join(skillsDir, skillName);
17
+ removePathIfExists(linkPath);
18
+ symlinkSync(skillDir, linkPath, 'dir');
19
+ return normalizeDisplayPath(repoRoot, linkPath);
20
+ }
21
+
22
+ export function removeSkillLinks(repoRoot, name, normalizeDisplayPath) {
23
+ const removed = [];
24
+ for (const pathValue of [
25
+ join(repoRoot, '.claude', 'skills', name),
26
+ join(repoRoot, '.agents', 'skills', name),
27
+ ]) {
28
+ if (!existsSync(pathValue)) continue;
29
+ removePathIfExists(pathValue);
30
+ removed.push(normalizeDisplayPath(repoRoot, pathValue));
31
+ }
32
+ return removed;
33
+ }
34
+
35
+ export function removeSkillLinksByNames(repoRoot, names, normalizeDisplayPath) {
36
+ const removed = [];
37
+ for (const name of names) {
38
+ removed.push(...removeSkillLinks(repoRoot, name, normalizeDisplayPath));
39
+ }
40
+ return [...new Set(removed)];
41
+ }
42
+
43
+ function ensureSymlink(targetPath, linkPath) {
44
+ removePathIfExists(linkPath);
45
+ mkdirSync(dirname(linkPath), { recursive: true });
46
+ symlinkSync(targetPath, linkPath, 'dir');
47
+ }
48
+
49
+ export function buildInstallRecord(repoRoot, packageDir, directTargetMap, {
50
+ parseSkillFrontmatterFile,
51
+ readPackageMetadata,
52
+ normalizeRelativePath,
53
+ } = {}) {
54
+ const packageMetadata = readPackageMetadata(packageDir);
55
+ if (!packageMetadata.packageName) return null;
56
+
57
+ const skillMetadata = parseSkillFrontmatterFile(join(packageDir, 'SKILL.md'));
58
+ const skillDirName = skillMetadata.name;
59
+ const materializations = [];
60
+
61
+ const claudeTargetAbs = join(repoRoot, '.claude', 'skills', skillDirName);
62
+ ensureSymlink(packageDir, claudeTargetAbs);
63
+ materializations.push({
64
+ target: normalizeRelativePath(relative(repoRoot, claudeTargetAbs)),
65
+ mode: 'symlink',
66
+ });
67
+
68
+ const agentsTargetAbs = join(repoRoot, '.agents', 'skills', skillDirName);
69
+ ensureSymlink(packageDir, agentsTargetAbs);
70
+ materializations.push({
71
+ target: normalizeRelativePath(relative(repoRoot, agentsTargetAbs)),
72
+ mode: 'symlink',
73
+ });
74
+
75
+ return {
76
+ packageName: packageMetadata.packageName,
77
+ direct: directTargetMap.has(packageMetadata.packageName),
78
+ requestedTarget: directTargetMap.get(packageMetadata.packageName) || null,
79
+ packageVersion: packageMetadata.packageVersion,
80
+ sourcePackagePath: normalizeRelativePath(relative(repoRoot, packageDir)),
81
+ materializations,
82
+ };
83
+ }
84
+
85
+ export function rebuildInstallState(repoRoot, directTargetMap, {
86
+ listInstalledPackageDirs,
87
+ parseSkillFrontmatterFile,
88
+ readPackageMetadata,
89
+ normalizeRelativePath,
90
+ } = {}) {
91
+ const packageDirs = listInstalledPackageDirs(join(repoRoot, 'node_modules'));
92
+ const installs = {};
93
+
94
+ for (const packageDir of packageDirs) {
95
+ const skillFile = join(packageDir, 'SKILL.md');
96
+ if (!existsSync(skillFile)) continue;
97
+
98
+ const record = buildInstallRecord(repoRoot, packageDir, directTargetMap, {
99
+ parseSkillFrontmatterFile,
100
+ readPackageMetadata,
101
+ normalizeRelativePath,
102
+ });
103
+ if (!record) continue;
104
+
105
+ installs[record.packageName] = {
106
+ direct: record.direct,
107
+ requested_target: record.requestedTarget,
108
+ package_version: record.packageVersion,
109
+ source_package_path: record.sourcePackagePath,
110
+ materializations: record.materializations,
111
+ };
112
+ }
113
+
114
+ const state = { version: 1, installs };
115
+ writeInstallState(repoRoot, state);
116
+ return state;
117
+ }
@@ -0,0 +1,20 @@
1
+ import { spawn } from 'node:child_process';
2
+
3
+ export function openBrowser(url) {
4
+ if (process.env.AGENTPACK_DISABLE_BROWSER === '1') return;
5
+
6
+ const command = process.platform === 'darwin'
7
+ ? 'open'
8
+ : process.platform === 'win32'
9
+ ? 'start'
10
+ : 'xdg-open';
11
+
12
+ try {
13
+ spawn(command, [url], {
14
+ detached: true,
15
+ stdio: 'ignore',
16
+ }).unref();
17
+ } catch {
18
+ // Browser launch failure should not fail the core dev workflow.
19
+ }
20
+ }
@@ -0,0 +1,96 @@
1
+ import http from 'node:http';
2
+ import { existsSync, readFileSync } from 'node:fs';
3
+ import { dirname, join, resolve } from 'node:path';
4
+ import { fileURLToPath } from 'node:url';
5
+ import { AgentpackError } from '../../utils/errors.js';
6
+
7
+ const __dirname = dirname(fileURLToPath(import.meta.url));
8
+ const projectRoot = join(__dirname, '..', '..', '..');
9
+ const htmlPath = join(projectRoot, 'src', 'dashboard', 'index.html');
10
+ const bundlePath = process.env.AGENTPACK_DASHBOARD_BUNDLE_PATH
11
+ ? resolve(process.cwd(), process.env.AGENTPACK_DASHBOARD_BUNDLE_PATH)
12
+ : join(projectRoot, 'src', 'dashboard', 'dist', 'dashboard.js');
13
+
14
+ function ensureDashboardBundle() {
15
+ if (existsSync(bundlePath)) return;
16
+
17
+ throw new AgentpackError('Skill workbench bundle is missing from this install', {
18
+ code: 'missing_skill_workbench_bundle',
19
+ suggestion: 'Reinstall agentpack or rebuild the dashboard bundle before starting skills dev.',
20
+ path: bundlePath,
21
+ });
22
+ }
23
+
24
+ export async function startSkillDevWorkbenchServer({ buildModel, defaultSkill, onAction = null }) {
25
+ ensureDashboardBundle();
26
+
27
+ const server = http.createServer(async (req, res) => {
28
+ if (!req.url) {
29
+ res.writeHead(404);
30
+ res.end();
31
+ return;
32
+ }
33
+
34
+ const parsedUrl = new URL(req.url, `http://${req.headers.host || 'localhost'}`);
35
+
36
+ if (parsedUrl.pathname === '/api/model') {
37
+ const skillParam = parsedUrl.searchParams.get('skill') || defaultSkill;
38
+ try {
39
+ const model = buildModel(skillParam);
40
+ if (!model) {
41
+ res.writeHead(404, { 'content-type': 'application/json' });
42
+ res.end(JSON.stringify({ error: 'Skill not found' }));
43
+ return;
44
+ }
45
+ res.writeHead(200, { 'content-type': 'application/json' });
46
+ res.end(JSON.stringify(model));
47
+ } catch (error) {
48
+ res.writeHead(500, { 'content-type': 'application/json' });
49
+ res.end(JSON.stringify({ error: error.message }));
50
+ }
51
+ return;
52
+ }
53
+
54
+ if (req.method === 'POST' && parsedUrl.pathname.startsWith('/api/actions/')) {
55
+ const action = parsedUrl.pathname.slice('/api/actions/'.length);
56
+ res.writeHead(200, { 'content-type': 'application/json' });
57
+ try {
58
+ const result = onAction ? await onAction(action) : { refreshed: true };
59
+ res.end(JSON.stringify({ ok: true, action, result }));
60
+ } catch (error) {
61
+ res.statusCode = 500;
62
+ res.end(JSON.stringify({ ok: false, action, error: error.message }));
63
+ }
64
+ return;
65
+ }
66
+
67
+ if (parsedUrl.pathname === '/') {
68
+ res.writeHead(200, { 'content-type': 'text/html; charset=utf-8' });
69
+ res.end(readFileSync(htmlPath, 'utf-8'));
70
+ return;
71
+ }
72
+
73
+ if (parsedUrl.pathname === '/assets/dashboard.js') {
74
+ res.writeHead(200, { 'content-type': 'text/javascript; charset=utf-8' });
75
+ res.end(readFileSync(bundlePath, 'utf-8'));
76
+ return;
77
+ }
78
+
79
+ res.writeHead(404);
80
+ res.end();
81
+ });
82
+
83
+ return new Promise((resolve, reject) => {
84
+ server.once('error', reject);
85
+ server.listen(0, '127.0.0.1', () => {
86
+ const address = server.address();
87
+ resolve({
88
+ port: address.port,
89
+ url: `http://127.0.0.1:${address.port}`,
90
+ close() {
91
+ server.close();
92
+ },
93
+ });
94
+ });
95
+ });
96
+ }
@@ -0,0 +1,68 @@
1
+ import { watch } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import { parseSkillFrontmatterFile } from '../../domain/skills/skill-model.js';
4
+
5
+ export function watchSkillWorkbench(repoRoot, skillDir, onRefresh) {
6
+ const staticWatchers = [];
7
+ const sourceWatchers = new Map();
8
+ let timer = null;
9
+
10
+ const refresh = () => {
11
+ clearTimeout(timer);
12
+ timer = setTimeout(() => {
13
+ onRefresh();
14
+ }, 50);
15
+ };
16
+
17
+ const watchPath = (pathValue) => {
18
+ try {
19
+ const watcher = watch(pathValue, refresh);
20
+ return watcher;
21
+ } catch {
22
+ // Ignore paths that cannot be watched for now.
23
+ return null;
24
+ }
25
+ };
26
+
27
+ const syncSourceWatchers = () => {
28
+ let metadata;
29
+ try {
30
+ metadata = parseSkillFrontmatterFile(skillFile);
31
+ } catch {
32
+ return;
33
+ }
34
+
35
+ const nextSources = new Set(metadata.sources.map((source) => join(repoRoot, source)));
36
+
37
+ for (const [pathValue, watcher] of sourceWatchers.entries()) {
38
+ if (nextSources.has(pathValue)) continue;
39
+ watcher.close();
40
+ sourceWatchers.delete(pathValue);
41
+ }
42
+
43
+ for (const pathValue of nextSources) {
44
+ if (sourceWatchers.has(pathValue)) continue;
45
+ const watcher = watchPath(pathValue);
46
+ if (watcher) sourceWatchers.set(pathValue, watcher);
47
+ }
48
+ };
49
+
50
+ const skillFile = join(skillDir, 'SKILL.md');
51
+ const packageJsonPath = join(skillDir, 'package.json');
52
+ const skillFileWatcher = watch(skillFile, () => {
53
+ syncSourceWatchers();
54
+ refresh();
55
+ });
56
+ staticWatchers.push(skillFileWatcher);
57
+ const packageWatcher = watchPath(packageJsonPath);
58
+ if (packageWatcher) staticWatchers.push(packageWatcher);
59
+ syncSourceWatchers();
60
+
61
+ return {
62
+ close() {
63
+ clearTimeout(timer);
64
+ for (const watcher of staticWatchers) watcher.close();
65
+ for (const watcher of sourceWatchers.values()) watcher.close();
66
+ },
67
+ };
68
+ }
@@ -0,0 +1,44 @@
1
+ import { existsSync, readdirSync, watch } from 'node:fs';
2
+ import { join } from 'node:path';
3
+
4
+ export function watchDirectoryTree(rootDir, onChange) {
5
+ const watchers = new Map();
6
+
7
+ const watchDir = (dirPath) => {
8
+ if (watchers.has(dirPath) || !existsSync(dirPath)) return;
9
+
10
+ let entries = [];
11
+ try {
12
+ entries = readdirSync(dirPath, { withFileTypes: true });
13
+ } catch {
14
+ return;
15
+ }
16
+
17
+ const watcher = watch(dirPath, (_eventType, filename) => {
18
+ if (filename) {
19
+ const changedPath = join(dirPath, String(filename));
20
+ if (existsSync(changedPath)) {
21
+ watchDir(changedPath);
22
+ }
23
+ }
24
+
25
+ onChange();
26
+ });
27
+
28
+ watchers.set(dirPath, watcher);
29
+
30
+ for (const entry of entries) {
31
+ if (!entry.isDirectory()) continue;
32
+ watchDir(join(dirPath, entry.name));
33
+ }
34
+ };
35
+
36
+ watchDir(rootDir);
37
+
38
+ return {
39
+ close() {
40
+ for (const watcher of watchers.values()) watcher.close();
41
+ watchers.clear();
42
+ },
43
+ };
44
+ }
@@ -1,11 +1,12 @@
1
- import { cpSync, existsSync, mkdirSync, readFileSync, readdirSync, renameSync, rmSync, watch, writeFileSync } from 'node:fs';
1
+ import { cpSync, existsSync, mkdirSync, readFileSync, readdirSync, renameSync, rmSync, writeFileSync } from 'node:fs';
2
2
  import { dirname, join, resolve } from 'node:path';
3
3
  import { createRequire } from 'node:module';
4
+ import { resolveDependencyClosure } from '../domain/skills/skill-graph.js';
5
+ import { normalizeRepoPath, parseSkillFrontmatterFile, readPackageMetadata } from '../domain/skills/skill-model.js';
6
+ import { loadPluginDefinition } from '../domain/plugins/load-plugin-definition.js';
7
+ import { watchDirectoryTree } from '../infrastructure/runtime/watch-tree.js';
4
8
  import {
5
9
  findPackageDirByName,
6
- normalizeRepoPath,
7
- parseSkillFrontmatterFile,
8
- readPackageMetadata,
9
10
  syncSkillDependencies,
10
11
  } from './skills.js';
11
12
  import { findRepoRoot } from './context.js';
@@ -76,50 +77,30 @@ function collectPluginLocalSkillDirs(pluginDir) {
76
77
  }
77
78
 
78
79
  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();
80
+ const { resolved, unresolved } = resolveDependencyClosure(directRequires, {
81
+ resolveNode(packageName) {
82
+ const resolvedPackage = resolvePackageDir(repoRoot, pluginDir, packageName);
83
+ if (!resolvedPackage) return null;
84
+
85
+ const skillFile = join(resolvedPackage.packageDir, 'SKILL.md');
86
+ if (!existsSync(skillFile)) return null;
87
+
88
+ const metadata = parseSkillFrontmatterFile(skillFile);
89
+ const packageMetadata = readPackageMetadata(resolvedPackage.packageDir);
90
+
91
+ return {
92
+ packageName,
93
+ packageVersion: packageMetadata.packageVersion,
94
+ skillName: metadata.name,
95
+ skillFile,
96
+ packageDir: resolvedPackage.packageDir,
97
+ source: resolvedPackage.source,
98
+ requires: metadata.requires,
99
+ };
100
+ },
101
+ });
121
102
 
122
- return { bundled, unresolved };
103
+ return { bundled: resolved, unresolved };
123
104
  }
124
105
 
125
106
  function stagePluginRuntimeFiles(pluginDir, stageDir) {
@@ -130,48 +111,6 @@ function stagePluginRuntimeFiles(pluginDir, stageDir) {
130
111
  }
131
112
  }
132
113
 
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
114
  export function buildPlugin(target, {
176
115
  cwd = process.cwd(),
177
116
  clean = false,
@@ -278,27 +217,16 @@ export function startPluginDev(target, {
278
217
  }
279
218
 
280
219
  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
- }
220
+ const definition = loadPluginDefinition(target, { cwd, requirementLevel: 'inspect' });
221
+ const {
222
+ repoRoot,
223
+ pluginDir,
224
+ packageJson,
225
+ packageName,
226
+ packageVersion,
227
+ pluginManifest,
228
+ pluginManifestPath,
229
+ } = definition;
302
230
 
303
231
  const localSkills = collectPluginLocalSkills(pluginDir);
304
232
  const directRequires = [...new Set(localSkills.flatMap((skill) => skill.requires))].sort();
@@ -306,9 +234,9 @@ export function inspectPluginBundle(target, { cwd = process.cwd() } = {}) {
306
234
  const directPackageSet = new Set(directRequires);
307
235
 
308
236
  return {
309
- pluginName: pluginManifest.name || packageMetadata.packageName,
310
- packageName: packageMetadata.packageName,
311
- packageVersion: packageMetadata.packageVersion,
237
+ pluginName: pluginManifest.name || packageName,
238
+ packageName,
239
+ packageVersion,
312
240
  pluginPath: normalizeRepoPath(repoRoot, pluginDir),
313
241
  pluginManifestPath: normalizeRepoPath(repoRoot, pluginManifestPath),
314
242
  localSkills: localSkills.map((skill) => ({
@@ -341,10 +269,11 @@ export function inspectPluginBundle(target, { cwd = process.cwd() } = {}) {
341
269
  }
342
270
 
343
271
  export function validatePluginBundle(target, { cwd = process.cwd() } = {}) {
344
- const repoRoot = findRepoRoot(cwd);
345
- const pluginDir = resolvePluginDir(repoRoot, target);
272
+ const { repoRoot, pluginDir, packageJson } = loadPluginDefinition(target, {
273
+ cwd,
274
+ requirementLevel: 'validate',
275
+ });
346
276
  const result = inspectPluginBundle(target, { cwd });
347
- const packageMetadata = readPackageMetadata(pluginDir);
348
277
  const issues = [];
349
278
  const localSkillNames = new Set(result.localSkills.map((skill) => skill.localName));
350
279
  const bundledSkillNames = new Map();
@@ -361,7 +290,7 @@ export function validatePluginBundle(target, { cwd = process.cwd() } = {}) {
361
290
 
362
291
  for (const localSkill of result.localSkills) {
363
292
  for (const packageName of localSkill.requires) {
364
- const declared = packageMetadata.devDependencies[packageName];
293
+ const declared = packageJson.devDependencies?.[packageName];
365
294
  const resolvedPackage = resolvePackageDir(repoRoot, pluginDir, packageName);
366
295
  if (!declared || !resolvedPackage) {
367
296
  coveredPackages.add(packageName);