UNPKG

@alavida/agentpack

Version:

Compiler-driven lifecycle CLI for source-backed agent skills

181 lines (161 loc) 4.97 kB
import { lstatSync, readlinkSync, readdirSync } from 'node:fs'; import { join, resolve } from 'node:path'; import { readDevSession } from '../fs/dev-session-repository.js'; import { readMaterializationState } from '../fs/materialization-state-repository.js'; function readPathType(pathValue) { try { const stat = lstatSync(pathValue); return { exists: true, isSymlink: stat.isSymbolicLink(), type: stat.isDirectory() ? 'directory' : stat.isFile() ? 'file' : 'other', }; } catch { return { exists: false, isSymlink: false, type: null, }; } } export function inspectRecordedMaterialization(repoRoot, { target, expectedSourcePath, packageName, runtimeName = null, } = {}) { const absTarget = resolve(repoRoot, target); const expectedTarget = resolve(repoRoot, expectedSourcePath); const pathState = readPathType(absTarget); if (!pathState.exists) { return { packageName, runtimeName, target, expectedSourcePath, code: 'missing_path', }; } if (!pathState.isSymlink) { return { packageName, runtimeName, target, expectedSourcePath, code: 'wrong_type', actualType: pathState.type, }; } const rawLinkTarget = readlinkSync(absTarget); const actualTarget = resolve(join(absTarget, '..'), rawLinkTarget); if (actualTarget !== expectedTarget) { return { packageName, runtimeName, target, expectedSourcePath, code: 'wrong_target', actualTarget, }; } const resolvedState = readPathType(actualTarget); if (!resolvedState.exists) { return { packageName, runtimeName, target, expectedSourcePath, code: 'dangling_target', actualTarget, }; } return null; } export function inspectMaterializedSkills(repoRoot, state) { const runtimeDrift = []; const ownedTargets = new Set(); const devSession = readDevSession(repoRoot); const materializationState = readMaterializationState(repoRoot); if (devSession?.status === 'active') { for (const target of devSession.links || []) { ownedTargets.add(target); } } const issuesByPackage = new Map(); const recordedAdapters = materializationState?.adapters ? Object.values(materializationState.adapters).flatMap((entries) => entries || []) : []; if (recordedAdapters.length > 0) { for (const entry of recordedAdapters) { ownedTargets.add(entry.target); const issue = inspectRecordedMaterialization(repoRoot, { target: entry.target, expectedSourcePath: entry.sourceSkillPath || entry.source || '', packageName: entry.packageName || null, runtimeName: entry.runtimeName || null, }); if (!issue || !issue.packageName) continue; const issues = issuesByPackage.get(issue.packageName) || []; issues.push(issue); issuesByPackage.set(issue.packageName, issues); } } else { for (const [packageName, install] of Object.entries(state.installs || {})) { const issues = []; for (const skill of install.skills || []) { for (const materialization of skill.materializations || []) { ownedTargets.add(materialization.target); const issue = inspectRecordedMaterialization(repoRoot, { target: materialization.target, expectedSourcePath: skill.source_skill_path, packageName, runtimeName: skill.runtime_name, }); if (issue) issues.push(issue); } } if (issues.length > 0) { issuesByPackage.set(packageName, issues); } } } for (const [packageName, issues] of issuesByPackage.entries()) { runtimeDrift.push({ packageName, issues, }); } const orphanedMaterializations = []; for (const root of [ join(repoRoot, '.claude', 'skills'), join(repoRoot, '.agents', 'skills'), ]) { let entries = []; try { entries = readdirSync(root, { withFileTypes: true }); } catch { continue; } for (const entry of entries) { const relativeTarget = root.startsWith(join(repoRoot, '.claude')) ? `.claude/skills/${entry.name}` : `.agents/skills/${entry.name}`; if (ownedTargets.has(relativeTarget)) continue; const absPath = join(root, entry.name); const pathState = readPathType(absPath); orphanedMaterializations.push({ target: relativeTarget, code: 'orphaned_materialization', actualType: pathState.isSymlink ? 'symlink' : pathState.type, }); } } runtimeDrift.sort((a, b) => a.packageName.localeCompare(b.packageName)); orphanedMaterializations.sort((a, b) => a.target.localeCompare(b.target)); return { runtimeDriftCount: runtimeDrift.length, runtimeDrift, orphanedMaterializationCount: orphanedMaterializations.length, orphanedMaterializations, }; }