arela
Version:
AI-powered CTO with multi-agent orchestration, code summarization, visual testing (web + mobile) for blazing fast development.
348 lines โข 13.7 kB
JavaScript
/**
* Main orchestrator for slice extraction
*/
import pc from "picocolors";
import { detectSlices } from "../detect/index.js";
import { FileMover } from "./file-mover.js";
import { ImportUpdater } from "./import-updater.js";
import { TestRunner } from "./test-runner.js";
import { GitManager } from "./git-manager.js";
export class SliceExtractor {
progress = {
currentSlice: 0,
totalSlices: 0,
currentStep: "planning",
status: "planning",
errors: [],
};
fileMover;
importUpdater;
testRunner;
gitManager;
constructor() {
this.fileMover = new FileMover();
this.importUpdater = new ImportUpdater();
this.testRunner = new TestRunner();
this.gitManager = new GitManager();
}
/**
* Main entry point - extract all slices
*/
async extractAllSlices(options = {}) {
const startTime = new Date();
const cwd = options.cwd || process.cwd();
try {
// Check for uncommitted changes
if (await this.gitManager.hasUncommittedChanges(cwd)) {
throw new Error("Uncommitted changes detected. Please commit or stash changes before extraction.");
}
// 1. Detect slices
this.log("๐ Detecting slices...");
const sliceReport = await this.detectSlices(cwd);
if (sliceReport.slices.length === 0) {
throw new Error("No slices detected. Unable to proceed with extraction.");
}
// 2. Validate slices
const validSlices = this.filterSlicesByQuality(sliceReport.slices, options.minCohesion || 70);
if (validSlices.length === 0) {
throw new Error(`No slices met quality threshold (${options.minCohesion || 70}% cohesion)`);
}
this.log(`โ
Found ${validSlices.length} slices with high quality\n`);
// 3. Plan extraction
this.log("๐ Creating extraction plan...");
const plan = await this.createExtractionPlan(validSlices, cwd);
this.logPlan(plan);
// 4. Dry run (if requested)
if (options.dryRun) {
this.log("\nโจ Dry run complete - no changes made\n");
return {
success: true,
extractedSlices: plan.slices.length,
movedFiles: plan.totalFiles,
updatedImports: plan.totalImports,
createdCommits: 0,
testsStatus: null,
errors: [],
startTime,
endTime: new Date(),
duration: Date.now() - startTime.getTime(),
};
}
// 5. Interactive mode (if requested)
if (options.interactive) {
const confirmed = await this.confirmExtraction(plan);
if (!confirmed) {
this.log("โ Extraction cancelled by user\n");
return {
success: false,
extractedSlices: 0,
movedFiles: 0,
updatedImports: 0,
createdCommits: 0,
testsStatus: null,
errors: ["Extraction cancelled by user"],
startTime,
endTime: new Date(),
duration: Date.now() - startTime.getTime(),
};
}
}
// 6. Execute extraction
this.log("\n๐ Starting extraction...\n");
this.progress.status = "moving";
const extractionResult = await this.executeExtraction(plan, cwd, options);
if (!extractionResult.success) {
// Rollback on failure
this.log("\nโ ๏ธ Extraction failed, rolling back...");
await this.fileMover.rollback();
await this.importUpdater.rollback();
await this.gitManager.resetToHead(cwd);
return {
success: false,
extractedSlices: 0,
movedFiles: 0,
updatedImports: 0,
createdCommits: 0,
testsStatus: null,
errors: extractionResult.errors,
startTime,
endTime: new Date(),
duration: Date.now() - startTime.getTime(),
};
}
// 7. Verify with tests (if not skipped)
let testResult = null;
if (!options.skipTests) {
this.log("\n๐งช Running tests...");
this.progress.status = "testing";
testResult = await this.testRunner.runTests(cwd);
if (!testResult.passed) {
this.log(`\nโ Tests failed! Rolling back...\n`);
await this.fileMover.rollback();
await this.importUpdater.rollback();
await this.gitManager.resetToHead(cwd);
return {
success: false,
extractedSlices: 0,
movedFiles: 0,
updatedImports: 0,
createdCommits: 0,
testsStatus: testResult,
errors: [
`${testResult.failedTests} tests failed`,
...testResult.failedTestNames,
],
startTime,
endTime: new Date(),
duration: Date.now() - startTime.getTime(),
};
}
this.log(`โ
All tests passed (${testResult.passedTests}/${testResult.totalTests})\n`);
}
// 8. Commit changes
this.log("๐ Creating commits...");
this.progress.status = "committing";
const commitCount = await this.commitSlices(plan.slices, cwd);
this.log(`โ
Created ${commitCount} commits\n`);
// 9. Summary
this.logSuccess(plan, testResult);
return {
success: true,
extractedSlices: plan.slices.length,
movedFiles: plan.totalFiles,
updatedImports: plan.totalImports,
createdCommits: commitCount,
testsStatus: testResult,
errors: [],
startTime,
endTime: new Date(),
duration: Date.now() - startTime.getTime(),
};
}
catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
this.progress.errors.push(errorMessage);
this.progress.status = "rolled-back";
return {
success: false,
extractedSlices: 0,
movedFiles: 0,
updatedImports: 0,
createdCommits: 0,
testsStatus: null,
errors: [errorMessage],
startTime,
endTime: new Date(),
duration: Date.now() - startTime.getTime(),
};
}
}
/**
* Detect slices using existing detection logic
*/
async detectSlices(cwd) {
try {
return await detectSlices(["."], cwd);
}
catch (error) {
throw new Error(`Failed to detect slices: ${error instanceof Error ? error.message : String(error)}`);
}
}
/**
* Filter slices by quality metrics
*/
filterSlicesByQuality(slices, minCohesion) {
return slices.filter(slice => slice.cohesion >= minCohesion);
}
/**
* Create extraction plan with file movements and import updates
*/
async createExtractionPlan(slices, cwd) {
const plans = [];
let totalFiles = 0;
let totalImports = 0;
const sharedFiles = [];
// Create individual plans for each slice
for (const slice of slices) {
const fileMover = new FileMover();
const importUpdater = new ImportUpdater();
const sourceFiles = await fileMover.planFileMovement(slice, cwd);
importUpdater.buildFileMapping([{ ...slice, sourceFiles, importUpdates: [], newImportCount: 0 }], cwd);
const importUpdates = await importUpdater.planImportUpdates([{ ...slice, sourceFiles, importUpdates: [], newImportCount: 0 }], cwd);
totalFiles += slice.fileCount;
totalImports += importUpdates.length;
plans.push({
...slice,
sourceFiles,
importUpdates,
newImportCount: importUpdates.length,
});
}
// Identify shared files (would be used by multiple slices)
// For now, we'll keep them as-is - future improvement
return {
slices: plans,
totalFiles,
totalImports,
estimatedTime: Math.ceil((totalFiles * 0.1) + (totalImports * 0.005)), // Very rough estimate
sharedFiles,
};
}
/**
* Execute the extraction
*/
async executeExtraction(plan, cwd, options) {
const errors = [];
try {
// 1. Move all files
this.log("๐ Moving files...");
const allMovements = plan.slices.flatMap(s => s.sourceFiles);
await this.fileMover.moveFiles(allMovements, options.dryRun || false);
this.log("โ
Files moved successfully");
// 2. Update all imports
this.log("๐ Updating imports...");
this.importUpdater.buildFileMapping(plan.slices, cwd);
const allImportUpdates = plan.slices.flatMap(s => s.importUpdates);
await this.importUpdater.updateImports(allImportUpdates, options.dryRun || false, cwd);
this.log(`โ
Updated ${allImportUpdates.length} imports`);
// 3. Clean up empty directories
await this.fileMover.cleanupEmptyDirs(cwd);
return { success: true, errors };
}
catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
errors.push(errorMessage);
return { success: false, errors };
}
}
/**
* Commit extracted slices
*/
async commitSlices(slices, cwd) {
try {
// Stage all changes at once (new files + deletions)
await this.gitManager.stageFiles([], cwd); // Empty array triggers git add -A
// Create a single commit for all slices
const sliceNames = slices.map(s => s.name).join(', ');
const totalFiles = slices.reduce((sum, s) => sum + s.fileCount, 0);
const commitMessage = [
`feat: extract ${slices.length} vertical slices`,
'',
`Slices: ${sliceNames}`,
`Files moved: ${totalFiles}`,
'',
'Generated by Arela v4.0.0'
].join('\n');
await this.gitManager.commitWithMessage(commitMessage, cwd);
this.log(` โ
Committed: ${slices.length} slices in single commit`);
return 1; // One commit for all slices
}
catch (error) {
this.log(` โ ๏ธ Failed to commit slices: ${error instanceof Error ? error.message : "Unknown error"}`);
return 0;
}
}
/**
* Confirm extraction with user
*/
async confirmExtraction(plan) {
// For now, always confirm in CLI
// In real implementation, would prompt user
return true;
}
/**
* Logging utilities
*/
log(message) {
console.log(message);
}
logPlan(plan) {
this.log("");
for (const slice of plan.slices) {
const emoji = this.getSliceEmoji(slice.name);
this.log(` ${emoji} ${slice.name} (${slice.fileCount} files, ${slice.newImportCount} imports)`);
}
this.log("");
this.log(` Total: ${plan.totalFiles} files, ${plan.totalImports} imports`);
this.log(` Estimated time: ~${plan.estimatedTime}s\n`);
}
logSuccess(plan, testResult) {
this.log(pc.bold(pc.green("\n๐ Extraction complete!")));
this.log(` - ${plan.slices.length} slices extracted`);
this.log(` - ${plan.totalFiles} files moved`);
this.log(` - ${plan.totalImports} imports updated`);
if (testResult) {
this.log(` - ${testResult.passedTests} tests passed`);
}
this.log(pc.yellow("\nYour architecture is now vertical! ๐ฏ\n"));
}
getSliceEmoji(name) {
const emojiMap = {
auth: "๐",
authentication: "๐",
user: "๐ค",
profile: "๐ค",
workout: "๐ช",
exercise: "๐ช",
nutrition: "๐",
social: "๐ฅ",
messaging: "๐ฌ",
notification: "๐",
payment: "๐ณ",
ui: "๐จ",
component: "๐จ",
util: "๐ง",
helper: "๐ง",
common: "๐ฆ",
shared: "๐ฆ",
};
const lower = name.toLowerCase();
for (const [key, emoji] of Object.entries(emojiMap)) {
if (lower.includes(key)) {
return emoji;
}
}
return "๐ฆ";
}
}
//# sourceMappingURL=slice-extractor.js.map