aiwg
Version:
Cognitive architecture for AI-augmented software development with structured memory, ensemble validation, and closed-loop correction. FAIR-aligned artifacts, 84% cost reduction via human-in-the-loop, standards adopted by 100+ organizations.
1,070 lines (898 loc) • 31.7 kB
JavaScript
/**
* Migration Tool for Framework-Scoped Workspace Management
*
* Safely migrates legacy .aiwg/ structure to framework-scoped organization.
* Implements ADR-006 reset + redeploy pattern with comprehensive safety checks.
*
* @module tools/workspace/migration-tool
*/
import fs from 'fs/promises';
import path from 'path';
import crypto from 'crypto';
import { glob } from 'glob';
/**
* Migration validation error
*/
class MigrationValidationError extends Error {
constructor(failedChecks) {
const messages = failedChecks.map(([name, result]) =>
`${name}: ${result.error || 'FAILED'}`
).join('\n ');
super(`Migration validation failed:\n ${messages}`);
this.name = 'MigrationValidationError';
this.failedChecks = failedChecks;
}
}
/**
* Migration execution error
*/
class MigrationError extends Error {
constructor(message) {
super(message);
this.name = 'MigrationError';
}
}
/**
* Rollback error
*/
class RollbackError extends Error {
constructor(message) {
super(message);
this.name = 'RollbackError';
}
}
/**
* MigrationTool - Safely migrate legacy .aiwg/ to framework-scoped structure
*
* Migration Mapping:
* - .aiwg/intake/ → .aiwg/frameworks/sdlc-complete/projects/{project-id}/intake/
* - .aiwg/requirements/ → .aiwg/frameworks/sdlc-complete/projects/{project-id}/requirements/
* - .aiwg/architecture/ → .aiwg/frameworks/sdlc-complete/projects/{project-id}/architecture/
* - .aiwg/planning/ → .aiwg/frameworks/sdlc-complete/projects/{project-id}/planning/
* - .aiwg/testing/ → .aiwg/frameworks/sdlc-complete/projects/{project-id}/testing/
* - .aiwg/security/ → .aiwg/frameworks/sdlc-complete/projects/{project-id}/security/
* - .aiwg/deployment/ → .aiwg/frameworks/sdlc-complete/projects/{project-id}/deployment/
* - .aiwg/risks/ → .aiwg/frameworks/sdlc-complete/projects/{project-id}/risks/
* - .aiwg/gates/ → .aiwg/frameworks/sdlc-complete/projects/{project-id}/gates/
* - .aiwg/reports/ → .aiwg/frameworks/sdlc-complete/projects/{project-id}/reports/
* - .aiwg/team/ → .aiwg/frameworks/sdlc-complete/projects/{project-id}/team/
* - .aiwg/working/ → .aiwg/frameworks/sdlc-complete/working/
*
* Safety Features:
* - Pre-migration validation (disk space, permissions, conflicts)
* - Full backup before migration
* - Checksum validation (no data loss)
* - Internal reference updates (paths in markdown)
* - Rollback support (<5s target)
* - Dry-run mode (preview changes)
* - Incremental migration for large projects (>1GB)
*/
export class MigrationTool {
/**
* Create migration tool
* @param {string} basePath - Base path for .aiwg/ directory (default: '.aiwg')
*/
constructor(basePath = '.aiwg') {
this.basePath = basePath;
this.migrationLockFile = path.join(basePath, '.migration-lock');
// Legacy directory mapping (project-level artifacts)
this.projectDirMapping = {
'intake': 'intake',
'requirements': 'requirements',
'architecture': 'architecture',
'planning': 'planning',
'testing': 'testing',
'security': 'security',
'deployment': 'deployment',
'risks': 'risks',
'gates': 'gates',
'reports': 'reports',
'team': 'team',
'quality': 'quality',
'handoffs': 'handoffs',
'decisions': 'decisions'
};
// Working area (shared across projects)
this.workingDir = 'working';
}
// ==================== PRE-MIGRATION VALIDATION ====================
/**
* Validate migration prerequisites
* @returns {Promise<{passed: boolean, checks: Object}>}
* @throws {MigrationValidationError} If validation fails
*/
async validate() {
const checks = {
diskSpace: await this.checkDiskSpace(),
permissions: await this.checkPermissions(),
conflicts: await this.detectConflicts(),
lock: await this.checkMigrationLock()
};
const failed = Object.entries(checks).filter(([, result]) => !result.passed);
if (failed.length > 0) {
throw new MigrationValidationError(failed);
}
return { passed: true, checks };
}
/**
* Check disk space (need 2x current .aiwg/ size for backup)
* @returns {Promise<{passed: boolean, available: number, required: number}>}
*/
async checkDiskSpace() {
try {
const aiwgSize = await this._getDirectorySize(this.basePath);
const requiredSpace = aiwgSize * 2; // Need 2x for backup
// Get available disk space (platform-specific)
const { execSync } = await import('child_process');
let availableBytes;
if (process.platform === 'win32') {
// Windows: use fsutil (returns bytes free)
const drive = path.parse(process.cwd()).root;
const output = execSync(`fsutil volume diskfree ${drive}`).toString();
const match = output.match(/Total # of free bytes\s+:\s+(\d+)/);
availableBytes = match ? parseInt(match[1], 10) : Infinity;
} else {
// Unix-like: use df (returns KB available)
const output = execSync(`df -k "${process.cwd()}" | tail -1`).toString();
const parts = output.split(/\s+/);
availableBytes = parseInt(parts[3], 10) * 1024; // Convert KB to bytes
}
return {
passed: availableBytes >= requiredSpace,
available: availableBytes,
required: requiredSpace,
error: availableBytes < requiredSpace
? `Insufficient disk space. Need ${this._formatBytes(requiredSpace)}, have ${this._formatBytes(availableBytes)}`
: null
};
} catch (error) {
// If we can't determine disk space, warn but don't fail
console.warn('⚠️ Could not determine disk space:', error.message);
return { passed: true, available: Infinity, required: 0, warning: error.message };
}
}
/**
* Check write permissions
* @returns {Promise<{passed: boolean}>}
*/
async checkPermissions() {
try {
// Test write access to .aiwg/
const testFile = path.join(this.basePath, '.migration-test');
await fs.writeFile(testFile, 'test');
await fs.unlink(testFile);
// Test ability to create frameworks directory
const frameworksDir = path.join(this.basePath, 'frameworks');
await fs.mkdir(frameworksDir, { recursive: true });
return { passed: true };
} catch (error) {
return {
passed: false,
error: `No write permission to ${this.basePath}: ${error.message}`
};
}
}
/**
* Detect conflicts (framework structure already exists)
* @returns {Promise<{passed: boolean, conflicts: string[]}>}
*/
async detectConflicts() {
const conflicts = [];
const frameworksDir = path.join(this.basePath, 'frameworks');
try {
// Check if frameworks directory already has project files
const sdlcProjectsDir = path.join(frameworksDir, 'sdlc-complete', 'projects');
if (await this._exists(sdlcProjectsDir)) {
const entries = await fs.readdir(sdlcProjectsDir);
if (entries.length > 0) {
conflicts.push(`Framework projects directory not empty: ${sdlcProjectsDir}`);
}
}
return {
passed: conflicts.length === 0,
conflicts,
error: conflicts.length > 0
? `Migration target already exists. Run with --force to overwrite.`
: null
};
} catch (error) {
// If frameworks directory doesn't exist, that's fine
return { passed: true, conflicts: [] };
}
}
/**
* Check for migration lock (prevent concurrent migrations)
* @returns {Promise<{passed: boolean}>}
*/
async checkMigrationLock() {
const lockExists = await this._exists(this.migrationLockFile);
if (lockExists) {
try {
const lockContent = await fs.readFile(this.migrationLockFile, 'utf8');
const lockData = JSON.parse(lockContent);
return {
passed: false,
error: `Migration already in progress (started ${lockData.timestamp})`
};
} catch {
// Corrupted lock file, can proceed
await fs.unlink(this.migrationLockFile).catch(() => {});
return { passed: true };
}
}
return { passed: true };
}
// ==================== MIGRATION OPERATIONS ====================
/**
* Execute full migration
* @param {Object} options - Migration options
* @param {string} options.projectId - Target project ID (default: derived from package.json or 'default-project')
* @param {string} options.frameworkId - Target framework (default: 'sdlc-complete')
* @param {boolean} options.dryRun - Preview only, don't execute (default: false)
* @param {boolean} options.force - Overwrite existing framework structure (default: false)
* @param {boolean} options.skipBackup - Skip backup creation (NOT RECOMMENDED, default: false)
* @returns {Promise<{success: boolean, backupPath?: string, migratedFiles: number}>}
*/
async migrate(options = {}) {
const {
projectId = await this._deriveProjectId(),
frameworkId = 'sdlc-complete',
dryRun = false,
force = false,
skipBackup = false
} = options;
// Dry-run mode
if (dryRun) {
return await this.dryRun(projectId, frameworkId);
}
// Create migration lock
await this._createLock();
try {
// Pre-migration validation
if (!force) {
await this.validate();
}
// Analyze existing structure
const analysis = await this.analyzeExistingStructure();
// Check if incremental migration needed (>1GB)
const useIncremental = analysis.totalSize > 1024 * 1024 * 1024;
// Create backup
let backupPath = null;
if (!skipBackup) {
console.log('Creating backup...');
backupPath = await this.createBackup();
console.log(`✓ Backup created: ${backupPath}`);
}
// Execute migration
let migratedFiles;
if (useIncremental) {
console.log('⚠️ Large project detected. Using incremental migration...');
migratedFiles = await this._migrateIncrementally(projectId, frameworkId, analysis);
} else {
migratedFiles = await this._executeMigration(projectId, frameworkId, analysis);
}
// Update internal references
console.log('Updating internal references...');
await this.updateInternalReferences(projectId, frameworkId);
// Validate migration
console.log('Validating migration...');
const validation = await this.verifyMigration(backupPath, projectId, frameworkId);
if (!validation.passed) {
throw new MigrationError('Migration validation failed. Rolling back...');
}
// Remove migration lock
await this._removeLock();
console.log(`✓ Migration complete: ${migratedFiles} files migrated`);
return {
success: true,
backupPath,
migratedFiles,
projectId,
frameworkId
};
} catch (error) {
console.error('✗ Migration failed:', error.message);
// Automatic rollback on failure
if (!skipBackup) {
console.log('Attempting automatic rollback...');
try {
await this.rollback();
console.log('✓ Rollback successful');
} catch (rollbackError) {
console.error('✗ Rollback failed:', rollbackError.message);
console.error('⚠️ System may be in inconsistent state. Manual recovery required.');
}
}
// Remove lock
await this._removeLock();
throw error;
}
}
/**
* Preview migration without executing
* @param {string} projectId - Target project ID
* @param {string} frameworkId - Target framework ID
* @returns {Promise<{dryRun: true, actions: Array, totalFiles: number, estimatedTime: string}>}
*/
async dryRun(projectId, frameworkId) {
const analysis = await this.analyzeExistingStructure();
const actions = [];
// Detect legacy directories
for (const [legacyDir, targetSubdir] of Object.entries(this.projectDirMapping)) {
const source = path.join(this.basePath, legacyDir);
if (await this._exists(source)) {
const target = path.join(
this.basePath,
'frameworks',
frameworkId,
'projects',
projectId,
targetSubdir
);
const fileCount = await this._countFiles(source);
const size = await this._getDirectorySize(source);
actions.push({
action: 'move',
source,
target,
fileCount,
size: this._formatBytes(size)
});
}
}
// Working directory
const workingSource = path.join(this.basePath, this.workingDir);
if (await this._exists(workingSource)) {
const workingTarget = path.join(
this.basePath,
'frameworks',
frameworkId,
'working'
);
const fileCount = await this._countFiles(workingSource);
const size = await this._getDirectorySize(workingSource);
actions.push({
action: 'move',
source: workingSource,
target: workingTarget,
fileCount,
size: this._formatBytes(size)
});
}
// Estimate reference updates
const mdFiles = await glob(`${this.basePath}/**/*.md`);
actions.push({
action: 'update-references',
fileCount: mdFiles.length,
estimatedReferences: mdFiles.length * 3 // Rough estimate
});
const totalFiles = actions.reduce((sum, a) => sum + (a.fileCount || 0), 0);
const estimatedTime = totalFiles > 100 ? '10-15 seconds' : '5-10 seconds';
return {
dryRun: true,
actions,
totalFiles,
estimatedTime,
projectId,
frameworkId
};
}
// ==================== BACKUP/RESTORE ====================
/**
* Create full backup of .aiwg/
* @returns {Promise<string>} Backup directory path
*/
async createBackup() {
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
const backupPath = `${this.basePath}.backup.${timestamp}`;
// Copy entire .aiwg/ to backup
await this._copyDirectory(this.basePath, backupPath);
// Create manifest
const manifest = {
timestamp,
sourcePath: this.basePath,
backupPath,
checksum: await this.computeChecksum(this.basePath),
fileCount: await this._countFiles(this.basePath),
totalSize: await this._getDirectorySize(this.basePath)
};
await fs.writeFile(
path.join(backupPath, 'migration-manifest.json'),
JSON.stringify(manifest, null, 2)
);
return backupPath;
}
/**
* Rollback to most recent backup
* @returns {Promise<{restoredFrom: string, checksum: string, timestamp: string}>}
* @throws {RollbackError} If rollback fails
*/
async rollback() {
const backups = await this.listBackups();
if (backups.length === 0) {
throw new RollbackError('No backups found. Cannot rollback.');
}
const latestBackup = backups[0];
// Verify backup integrity
const manifestPath = path.join(latestBackup, 'migration-manifest.json');
const manifest = JSON.parse(await fs.readFile(manifestPath, 'utf8'));
const currentChecksum = await this.computeChecksum(latestBackup);
if (currentChecksum !== manifest.checksum) {
throw new RollbackError('Backup corrupted. Checksum mismatch.');
}
// Remove migration manifest from backup (not part of original structure)
await fs.unlink(manifestPath).catch(() => {});
// Remove current .aiwg/
await fs.rm(this.basePath, { recursive: true, force: true });
// Restore from backup
await this._copyDirectory(latestBackup, this.basePath);
console.log(`✓ Restored from ${latestBackup}`);
return {
restoredFrom: latestBackup,
checksum: currentChecksum,
timestamp: manifest.timestamp
};
}
/**
* List available backups (sorted newest first)
* @returns {Promise<string[]>} Array of backup directory paths
*/
async listBackups() {
const parentDir = path.dirname(this.basePath);
const baseName = path.basename(this.basePath);
const pattern = `${baseName}.backup.*`;
try {
const entries = await fs.readdir(parentDir);
const backups = entries
.filter(name => name.startsWith(`${baseName}.backup.`))
.map(name => path.join(parentDir, name))
.sort()
.reverse(); // Newest first
return backups;
} catch {
return [];
}
}
/**
* Clean old backups
* @param {number} olderThanDays - Delete backups older than N days
* @returns {Promise<number>} Number of backups deleted
*/
async cleanBackups(olderThanDays) {
const backups = await this.listBackups();
const cutoffDate = new Date(Date.now() - olderThanDays * 24 * 60 * 60 * 1000);
let deleted = 0;
for (const backupPath of backups) {
try {
const manifestPath = path.join(backupPath, 'migration-manifest.json');
const manifest = JSON.parse(await fs.readFile(manifestPath, 'utf8'));
const backupDate = new Date(manifest.timestamp);
if (backupDate < cutoffDate) {
await fs.rm(backupPath, { recursive: true, force: true });
deleted++;
console.log(`Deleted old backup: ${backupPath}`);
}
} catch {
// Skip if can't read manifest
continue;
}
}
return deleted;
}
// ==================== REFERENCE UPDATES ====================
/**
* Update internal references in markdown files
* @param {string} projectId - Project ID for path replacement
* @param {string} frameworkId - Framework ID for path replacement
* @returns {Promise<number>} Number of files updated
*/
async updateInternalReferences(projectId, frameworkId) {
const mdFiles = await glob(`${this.basePath}/**/*.md`);
const mapping = this._buildPathMapping(projectId, frameworkId);
let updatedCount = 0;
for (const file of mdFiles) {
const content = await fs.readFile(file, 'utf8');
const updated = this.replaceReferences(content, mapping);
if (updated !== content) {
await fs.writeFile(file, updated, 'utf8');
updatedCount++;
}
}
return updatedCount;
}
/**
* Find .aiwg/ path references in content
* @param {string} content - File content
* @returns {string[]} Array of found references
*/
findReferences(content) {
const regex = /\.aiwg\/(intake|requirements|architecture|planning|testing|security|deployment|risks|gates|reports|team|working|quality|handoffs|decisions)\//g;
const matches = [...content.matchAll(regex)];
return [...new Set(matches.map(m => m[0]))]; // Unique references
}
/**
* Replace references with new paths
* @param {string} content - File content
* @param {Object} mapping - Old path → new path mapping
* @returns {string} Updated content
*/
replaceReferences(content, mapping) {
let updated = content;
for (const [oldPath, newPath] of Object.entries(mapping)) {
// Escape special regex characters in path
const escapedOldPath = oldPath.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
const regex = new RegExp(escapedOldPath, 'g');
updated = updated.replace(regex, newPath);
}
return updated;
}
// ==================== VALIDATION ====================
/**
* Verify migration integrity
* @param {string} backupPath - Backup directory path
* @param {string} projectId - Project ID
* @param {string} frameworkId - Framework ID
* @returns {Promise<{passed: boolean, checks: Object}>}
*/
async verifyMigration(backupPath, projectId, frameworkId) {
const checks = {};
// Check all project directories migrated
for (const [legacyDir, targetSubdir] of Object.entries(this.projectDirMapping)) {
const legacyPath = path.join(this.basePath, legacyDir);
const newPath = path.join(
this.basePath,
'frameworks',
frameworkId,
'projects',
projectId,
targetSubdir
);
const legacyExists = await this._exists(legacyPath);
const newExists = await this._exists(newPath);
checks[legacyDir] = {
legacyRemoved: !legacyExists,
newCreated: newExists,
passed: !legacyExists && newExists
};
}
// Check working directory migrated
const workingLegacy = path.join(this.basePath, this.workingDir);
const workingNew = path.join(this.basePath, 'frameworks', frameworkId, 'working');
checks.working = {
legacyRemoved: !(await this._exists(workingLegacy)),
newCreated: await this._exists(workingNew),
passed: !(await this._exists(workingLegacy)) && (await this._exists(workingNew))
};
// Overall pass/fail
const allPassed = Object.values(checks).every(check => check.passed);
return {
passed: allPassed,
checks
};
}
/**
* Compute SHA256 checksum of directory
* @param {string} directory - Directory path
* @returns {Promise<string>} Hex checksum
*/
async computeChecksum(directory) {
const files = await glob(`${directory}/**/*`, { nodir: true });
const hash = crypto.createHash('sha256');
// Sort files for consistent checksum
files.sort();
for (const file of files) {
const content = await fs.readFile(file);
hash.update(content);
}
return hash.digest('hex');
}
/**
* Compare directory structures
* @param {string} source - Source directory
* @param {string} target - Target directory
* @returns {Promise<{fileCountMatch: boolean, sizeMatch: boolean}>}
*/
async compareDirStructures(source, target) {
const sourceCount = await this._countFiles(source);
const targetCount = await this._countFiles(target);
const sourceSize = await this._getDirectorySize(source);
const targetSize = await this._getDirectorySize(target);
return {
fileCountMatch: sourceCount === targetCount,
sizeMatch: Math.abs(sourceSize - targetSize) < 1024, // Allow 1KB tolerance
sourceCount,
targetCount,
sourceSize,
targetSize
};
}
// ==================== STATUS/REPORTING ====================
/**
* Get migration status
* @returns {Promise<string>} Status: 'pending' | 'in-progress' | 'completed' | 'failed'
*/
async getMigrationStatus() {
// Check if migration lock exists
if (await this._exists(this.migrationLockFile)) {
return 'in-progress';
}
// Check if framework structure exists
const frameworksDir = path.join(this.basePath, 'frameworks');
if (await this._exists(frameworksDir)) {
const entries = await fs.readdir(frameworksDir).catch(() => []);
if (entries.length > 0) {
return 'completed';
}
}
// Check if legacy structure exists
const legacyDirs = Object.keys(this.projectDirMapping);
const legacyExists = await Promise.all(
legacyDirs.map(dir => this._exists(path.join(this.basePath, dir)))
);
if (legacyExists.some(exists => exists)) {
return 'pending';
}
return 'unknown';
}
/**
* Get detailed migration report
* @returns {Promise<Object>} Migration statistics
*/
async getMigrationReport() {
const status = await this.getMigrationStatus();
const analysis = await this.analyzeExistingStructure();
const backups = await this.listBackups();
return {
status,
legacyStructure: analysis,
backupsAvailable: backups.length,
latestBackup: backups[0] || null,
frameworksInstalled: await this._listFrameworks()
};
}
/**
* Analyze existing structure
* @returns {Promise<Object>} Structure analysis
*/
async analyzeExistingStructure() {
const analysis = {
directories: {},
totalFiles: 0,
totalSize: 0
};
for (const dir of Object.keys(this.projectDirMapping)) {
const dirPath = path.join(this.basePath, dir);
if (await this._exists(dirPath)) {
const fileCount = await this._countFiles(dirPath);
const size = await this._getDirectorySize(dirPath);
analysis.directories[dir] = {
fileCount,
size,
sizeFormatted: this._formatBytes(size)
};
analysis.totalFiles += fileCount;
analysis.totalSize += size;
}
}
// Check working directory
const workingPath = path.join(this.basePath, this.workingDir);
if (await this._exists(workingPath)) {
const fileCount = await this._countFiles(workingPath);
const size = await this._getDirectorySize(workingPath);
analysis.directories[this.workingDir] = {
fileCount,
size,
sizeFormatted: this._formatBytes(size)
};
analysis.totalFiles += fileCount;
analysis.totalSize += size;
}
analysis.totalSizeFormatted = this._formatBytes(analysis.totalSize);
return analysis;
}
// ==================== INTERNAL HELPERS ====================
/**
* Execute migration (non-incremental)
* @private
*/
async _executeMigration(projectId, frameworkId, analysis) {
let migratedFiles = 0;
// Create framework structure
const frameworkBase = path.join(this.basePath, 'frameworks', frameworkId);
const projectBase = path.join(frameworkBase, 'projects', projectId);
await fs.mkdir(projectBase, { recursive: true });
// Migrate project directories
for (const [legacyDir, targetSubdir] of Object.entries(this.projectDirMapping)) {
const source = path.join(this.basePath, legacyDir);
if (await this._exists(source)) {
const target = path.join(projectBase, targetSubdir);
console.log(` Migrating ${legacyDir}/ → ${targetSubdir}/...`);
await this._moveDirectory(source, target);
migratedFiles += analysis.directories[legacyDir]?.fileCount || 0;
}
}
// Migrate working directory
const workingSource = path.join(this.basePath, this.workingDir);
if (await this._exists(workingSource)) {
const workingTarget = path.join(frameworkBase, 'working');
console.log(` Migrating ${this.workingDir}/ → frameworks/${frameworkId}/working/...`);
await this._moveDirectory(workingSource, workingTarget);
migratedFiles += analysis.directories[this.workingDir]?.fileCount || 0;
}
return migratedFiles;
}
/**
* Execute incremental migration (for large projects >1GB)
* @private
*/
async _migrateIncrementally(projectId, frameworkId, analysis, batchSize = 50) {
let migratedFiles = 0;
// Group directories into batches
const allDirs = Object.keys(this.projectDirMapping);
const batches = [];
for (let i = 0; i < allDirs.length; i += batchSize) {
batches.push(allDirs.slice(i, i + batchSize));
}
// Migrate in batches
for (let i = 0; i < batches.length; i++) {
console.log(` Batch ${i + 1}/${batches.length}...`);
const batchAnalysis = {
directories: {}
};
for (const dir of batches[i]) {
if (analysis.directories[dir]) {
batchAnalysis.directories[dir] = analysis.directories[dir];
}
}
const batchFiles = await this._executeMigration(projectId, frameworkId, batchAnalysis);
migratedFiles += batchFiles;
}
return migratedFiles;
}
/**
* Build path mapping for reference updates
* @private
*/
_buildPathMapping(projectId, frameworkId) {
const mapping = {};
for (const [legacyDir, targetSubdir] of Object.entries(this.projectDirMapping)) {
const oldPath = `.aiwg/${legacyDir}/`;
const newPath = `.aiwg/frameworks/${frameworkId}/projects/${projectId}/${targetSubdir}/`;
mapping[oldPath] = newPath;
}
// Working directory
mapping[`.aiwg/${this.workingDir}/`] = `.aiwg/frameworks/${frameworkId}/working/`;
return mapping;
}
/**
* Derive project ID from package.json or use default
* @private
*/
async _deriveProjectId() {
try {
const packagePath = path.join(process.cwd(), 'package.json');
const packageJson = JSON.parse(await fs.readFile(packagePath, 'utf8'));
return packageJson.name || 'default-project';
} catch {
return 'default-project';
}
}
/**
* Create migration lock
* @private
*/
async _createLock() {
const lockData = {
timestamp: new Date().toISOString(),
pid: process.pid
};
await fs.mkdir(path.dirname(this.migrationLockFile), { recursive: true });
await fs.writeFile(this.migrationLockFile, JSON.stringify(lockData, null, 2));
}
/**
* Remove migration lock
* @private
*/
async _removeLock() {
await fs.unlink(this.migrationLockFile).catch(() => {});
}
/**
* List installed frameworks
* @private
*/
async _listFrameworks() {
const frameworksDir = path.join(this.basePath, 'frameworks');
if (!(await this._exists(frameworksDir))) {
return [];
}
const entries = await fs.readdir(frameworksDir);
return entries.filter(async name => {
const stat = await fs.stat(path.join(frameworksDir, name));
return stat.isDirectory();
});
}
/**
* Copy directory recursively
* @private
*/
async _copyDirectory(source, target) {
await fs.mkdir(target, { recursive: true });
const entries = await fs.readdir(source, { withFileTypes: true });
for (const entry of entries) {
const sourcePath = path.join(source, entry.name);
const targetPath = path.join(target, entry.name);
if (entry.isDirectory()) {
await this._copyDirectory(sourcePath, targetPath);
} else {
await fs.copyFile(sourcePath, targetPath);
}
}
}
/**
* Move directory (atomic rename where possible)
* @private
*/
async _moveDirectory(source, target) {
await fs.mkdir(path.dirname(target), { recursive: true });
try {
// Try atomic rename first (fast)
await fs.rename(source, target);
} catch {
// Fallback: copy then delete (slower but works across filesystems)
await this._copyDirectory(source, target);
await fs.rm(source, { recursive: true, force: true });
}
}
/**
* Check if path exists
* @private
*/
async _exists(filePath) {
try {
await fs.access(filePath);
return true;
} catch {
return false;
}
}
/**
* Count files in directory
* @private
*/
async _countFiles(directory) {
if (!(await this._exists(directory))) {
return 0;
}
const files = await glob(`${directory}/**/*`, { nodir: true });
return files.length;
}
/**
* Get directory size in bytes
* @private
*/
async _getDirectorySize(directory) {
if (!(await this._exists(directory))) {
return 0;
}
const files = await glob(`${directory}/**/*`, { nodir: true });
let totalSize = 0;
for (const file of files) {
const stat = await fs.stat(file);
totalSize += stat.size;
}
return totalSize;
}
/**
* Format bytes to human-readable string
* @private
*/
_formatBytes(bytes) {
if (bytes === 0) return '0 B';
const k = 1024;
const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
}
}
// Export error classes
export { MigrationValidationError, MigrationError, RollbackError };