@alavida/agentpack 0.1.5 → 0.1.6

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.
@@ -0,0 +1,74 @@
1
+ import { existsSync, readFileSync, writeFileSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import { homedir } from 'node:os';
4
+
5
+ export function parseNpmrc(content) {
6
+ const config = {};
7
+ for (const rawLine of content.split('\n')) {
8
+ const line = rawLine.trim();
9
+ if (!line || line.startsWith('#') || line.startsWith(';')) continue;
10
+ const eqIndex = line.indexOf('=');
11
+ if (eqIndex === -1) continue;
12
+ const key = line.slice(0, eqIndex).trim();
13
+ const value = line.slice(eqIndex + 1).trim();
14
+ config[key] = value;
15
+ }
16
+ return config;
17
+ }
18
+
19
+ export function getUserNpmrcPath({ env = process.env } = {}) {
20
+ return join(env.HOME || homedir(), '.npmrc');
21
+ }
22
+
23
+ export function readUserNpmrc({ env = process.env } = {}) {
24
+ const npmrcPath = getUserNpmrcPath({ env });
25
+ if (!existsSync(npmrcPath)) return {};
26
+ return parseNpmrc(readFileSync(npmrcPath, 'utf-8'));
27
+ }
28
+
29
+ function upsertLine(lines, key, value) {
30
+ const prefix = `${key}=`;
31
+ const nextLine = `${key}=${value}`;
32
+ const index = lines.findIndex((line) => line.trim().startsWith(prefix));
33
+ if (index === -1) {
34
+ lines.push(nextLine);
35
+ return;
36
+ }
37
+ lines[index] = nextLine;
38
+ }
39
+
40
+ function removeLine(lines, key) {
41
+ const prefix = `${key}=`;
42
+ return lines.filter((line) => !line.trim().startsWith(prefix));
43
+ }
44
+
45
+ export function writeManagedNpmrcEntries({
46
+ entries,
47
+ env = process.env,
48
+ } = {}) {
49
+ const npmrcPath = getUserNpmrcPath({ env });
50
+ const lines = existsSync(npmrcPath)
51
+ ? readFileSync(npmrcPath, 'utf-8').split('\n').filter((line, index, all) => !(index === all.length - 1 && line === ''))
52
+ : [];
53
+
54
+ for (const [key, value] of Object.entries(entries)) {
55
+ upsertLine(lines, key, value);
56
+ }
57
+
58
+ writeFileSync(npmrcPath, `${lines.join('\n')}\n`);
59
+ }
60
+
61
+ export function removeManagedNpmrcEntries({
62
+ keys,
63
+ env = process.env,
64
+ } = {}) {
65
+ const npmrcPath = getUserNpmrcPath({ env });
66
+ if (!existsSync(npmrcPath)) return;
67
+
68
+ let lines = readFileSync(npmrcPath, 'utf-8').split('\n').filter((line, index, all) => !(index === all.length - 1 && line === ''));
69
+ for (const key of keys) {
70
+ lines = removeLine(lines, key);
71
+ }
72
+
73
+ writeFileSync(npmrcPath, `${lines.join('\n')}\n`);
74
+ }
@@ -0,0 +1,153 @@
1
+ import { lstatSync, readlinkSync, readdirSync } from 'node:fs';
2
+ import { join, resolve } from 'node:path';
3
+ import { readDevSession } from '../fs/dev-session-repository.js';
4
+
5
+ function readPathType(pathValue) {
6
+ try {
7
+ const stat = lstatSync(pathValue);
8
+ return {
9
+ exists: true,
10
+ isSymlink: stat.isSymbolicLink(),
11
+ type: stat.isDirectory() ? 'directory' : stat.isFile() ? 'file' : 'other',
12
+ };
13
+ } catch {
14
+ return {
15
+ exists: false,
16
+ isSymlink: false,
17
+ type: null,
18
+ };
19
+ }
20
+ }
21
+
22
+ export function inspectRecordedMaterialization(repoRoot, {
23
+ target,
24
+ expectedSourcePath,
25
+ packageName,
26
+ runtimeName = null,
27
+ } = {}) {
28
+ const absTarget = resolve(repoRoot, target);
29
+ const expectedTarget = resolve(repoRoot, expectedSourcePath);
30
+ const pathState = readPathType(absTarget);
31
+
32
+ if (!pathState.exists) {
33
+ return {
34
+ packageName,
35
+ runtimeName,
36
+ target,
37
+ expectedSourcePath,
38
+ code: 'missing_path',
39
+ };
40
+ }
41
+
42
+ if (!pathState.isSymlink) {
43
+ return {
44
+ packageName,
45
+ runtimeName,
46
+ target,
47
+ expectedSourcePath,
48
+ code: 'wrong_type',
49
+ actualType: pathState.type,
50
+ };
51
+ }
52
+
53
+ const rawLinkTarget = readlinkSync(absTarget);
54
+ const actualTarget = resolve(join(absTarget, '..'), rawLinkTarget);
55
+ if (actualTarget !== expectedTarget) {
56
+ return {
57
+ packageName,
58
+ runtimeName,
59
+ target,
60
+ expectedSourcePath,
61
+ code: 'wrong_target',
62
+ actualTarget,
63
+ };
64
+ }
65
+
66
+ const resolvedState = readPathType(actualTarget);
67
+ if (!resolvedState.exists) {
68
+ return {
69
+ packageName,
70
+ runtimeName,
71
+ target,
72
+ expectedSourcePath,
73
+ code: 'dangling_target',
74
+ actualTarget,
75
+ };
76
+ }
77
+
78
+ return null;
79
+ }
80
+
81
+ export function inspectMaterializedSkills(repoRoot, state) {
82
+ const runtimeDrift = [];
83
+ const ownedTargets = new Set();
84
+ const devSession = readDevSession(repoRoot);
85
+
86
+ if (devSession?.status === 'active') {
87
+ for (const target of devSession.links || []) {
88
+ ownedTargets.add(target);
89
+ }
90
+ }
91
+
92
+ for (const [packageName, install] of Object.entries(state.installs || {})) {
93
+ const issues = [];
94
+
95
+ for (const skill of install.skills || []) {
96
+ for (const materialization of skill.materializations || []) {
97
+ ownedTargets.add(materialization.target);
98
+ const issue = inspectRecordedMaterialization(repoRoot, {
99
+ target: materialization.target,
100
+ expectedSourcePath: skill.source_skill_path,
101
+ packageName,
102
+ runtimeName: skill.runtime_name,
103
+ });
104
+ if (issue) issues.push(issue);
105
+ }
106
+ }
107
+
108
+ if (issues.length > 0) {
109
+ runtimeDrift.push({
110
+ packageName,
111
+ issues,
112
+ });
113
+ }
114
+ }
115
+
116
+ const orphanedMaterializations = [];
117
+ for (const root of [
118
+ join(repoRoot, '.claude', 'skills'),
119
+ join(repoRoot, '.agents', 'skills'),
120
+ ]) {
121
+ let entries = [];
122
+ try {
123
+ entries = readdirSync(root, { withFileTypes: true });
124
+ } catch {
125
+ continue;
126
+ }
127
+
128
+ for (const entry of entries) {
129
+ const relativeTarget = root.startsWith(join(repoRoot, '.claude'))
130
+ ? `.claude/skills/${entry.name}`
131
+ : `.agents/skills/${entry.name}`;
132
+ if (ownedTargets.has(relativeTarget)) continue;
133
+
134
+ const absPath = join(root, entry.name);
135
+ const pathState = readPathType(absPath);
136
+ orphanedMaterializations.push({
137
+ target: relativeTarget,
138
+ code: 'orphaned_materialization',
139
+ actualType: pathState.isSymlink ? 'symlink' : pathState.type,
140
+ });
141
+ }
142
+ }
143
+
144
+ runtimeDrift.sort((a, b) => a.packageName.localeCompare(b.packageName));
145
+ orphanedMaterializations.sort((a, b) => a.target.localeCompare(b.target));
146
+
147
+ return {
148
+ runtimeDriftCount: runtimeDrift.length,
149
+ runtimeDrift,
150
+ orphanedMaterializationCount: orphanedMaterializations.length,
151
+ orphanedMaterializations,
152
+ };
153
+ }
@@ -1,4 +1,4 @@
1
- import { existsSync, mkdirSync, rmSync, symlinkSync } from 'node:fs';
1
+ import { existsSync, lstatSync, mkdirSync, rmSync, symlinkSync, unlinkSync } from 'node:fs';
2
2
  import { dirname, join, relative, resolve } from 'node:path';
3
3
  import { writeInstallState } from '../fs/install-state-repository.js';
4
4
 
@@ -7,7 +7,17 @@ function ensureDir(pathValue) {
7
7
  }
8
8
 
9
9
  export function removePathIfExists(pathValue) {
10
- rmSync(pathValue, { recursive: true, force: true });
10
+ try {
11
+ const stat = lstatSync(pathValue);
12
+ if (stat.isSymbolicLink() || stat.isFile()) {
13
+ unlinkSync(pathValue);
14
+ return;
15
+ }
16
+ rmSync(pathValue, { recursive: true, force: true });
17
+ } catch (error) {
18
+ if (error?.code === 'ENOENT') return;
19
+ throw error;
20
+ }
11
21
  }
12
22
 
13
23
  export function ensureSkillLink(repoRoot, baseDir, skillName, skillDir, normalizeDisplayPath) {
@@ -25,9 +35,13 @@ export function removeSkillLinks(repoRoot, name, normalizeDisplayPath) {
25
35
  join(repoRoot, '.claude', 'skills', name),
26
36
  join(repoRoot, '.agents', 'skills', name),
27
37
  ]) {
28
- if (!existsSync(pathValue)) continue;
29
- removePathIfExists(pathValue);
30
- removed.push(normalizeDisplayPath(repoRoot, pathValue));
38
+ try {
39
+ removePathIfExists(pathValue);
40
+ removed.push(normalizeDisplayPath(repoRoot, pathValue));
41
+ } catch (error) {
42
+ if (error?.code === 'ENOENT') continue;
43
+ throw error;
44
+ }
31
45
  }
32
46
  return removed;
33
47
  }
