@alavida/agentpack 0.1.5 → 0.1.7
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/bin/intent.js +30 -5
- package/package.json +6 -3
- package/src/application/auth/get-auth-status.js +97 -0
- package/src/application/auth/login.js +110 -0
- package/src/application/auth/logout.js +16 -0
- package/src/application/auth/verify-auth.js +37 -0
- package/src/cli.js +2 -0
- package/src/commands/auth.js +78 -0
- package/src/commands/skills.js +62 -5
- package/src/domain/auth/registry-resolution.js +55 -0
- package/src/domain/skills/skill-catalog.js +116 -0
- package/src/domain/skills/skill-model.js +70 -1
- package/src/domain/skills/skill-target-resolution.js +100 -0
- package/src/infrastructure/fs/user-config-repository.js +40 -0
- package/src/infrastructure/fs/user-credentials-repository.js +30 -0
- package/src/infrastructure/fs/user-npmrc-repository.js +74 -0
- package/src/infrastructure/runtime/inspect-materialized-skills.js +153 -0
- package/src/infrastructure/runtime/materialize-skills.js +78 -31
- package/src/infrastructure/runtime/open-browser.js +6 -0
- package/src/lib/skills.js +359 -192
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { existsSync, readdirSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import {
|
|
4
|
+
buildCanonicalSkillRequirement,
|
|
5
|
+
normalizeDisplayPath,
|
|
6
|
+
readInstalledSkillExports,
|
|
7
|
+
readPackageMetadata,
|
|
8
|
+
} from './skill-model.js';
|
|
9
|
+
|
|
10
|
+
function isIgnoredEntry(name) {
|
|
11
|
+
return name === '.git' || name === 'node_modules' || name === '.agentpack';
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function listSkillPackageDirs(repoRoot, { installed = false } = {}) {
|
|
15
|
+
const root = installed ? join(repoRoot, 'node_modules') : repoRoot;
|
|
16
|
+
if (!existsSync(root)) return [];
|
|
17
|
+
|
|
18
|
+
const stack = [root];
|
|
19
|
+
const results = [];
|
|
20
|
+
|
|
21
|
+
while (stack.length > 0) {
|
|
22
|
+
const current = stack.pop();
|
|
23
|
+
let entries = [];
|
|
24
|
+
try {
|
|
25
|
+
entries = readdirSync(current, { withFileTypes: true });
|
|
26
|
+
} catch {
|
|
27
|
+
continue;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
let hasRootSkillFile = false;
|
|
31
|
+
let packageMetadata = null;
|
|
32
|
+
|
|
33
|
+
for (const entry of entries) {
|
|
34
|
+
if (entry.isDirectory()) {
|
|
35
|
+
if (!installed && isIgnoredEntry(entry.name)) continue;
|
|
36
|
+
stack.push(join(current, entry.name));
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (entry.name === 'SKILL.md') hasRootSkillFile = true;
|
|
41
|
+
if (entry.name !== 'package.json') continue;
|
|
42
|
+
|
|
43
|
+
try {
|
|
44
|
+
packageMetadata = readPackageMetadata(current);
|
|
45
|
+
} catch {
|
|
46
|
+
packageMetadata = null;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (!packageMetadata?.packageName) continue;
|
|
51
|
+
if (packageMetadata.exportedSkills || hasRootSkillFile) {
|
|
52
|
+
results.push(current);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return results.sort();
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function buildCatalogKey(packageName, exportedSkills, entry) {
|
|
60
|
+
if (!packageName) return null;
|
|
61
|
+
if (exportedSkills.length <= 1) return packageName;
|
|
62
|
+
return buildCanonicalSkillRequirement(packageName, entry.name);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function readSkillPackage(repoRoot, packageDir, { origin = 'authored' } = {}) {
|
|
66
|
+
const packageMetadata = readPackageMetadata(packageDir);
|
|
67
|
+
if (!packageMetadata.packageName) return null;
|
|
68
|
+
|
|
69
|
+
const exportedSkills = readInstalledSkillExports(packageDir);
|
|
70
|
+
if (exportedSkills.length === 0) return null;
|
|
71
|
+
|
|
72
|
+
return {
|
|
73
|
+
origin,
|
|
74
|
+
packageDir,
|
|
75
|
+
packagePath: normalizeDisplayPath(repoRoot, packageDir),
|
|
76
|
+
packageName: packageMetadata.packageName,
|
|
77
|
+
packageVersion: packageMetadata.packageVersion,
|
|
78
|
+
packageMetadata,
|
|
79
|
+
exports: exportedSkills.map((entry) => ({
|
|
80
|
+
...entry,
|
|
81
|
+
key: buildCatalogKey(packageMetadata.packageName, exportedSkills, entry),
|
|
82
|
+
packageName: packageMetadata.packageName,
|
|
83
|
+
packageVersion: packageMetadata.packageVersion,
|
|
84
|
+
packageDir,
|
|
85
|
+
packagePath: normalizeDisplayPath(repoRoot, packageDir),
|
|
86
|
+
skillDirPath: entry.skillDir,
|
|
87
|
+
skillFilePath: entry.skillFile,
|
|
88
|
+
skillPath: normalizeDisplayPath(repoRoot, entry.skillDir),
|
|
89
|
+
skillFile: normalizeDisplayPath(repoRoot, entry.skillFile),
|
|
90
|
+
})),
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function listAuthoredSkillPackages(repoRoot) {
|
|
95
|
+
return listSkillPackageDirs(repoRoot)
|
|
96
|
+
.map((packageDir) => {
|
|
97
|
+
try {
|
|
98
|
+
return readSkillPackage(repoRoot, packageDir);
|
|
99
|
+
} catch {
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
})
|
|
103
|
+
.filter(Boolean);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export function listInstalledSkillPackages(repoRoot) {
|
|
107
|
+
return listSkillPackageDirs(repoRoot, { installed: true })
|
|
108
|
+
.map((packageDir) => {
|
|
109
|
+
try {
|
|
110
|
+
return readSkillPackage(repoRoot, packageDir, { origin: 'installed' });
|
|
111
|
+
} catch {
|
|
112
|
+
return null;
|
|
113
|
+
}
|
|
114
|
+
})
|
|
115
|
+
.filter(Boolean);
|
|
116
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { existsSync, readFileSync } from 'node:fs';
|
|
2
|
-
import { join, relative } from 'node:path';
|
|
2
|
+
import { dirname, join, relative } from 'node:path';
|
|
3
3
|
import { NotFoundError, ValidationError } from '../../utils/errors.js';
|
|
4
4
|
|
|
5
5
|
function parseScalar(value) {
|
|
@@ -149,6 +149,12 @@ export function parseSkillFrontmatterFile(skillFilePath) {
|
|
|
149
149
|
status: typeof fields.metadata?.status === 'string' ? fields.metadata.status : null,
|
|
150
150
|
replacement: typeof fields.metadata?.replacement === 'string' ? fields.metadata.replacement : null,
|
|
151
151
|
message: typeof fields.metadata?.message === 'string' ? fields.metadata.message : null,
|
|
152
|
+
wraps: typeof fields.metadata?.wraps === 'string'
|
|
153
|
+
? fields.metadata.wraps
|
|
154
|
+
: (typeof fields.wraps === 'string' ? fields.wraps : null),
|
|
155
|
+
overrides: Array.isArray(fields.metadata?.overrides)
|
|
156
|
+
? fields.metadata.overrides
|
|
157
|
+
: (Array.isArray(fields.overrides) ? fields.overrides : []),
|
|
152
158
|
};
|
|
153
159
|
}
|
|
154
160
|
|
|
@@ -171,6 +177,7 @@ export function readPackageMetadata(packageDir) {
|
|
|
171
177
|
files: null,
|
|
172
178
|
repository: null,
|
|
173
179
|
publishConfigRegistry: null,
|
|
180
|
+
exportedSkills: null,
|
|
174
181
|
};
|
|
175
182
|
}
|
|
176
183
|
|
|
@@ -183,5 +190,67 @@ export function readPackageMetadata(packageDir) {
|
|
|
183
190
|
files: Array.isArray(pkg.files) ? pkg.files : null,
|
|
184
191
|
repository: pkg.repository || null,
|
|
185
192
|
publishConfigRegistry: pkg.publishConfig?.registry || null,
|
|
193
|
+
exportedSkills: pkg.agentpack?.skills || null,
|
|
186
194
|
};
|
|
187
195
|
}
|
|
196
|
+
|
|
197
|
+
export function buildCanonicalSkillRequirement(packageName, skillName) {
|
|
198
|
+
if (!packageName || !skillName) return null;
|
|
199
|
+
return `${packageName}:${skillName}`;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
export function readInstalledSkillExports(packageDir) {
|
|
203
|
+
const packageMetadata = readPackageMetadata(packageDir);
|
|
204
|
+
const exports = [];
|
|
205
|
+
|
|
206
|
+
if (packageMetadata.exportedSkills && typeof packageMetadata.exportedSkills === 'object') {
|
|
207
|
+
for (const [declaredName, entry] of Object.entries(packageMetadata.exportedSkills)) {
|
|
208
|
+
const relativeSkillFile = typeof entry === 'string' ? entry : entry?.path;
|
|
209
|
+
if (!relativeSkillFile) continue;
|
|
210
|
+
|
|
211
|
+
const skillFile = join(packageDir, relativeSkillFile);
|
|
212
|
+
if (!existsSync(skillFile)) continue;
|
|
213
|
+
|
|
214
|
+
const metadata = parseSkillFrontmatterFile(skillFile);
|
|
215
|
+
exports.push({
|
|
216
|
+
declaredName,
|
|
217
|
+
name: metadata.name,
|
|
218
|
+
description: metadata.description,
|
|
219
|
+
sources: metadata.sources,
|
|
220
|
+
requires: metadata.requires,
|
|
221
|
+
status: metadata.status,
|
|
222
|
+
replacement: metadata.replacement,
|
|
223
|
+
message: metadata.message,
|
|
224
|
+
wraps: metadata.wraps,
|
|
225
|
+
overrides: metadata.overrides,
|
|
226
|
+
skillDir: dirname(skillFile),
|
|
227
|
+
skillFile,
|
|
228
|
+
relativeSkillFile,
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
if (exports.length > 0) {
|
|
234
|
+
return exports.sort((a, b) => a.name.localeCompare(b.name));
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const rootSkillFile = join(packageDir, 'SKILL.md');
|
|
238
|
+
if (!existsSync(rootSkillFile)) return [];
|
|
239
|
+
|
|
240
|
+
const metadata = parseSkillFrontmatterFile(rootSkillFile);
|
|
241
|
+
return [{
|
|
242
|
+
declaredName: metadata.name,
|
|
243
|
+
name: metadata.name,
|
|
244
|
+
description: metadata.description,
|
|
245
|
+
sources: metadata.sources,
|
|
246
|
+
requires: metadata.requires,
|
|
247
|
+
status: metadata.status,
|
|
248
|
+
replacement: metadata.replacement,
|
|
249
|
+
message: metadata.message,
|
|
250
|
+
wraps: metadata.wraps,
|
|
251
|
+
overrides: metadata.overrides,
|
|
252
|
+
skillDir: packageDir,
|
|
253
|
+
skillFile: rootSkillFile,
|
|
254
|
+
relativeSkillFile: 'SKILL.md',
|
|
255
|
+
}];
|
|
256
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import { isAbsolute, resolve } from 'node:path';
|
|
3
|
+
import { listAuthoredSkillPackages, listInstalledSkillPackages } from './skill-catalog.js';
|
|
4
|
+
import { NotFoundError, ValidationError } from '../../utils/errors.js';
|
|
5
|
+
|
|
6
|
+
function dedupePackages(authoredPackages, installedPackages) {
|
|
7
|
+
const seen = new Set(authoredPackages.map((pkg) => pkg.packageName));
|
|
8
|
+
return [
|
|
9
|
+
...authoredPackages,
|
|
10
|
+
...installedPackages.filter((pkg) => !seen.has(pkg.packageName)),
|
|
11
|
+
];
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function loadSkillTargetContext(repoRoot, {
|
|
15
|
+
includeAuthored = true,
|
|
16
|
+
includeInstalled = true,
|
|
17
|
+
} = {}) {
|
|
18
|
+
const authoredPackages = includeAuthored ? listAuthoredSkillPackages(repoRoot) : [];
|
|
19
|
+
const installedPackages = includeInstalled ? listInstalledSkillPackages(repoRoot) : [];
|
|
20
|
+
|
|
21
|
+
return {
|
|
22
|
+
authoredPackages,
|
|
23
|
+
installedPackages,
|
|
24
|
+
packages: dedupePackages(authoredPackages, installedPackages),
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function buildPackageResolution(pkg, source) {
|
|
29
|
+
return {
|
|
30
|
+
kind: 'package',
|
|
31
|
+
source,
|
|
32
|
+
package: pkg,
|
|
33
|
+
exports: pkg.exports,
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function buildExportResolution(pkg, skillExport, source) {
|
|
38
|
+
return {
|
|
39
|
+
kind: 'export',
|
|
40
|
+
source,
|
|
41
|
+
package: pkg,
|
|
42
|
+
export: skillExport,
|
|
43
|
+
exports: [skillExport],
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function resolveSkillTarget(repoRoot, target, options = {}) {
|
|
48
|
+
const context = loadSkillTargetContext(repoRoot, options);
|
|
49
|
+
const { packages } = context;
|
|
50
|
+
|
|
51
|
+
if (typeof target !== 'string' || target.length === 0) {
|
|
52
|
+
throw new NotFoundError('skill not found', {
|
|
53
|
+
code: 'skill_not_found',
|
|
54
|
+
suggestion: `Target: ${target}`,
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const absoluteTarget = isAbsolute(target) ? target : resolve(repoRoot, target);
|
|
59
|
+
|
|
60
|
+
if (existsSync(absoluteTarget)) {
|
|
61
|
+
for (const pkg of packages) {
|
|
62
|
+
if (pkg.packageDir === absoluteTarget) {
|
|
63
|
+
return buildPackageResolution(pkg, 'package_path');
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
for (const skillExport of pkg.exports) {
|
|
67
|
+
if (skillExport.skillDirPath === absoluteTarget) {
|
|
68
|
+
return buildExportResolution(pkg, skillExport, 'skill_path');
|
|
69
|
+
}
|
|
70
|
+
if (skillExport.skillFilePath === absoluteTarget) {
|
|
71
|
+
return buildExportResolution(pkg, skillExport, 'skill_file');
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const pkg = packages.find((entry) => entry.packageName === target);
|
|
78
|
+
if (pkg) {
|
|
79
|
+
return buildPackageResolution(pkg, 'package_name');
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
throw new NotFoundError('skill not found', {
|
|
83
|
+
code: 'skill_not_found',
|
|
84
|
+
suggestion: `Target: ${target}`,
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function resolveSingleSkillTarget(repoRoot, target, options = {}) {
|
|
89
|
+
const resolved = resolveSkillTarget(repoRoot, target, options);
|
|
90
|
+
|
|
91
|
+
if (resolved.kind === 'export') return resolved;
|
|
92
|
+
if (resolved.exports.length === 1) {
|
|
93
|
+
return buildExportResolution(resolved.package, resolved.exports[0], resolved.source);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
throw new ValidationError('ambiguous skill target', {
|
|
97
|
+
code: 'ambiguous_skill_target',
|
|
98
|
+
suggestion: resolved.exports.map((entry) => entry.skillPath).join(', '),
|
|
99
|
+
});
|
|
100
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { dirname, join } from 'node:path';
|
|
3
|
+
import { homedir } from 'node:os';
|
|
4
|
+
|
|
5
|
+
const DEFAULT_CONFIG = {
|
|
6
|
+
version: 1,
|
|
7
|
+
provider: 'github-packages',
|
|
8
|
+
scope: '@alavida-ai',
|
|
9
|
+
registry: 'https://npm.pkg.github.com',
|
|
10
|
+
verificationPackage: '@alavida-ai/agentpack-auth-probe',
|
|
11
|
+
managedNpmKeys: [],
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
function resolveConfigDir(env = process.env) {
|
|
15
|
+
const xdgConfigHome = env.XDG_CONFIG_HOME;
|
|
16
|
+
if (xdgConfigHome) return join(xdgConfigHome, 'agentpack');
|
|
17
|
+
return join(env.HOME || homedir(), '.config', 'agentpack');
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function getUserConfigPath({ env = process.env } = {}) {
|
|
21
|
+
return join(resolveConfigDir(env), 'config.json');
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function readUserConfig({ env = process.env } = {}) {
|
|
25
|
+
const configPath = getUserConfigPath({ env });
|
|
26
|
+
if (!existsSync(configPath)) {
|
|
27
|
+
return { ...DEFAULT_CONFIG };
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return {
|
|
31
|
+
...DEFAULT_CONFIG,
|
|
32
|
+
...JSON.parse(readFileSync(configPath, 'utf-8')),
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function writeUserConfig(config, { env = process.env } = {}) {
|
|
37
|
+
const configPath = getUserConfigPath({ env });
|
|
38
|
+
mkdirSync(dirname(configPath), { recursive: true });
|
|
39
|
+
writeFileSync(configPath, JSON.stringify(config, null, 2) + '\n');
|
|
40
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { chmodSync, existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { dirname, join } from 'node:path';
|
|
3
|
+
import { homedir } from 'node:os';
|
|
4
|
+
|
|
5
|
+
function resolveConfigDir(env = process.env) {
|
|
6
|
+
const xdgConfigHome = env.XDG_CONFIG_HOME;
|
|
7
|
+
if (xdgConfigHome) return join(xdgConfigHome, 'agentpack');
|
|
8
|
+
return join(env.HOME || homedir(), '.config', 'agentpack');
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function getUserCredentialsPath({ env = process.env } = {}) {
|
|
12
|
+
return join(resolveConfigDir(env), 'credentials.json');
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function readUserCredentials({ env = process.env } = {}) {
|
|
16
|
+
const credentialsPath = getUserCredentialsPath({ env });
|
|
17
|
+
if (!existsSync(credentialsPath)) return null;
|
|
18
|
+
return JSON.parse(readFileSync(credentialsPath, 'utf-8'));
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function writeUserCredentials(credentials, { env = process.env } = {}) {
|
|
22
|
+
const credentialsPath = getUserCredentialsPath({ env });
|
|
23
|
+
mkdirSync(dirname(credentialsPath), { recursive: true });
|
|
24
|
+
writeFileSync(credentialsPath, JSON.stringify(credentials, null, 2) + '\n', { mode: 0o600 });
|
|
25
|
+
chmodSync(credentialsPath, 0o600);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function deleteUserCredentials({ env = process.env } = {}) {
|
|
29
|
+
rmSync(getUserCredentialsPath({ env }), { force: true });
|
|
30
|
+
}
|
|
@@ -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
|
+
}
|