@alavida/agentpack 0.1.4 → 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.
- package/package.json +4 -1
- package/skills/agentpack-cli/SKILL.md +17 -14
- package/skills/agentpack-cli/references/skill-lifecycle.md +13 -11
- package/skills/authoring-skillgraphs-from-knowledge/SKILL.md +37 -16
- package/skills/authoring-skillgraphs-from-knowledge/references/authored-metadata.md +4 -3
- package/skills/getting-started-skillgraphs/SKILL.md +3 -3
- package/skills/shipping-production-plugins-and-packages/SKILL.md +16 -8
- 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 +27 -0
- package/src/domain/auth/registry-resolution.js +55 -0
- package/src/domain/skills/skill-model.js +58 -1
- 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 +199 -21
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import { login } from '../application/auth/login.js';
|
|
3
|
+
import { logout } from '../application/auth/logout.js';
|
|
4
|
+
import { output } from '../utils/output.js';
|
|
5
|
+
import { getAuthStatus } from '../application/auth/get-auth-status.js';
|
|
6
|
+
|
|
7
|
+
export function authCommand() {
|
|
8
|
+
const cmd = new Command('auth')
|
|
9
|
+
.description('Configure and inspect package registry authentication');
|
|
10
|
+
|
|
11
|
+
cmd.addHelpText('after', `
|
|
12
|
+
Defaults:
|
|
13
|
+
Scope: @alavida-ai
|
|
14
|
+
Registry: https://npm.pkg.github.com
|
|
15
|
+
Token: GitHub personal access token with package read access
|
|
16
|
+
`);
|
|
17
|
+
|
|
18
|
+
cmd
|
|
19
|
+
.command('login')
|
|
20
|
+
.description('Configure GitHub Packages authentication for this machine')
|
|
21
|
+
.option('--scope <scope>', 'Override the package scope to configure')
|
|
22
|
+
.option('--registry <url>', 'Override the package registry URL')
|
|
23
|
+
.option('--verify-package <packageName>', 'Override the package used for live verification')
|
|
24
|
+
.action(async (opts, command) => {
|
|
25
|
+
const globalOpts = command.optsWithGlobals();
|
|
26
|
+
const result = await login({
|
|
27
|
+
scope: opts.scope || null,
|
|
28
|
+
registry: opts.registry || null,
|
|
29
|
+
verificationPackage: opts.verifyPackage || null,
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
if (globalOpts.json) {
|
|
33
|
+
output.json(result);
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
output.write(`Configured auth for ${result.scope}`);
|
|
38
|
+
output.write(`Registry: ${result.registry}`);
|
|
39
|
+
output.write(`Storage: ${result.storage.mode}`);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
cmd
|
|
43
|
+
.command('status')
|
|
44
|
+
.description('Show authentication status for the configured package registry')
|
|
45
|
+
.option('--verify', 'Check whether the stored credential works against the configured registry')
|
|
46
|
+
.action(async (opts, command) => {
|
|
47
|
+
const globalOpts = command.optsWithGlobals();
|
|
48
|
+
const result = await getAuthStatus({ verify: opts.verify });
|
|
49
|
+
|
|
50
|
+
if (globalOpts.json) {
|
|
51
|
+
output.json(result);
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
output.write(`Provider: ${result.provider}`);
|
|
56
|
+
output.write(`Configured: ${result.configured}`);
|
|
57
|
+
output.write(`Storage: ${result.storage.mode}`);
|
|
58
|
+
output.write(`Verification: ${result.verification.status}`);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
cmd
|
|
62
|
+
.command('logout')
|
|
63
|
+
.description('Remove configured package registry authentication')
|
|
64
|
+
.action((opts, command) => {
|
|
65
|
+
const globalOpts = command.optsWithGlobals();
|
|
66
|
+
const result = logout();
|
|
67
|
+
|
|
68
|
+
if (globalOpts.json) {
|
|
69
|
+
output.json(result);
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
output.write(`Removed Credentials: ${result.removedCredentials}`);
|
|
74
|
+
output.write(`Removed npm Keys: ${result.removedNpmKeys}`);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
return cmd;
|
|
78
|
+
}
|
package/src/commands/skills.js
CHANGED
|
@@ -136,6 +136,8 @@ export function skillsCommand() {
|
|
|
136
136
|
output.write(`Outdated Skills: ${result.outdatedCount}`);
|
|
137
137
|
output.write(`Deprecated Skills: ${result.deprecatedCount}`);
|
|
138
138
|
output.write(`Incomplete Skills: ${result.incompleteCount}`);
|
|
139
|
+
output.write(`Runtime Drifted Skills: ${result.runtimeDriftCount}`);
|
|
140
|
+
output.write(`Orphaned Materializations: ${result.orphanedMaterializationCount}`);
|
|
139
141
|
output.write(`Registry Configured: ${result.registry.configured}`);
|
|
140
142
|
|
|
141
143
|
if (result.outdated.length > 0) {
|
|
@@ -170,6 +172,28 @@ export function skillsCommand() {
|
|
|
170
172
|
}
|
|
171
173
|
}
|
|
172
174
|
}
|
|
175
|
+
|
|
176
|
+
if (result.runtimeDrift.length > 0) {
|
|
177
|
+
output.write('');
|
|
178
|
+
output.write('Runtime Drift:');
|
|
179
|
+
for (const install of result.runtimeDrift) {
|
|
180
|
+
output.write(`- ${install.packageName}`);
|
|
181
|
+
for (const issue of install.issues) {
|
|
182
|
+
output.write(` issue: ${issue.code}`);
|
|
183
|
+
output.write(` target: ${issue.target}`);
|
|
184
|
+
if (issue.runtimeName) output.write(` runtime: ${issue.runtimeName}`);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
if (result.orphanedMaterializations.length > 0) {
|
|
190
|
+
output.write('');
|
|
191
|
+
output.write('Orphaned Materializations:');
|
|
192
|
+
for (const entry of result.orphanedMaterializations) {
|
|
193
|
+
output.write(`- ${entry.target}`);
|
|
194
|
+
output.write(` issue: ${entry.code}`);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
173
197
|
});
|
|
174
198
|
|
|
175
199
|
cmd
|
|
@@ -459,6 +483,9 @@ export function skillsCommand() {
|
|
|
459
483
|
output.write(` direct: ${install.direct}`);
|
|
460
484
|
output.write(` version: ${install.packageVersion}`);
|
|
461
485
|
output.write(` source: ${install.sourcePackagePath}`);
|
|
486
|
+
if (install.skills?.length > 0) {
|
|
487
|
+
output.write(` skills: ${install.skills.map((skill) => skill.name).join(', ')}`);
|
|
488
|
+
}
|
|
462
489
|
for (const materialization of install.materializations) {
|
|
463
490
|
output.write(` materialized: ${materialization.target} (${materialization.mode})`);
|
|
464
491
|
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
function getScopedRegistry(config, scope) {
|
|
2
|
+
return config?.[`${scope}:registry`] || null;
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
function getRegistryHostKey(registry) {
|
|
6
|
+
if (!registry) return null;
|
|
7
|
+
try {
|
|
8
|
+
const url = new URL(registry);
|
|
9
|
+
return `//${url.host}/:_authToken`;
|
|
10
|
+
} catch {
|
|
11
|
+
return null;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function getAuthToken(config, registry) {
|
|
16
|
+
const hostKey = getRegistryHostKey(registry);
|
|
17
|
+
return hostKey ? (config?.[hostKey] || null) : null;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function resolveRegistryConfig({
|
|
21
|
+
scope,
|
|
22
|
+
defaults = {},
|
|
23
|
+
userNpmrc = {},
|
|
24
|
+
repoNpmrc = {},
|
|
25
|
+
} = {}) {
|
|
26
|
+
const repoRegistry = getScopedRegistry(repoNpmrc, scope);
|
|
27
|
+
if (repoRegistry) {
|
|
28
|
+
return {
|
|
29
|
+
scope,
|
|
30
|
+
registry: repoRegistry,
|
|
31
|
+
authToken: getAuthToken(repoNpmrc, repoRegistry),
|
|
32
|
+
verificationPackage: defaults.verificationPackage || null,
|
|
33
|
+
source: 'repo',
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const userRegistry = getScopedRegistry(userNpmrc, scope);
|
|
38
|
+
if (userRegistry) {
|
|
39
|
+
return {
|
|
40
|
+
scope,
|
|
41
|
+
registry: userRegistry,
|
|
42
|
+
authToken: getAuthToken(userNpmrc, userRegistry),
|
|
43
|
+
verificationPackage: defaults.verificationPackage || null,
|
|
44
|
+
source: 'user',
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return {
|
|
49
|
+
scope,
|
|
50
|
+
registry: defaults.registry || null,
|
|
51
|
+
authToken: null,
|
|
52
|
+
verificationPackage: defaults.verificationPackage || null,
|
|
53
|
+
source: 'default',
|
|
54
|
+
};
|
|
55
|
+
}
|
|
@@ -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) {
|
|
@@ -171,6 +171,7 @@ export function readPackageMetadata(packageDir) {
|
|
|
171
171
|
files: null,
|
|
172
172
|
repository: null,
|
|
173
173
|
publishConfigRegistry: null,
|
|
174
|
+
exportedSkills: null,
|
|
174
175
|
};
|
|
175
176
|
}
|
|
176
177
|
|
|
@@ -183,5 +184,61 @@ export function readPackageMetadata(packageDir) {
|
|
|
183
184
|
files: Array.isArray(pkg.files) ? pkg.files : null,
|
|
184
185
|
repository: pkg.repository || null,
|
|
185
186
|
publishConfigRegistry: pkg.publishConfig?.registry || null,
|
|
187
|
+
exportedSkills: pkg.agentpack?.skills || null,
|
|
186
188
|
};
|
|
187
189
|
}
|
|
190
|
+
|
|
191
|
+
export function buildCanonicalSkillRequirement(packageName, skillName) {
|
|
192
|
+
if (!packageName || !skillName) return null;
|
|
193
|
+
return `${packageName}:${skillName}`;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
export function readInstalledSkillExports(packageDir) {
|
|
197
|
+
const packageMetadata = readPackageMetadata(packageDir);
|
|
198
|
+
const exports = [];
|
|
199
|
+
|
|
200
|
+
if (packageMetadata.exportedSkills && typeof packageMetadata.exportedSkills === 'object') {
|
|
201
|
+
for (const [declaredName, entry] of Object.entries(packageMetadata.exportedSkills)) {
|
|
202
|
+
const relativeSkillFile = typeof entry === 'string' ? entry : entry?.path;
|
|
203
|
+
if (!relativeSkillFile) continue;
|
|
204
|
+
|
|
205
|
+
const skillFile = join(packageDir, relativeSkillFile);
|
|
206
|
+
if (!existsSync(skillFile)) continue;
|
|
207
|
+
|
|
208
|
+
const metadata = parseSkillFrontmatterFile(skillFile);
|
|
209
|
+
exports.push({
|
|
210
|
+
declaredName,
|
|
211
|
+
name: metadata.name,
|
|
212
|
+
description: metadata.description,
|
|
213
|
+
requires: metadata.requires,
|
|
214
|
+
status: metadata.status,
|
|
215
|
+
replacement: metadata.replacement,
|
|
216
|
+
message: metadata.message,
|
|
217
|
+
skillDir: dirname(skillFile),
|
|
218
|
+
skillFile,
|
|
219
|
+
relativeSkillFile,
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
if (exports.length > 0) {
|
|
225
|
+
return exports.sort((a, b) => a.name.localeCompare(b.name));
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const rootSkillFile = join(packageDir, 'SKILL.md');
|
|
229
|
+
if (!existsSync(rootSkillFile)) return [];
|
|
230
|
+
|
|
231
|
+
const metadata = parseSkillFrontmatterFile(rootSkillFile);
|
|
232
|
+
return [{
|
|
233
|
+
declaredName: metadata.name,
|
|
234
|
+
name: metadata.name,
|
|
235
|
+
description: metadata.description,
|
|
236
|
+
requires: metadata.requires,
|
|
237
|
+
status: metadata.status,
|
|
238
|
+
replacement: metadata.replacement,
|
|
239
|
+
message: metadata.message,
|
|
240
|
+
skillDir: packageDir,
|
|
241
|
+
skillFile: rootSkillFile,
|
|
242
|
+
relativeSkillFile: 'SKILL.md',
|
|
243
|
+
}];
|
|
244
|
+
}
|
|
@@ -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
|
+
}
|