@@ -50,9 +64,13 @@ export function removeSkillLinksByPaths(repoRoot, paths, normalizeDisplayPath) {
50
64
  const pathValue = resolve(repoRoot, relativePath);
51
65
  const inAllowedRoot = allowedRoots.some((root) => pathValue === root || pathValue.startsWith(`${root}/`));
52
66
  if (!inAllowedRoot) continue;
53
- if (!existsSync(pathValue)) continue;
54
- removePathIfExists(pathValue);
55
- removed.push(normalizeDisplayPath(repoRoot, pathValue));
67
+ try {
68
+ removePathIfExists(pathValue);
69
+ removed.push(normalizeDisplayPath(repoRoot, pathValue));
70
+ } catch (error) {
71
+ if (error?.code === 'ENOENT') continue;
72
+ throw error;
73
+ }
56
74
  }
57
75
  return [...new Set(removed)];
58
76
  }
@@ -63,31 +81,62 @@ function ensureSymlink(targetPath, linkPath) {
63
81
  symlinkSync(targetPath, linkPath, 'dir');
64
82
  }
65
83
 
84
+ function inferPackageRuntimeNamespace(packageName) {
85
+ return packageName?.split('/').pop() || null;
86
+ }
87
+
88
+ function buildRuntimeName(packageName, exportedSkills, entry) {
89
+ if (exportedSkills.length <= 1) return entry.name;
90
+
91
+ const namespace = inferPackageRuntimeNamespace(packageName);
92
+ if (!namespace) return entry.name;
93
+ if (entry.name === namespace) return namespace;
94
+ return `${namespace}:${entry.name}`;
95
+ }
96
+
66
97
  export function buildInstallRecord(repoRoot, packageDir, directTargetMap, {
67
- parseSkillFrontmatterFile,
68
98
  readPackageMetadata,
99
+ readInstalledSkillExports,
69
100
  normalizeRelativePath,
70
101
  } = {}) {
71
102
  const packageMetadata = readPackageMetadata(packageDir);
72
103
  if (!packageMetadata.packageName) return null;
73
-
74
- const skillMetadata = parseSkillFrontmatterFile(join(packageDir, 'SKILL.md'));
75
- const skillDirName = skillMetadata.name;
104
+ const exportedSkills = readInstalledSkillExports(packageDir);
105
+ if (exportedSkills.length === 0) return null;
76
106
  const materializations = [];
107
+ const skills = [];
77
108
 
78
- const claudeTargetAbs = join(repoRoot, '.claude', 'skills', skillDirName);
79
- ensureSymlink(packageDir, claudeTargetAbs);
80
- materializations.push({
81
- target: normalizeRelativePath(relative(repoRoot, claudeTargetAbs)),
82
- mode: 'symlink',
83
- });
109
+ for (const entry of exportedSkills) {
110
+ const runtimeName = buildRuntimeName(packageMetadata.packageName, exportedSkills, entry);
111
+ const skillMaterializations = [];
84
112
 
85
- const agentsTargetAbs = join(repoRoot, '.agents', 'skills', skillDirName);
86
- ensureSymlink(packageDir, agentsTargetAbs);
87
- materializations.push({
88
- target: normalizeRelativePath(relative(repoRoot, agentsTargetAbs)),
89
- mode: 'symlink',
90
- });
113
+ const claudeTargetAbs = join(repoRoot, '.claude', 'skills', runtimeName);
114
+ ensureSymlink(entry.skillDir, claudeTargetAbs);
115
+ skillMaterializations.push({
116
+ target: normalizeRelativePath(relative(repoRoot, claudeTargetAbs)),
117
+ mode: 'symlink',
118
+ });
119
+
120
+ const agentsTargetAbs = join(repoRoot, '.agents', 'skills', runtimeName);
121
+ ensureSymlink(entry.skillDir, agentsTargetAbs);
122
+ skillMaterializations.push({
123
+ target: normalizeRelativePath(relative(repoRoot, agentsTargetAbs)),
124
+ mode: 'symlink',
125
+ });
126
+
127
+ materializations.push(...skillMaterializations);
128
+ skills.push({
129
+ name: entry.name,
130
+ runtime_name: runtimeName,
131
+ source_skill_path: normalizeRelativePath(relative(repoRoot, entry.skillDir)),
132
+ source_skill_file: normalizeRelativePath(relative(repoRoot, entry.skillFile)),
133
+ requires: entry.requires,
134
+ status: entry.status,
135
+ replacement: entry.replacement,
136
+ message: entry.message,
137
+ materializations: skillMaterializations,
138
+ });
139
+ }
91
140
 
92
141
  return {
93
142
  packageName: packageMetadata.packageName,
@@ -95,26 +144,23 @@ export function buildInstallRecord(repoRoot, packageDir, directTargetMap, {
95
144
  requestedTarget: directTargetMap.get(packageMetadata.packageName) || null,
96
145
  packageVersion: packageMetadata.packageVersion,
97
146
  sourcePackagePath: normalizeRelativePath(relative(repoRoot, packageDir)),
147
+ skills,
98
148
  materializations,
99
149
  };
100
150
  }
101
151
 
102
152
  export function rebuildInstallState(repoRoot, directTargetMap, {
103
- listInstalledPackageDirs,
104
- parseSkillFrontmatterFile,
153
+ packageDirs = [],
105
154
  readPackageMetadata,
155
+ readInstalledSkillExports,
106
156
  normalizeRelativePath,
107
157
  } = {}) {
108
- const packageDirs = listInstalledPackageDirs(join(repoRoot, 'node_modules'));
109
158
  const installs = {};
110
159
 
111
160
  for (const packageDir of packageDirs) {
112
- const skillFile = join(packageDir, 'SKILL.md');
113
- if (!existsSync(skillFile)) continue;
114
-
115
161
  const record = buildInstallRecord(repoRoot, packageDir, directTargetMap, {
116
- parseSkillFrontmatterFile,
117
162
  readPackageMetadata,
163
+ readInstalledSkillExports,
118
164
  normalizeRelativePath,
119
165
  });
120
166
  if (!record) continue;
@@ -124,6 +170,7 @@ export function rebuildInstallState(repoRoot, directTargetMap, {
124
170
  requested_target: record.requestedTarget,
125
171
  package_version: record.packageVersion,
126
172
  source_package_path: record.sourcePackagePath,
173
+ skills: record.skills,
127
174
  materializations: record.materializations,
128
175
  };
129
176
  }
@@ -1,6 +1,12 @@
1
+ import { writeFileSync } from 'node:fs';
1
2
  import { spawn } from 'node:child_process';
2
3
 
3
4
  export function openBrowser(url) {
5
+ if (process.env.AGENTPACK_BROWSER_CAPTURE_PATH) {
6
+ writeFileSync(process.env.AGENTPACK_BROWSER_CAPTURE_PATH, `${url}\n`);
7
+ return;
8
+ }
9
+
4
10
  if (process.env.AGENTPACK_DISABLE_BROWSER === '1') return;
5
11
 
6
12
  const command = process.platform === 'darwin'