arela
Version:
AI-powered CTO with multi-agent orchestration, code summarization, visual testing (web + mobile) for blazing fast development.
160 lines • 5.32 kB
JavaScript
/**
* File movement operations for slice extraction
*/
import path from "node:path";
import fs from "fs-extra";
export class FileMover {
rollbackInfo = {
originalState: new Map(),
stagedChanges: [],
createdDirs: [],
};
/**
* Create extraction plan for files - determine where each file should go
*/
async planFileMovement(slice, cwd = process.cwd()) {
const movements = [];
const sliceDir = path.join(cwd, "features", this.slugify(slice.name));
for (const filePath of slice.files) {
const fullPath = path.join(cwd, filePath);
// Validate file exists before planning movement
if (!await fs.pathExists(fullPath)) {
console.warn(`⚠️ Skipping non-existent file: ${filePath}`);
continue;
}
// Preserve directory structure within the slice
// e.g., src/components/Button.tsx -> features/ui/components/Button.tsx
let relativePath = filePath;
// Remove src/ prefix if present
if (relativePath.startsWith("src/")) {
relativePath = relativePath.slice(4);
}
const destination = path.join(sliceDir, relativePath);
movements.push({
source: fullPath,
destination,
type: path.extname(filePath).slice(1) || "unknown",
});
}
return movements;
}
/**
* Move files to their target locations
*/
async moveFiles(movements, dryRun = false) {
// Group by destination directory to create them first
const dirSet = new Set();
for (const movement of movements) {
const dir = path.dirname(movement.destination);
dirSet.add(dir);
}
// Create all directories
for (const dir of dirSet) {
if (!dryRun) {
await fs.ensureDir(dir);
this.rollbackInfo.createdDirs.push(dir);
}
}
// Move all files
for (const movement of movements) {
if (dryRun) {
continue;
}
// Store original content for rollback
if (await fs.pathExists(movement.source)) {
const content = await fs.readFile(movement.source, "utf-8");
this.rollbackInfo.originalState.set(movement.source, content);
}
// Move the file
await fs.move(movement.source, movement.destination, {
overwrite: true,
});
this.rollbackInfo.stagedChanges.push(movement.destination);
}
}
/**
* Clean up empty directories after file movements
*/
async cleanupEmptyDirs(cwd = process.cwd()) {
const srcDir = path.join(cwd, "src");
if (!await fs.pathExists(srcDir)) {
return;
}
// Walk the directory and remove empty folders
await this.removeEmptyDirsRecursive(srcDir);
}
/**
* Recursively remove empty directories
*/
async removeEmptyDirsRecursive(dirPath) {
if (!await fs.pathExists(dirPath)) {
return true;
}
const entries = await fs.readdir(dirPath);
for (const entry of entries) {
const fullPath = path.join(dirPath, entry);
const stat = await fs.stat(fullPath);
if (stat.isDirectory()) {
await this.removeEmptyDirsRecursive(fullPath);
}
}
// Check if directory is now empty
const remaining = await fs.readdir(dirPath);
if (remaining.length === 0) {
await fs.remove(dirPath);
return true;
}
return false;
}
/**
* Rollback file movements
*/
async rollback() {
// Restore original files
for (const [originalPath, content] of this.rollbackInfo.originalState) {
if (await fs.pathExists(path.dirname(originalPath))) {
await fs.writeFile(originalPath, content);
}
}
// Remove moved files
for (const movedFile of this.rollbackInfo.stagedChanges) {
if (await fs.pathExists(movedFile)) {
await fs.remove(movedFile);
}
}
// Remove created directories (in reverse order)
for (const dir of this.rollbackInfo.createdDirs.reverse()) {
if (await fs.pathExists(dir)) {
try {
await fs.remove(dir);
}
catch {
// Directory may not be empty, which is fine
}
}
}
// Cleanup
this.rollbackInfo = {
originalState: new Map(),
stagedChanges: [],
createdDirs: [],
};
}
/**
* Convert slice name to directory slug
* e.g., "Authentication" -> "authentication"
*/
slugify(name) {
return name
.toLowerCase()
.replace(/\s+/g, "-")
.replace(/[^a-z0-9-]/g, "");
}
/**
* Get rollback info for inspection/testing
*/
getRollbackInfo() {
return this.rollbackInfo;
}
}
//# sourceMappingURL=file-mover.js.map