UNPKG

@puls-atlas/cli

Version:

The Puls Atlas CLI tool for managing Atlas projects

342 lines 14.4 kB
import fs from 'fs'; import path from 'path'; import { createHash } from 'node:crypto'; import { readJsonFile, writeJsonFile, writeTextFile } from '../../utils/file.js'; import { normalizeOptionalString } from '../../utils/value.js'; import { createAtlasAiConsumerImpactSummaryRows, createAtlasAiConsumerQualitySummaryRows, createAtlasAiCopilotInstructions, createAtlasAiSnapshotData, createCoverageFindings, createLockComparison, createVerificationFindingRecord, logVerificationFindingRecords, normalizeAtlasAiRefreshMode, resolveAtlasAiContextState, resolveOutputPaths } from './consumerContextRuntime.js'; const SCHEMA_VERSION = '1.0.0'; const DEFAULT_VERIFY_POLICY_MODE = 'localDevelopment'; const createChecksum = value => createHash('sha256').update(value).digest('hex'); const readTextFile = (filePath, dependencies = {}) => { const { fsImpl = fs } = dependencies; if (!fsImpl.existsSync(filePath)) { return null; } try { return fsImpl.readFileSync(filePath, 'utf-8'); } catch (error) { throw new Error(`Failed to read file: ${filePath}\nError: ${error.message}`); } }; export { createCoverageFindings, normalizeAtlasAiRefreshMode }; const createAtlasAiLockData = (state, outputPaths, agentFiles, skillFiles, instructionsChecksum, snapshotChecksum, commandName) => ({ schemaVersion: SCHEMA_VERSION, framework: 'Atlas', project: { name: state.consumerPackage?.name ?? null }, sharedPackage: { checksum: state.sharedManifestChecksum, manifestPath: state.sharedManifestPath, name: state.sharedManifest?.package?.name ?? state.sharedPackageName, version: state.sharedPackageVersion ?? null }, packages: state.packageContexts.map(entry => ({ checksum: entry.checksum, guidesChecksum: entry.guidesChecksum, guidesManifestPath: entry.guidesManifestPath, manifestPath: entry.manifestPath, name: entry.name, symbolsChecksum: entry.symbolsChecksum, symbolsManifestPath: entry.symbolsManifestPath, version: entry.version })), missingContexts: state.missingContexts.map(entry => ({ expectedPath: entry.expectedPath, name: entry.name, version: entry.version })), generatedFiles: [...agentFiles.map(agentFile => ({ checksum: agentFile.checksum, path: agentFile.relativeOutputPath })), ...skillFiles.map(skillFile => ({ checksum: skillFile.checksum, path: skillFile.relativeOutputPath })), { checksum: snapshotChecksum, path: outputPaths.relativeSnapshotPath }, { checksum: instructionsChecksum, path: outputPaths.relativeInstructionsPath }], lockFilePath: outputPaths.relativeLockFilePath, lastSyncedAt: new Date().toISOString(), generatedBy: commandName }); export const createAtlasAiArtifacts = (options = {}, dependencies = {}, cwd = process.cwd(), commandName = 'atlas ai sync') => { const { pathImpl = path } = dependencies; const state = resolveAtlasAiContextState(options, dependencies, cwd); const outputPaths = resolveOutputPaths(cwd, options, state.sharedManifest, pathImpl); const agentFiles = state.sharedAgentPaths.map((sharedAgentPath, index) => { const content = readTextFile(pathImpl.resolve(cwd, sharedAgentPath), dependencies); if (content === null) { throw new Error(`Atlas AI shared agent is missing at ${sharedAgentPath}. ` + 'Reinstall or rebuild the shared Atlas AI package before running this command.'); } return { checksum: createChecksum(content), content, outputPath: outputPaths.agentPaths[index], relativeOutputPath: outputPaths.relativeAgentPaths[index], sharedPath: sharedAgentPath }; }); const skillFiles = state.sharedSkillPaths.map((sharedSkillPath, index) => { const content = readTextFile(pathImpl.resolve(cwd, sharedSkillPath), dependencies); if (content === null) { throw new Error(`Atlas AI shared skill is missing at ${sharedSkillPath}. ` + 'Reinstall or rebuild the shared Atlas AI package before running this command.'); } return { checksum: createChecksum(content), content, outputPath: outputPaths.skillPaths[index], relativeOutputPath: outputPaths.relativeSkillPaths[index], sharedPath: sharedSkillPath }; }); const instructionsContent = createAtlasAiCopilotInstructions(state, outputPaths, dependencies, cwd); const snapshotData = createAtlasAiSnapshotData(state, commandName); const instructionsChecksum = createChecksum(instructionsContent); const snapshotChecksum = createChecksum(JSON.stringify(snapshotData)); return { ...state, agentContent: agentFiles[0]?.content ?? '', agentFiles, instructionsContent, snapshotData, lockData: createAtlasAiLockData(state, outputPaths, agentFiles, skillFiles, instructionsChecksum, snapshotChecksum, commandName), outputPaths, skillFiles }; }; const createGeneratedFileDriftFindingRecords = (artifacts, dependencies = {}) => { const issues = []; const instructionsText = readTextFile(artifacts.outputPaths.instructionsPath, dependencies); const existingSnapshot = readJsonFile(artifacts.outputPaths.snapshotPath, { allowMissing: true }, dependencies); const existingLock = readJsonFile(artifacts.outputPaths.lockFilePath, { allowMissing: true }, dependencies); for (const agentFile of artifacts.agentFiles) { const existingAgentText = readTextFile(agentFile.outputPath, dependencies); if (existingAgentText === null) { issues.push(createVerificationFindingRecord('error', `Generated Atlas AI agent is missing at ${agentFile.relativeOutputPath}.`, { code: 'missing-generated-agent' })); continue; } if (existingAgentText !== agentFile.content) { issues.push(createVerificationFindingRecord('error', `Generated Atlas AI agent is out of date at ${agentFile.relativeOutputPath}. Run "atlas ai sync".`, { code: 'stale-generated-agent' })); } } for (const skillFile of artifacts.skillFiles) { const existingSkillText = readTextFile(skillFile.outputPath, dependencies); if (existingSkillText === null) { issues.push(createVerificationFindingRecord('error', `Generated Atlas AI skill is missing at ${skillFile.relativeOutputPath}.`, { code: 'missing-generated-skill' })); continue; } if (existingSkillText !== skillFile.content) { issues.push(createVerificationFindingRecord('error', `Generated Atlas AI skill is out of date at ${skillFile.relativeOutputPath}. Run "atlas ai sync".`, { code: 'stale-generated-skill' })); } } if (instructionsText === null) { issues.push(createVerificationFindingRecord('error', `Generated Atlas AI instructions are missing at ${artifacts.outputPaths.relativeInstructionsPath}.`, { code: 'missing-generated-instructions' })); } else if (instructionsText !== artifacts.instructionsContent) { issues.push(createVerificationFindingRecord('error', 'Generated Atlas AI instructions are out of date. Run "atlas ai sync".', { code: 'stale-generated-instructions' })); } if (existingSnapshot === null) { issues.push(createVerificationFindingRecord('error', `Atlas AI snapshot is missing at ${artifacts.outputPaths.relativeSnapshotPath}.`, { code: 'missing-snapshot' })); } else if (JSON.stringify(existingSnapshot) !== JSON.stringify(artifacts.snapshotData)) { issues.push(createVerificationFindingRecord('error', 'Atlas AI snapshot is out of date. Run "atlas ai sync".', { code: 'stale-snapshot' })); } if (existingLock === null) { issues.push(createVerificationFindingRecord('error', `Atlas AI lock file is missing at ${artifacts.outputPaths.relativeLockFilePath}.`, { code: 'missing-lock-file' })); } else if (JSON.stringify(createLockComparison(existingLock)) !== JSON.stringify(createLockComparison(artifacts.lockData))) { issues.push(createVerificationFindingRecord('error', 'Atlas AI lock file is out of date. Run "atlas ai sync".', { code: 'stale-lock-file' })); } return issues; }; export const createGeneratedFileDriftIssues = (artifacts, dependencies = {}) => createGeneratedFileDriftFindingRecords(artifacts, dependencies).map(finding => finding.message); export const ensureInitWriteAllowed = (artifacts, options = {}, dependencies = {}) => { const { fsImpl = fs } = dependencies; if (options.force) { return; } const existingFiles = []; for (const [index, agentPath] of artifacts.outputPaths.agentPaths.entries()) { if (fsImpl.existsSync(agentPath)) { existingFiles.push(artifacts.outputPaths.relativeAgentPaths[index]); } } for (const [index, skillPath] of artifacts.outputPaths.skillPaths.entries()) { if (fsImpl.existsSync(skillPath)) { existingFiles.push(artifacts.outputPaths.relativeSkillPaths[index]); } } if (fsImpl.existsSync(artifacts.outputPaths.instructionsPath)) { existingFiles.push(artifacts.outputPaths.relativeInstructionsPath); } if (fsImpl.existsSync(artifacts.outputPaths.lockFilePath)) { existingFiles.push(artifacts.outputPaths.relativeLockFilePath); } if (fsImpl.existsSync(artifacts.outputPaths.snapshotPath)) { existingFiles.push(artifacts.outputPaths.relativeSnapshotPath); } if (existingFiles.length === 0) { return; } throw new Error(`Atlas AI files already exist: ${existingFiles.join(', ')}. ` + 'Use --force or run "atlas ai sync" instead.'); }; const removeStaleGeneratedFiles = (artifacts, dependencies = {}) => { const { fsImpl = fs, pathImpl = path } = dependencies; const existingLock = readJsonFile(artifacts.outputPaths.lockFilePath, { allowMissing: true }, dependencies); if (!existingLock || typeof fsImpl.rmSync !== 'function') { return; } const currentGeneratedFilePaths = new Set(artifacts.lockData.generatedFiles.map(entry => entry.path)); for (const generatedFile of Array.isArray(existingLock.generatedFiles) ? existingLock.generatedFiles : []) { const relativePath = normalizeOptionalString(generatedFile?.path); if (!relativePath || currentGeneratedFilePaths.has(relativePath)) { continue; } const staleFilePath = pathImpl.resolve(artifacts.outputPaths.workspaceRootPath, relativePath); if (fsImpl.existsSync(staleFilePath)) { fsImpl.rmSync(staleFilePath, { force: true }); } } }; export const writeAtlasAiArtifacts = (artifacts, dependencies = {}) => { removeStaleGeneratedFiles(artifacts, dependencies); for (const agentFile of artifacts.agentFiles) { writeTextFile(agentFile.outputPath, agentFile.content, { trailingNewline: false }, dependencies); } for (const skillFile of artifacts.skillFiles) { writeTextFile(skillFile.outputPath, skillFile.content, { trailingNewline: false }, dependencies); } writeTextFile(artifacts.outputPaths.instructionsPath, artifacts.instructionsContent, { trailingNewline: false }, dependencies); writeJsonFile(artifacts.outputPaths.snapshotPath, artifacts.snapshotData, {}, dependencies); writeJsonFile(artifacts.outputPaths.lockFilePath, artifacts.lockData, {}, dependencies); }; const createAiSummaryRows = (artifacts, actionLabel) => [{ label: 'Action', value: actionLabel }, { label: 'Project', value: artifacts.consumerPackage?.name ?? '(unnamed project)' }, { label: 'Shared package', value: `${artifacts.sharedManifest?.package?.name ?? artifacts.sharedPackageName} ` + `(${artifacts.sharedPackageVersion ?? 'unknown'})` }, { label: 'Atlas package contexts', value: artifacts.packageContexts.length }, { label: 'Symbols catalogs', value: artifacts.packageContexts.filter(entry => entry.symbolsManifestPath).length }, { label: 'Guide catalogs', value: artifacts.packageContexts.filter(entry => entry.guidesManifestPath).length }, { label: 'Missing manifests', value: artifacts.missingContexts.length }, { label: 'Lead agent', value: artifacts.outputPaths.relativeAgentPath }, { label: 'Specialist agents', value: Math.max(artifacts.outputPaths.relativeAgentPaths.length - 1, 0) }, { label: 'Skills', value: artifacts.outputPaths.relativeSkillPaths.length }, { label: 'Instructions', value: artifacts.outputPaths.relativeInstructionsPath }, { label: 'Snapshot', value: artifacts.outputPaths.relativeSnapshotPath }, { label: 'Lock file', value: artifacts.outputPaths.relativeLockFilePath }]; export const logAiWriteOutcome = (loggerImpl, artifacts, actionLabel) => { loggerImpl.summary('Atlas AI summary', createAiSummaryRows(artifacts, actionLabel)); loggerImpl.summary('Atlas AI consumer quality', createAtlasAiConsumerQualitySummaryRows(artifacts)); const consumerImpactSummaryRows = createAtlasAiConsumerImpactSummaryRows(artifacts); if (consumerImpactSummaryRows.length > 0) { loggerImpl.summary('Atlas AI consumer impact priorities', consumerImpactSummaryRows); } if (artifacts.packageContexts.length > 0) { loggerImpl.summary('Detected Atlas AI manifests', artifacts.packageContexts.map(entry => ({ label: entry.name, value: entry.version ?? 'unknown' }))); } const coverageFindings = createCoverageFindings(artifacts, { policyMode: DEFAULT_VERIFY_POLICY_MODE }); for (const warning of [...coverageFindings.issues, ...coverageFindings.warnings]) { loggerImpl.warning(warning); } }; export const createVerificationFindings = (artifacts, options = {}, dependencies = {}) => { const coverageFindings = createCoverageFindings(artifacts, options); const generatedFileFindings = createGeneratedFileDriftFindingRecords(artifacts, dependencies); const records = [...coverageFindings.records, ...generatedFileFindings]; return { issues: records.filter(record => record.severity !== 'warning').map(record => record.message), records, warnings: records.filter(record => record.severity === 'warning').map(record => record.message) }; }; export const logAiVerifyOutcome = (loggerImpl, result) => { loggerImpl.summary('Atlas AI verification', [{ label: 'Project', value: result.artifacts.consumerPackage?.name ?? '(unnamed project)' }, { label: 'Status', value: result.status }, { label: 'Issues', value: result.issues.length }, { label: 'Warnings', value: result.warnings.length }]); logVerificationFindingRecords(loggerImpl, result.findings); };