UNPKG

aios-core

Version:

Synkra AIOS: AI-Orchestrated System for Full Stack Development - Core Framework

353 lines (296 loc) 9.59 kB
/** * Migration Backup Module * * Handles backup creation, verification, and manifest generation * for v2.0 → v4.0.4 migration. * * @module cli/commands/migrate/backup * @version 1.0.0 * @story 2.14 - Migration Script v2.0 → v4.0.4 */ const fs = require('fs'); const path = require('path'); const crypto = require('crypto'); /** * Create a backup directory name with timestamp * @returns {string} Backup directory name */ function createBackupDirName() { const date = new Date(); const dateStr = date.toISOString().split('T')[0]; return `.aios-backup-${dateStr}`; } /** * Calculate MD5 checksum for a file * @param {string} filePath - Path to file * @returns {Promise<string>} MD5 checksum */ async function calculateChecksum(filePath) { return new Promise((resolve, reject) => { const hash = crypto.createHash('md5'); const stream = fs.createReadStream(filePath); stream.on('data', data => hash.update(data)); stream.on('end', () => resolve(hash.digest('hex'))); stream.on('error', reject); }); } /** * Get file stats including permissions * @param {string} filePath - Path to file * @returns {Promise<Object>} File stats */ async function getFileStats(filePath) { const stats = await fs.promises.stat(filePath); return { size: stats.size, mode: stats.mode, mtime: stats.mtime.toISOString(), isDirectory: stats.isDirectory(), }; } /** * Copy file with metadata preservation * @param {string} src - Source path * @param {string} dest - Destination path * @returns {Promise<Object>} Copy result with checksum */ async function copyFileWithMetadata(src, dest) { const stats = await fs.promises.stat(src); // Ensure destination directory exists await fs.promises.mkdir(path.dirname(dest), { recursive: true }); // Copy file await fs.promises.copyFile(src, dest); // Preserve timestamps and permissions await fs.promises.utimes(dest, stats.atime, stats.mtime); await fs.promises.chmod(dest, stats.mode); // Calculate checksum const checksum = await calculateChecksum(dest); return { src, dest, size: stats.size, mode: stats.mode, checksum, }; } /** * Recursively get all files in a directory * @param {string} dir - Directory path * @param {string[]} [fileList] - Accumulated file list * @returns {Promise<string[]>} Array of file paths */ async function getAllFiles(dir, fileList = []) { const entries = await fs.promises.readdir(dir, { withFileTypes: true }); for (const entry of entries) { const fullPath = path.join(dir, entry.name); if (entry.isDirectory()) { await getAllFiles(fullPath, fileList); } else { fileList.push(fullPath); } } return fileList; } /** * Create backup of .aios-core directory * @param {string} projectRoot - Project root directory * @param {Object} options - Backup options * @param {boolean} options.verbose - Show detailed progress * @param {Function} options.onProgress - Progress callback * @returns {Promise<Object>} Backup result */ async function createBackup(projectRoot, options = {}) { const { verbose = false, onProgress = () => {} } = options; const aiosCoreDir = path.join(projectRoot, '.aios-core'); const backupDirName = createBackupDirName(); const backupDir = path.join(projectRoot, backupDirName); // Check if .aios-core exists if (!fs.existsSync(aiosCoreDir)) { throw new Error('No .aios-core directory found. Is this an AIOS v2.0 project?'); } // Check if backup already exists if (fs.existsSync(backupDir)) { throw new Error(`Backup directory ${backupDirName} already exists. Remove it or use a different name.`); } onProgress({ phase: 'start', message: 'Creating backup directory...' }); // Create backup directory await fs.promises.mkdir(backupDir, { recursive: true }); // Get all files to backup const files = await getAllFiles(aiosCoreDir); const manifest = { version: '2.0', created: new Date().toISOString(), projectRoot, backupDir, files: [], totalSize: 0, totalFiles: files.length, checksums: {}, }; onProgress({ phase: 'copying', message: `Backing up ${files.length} files...`, total: files.length }); // Copy each file for (let i = 0; i < files.length; i++) { const srcFile = files[i]; const relativePath = path.relative(aiosCoreDir, srcFile); const destFile = path.join(backupDir, '.aios-core', relativePath); const result = await copyFileWithMetadata(srcFile, destFile); manifest.files.push({ relativePath, size: result.size, mode: result.mode, checksum: result.checksum, }); manifest.checksums[relativePath] = result.checksum; manifest.totalSize += result.size; if (verbose) { onProgress({ phase: 'file', message: ` → ${relativePath}`, current: i + 1, total: files.length }); } } // Backup config files if they exist const configFiles = [ 'aios.config.js', 'aios.config.json', '.aios/config.yaml', '.mcp.json', ]; for (const configFile of configFiles) { const srcPath = path.join(projectRoot, configFile); if (fs.existsSync(srcPath)) { const destPath = path.join(backupDir, configFile); const result = await copyFileWithMetadata(srcPath, destPath); manifest.files.push({ relativePath: configFile, size: result.size, mode: result.mode, checksum: result.checksum, isConfig: true, }); manifest.checksums[configFile] = result.checksum; manifest.totalSize += result.size; } } onProgress({ phase: 'manifest', message: 'Creating backup manifest...' }); // Write manifest const manifestPath = path.join(backupDir, 'backup-manifest.json'); await fs.promises.writeFile(manifestPath, JSON.stringify(manifest, null, 2)); onProgress({ phase: 'complete', message: 'Backup complete!' }); return { success: true, backupDir, backupDirName, manifest, }; } /** * Verify backup integrity using checksums * @param {string} backupDir - Backup directory path * @returns {Promise<Object>} Verification result */ async function verifyBackup(backupDir) { const manifestPath = path.join(backupDir, 'backup-manifest.json'); if (!fs.existsSync(manifestPath)) { throw new Error('Backup manifest not found. Is this a valid AIOS backup?'); } const manifest = JSON.parse(await fs.promises.readFile(manifestPath, 'utf8')); const results = { valid: true, totalFiles: manifest.files.length, verified: 0, failed: [], missing: [], }; for (const file of manifest.files) { const filePath = file.isConfig ? path.join(backupDir, file.relativePath) : path.join(backupDir, '.aios-core', file.relativePath); if (!fs.existsSync(filePath)) { results.missing.push(file.relativePath); results.valid = false; continue; } const checksum = await calculateChecksum(filePath); if (checksum !== file.checksum) { results.failed.push({ file: file.relativePath, expected: file.checksum, actual: checksum, }); results.valid = false; } else { results.verified++; } } return results; } /** * Find the most recent backup directory * @param {string} projectRoot - Project root directory * @returns {Promise<Object|null>} Backup info or null */ async function findLatestBackup(projectRoot) { const entries = await fs.promises.readdir(projectRoot, { withFileTypes: true }); const backups = entries .filter(entry => entry.isDirectory() && entry.name.startsWith('.aios-backup-')) .map(entry => ({ name: entry.name, path: path.join(projectRoot, entry.name), date: entry.name.replace('.aios-backup-', ''), })) .sort((a, b) => b.date.localeCompare(a.date)); if (backups.length === 0) { return null; } const latestBackup = backups[0]; const manifestPath = path.join(latestBackup.path, 'backup-manifest.json'); if (fs.existsSync(manifestPath)) { const manifest = JSON.parse(await fs.promises.readFile(manifestPath, 'utf8')); return { ...latestBackup, manifest, }; } return latestBackup; } /** * List all available backups * @param {string} projectRoot - Project root directory * @returns {Promise<Object[]>} Array of backup info */ async function listBackups(projectRoot) { const entries = await fs.promises.readdir(projectRoot, { withFileTypes: true }); const backups = []; for (const entry of entries) { if (entry.isDirectory() && entry.name.startsWith('.aios-backup-')) { const backupPath = path.join(projectRoot, entry.name); const manifestPath = path.join(backupPath, 'backup-manifest.json'); const backup = { name: entry.name, path: backupPath, date: entry.name.replace('.aios-backup-', ''), hasManifest: false, fileCount: 0, totalSize: 0, }; if (fs.existsSync(manifestPath)) { const manifest = JSON.parse(await fs.promises.readFile(manifestPath, 'utf8')); backup.hasManifest = true; backup.fileCount = manifest.totalFiles; backup.totalSize = manifest.totalSize; backup.created = manifest.created; } backups.push(backup); } } return backups.sort((a, b) => b.date.localeCompare(a.date)); } module.exports = { createBackupDirName, calculateChecksum, getFileStats, copyFileWithMetadata, getAllFiles, createBackup, verifyBackup, findLatestBackup, listBackups, };