@alavida/agentpack 0.1.1 → 0.1.2

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/src/lib/skills.js CHANGED
@@ -1,8 +1,34 @@
1
- import { existsSync, mkdirSync, readFileSync, readdirSync, rmSync, symlinkSync, watch, writeFileSync } from 'node:fs';
1
+ import { existsSync, readFileSync, readdirSync, watch, writeFileSync } from 'node:fs';
2
2
  import { dirname, isAbsolute, join, relative, resolve } from 'node:path';
3
- import { createHash } from 'node:crypto';
4
3
  import { execFileSync } from 'node:child_process';
5
4
  import { findAllWorkbenches, findRepoRoot, findWorkbenchContext, resolveWorkbenchFlag } from './context.js';
5
+ import {
6
+ buildReverseDependencies,
7
+ buildSkillGraph,
8
+ buildSkillStatusMap,
9
+ readNodeStatus,
10
+ } from '../domain/skills/skill-graph.js';
11
+ import { readInstallState } from '../infrastructure/fs/install-state-repository.js';
12
+ import {
13
+ ensureSkillLink,
14
+ rebuildInstallState,
15
+ removePathIfExists,
16
+ removeSkillLinks,
17
+ removeSkillLinksByNames,
18
+ } from '../infrastructure/runtime/materialize-skills.js';
19
+ import {
20
+ normalizeDisplayPath,
21
+ normalizeRepoPath,
22
+ parseSkillFrontmatterFile,
23
+ readPackageMetadata,
24
+ } from '../domain/skills/skill-model.js';
25
+ import {
26
+ buildStateRecordForPackageDir,
27
+ compareRecordedSources,
28
+ hashFile,
29
+ readBuildState,
30
+ writeBuildState,
31
+ } from '../domain/skills/skill-provenance.js';
6
32
  import { AgentpackError, EXIT_CODES, NetworkError, NotFoundError, ValidationError } from '../utils/errors.js';
7
33
 
8
34
  const GITHUB_PACKAGES_REGISTRY = 'https://npm.pkg.github.com';
@@ -17,173 +43,6 @@ function inferManagedScope(packageName) {
17
43
  return MANAGED_PACKAGE_SCOPES.find((scope) => packageName?.startsWith(`${scope}/`)) || null;
18
44
  }
