UNPKG

@alavida/agentpack

Version:

Compiler-driven lifecycle CLI for source-backed agent skills

341 lines (307 loc) 11.3 kB
import { existsSync, readFileSync, readdirSync } from 'node:fs'; import { basename, join } from 'node:path'; import { compileSkillDocument } from '../compiler/skill-compiler.js'; import { extractFrontmatter, hasLegacyFrontmatterFields } from '../compiler/skill-document-parser.js'; import { AgentpackError, ValidationError } from '../../utils/errors.js'; import { buildExpectedRuntimeSkillName, listPackageSkillEntries, normalizeDisplayPath, parseSkillFrontmatterFile, readPackageMetadata, } from './skill-model.js'; function isIgnoredEntry(name) { return name === '.git' || name === 'node_modules' || name === '.agentpack'; } function listAuthoredPackageDirs(repoRoot) { const stack = [repoRoot]; const results = []; while (stack.length > 0) { const current = stack.pop(); let entries = []; try { entries = readdirSync(current, { withFileTypes: true }); } catch { continue; } let hasPackageFile = false; for (const entry of entries) { if (entry.isDirectory()) { if (isIgnoredEntry(entry.name)) continue; stack.push(join(current, entry.name)); continue; } if (entry.name === 'package.json') hasPackageFile = true; } if (!hasPackageFile) continue; const packageMetadata = readPackageMetadata(current); if (!packageMetadata.packageName) continue; if (listPackageSkillEntries(current).length === 0 && !packageMetadata.exportedSkills) continue; results.push(current); } return results.sort((a, b) => a.localeCompare(b)); } function defaultExportName(entry, packageName) { if (entry.kind === 'primary') { const segments = packageName.split('/'); return segments[segments.length - 1] || packageName; } return basename(entry.skillDir); } function buildNextSteps(error, displayPath) { if (error?.code === 'legacy_export_table_not_supported') { return [{ action: 'edit_file', path: displayPath, reason: 'Replace `agentpack.skills` with `agentpack.root`, then discover named exports from the filesystem.', }]; } if (error?.code === 'invalid_agentpack_declaration' && /source\s+\w+\s+from\s+"[^"]+"/i.test(error.message || '')) { return [{ action: 'edit_file', path: displayPath, reason: 'Replace unsupported source declaration syntax with `source alias = "repo-relative-path"`.', }]; } if (error?.code === 'legacy_authoring_not_supported') { return [{ action: 'edit_file', path: displayPath, reason: 'Convert this skill to compiler-mode authoring with one `agentpack` block and explicit body references.', }]; } return [{ action: 'edit_file', path: displayPath, reason: 'Fix the compiler error in this skill export, then rerun the command.', }]; } function buildDiagnostic(repoRoot, scope, pathValue, error, extra = {}) { const displayPath = pathValue ? normalizeDisplayPath(repoRoot, pathValue) : null; return { code: error?.code || 'compiler_error', message: error?.message || String(error), level: 'error', scope, ...(displayPath ? { path: displayPath } : {}), ...(error?.location ? { location: error.location } : {}), ...extra, nextSteps: buildNextSteps(error, displayPath), }; } function compileExportNode(repoRoot, packageNode, entry) { let frontmatter = null; try { frontmatter = parseSkillFrontmatterFile(entry.skillFile); } catch { frontmatter = null; } const declaredName = frontmatter?.name || defaultExportName(entry, packageNode.packageName); const moduleName = defaultExportName(entry, packageNode.packageName); const exportId = entry.kind === 'primary' ? packageNode.packageName : `${packageNode.packageName}:${moduleName}`; const baseNode = { id: exportId, kind: entry.kind, packageName: packageNode.packageName, packageVersion: packageNode.packageVersion, packageDir: packageNode.packageDir, packagePath: packageNode.packagePath, declaredName, name: buildExpectedRuntimeSkillName(packageNode.packageName, { ...entry, moduleName, }), moduleName, runtimeName: buildExpectedRuntimeSkillName(packageNode.packageName, { ...entry, moduleName, }), description: frontmatter?.description || null, skillDirPath: entry.skillDir, skillFilePath: entry.skillFile, skillPath: normalizeDisplayPath(repoRoot, entry.skillDir), skillFile: normalizeDisplayPath(repoRoot, entry.skillFile), relativeSkillFile: entry.relativeSkillFile, key: exportId, isPrimary: entry.kind === 'primary', diagnostics: [], compiled: null, sources: [], requires: [], status: 'valid', replacement: frontmatter?.replacement || null, message: frontmatter?.message || null, wraps: frontmatter?.wraps || null, overrides: frontmatter?.overrides || [], }; try { const content = readFileSync(entry.skillFile, 'utf-8'); const { frontmatterText } = extractFrontmatter(content); if (!content.includes('```agentpack') && hasLegacyFrontmatterFields(frontmatterText)) { throw new ValidationError( 'Legacy SKILL.md authoring is not supported. Use an `agentpack` declaration block and explicit body references.', { code: 'legacy_authoring_not_supported', path: entry.skillFile, } ); } const compiled = compileSkillDocument(content); const expectedName = baseNode.runtimeName; if (compiled.metadata.name !== expectedName) { throw new ValidationError( `SKILL.md frontmatter name must be "${expectedName}"`, { code: 'invalid_skill_name', path: entry.skillFile, } ); } return { ...baseNode, declaredName: compiled.metadata.name, name: baseNode.runtimeName, description: compiled.metadata.description, compiled, sources: Object.values(compiled.sourceBindings).map((binding) => binding.sourcePath), requires: Object.values(compiled.skillImports).map((skillImport) => skillImport.target), lifecycleStatus: frontmatter?.status || null, }; } catch (error) { return { ...baseNode, status: 'invalid', diagnostics: [buildDiagnostic(repoRoot, 'export', entry.skillFile, error, { exportId })], }; } } function registerTarget(targets, key, value) { if (!key) return; targets[key] = value; } function toTargetRef(kind, packageNode, exportNode = null) { return exportNode ? { kind, packageName: packageNode.packageName, exportId: exportNode.id } : { kind, packageName: packageNode.packageName }; } function buildPackageNode(repoRoot, packageDir) { const packageMetadata = readPackageMetadata(packageDir); const packageNode = { packageName: packageMetadata.packageName, packageVersion: packageMetadata.packageVersion, packageDir, packagePath: normalizeDisplayPath(repoRoot, packageDir), packageMetadata, primaryExport: null, exports: [], status: 'valid', diagnostics: [], }; const skillEntries = listPackageSkillEntries(packageDir); if (skillEntries.length === 0 && packageMetadata.exportedSkills) { packageNode.status = 'invalid'; packageNode.diagnostics = [ buildDiagnostic( repoRoot, 'package', join(packageDir, 'package.json'), new AgentpackError( 'package.json agentpack.skills export tables are no longer supported. Use agentpack.root and discover skills from the package filesystem.', { code: 'legacy_export_table_not_supported' } ), { packageName: packageNode.packageName } ), ]; return { packageNode, exportNodes: [], }; } const exportNodes = skillEntries.map((entry) => compileExportNode(repoRoot, packageNode, entry)); const primaryExport = exportNodes.find((entry) => entry.isPrimary) || null; packageNode.primaryExport = primaryExport?.id || null; packageNode.exports = exportNodes.map((entry) => entry.id).sort((a, b) => a.localeCompare(b)); packageNode.status = exportNodes.some((entry) => entry.status === 'invalid') ? 'invalid' : 'valid'; packageNode.diagnostics = exportNodes.flatMap((entry) => entry.diagnostics); return { packageNode, exportNodes, }; } export function buildAuthoredWorkspaceGraph(repoRoot) { const packages = {}; const exports = {}; const targets = {}; const diagnostics = []; for (const packageDir of listAuthoredPackageDirs(repoRoot)) { const { packageNode, exportNodes } = buildPackageNode(repoRoot, packageDir); packages[packageNode.packageName] = packageNode; diagnostics.push(...packageNode.diagnostics); registerTarget(targets, packageNode.packageName, toTargetRef('package', packageNode)); registerTarget(targets, packageNode.packagePath, toTargetRef('package', packageNode)); registerTarget(targets, packageNode.packageDir, toTargetRef('package', packageNode)); for (const exportNode of exportNodes) { exports[exportNode.id] = exportNode; if (!exportNode.isPrimary) { registerTarget(targets, exportNode.id, toTargetRef('export', packageNode, exportNode)); registerTarget(targets, exportNode.skillPath, toTargetRef('export', packageNode, exportNode)); registerTarget(targets, exportNode.skillDirPath, toTargetRef('export', packageNode, exportNode)); } registerTarget(targets, exportNode.skillFile, toTargetRef('export', packageNode, exportNode)); registerTarget(targets, exportNode.skillFilePath, toTargetRef('export', packageNode, exportNode)); } } return { packages, exports, targets, diagnostics, }; } export function collectDiagnosticNextSteps(diagnostics) { const seen = new Set(); const nextSteps = []; for (const diagnostic of diagnostics || []) { for (const step of diagnostic.nextSteps || []) { const key = JSON.stringify(step); if (seen.has(key)) continue; seen.add(key); nextSteps.push(step); } } return nextSteps; } export function buildInvalidExportError(exportNode) { const diagnostics = exportNode?.diagnostics || []; const primaryMessage = diagnostics.length === 1 ? diagnostics[0].message : `skill export is invalid: ${exportNode?.id || 'unknown export'}`; return new ValidationError(`skill export is invalid: ${exportNode?.id || 'unknown export'}`, { code: 'export_invalid', suggestion: primaryMessage, path: exportNode?.skillFile || null, nextSteps: collectDiagnosticNextSteps(diagnostics), details: { exportId: exportNode?.id || null, diagnostics, }, }); } export function buildInvalidPackageError(packageNode) { const diagnostics = packageNode?.diagnostics || []; const primaryMessage = diagnostics.length === 1 ? diagnostics[0].message : `skill package is invalid: ${packageNode?.packageName || 'unknown package'}`; return new ValidationError(`skill package is invalid: ${packageNode?.packageName || 'unknown package'}`, { code: 'package_invalid', path: packageNode?.packagePath || null, suggestion: primaryMessage, nextSteps: collectDiagnosticNextSteps(diagnostics), details: { packageName: packageNode?.packageName || null, diagnostics, }, }); }