UNPKG

@alavida/agentpack

Version:

Compiler-driven lifecycle CLI for source-backed agent skills

973 lines (866 loc) 30.3 kB
import { existsSync, readFileSync, readdirSync, watch, writeFileSync } from 'node:fs'; import { dirname, isAbsolute, join, relative, resolve } from 'node:path'; import { execFileSync } from 'node:child_process'; import { buildCompiledStateUseCase } from '../application/skills/build-compiled-state.js'; import { findAllWorkbenches, findRepoRoot } from './context.js'; import { findPackageDirByName } from '../domain/skills/package-discovery.js'; import { buildSkillGraph, } from '../domain/skills/skill-graph.js'; import { readCompiledState } from '../infrastructure/fs/compiled-state-repository.js'; import { readDevSession, writeDevSession, removeDevSession } from '../infrastructure/fs/dev-session-repository.js'; import { ensureSkillLink, removePathIfExists, removeSkillLinks, removeSkillLinksByPaths, removeSkillLinksByNames, } from '../infrastructure/runtime/materialize-skills.js'; import { normalizeDisplayPath, normalizeRepoPath, readInstalledSkillExports, readPackageMetadata, } from '../domain/skills/skill-model.js'; import { isGeneratedPackagePath } from '../domain/skills/generated-package-paths.js'; import { extractFrontmatter, hasLegacyFrontmatterFields } from '../domain/compiler/skill-document-parser.js'; import { listAuthoredSkillPackages } from '../domain/skills/skill-catalog.js'; import { ensureResolvedExportIsValid, resolveSingleSkillTarget, resolveSkillTarget, loadSkillTargetContext, } from '../domain/skills/skill-target-resolution.js'; import { compileSkillDocument } from '../domain/compiler/skill-compiler.js'; import { hashFile } from '../domain/compiler/source-hash.js'; import { startSkillDevWorkbench } from '../application/skills/start-skill-dev-workbench.js'; import { computeRuntimeSelectionUseCase } from '../application/skills/compute-runtime-selection.js'; import { materializeRuntimeSelectionUseCase } from '../application/skills/materialize-runtime-selection.js'; import { AgentpackError, EXIT_CODES, NotFoundError, ValidationError } from '../utils/errors.js'; const MANAGED_PACKAGE_SCOPES = ['@alavida', '@alavida-ai']; function readCompilerSkillDocument(skillFilePath) { const content = readFileSync(skillFilePath, '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: skillFilePath, } ); } return compileSkillDocument(content); } function listCompilerSkillRequirements(compiledDocument) { return [...new Set( Object.values(compiledDocument.skillImports).map((entry) => entry.target) )].sort((a, b) => a.localeCompare(b)); } function listCompilerPackageDependencies(compiledDocument) { return [...new Set( Object.values(compiledDocument.skillImports).map((entry) => entry.packageSpecifier) )].sort((a, b) => a.localeCompare(b)); } function isManagedPackageName(packageName) { return typeof packageName === 'string' && MANAGED_PACKAGE_SCOPES.some((scope) => packageName.startsWith(`${scope}/`)); } function inferManagedScope(packageName) { return MANAGED_PACKAGE_SCOPES.find((scope) => packageName?.startsWith(`${scope}/`)) || null; } function collectUnresolvedSelectionRequirements(repoRoot, selection, { cwd = repoRoot } = {}) { const unresolved = new Set(); for (const selectedExport of selection.exports || []) { for (const requirement of selectedExport.skillImports?.map((entry) => entry.target) || []) { let dependency; try { dependency = ensureResolvedExportIsValid( resolveSingleSkillTarget(repoRoot, requirement, { includeInstalled: false, cwd }) ); } catch { unresolved.add(requirement); continue; } void dependency; } } return [...unresolved].sort((a, b) => a.localeCompare(b)); } function resolveLocalPackagedSkillDir(repoRoot, target) { const resolved = ensureResolvedExportIsValid( resolveSingleSkillTarget(repoRoot, target, { includeInstalled: false }) ); return { skillDir: resolved.export.skillDirPath, skillFile: resolved.export.skillFilePath, packageDir: resolved.package.packageDir, packageJsonPath: join(resolved.package.packageDir, 'package.json'), packageName: resolved.package.packageName, skillName: resolved.export.name, }; } function buildLocalDependencyPackages(repoRoot, rootResolved, { cwd }) { const context = loadSkillTargetContext(repoRoot, { includeInstalled: false }); const authoredGraph = context.authoredGraph; if (!authoredGraph || !rootResolved?.export) return; const queue = [rootResolved.export.id]; const seenExports = new Set(); const builtPackages = new Set([rootResolved.package.packageName]); while (queue.length > 0) { const exportId = queue.shift(); if (seenExports.has(exportId)) continue; seenExports.add(exportId); const exportNode = authoredGraph.exports?.[exportId]; if (!exportNode?.compiled) continue; for (const skillImport of Object.values(exportNode.compiled.skillImports || {})) { let dependency; try { dependency = ensureResolvedExportIsValid( resolveSingleSkillTarget(repoRoot, skillImport.target, { includeInstalled: false, cwd }) ); } catch { continue; } queue.push(dependency.export.id); if (builtPackages.has(dependency.package.packageName)) continue; buildCompiledStateUseCase(dependency.package.packageDir, { cwd }); builtPackages.add(dependency.package.packageName); } } } function resolveSkillFileTarget(repoRoot, target) { const absoluteTarget = isAbsolute(target) ? target : resolve(repoRoot, target); if (existsSync(absoluteTarget)) { if (absoluteTarget.endsWith('SKILL.md')) return absoluteTarget; const skillFile = join(absoluteTarget, 'SKILL.md'); if (existsSync(skillFile)) return skillFile; } return null; } function readPackageJson(packageDir) { const packageJsonPath = join(packageDir, 'package.json'); if (!existsSync(packageJsonPath)) { throw new NotFoundError(`package.json not found: ${packageJsonPath}`, { code: 'package_json_not_found', exitCode: EXIT_CODES.GENERAL, }); } return { packageJsonPath, packageJson: JSON.parse(readFileSync(packageJsonPath, 'utf-8')), }; } function isProcessAlive(pid) { if (!Number.isInteger(pid) || pid <= 0) return false; try { process.kill(pid, 0); return true; } catch (error) { if (error.code === 'EPERM') return true; if (error.code === 'ESRCH') return false; return false; } } function buildDevSessionNextSteps(command) { return [{ action: 'run_command', command, reason: 'Use the dev session cleanup flow to remove recorded linked skills for this repo', }]; } function toDevSessionRecord(repoRoot, target, result, existing = null) { const now = new Date().toISOString(); const rootSkill = result.linkedSkills.find((entry) => entry.name === result.name) || result.linkedSkills[0] || null; return { version: 1, session_id: existing?.session_id || `dev-${now.replaceAll(':', '-').replaceAll('.', '-')}`, status: 'active', pid: process.pid, repo_root: repoRoot, target, root_skill: rootSkill ? { name: rootSkill.name, package_name: rootSkill.packageName, path: rootSkill.path, } : null, linked_skills: result.linkedSkills.map((entry) => ({ name: entry.name, package_name: entry.packageName, path: entry.path, })), links: result.links, started_at: existing?.started_at || now, updated_at: now, }; } function cleanupRecordedDevSession(repoRoot, session, status = 'stale') { if (!session) { return { cleaned: false, removed: [], session: null, }; } writeDevSession(repoRoot, { ...session, status, updated_at: new Date().toISOString(), }); const removed = removeSkillLinksByPaths(repoRoot, session.links || [], normalizeDisplayPath); removeDevSession(repoRoot); return { cleaned: removed.length > 0 || Boolean(session), removed, session, }; } function reconcileDevSession(repoRoot) { const session = readDevSession(repoRoot); if (!session) return null; if (session.status === 'active' && isProcessAlive(session.pid)) { throw new AgentpackError('An author dev session is already active in this repo', { code: 'skills_dev_session_active', exitCode: EXIT_CODES.GENERAL, nextSteps: [ ...buildDevSessionNextSteps('agentpack author dev cleanup'), ...buildDevSessionNextSteps('agentpack author dev cleanup --force'), ], details: { rootSkill: session.root_skill?.name || null, pid: session.pid, startedAt: session.started_at || null, }, }); } return cleanupRecordedDevSession(repoRoot, session, 'stale'); } export function syncSkillDependencies(skillDir) { const { packageJsonPath, packageJson } = readPackageJson(skillDir); const currentPackageName = packageJson.name || null; const required = [...new Set( readInstalledSkillExports(skillDir).flatMap((entry) => { const compiled = readCompilerSkillDocument(entry.skillFile); return listCompilerPackageDependencies(compiled) .map((dependency) => packageNameForRequirement(dependency)) .filter((dependency) => dependency && dependency !== currentPackageName); }) )].sort((a, b) => a.localeCompare(b)); const nextDependencies = { ...(packageJson.dependencies || {}) }; const requiredSet = new Set(required); const added = []; const removed = []; for (const packageName of required) { if (!nextDependencies[packageName]) { nextDependencies[packageName] = '*'; added.push(packageName); } } for (const packageName of Object.keys(nextDependencies).sort((a, b) => a.localeCompare(b))) { if (!isManagedPackageName(packageName)) continue; if (requiredSet.has(packageName)) continue; delete nextDependencies[packageName]; removed.push(packageName); } const previousSerialized = JSON.stringify(packageJson.dependencies || {}); const nextSerialized = JSON.stringify(nextDependencies); if (previousSerialized !== nextSerialized) { packageJson.dependencies = nextDependencies; writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 2) + '\n'); } return { skillDir, packageJsonPath, added, removed, unchanged: added.length === 0 && removed.length === 0, }; } export function devSkill(target, { cwd = process.cwd(), sync = true, } = {}) { const repoRoot = findRepoRoot(cwd); try { const { skillDir, packageDir } = resolveLocalPackagedSkillDir(repoRoot, target); const resolved = ensureResolvedExportIsValid( resolveSingleSkillTarget(repoRoot, target, { includeInstalled: false, cwd }) ); const buildResult = buildCompiledStateUseCase(target, { cwd }); buildLocalDependencyPackages(repoRoot, resolved, { cwd }); const selection = computeRuntimeSelectionUseCase({ cwd, mode: 'closure', packageName: buildResult.packageName, exportId: resolved.export.id, }); const materialization = materializeRuntimeSelectionUseCase(repoRoot, selection); const synced = sync ? syncSkillDependencies(packageDir) : { skillDir: packageDir, packageJsonPath: join(packageDir, 'package.json'), added: [], removed: [], unchanged: true, }; const linkedSkills = selection.exports.map((entry) => ({ name: entry.name, path: entry.runtimePath || entry.skillPath, packageName: entry.packageName, })); const rootSkill = linkedSkills.find((entry) => entry.name === resolved.export.name) || linkedSkills[0]; const links = Object.values(materialization.outputs).flatMap((entries) => entries.map((entry) => entry.target)); const unresolved = collectUnresolvedSelectionRequirements(repoRoot, selection, { cwd }); return { name: rootSkill.name, path: normalizeDisplayPath(repoRoot, skillDir), linked: true, links, linkedSkills, unresolved, synced, }; } catch (error) { if (error instanceof AgentpackError) { throw error; } throw new AgentpackError(error.message, { code: error.code || 'skill_dev_failed', exitCode: EXIT_CODES.GENERAL, }); } } export function startSkillDev(target, { cwd = process.cwd(), sync = true, dashboard = true, onStart = () => {}, onRebuild = () => {}, } = {}) { const outerRepoRoot = findRepoRoot(cwd); let skillDir; let packageDir; try { ({ skillDir, packageDir } = resolveLocalPackagedSkillDir(outerRepoRoot, target)); } catch (error) { if (error instanceof AgentpackError) { throw error; } throw new AgentpackError(error.message, { code: error.code || 'skill_dev_failed', exitCode: EXIT_CODES.GENERAL, suggestion: error.suggestion, }); } const repoRoot = findRepoRoot(skillDir); reconcileDevSession(repoRoot); let closed = false; let timer = null; let currentNames = []; let watcher = null; let workbench = null; let initialResult = null; let sessionRecord = null; const cleanup = () => { if (closed) return { name: currentNames[0] || null, unlinked: false, removed: [] }; closed = true; clearTimeout(timer); if (watcher) watcher.close(); if (workbench) workbench.close(); if (sessionRecord) { writeDevSession(repoRoot, { ...sessionRecord, status: 'cleaning', updated_at: new Date().toISOString(), }); } const removed = sessionRecord ? removeSkillLinksByPaths(repoRoot, sessionRecord.links || [], normalizeDisplayPath) : removeSkillLinksByNames(repoRoot, currentNames, normalizeDisplayPath); removeDevSession(repoRoot); detachProcessCleanup(); return { name: sessionRecord?.root_skill?.name || currentNames[0] || null, unlinked: removed.length > 0, removed, }; }; const processCleanupHandlers = new Map(); const attachProcessCleanup = () => { const exitHandler = () => cleanup(); processCleanupHandlers.set('exit', exitHandler); process.once('exit', exitHandler); for (const eventName of ['SIGINT', 'SIGTERM', 'SIGHUP']) { const handler = () => { cleanup(); process.exit(0); }; processCleanupHandlers.set(eventName, handler); process.once(eventName, handler); } }; const detachProcessCleanup = () => { for (const [eventName, handler] of processCleanupHandlers.entries()) { process.removeListener(eventName, handler); } processCleanupHandlers.clear(); }; const enrichResult = (result) => ({ ...result, workbench: workbench ? { enabled: true, url: workbench.url, port: workbench.port, } : { enabled: false, url: null, port: null, }, }); const applyDevResult = (result) => { const nextNames = result.linkedSkills.map((entry) => entry.name); const staleNames = currentNames.filter((name) => !nextNames.includes(name)); if (staleNames.length > 0) { removeSkillLinksByNames(repoRoot, staleNames, normalizeDisplayPath); } currentNames = nextNames; sessionRecord = toDevSessionRecord(repoRoot, target, result, sessionRecord); writeDevSession(repoRoot, sessionRecord); return result; }; const startOrRefreshWorkbench = async () => { if (dashboard && !workbench) { workbench = await startSkillDevWorkbench({ repoRoot, skillDir, open: true, disableBrowser: process.env.AGENTPACK_DISABLE_BROWSER === '1', }); } else if (workbench) { workbench.refresh(); } }; initialResult = enrichResult(applyDevResult(devSkill(target, { cwd, sync }))); const ready = Promise.resolve(startOrRefreshWorkbench()) .then(() => { const result = enrichResult(initialResult); initialResult = result; onStart(result); return result; }) .catch((error) => { cleanup(); throw error; }); attachProcessCleanup(); watcher = watch(packageDir, { recursive: true }, (_eventType, filename) => { if (closed) return; if (filename && isGeneratedPackagePath(String(filename))) return; clearTimeout(timer); timer = setTimeout(() => { Promise.resolve() .then(() => { return applyDevResult(devSkill(target, { cwd, sync })); }) .then(async (result) => { await startOrRefreshWorkbench(); return enrichResult(result); }) .then(onRebuild) .catch((error) => { onRebuild({ error }); }); }, 100); }); return { initialResult, ready, close() { return cleanup(); }, }; } export function unlinkSkill(name, { cwd = process.cwd(), recursive = false } = {}) { const repoRoot = findRepoRoot(cwd); const session = readDevSession(repoRoot); if (recursive) { if (!session || session.root_skill?.name !== name) { throw new AgentpackError('Recursive unlink requires the active dev-session root skill', { code: 'linked_skill_recursive_unlink_requires_root', exitCode: EXIT_CODES.GENERAL, nextSteps: session?.root_skill?.name ? [{ action: 'run_command', command: `agentpack author unlink ${session.root_skill.name} --recursive`, reason: 'Recursive unlink in v1 only works for the recorded dev-session root skill', }] : buildDevSessionNextSteps('agentpack author dev cleanup --force'), details: { rootSkill: session?.root_skill?.name || null, }, }); } const removed = removeSkillLinksByPaths(repoRoot, session.links || [], normalizeDisplayPath); removeDevSession(repoRoot); return { name, unlinked: removed.length > 0, removed, }; } const existing = [ join(repoRoot, '.claude', 'skills', name), join(repoRoot, '.agents', 'skills', name), ].filter((pathValue) => existsSync(pathValue)); if (existing.length === 0) { throw new AgentpackError(`linked skill not found: ${name}`, { code: 'linked_skill_not_found', exitCode: EXIT_CODES.GENERAL, }); } const removed = removeSkillLinks(repoRoot, name, normalizeDisplayPath); return { name, unlinked: true, removed, }; } export function cleanupSkillDevSession({ cwd = process.cwd(), force = false } = {}) { const repoRoot = findRepoRoot(cwd); const session = readDevSession(repoRoot); if (!session) { return { cleaned: false, active: false, removed: [], }; } if (!force && session.status === 'active' && isProcessAlive(session.pid)) { throw new AgentpackError('An author dev session is still active in this repo', { code: 'skills_dev_session_active', exitCode: EXIT_CODES.GENERAL, nextSteps: [ ...buildDevSessionNextSteps('agentpack author dev cleanup'), ...buildDevSessionNextSteps('agentpack author dev cleanup --force'), ], details: { rootSkill: session.root_skill?.name || null, pid: session.pid, startedAt: session.started_at || null, }, }); } const result = cleanupRecordedDevSession(repoRoot, session, 'stale'); return { cleaned: true, active: false, forced: force, name: session.root_skill?.name || null, removed: result.removed, }; } function buildValidateNextSteps(packageMetadata, valid) { if (!valid || !packageMetadata.packageName) return []; const steps = [ { type: 'version', command: 'npm version patch', reason: 'assign the next package version after reviewed skill changes', }, ]; if (isManagedPackageName(packageMetadata.packageName)) { steps.push({ type: 'publish', command: 'npm publish', reason: 'publish the versioned package with npm', }); } return steps; } function parseNpmRcFile(content) { const config = {}; for (const rawLine of content.split('\n')) { const line = rawLine.trim(); if (!line || line.startsWith('#') || line.startsWith(';')) continue; const eqIndex = line.indexOf('='); if (eqIndex === -1) continue; const key = line.slice(0, eqIndex).trim(); const value = line.slice(eqIndex + 1).trim(); config[key] = value; } return config; } function isPublishedSkillFile(files, relativeSkillFile) { if (!files) return true; const normalizedSkillFile = normalizeFilesFieldEntry(relativeSkillFile); return files.some((entry) => { const normalizedEntry = normalizeFilesFieldEntry(entry); return normalizedEntry === normalizedSkillFile || normalizedSkillFile.startsWith(`${normalizedEntry}/`); }); } function packageNameForRequirement(requirement) { if (typeof requirement !== 'string') return null; const colonIndex = requirement.indexOf(':'); return colonIndex === -1 ? requirement : requirement.slice(0, colonIndex); } function normalizeFilesFieldEntry(entry) { if (typeof entry !== 'string') return ''; return entry .replace(/\\/g, '/') .replace(/^\.\//, '') .replace(/\/+$/, ''); } function isInvalidDependencyName(dependency) { return typeof dependency === 'string' && dependency.includes(':'); } function buildSourceValidationDetails(repoRoot, skillExport) { const compiledState = readCompiledState(repoRoot); const previousHashes = new Map( (compiledState?.sourceFiles || []).map((entry) => [entry.path, entry.hash]) ); return (skillExport.sources || []).map((sourcePath) => { const absolutePath = join(repoRoot, sourcePath); const exists = existsSync(absolutePath); const currentHash = exists ? hashFile(absolutePath) : null; const previousHash = previousHashes.get(sourcePath) || null; return { path: sourcePath, absolutePath, exists, currentHash, previousHash, status: previousHash === null ? 'new' : previousHash === currentHash ? 'unchanged' : 'changed', }; }); } function buildDependencyValidationDetails(skillExport, packageMetadata) { return (skillExport.requires || []).map((requirement) => { const dependency = packageNameForRequirement(requirement); return { requirement, dependency, samePackage: dependency === packageMetadata.packageName, declared: Boolean(dependency && packageMetadata.dependencies[dependency]), }; }); } export function validatePackagedSkillExport(repoRoot, pkg, skillExport, options = {}) { const packageMetadata = readPackageMetadata(pkg.packageDir); const issues = []; const metadataStatus = skillExport.lifecycleStatus || null; const rootSkillFilePath = join(pkg.packageDir, 'SKILL.md'); if (!packageMetadata.packageName) { issues.push({ code: 'missing_package_name', message: 'package.json missing "name"', }); } if (!packageMetadata.packageVersion) { issues.push({ code: 'missing_package_version', message: 'package.json missing "version"', }); } if (!isPublishedSkillFile(packageMetadata.files, skillExport.relativeSkillFile)) { issues.push({ code: 'skill_not_published', message: `package.json files does not include ${skillExport.relativeSkillFile}`, }); } if ( packageMetadata.skillRoot && packageMetadata.files?.some((entry) => normalizeFilesFieldEntry(entry) === 'SKILL.md') && !existsSync(rootSkillFilePath) ) { issues.push({ code: 'missing_root_skill_file', message: 'package.json files includes SKILL.md but the root SKILL.md is missing', path: normalizeDisplayPath(repoRoot, rootSkillFilePath), }); } if (isManagedPackageName(packageMetadata.packageName)) { if (!packageMetadata.repository) { issues.push({ code: 'missing_repository', message: 'package.json missing repository for private registry publishing', }); } } if (metadataStatus && !['deprecated', 'retired'].includes(metadataStatus)) { issues.push({ code: 'invalid_skill_status', message: 'metadata.status must be "deprecated" or "retired"', }); } if (skillExport.replacement && !skillExport.replacement.startsWith('@')) { issues.push({ code: 'invalid_replacement', message: 'metadata.replacement must be a package name', }); } for (const sourcePath of skillExport.sources || []) { if (!existsSync(join(repoRoot, sourcePath))) { issues.push({ code: 'missing_source', message: 'declared source file does not exist', path: sourcePath, }); } } for (const dependency of Object.keys(packageMetadata.dependencies || {}).sort((a, b) => a.localeCompare(b))) { if (!isInvalidDependencyName(dependency)) continue; issues.push({ code: 'invalid_dependency_name', message: 'package.json dependency name is not a valid npm package name', dependency, }); } for (const requirement of skillExport.requires || []) { const dependencyName = packageNameForRequirement(requirement); if (dependencyName === packageMetadata.packageName) { continue; } if (!packageMetadata.dependencies[dependencyName]) { issues.push({ code: 'missing_dependency_declaration', message: 'required skill package is not declared in package dependencies', dependency: dependencyName, }); } } const details = options.verbose ? { sources: buildSourceValidationDetails(repoRoot, skillExport), dependencies: buildDependencyValidationDetails(skillExport, packageMetadata), } : null; return { valid: issues.length === 0, key: skillExport.key, name: skillExport.name || null, packageName: packageMetadata.packageName, packageVersion: packageMetadata.packageVersion, skillFile: skillExport.skillFile, packagePath: pkg.packagePath, status: metadataStatus, replacement: skillExport.replacement || null, nextSteps: buildValidateNextSteps(packageMetadata, issues.length === 0), ...(details ? { details } : {}), issues, }; } export function validateSkills(target, { cwd = process.cwd() } = {}) { const repoRoot = findRepoRoot(cwd); let targets = []; if (target) { const resolved = resolveSkillTarget(repoRoot, target, { includeInstalled: false }); targets = resolved.kind === 'export' ? [{ package: resolved.package, export: resolved.export }] : resolved.exports.map((entry) => ({ package: resolved.package, export: entry })); } else { targets = listAuthoredSkillPackages(repoRoot) .flatMap((pkg) => pkg.exports.map((entry) => ({ package: pkg, export: entry }))); } for (const packageDir of [...new Set(targets.map((entry) => entry.package.packageDir))]) { syncSkillDependencies(packageDir); } const skills = targets .map((entry) => validatePackagedSkillExport(repoRoot, entry.package, entry.export)) .sort((a, b) => (a.key || a.packageName || a.packagePath).localeCompare(b.key || b.packageName || b.packagePath)); const validCount = skills.filter((skill) => skill.valid).length; const invalidCount = skills.length - validCount; return { valid: invalidCount === 0, count: skills.length, validCount, invalidCount, skills, }; } function normalizeRelativePath(pathValue) { return pathValue.split('\\').join('/'); } function normalizeRequestedTarget(target, cwd = process.cwd()) { if (typeof target !== 'string') return target; if (target.startsWith('@')) return target; return normalizeRelativePath(resolve(cwd, target)); } function listPackagedSkillDirs(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 hasSkillFile = false; let hasPackageFile = false; for (const entry of entries) { if (entry.name === '.git' || entry.name === 'node_modules' || entry.name === '.agentpack') continue; const fullPath = join(current, entry.name); if (entry.isDirectory()) { stack.push(fullPath); continue; } if (entry.name === 'SKILL.md') hasSkillFile = true; if (entry.name === 'package.json') hasPackageFile = true; } if (hasSkillFile && hasPackageFile) { results.push(current); } } return results.sort(); } function listAuthoredPackagedSkills(repoRoot) { return listAuthoredSkillPackages(repoRoot) .flatMap((pkg) => pkg.exports.map((entry) => ({ key: entry.key, name: entry.name, description: entry.description, packageName: entry.packageName, packageVersion: entry.packageVersion, skillPath: entry.skillPath, skillFile: entry.skillFile, sources: entry.sources, requires: entry.requires, wraps: entry.wraps, overrides: entry.overrides, declaredName: entry.declaredName, packagePath: pkg.packagePath, }))) .sort((a, b) => a.packageName.localeCompare(b.packageName)); } export function generateSkillsCatalog({ cwd = process.cwd() } = {}) { const repoRoot = findRepoRoot(cwd); const skills = {}; for (const skill of listAuthoredPackagedSkills(repoRoot)) { skills[skill.key] = { name: skill.name, description: skill.description, path: skill.skillPath, skill_file: skill.skillFile, package_name: skill.packageName, package_version: skill.packageVersion, sources: skill.sources, requires: skill.requires, ...(skill.wraps ? { wraps: skill.wraps } : {}), ...(skill.overrides?.length ? { overrides: skill.overrides } : {}), }; } return { version: 1, skills, }; }