19
45
 
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
46
  function resolveDevLinkedSkills(repoRoot, rootSkillDir) {
188
47
  const queue = [rootSkillDir];
189
48
  const seenDirs = new Set();
@@ -296,32 +155,6 @@ export function findPackageDirByName(repoRoot, packageName) {
296
155
  return null;
297
156
  }
298
157
 
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
158
  function readPackageJson(packageDir) {
326
159
  const packageJsonPath = join(packageDir, 'package.json');
327
160
  if (!existsSync(packageJsonPath)) {
@@ -398,8 +231,8 @@ export function devSkill(target, {
398
231
  const links = [];
399
232
 
400
233
  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));
234
+ links.push(ensureSkillLink(repoRoot, '.claude', linkedSkill.name, linkedSkill.skillDir, normalizeDisplayPath));
235
+ links.push(ensureSkillLink(repoRoot, '.agents', linkedSkill.name, linkedSkill.skillDir, normalizeDisplayPath));
403
236
  }
404
237
 
405
238
  return {
@@ -427,27 +260,6 @@ export function devSkill(target, {
427
260
  }
428
261
  }
429
262
 
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
263
  export function startSkillDev(target, {
452
264
  cwd = process.cwd(),
453
265
  sync = true,
@@ -466,7 +278,7 @@ export function startSkillDev(target, {
466
278
  closed = true;
467
279
  clearTimeout(timer);
468
280
  if (watcher) watcher.close();
469
- const removed = removeSkillLinksByNames(repoRoot, currentNames);
281
+ const removed = removeSkillLinksByNames(repoRoot, currentNames, normalizeDisplayPath);
470
282
  detachProcessCleanup();
471
283
  return {
472
284
  name: currentNames[0] || null,
@@ -498,7 +310,7 @@ export function startSkillDev(target, {
498
310
  const nextNames = result.linkedSkills.map((entry) => entry.name);
499
311
  const staleNames = currentNames.filter((name) => !nextNames.includes(name));
500
312
  if (staleNames.length > 0) {
501
- removeSkillLinksByNames(repoRoot, staleNames);
313
+ removeSkillLinksByNames(repoRoot, staleNames, normalizeDisplayPath);
502
314
  }
503
315
  currentNames = nextNames;
504
316
  return result;
@@ -544,7 +356,7 @@ export function unlinkSkill(name, { cwd = process.cwd() } = {}) {
544
356
  });
545
357
  }
546
358
 
547
- const removed = removeSkillLinks(repoRoot, name);
359
+ const removed = removeSkillLinks(repoRoot, name, normalizeDisplayPath);
548
360
 
549
361
  return {
550
362
  name,
@@ -553,10 +365,6 @@ export function unlinkSkill(name, { cwd = process.cwd() } = {}) {
553
365
  };
554
366
  }
555
367
 
556
- export function normalizeRepoPath(repoRoot, absolutePath) {
557
- return normalizeDisplayPath(repoRoot, absolutePath);
558
- }
559
-
560
368
  function buildValidateNextSteps(packageMetadata, valid) {
561
369
  if (!valid || !packageMetadata.packageName) return [];
562
370
 
@@ -737,48 +545,16 @@ export function inspectSkill(target, { cwd = process.cwd() } = {}) {
737
545
  };
738
546
  }
739
547
 
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
548
  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;
549
+ return buildSkillGraph(repoRoot, listPackagedSkillDirs(repoRoot), {
550
+ parseSkillFrontmatterFile,
551
+ readPackageMetadata,
552
+ findPackageDirByName,
553
+ normalizeDisplayPath,
554
+ });
778
555
  }
779
556
 
780
557
  function buildInstalledSkillGraph(repoRoot) {
781
- const nodes = new Map();
782
558
  const installState = readInstallState(repoRoot);
783
559
  const directInstallNames = new Set(
784
560
  Object.entries(installState.installs || {})
@@ -786,73 +562,19 @@ function buildInstalledSkillGraph(repoRoot) {
786
562
  .map(([packageName]) => packageName)
787
563
  );
788
564
 
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;
565
+ return buildSkillGraph(repoRoot, listInstalledPackageDirs(join(repoRoot, 'node_modules')), {
566
+ directInstallNames,
567
+ parseSkillFrontmatterFile,
568
+ readPackageMetadata,
569
+ findPackageDirByName,
570
+ normalizeDisplayPath,
571
+ });
811
572
  }
812
573
 
813
- function buildSkillStatusMap(repoRoot) {
574
+ function buildSkillStatusMapForRepo(repoRoot) {
814
575
  const nodes = buildAuthoredSkillGraph(repoRoot);
815
576
  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;
577
+ return buildSkillStatusMap(nodes, staleSkills);
856
578
  }
857
579
 
858
580
  export function inspectSkillDependencies(target, {
@@ -863,7 +585,7 @@ export function inspectSkillDependencies(target, {
863
585
  const authoredNodes = buildAuthoredSkillGraph(repoRoot);
864
586
  const installedNodes = buildInstalledSkillGraph(repoRoot);
865
587
  const statusRoot = discoveryRoot ? resolve(discoveryRoot) : repoRoot;
866
- const statusMap = buildSkillStatusMap(statusRoot);
588
+ const statusMap = buildSkillStatusMapForRepo(statusRoot);
867
589
 
868
590
  const authoredTarget = authoredNodes.get(target) || null;
869
591
  const installedTarget = installedNodes.get(target) || null;
@@ -1086,6 +808,26 @@ export function validateSkills(target, { cwd = process.cwd() } = {}) {
1086
808
  const validCount = skills.filter((skill) => skill.valid).length;
1087
809
  const invalidCount = skills.length - validCount;
1088
810
 
811
+ if (validCount > 0) {
812
+ const buildState = readBuildState(repoRoot);
813
+
814
+ for (const packageDir of packageDirs) {
815
+ const packageMetadata = readPackageMetadata(packageDir);
816
+ const result = skills.find((skill) => skill.packageName === packageMetadata.packageName);
817
+ if (!result?.valid) continue;
818
+
819
+ const { packageName, record } = buildStateRecordForPackageDir(repoRoot, packageDir, {
820
+ parseSkillFrontmatterFile,
821
+ readPackageMetadata,
822
+ normalizeDisplayPath,
823
+ });
824
+ if (!packageName) continue;
825
+ buildState.skills[packageName] = record;
826
+ }
827
+
828
+ writeBuildState(repoRoot, buildState);
829
+ }
830
+
1089
831
  return {
1090
832
  valid: invalidCount === 0,
1091
833
  count: skills.length,
@@ -1094,67 +836,16 @@ export function validateSkills(target, { cwd = process.cwd() } = {}) {
1094
836
  skills,
1095
837
  };
1096
838
  }
1097
-
1098
-
1099
- function hashFile(filePath) {
1100
- const digest = createHash('sha256').update(readFileSync(filePath)).digest('hex');
1101
- return `sha256:${digest}`;
1102
- }
1103
-
1104
839
  function normalizeRelativePath(pathValue) {
1105
840
  return pathValue.split('\\').join('/');
1106
841
  }
1107
842
 
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
843
  function normalizeRequestedTarget(target, cwd = process.cwd()) {
1132
844
  if (typeof target !== 'string') return target;
1133
845
  if (target.startsWith('@')) return target;
1134
846
  return normalizeRelativePath(resolve(cwd, target));
1135
847
  }
1136
848
 
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
849
  function listPackagedSkillDirs(repoRoot) {
1159
850
  const stack = [repoRoot];
1160
851
  const results = [];
@@ -1387,73 +1078,6 @@ function resolveNpmInstallTargets(directTargetMap) {
1387
1078
  return [...new Set(npmInstallTargets)];
1388
1079
  }
1389
1080
 
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
1081
  export function installSkills(targets, { cwd = process.cwd() } = {}) {
1458
1082
  const repoRoot = findRepoRoot(cwd);
1459
1083
  const previousState = readInstallState(repoRoot);
@@ -1493,7 +1117,12 @@ export function installSkills(targets, { cwd = process.cwd() } = {}) {
1493
1117
  stdio: ['pipe', 'pipe', 'pipe'],
1494
1118
  });
1495
1119
 
1496
- return rebuildInstallState(repoRoot, directTargetMap);
1120
+ return rebuildInstallState(repoRoot, directTargetMap, {
1121
+ listInstalledPackageDirs,
1122
+ parseSkillFrontmatterFile,
1123
+ readPackageMetadata,
1124
+ normalizeRelativePath,
1125
+ });
1497
1126
  }
1498
1127
 
1499
1128
  export function inspectSkillsEnv({ cwd = process.cwd() } = {}) {
@@ -1809,7 +1438,12 @@ export function uninstallSkills(target, { cwd = process.cwd() } = {}) {
1809
1438
  });
1810
1439
  }
1811
1440
 
1812
- const nextState = rebuildInstallState(repoRoot, nextDirectTargetMap);
1441
+ const nextState = rebuildInstallState(repoRoot, nextDirectTargetMap, {
1442
+ listInstalledPackageDirs,
1443
+ parseSkillFrontmatterFile,
1444
+ readPackageMetadata,
1445
+ normalizeRelativePath,
1446
+ });
1813
1447
  const remainingTargets = new Set(
1814
1448
  Object.values(nextState.installs)
1815
1449
  .flatMap((install) => (install.materializations || []).map((entry) => entry.target))