aiwg
Version:
Deployment tool and support utility for AI context. Copies agents, skills, commands, rules, and behaviors into the paths each AI platform reads (Claude Code, Codex, Copilot, Cursor, Warp, OpenClaw, and 6 more) so one source of truth works across 10 platfo
371 lines • 14.7 kB
JavaScript
/**
* FrameworkMigration - Migrate workspaces to framework-scoped structure
*
* Handles migration from legacy workspaces to framework-scoped structure,
* multi-framework setup, and duplicate resource merging.
*
* FID-007 Framework-Scoped Workspaces migration scenarios.
*
* @module src/plugin/framework-migration
* @version 1.0.0
* @since 2025-10-23
*/
import * as fs from 'fs/promises';
import * as path from 'path';
import * as crypto from 'crypto';
// ===========================
// FrameworkMigration Class
// ===========================
export class FrameworkMigration {
projectRoot;
constructor(projectRoot) {
this.projectRoot = path.resolve(projectRoot);
}
/**
* Migrate legacy workspace to framework-scoped structure
*
* @param options - Migration options
* @returns Migration result
*/
async migrateLegacyToScoped(options = {}) {
const migrationId = this.generateMigrationId();
const errors = [];
const conflicts = [];
let backupPath;
try {
// Check if already framework-scoped
const { FrameworkDetector } = await import('./framework-detector.js');
const detector = new FrameworkDetector(this.projectRoot);
const isLegacy = await detector.isLegacyWorkspace();
if (!isLegacy) {
return {
id: migrationId,
success: true,
skipped: true,
reason: 'Workspace is already framework-scoped',
validation: {},
report: { filesMoved: 0, frameworkSpecificCount: 0, sharedResourceCount: 0 },
errors: [],
conflicts: []
};
}
// Detect target framework
let targetFramework = options.defaultFramework || 'claude';
if (!options.defaultFramework) {
targetFramework = await this.detectTargetFramework();
}
// Create backup if requested
if (options.backup && !options.dryRun) {
backupPath = await this.createBackup(migrationId);
}
// Categorize resources
const { FrameworkIsolator } = await import('./framework-isolator.js');
const isolator = new FrameworkIsolator(this.projectRoot);
const sourcePath = options.sourcePath || path.join(this.projectRoot, '.aiwg');
const categorized = await isolator.categorizeResources(sourcePath);
// Dry-run: just report what would happen
if (options.dryRun) {
return {
id: migrationId,
success: true,
validation: { safe: true, warnings: [] },
report: {
filesMoved: 0,
frameworkSpecificCount: categorized.frameworkSpecific.length,
sharedResourceCount: categorized.shared.length
},
errors: [],
conflicts: [],
plan: {
frameworkSpecificMoves: categorized.frameworkSpecific.length,
sharedMoves: categorized.shared.length
}
};
}
// Perform actual migration
let filesMoved = 0;
// Always create the target framework directory
const targetFrameworkPath = path.join(this.projectRoot, '.aiwg', targetFramework);
await fs.mkdir(targetFrameworkPath, { recursive: true });
// Move framework-specific resources
for (const resource of categorized.frameworkSpecific) {
const sourcePath = path.join(this.projectRoot, '.aiwg', resource);
const targetPath = path.join(this.projectRoot, '.aiwg', targetFramework, resource);
try {
await this.moveResource(sourcePath, targetPath, options);
filesMoved++;
}
catch (error) {
errors.push({
path: resource,
error: error.message,
severity: 'error',
context: { source: sourcePath, target: targetPath }
});
}
}
// Move shared resources
for (const resource of categorized.shared) {
const sourcePath = path.join(this.projectRoot, '.aiwg', resource);
const targetPath = path.join(this.projectRoot, '.aiwg', 'shared', resource);
// Check for conflicts
try {
await fs.access(targetPath);
conflicts.push({
type: 'file',
path: resource,
resolution: options.conflictStrategy || 'skip'
});
if (options.conflictStrategy === 'skip') {
continue;
}
}
catch {
// No conflict
}
try {
await this.moveResource(sourcePath, targetPath, options);
filesMoved++;
}
catch (error) {
errors.push({
path: resource,
error: error.message,
severity: 'error'
});
}
}
return {
id: migrationId,
success: errors.filter(e => e.severity === 'critical').length === 0,
backupPath,
validation: {
frameworkSpecificMoved: categorized.frameworkSpecific.length > 0,
sharedResourcesMoved: categorized.shared.length > 0,
legacyCleanedUp: true
},
report: {
filesMoved,
frameworkSpecificCount: categorized.frameworkSpecific.length,
sharedResourceCount: categorized.shared.length
},
errors,
conflicts,
suggestions: errors.length > 0 ? ['Check error details and retry with backup enabled'] : []
};
}
catch (error) {
return {
id: migrationId,
success: false,
validation: {},
report: { filesMoved: 0, frameworkSpecificCount: 0, sharedResourceCount: 0 },
errors: [{
path: this.projectRoot,
error: error.message,
severity: 'critical'
}],
conflicts: [],
suggestions: ['Ensure .aiwg directory exists and is readable']
};
}
}
/**
* Detect target framework for migration
*
* @returns Framework name
*/
async detectTargetFramework() {
const { FrameworkDetector } = await import('./framework-detector.js');
const detector = new FrameworkDetector(this.projectRoot);
const frameworks = await detector.detectFrameworks();
return frameworks.length > 0 ? frameworks[0] : 'claude';
}
/**
* Migrate to multi-framework setup
*
* @param newFrameworks - Frameworks to add
*/
async migrateToMultiFramework(newFrameworks) {
const { WorkspaceCreator } = await import('./workspace-creator.js');
const creator = new WorkspaceCreator(this.projectRoot);
for (const framework of newFrameworks) {
await creator.addFrameworkToProject(framework);
}
}
/**
* Detect duplicate shared content across frameworks
*
* @returns Array of duplicates
*/
async detectDuplicateShared() {
const duplicates = [];
const frameworks = ['claude', 'codex', 'cursor'];
const sharedDirs = ['requirements', 'architecture', 'testing'];
for (const dir of sharedDirs) {
const fileMap = new Map();
for (const framework of frameworks) {
const frameworkDir = path.join(this.projectRoot, '.aiwg', framework, dir);
try {
const files = await this.listFilesRecursive(frameworkDir);
for (const file of files) {
const existing = fileMap.get(file) || [];
existing.push(framework);
fileMap.set(file, existing);
}
}
catch {
// Framework dir doesn't exist
}
}
// Find files present in multiple frameworks
for (const [file, foundInFrameworks] of fileMap.entries()) {
if (foundInFrameworks.length > 1) {
duplicates.push({
path: path.join(dir, file),
frameworks: foundInFrameworks
});
}
}
}
return duplicates;
}
/**
* Merge duplicate shared resources
*
* @param options - Merge options
* @returns Merge result
*/
async mergeDuplicateShared(options = {}) {
const duplicates = await this.detectDuplicateShared();
const conflicts = [];
let mergedCount = 0;
let removedCount = 0;
for (const duplicate of duplicates) {
const sharedPath = path.join(this.projectRoot, '.aiwg', 'shared', duplicate.path);
// Pick source (first framework, or newest if conflict strategy is keep-newest)
let sourcePath = path.join(this.projectRoot, '.aiwg', duplicate.frameworks[0], duplicate.path);
if (options.conflictStrategy === 'keep-newest') {
// Find newest version
let newestTime = 0;
for (const framework of duplicate.frameworks) {
const frameworkPath = path.join(this.projectRoot, '.aiwg', framework, duplicate.path);
try {
const stats = await fs.stat(frameworkPath);
if (stats.mtimeMs > newestTime) {
newestTime = stats.mtimeMs;
sourcePath = frameworkPath;
}
}
catch {
// File doesn't exist
}
}
conflicts.push({
path: duplicate.path,
resolution: 'keep-newest'
});
}
// Copy to shared
try {
await fs.mkdir(path.dirname(sharedPath), { recursive: true });
await fs.copyFile(sourcePath, sharedPath);
mergedCount++;
// Remove from framework-specific dirs
for (const framework of duplicate.frameworks) {
const frameworkPath = path.join(this.projectRoot, '.aiwg', framework, duplicate.path);
try {
await fs.unlink(frameworkPath);
removedCount++;
}
catch {
// Already removed or doesn't exist
}
}
}
catch {
// Merge failed
}
}
return {
conflicts,
report: {
duplicatesFound: duplicates.length,
mergedCount,
removedCount
}
};
}
/**
* Rollback migration
*
* @param migrationId - Migration ID
*/
async rollback(migrationId) {
const backupPath = path.join(this.projectRoot, '.aiwg', 'backups', migrationId);
try {
await fs.access(backupPath);
}
catch {
throw new Error(`Backup not found for migration: ${migrationId}`);
}
// Restore from backup
const files = await this.listFilesRecursive(backupPath);
for (const file of files) {
const sourcePath = path.join(backupPath, file);
const targetPath = path.join(this.projectRoot, '.aiwg', file);
await fs.mkdir(path.dirname(targetPath), { recursive: true });
await fs.copyFile(sourcePath, targetPath);
}
}
// ===========================
// Helper Methods
// ===========================
async createBackup(migrationId) {
const backupPath = path.join(this.projectRoot, '.aiwg', 'backups', migrationId);
const sourcePath = path.join(this.projectRoot, '.aiwg');
await fs.mkdir(backupPath, { recursive: true });
const files = await this.listFilesRecursive(sourcePath);
for (const file of files) {
const source = path.join(sourcePath, file);
const target = path.join(backupPath, file);
await fs.mkdir(path.dirname(target), { recursive: true });
await fs.copyFile(source, target);
}
return backupPath;
}
async moveResource(sourcePath, targetPath, _options) {
await fs.mkdir(path.dirname(targetPath), { recursive: true });
await fs.copyFile(sourcePath, targetPath);
await fs.unlink(sourcePath);
}
async listFilesRecursive(dirPath) {
const files = [];
const walk = async (currentPath, relativePath = '') => {
try {
const entries = await fs.readdir(currentPath, { withFileTypes: true });
for (const entry of entries) {
const fullPath = path.join(currentPath, entry.name);
const relPath = path.join(relativePath, entry.name);
if (entry.isDirectory()) {
await walk(fullPath, relPath);
}
else {
files.push(relPath);
}
}
}
catch {
// Directory doesn't exist or permission denied
}
};
await walk(dirPath);
return files;
}
generateMigrationId() {
const timestamp = Date.now();
const random = crypto.randomBytes(4).toString('hex');
return `migration-${timestamp}-${random}`;
}
}
//# sourceMappingURL=framework-migration.js.map