coderrr-cli
Version:
AI-powered coding agent that understands natural language requests and autonomously creates, modifies, and manages code across your projects
344 lines (291 loc) • 10.6 kB
JavaScript
const fs = require('fs');
const path = require('path');
const ui = require('./ui');
/**
* File Operations Module for Coderrr
*
* Provides safe file manipulation operations with automatic directory creation,
* path resolution, and comprehensive error handling. All operations are
* synchronous to ensure atomicity and predictable behavior.
*/
class FileOperations {
constructor(workingDir = process.cwd()) {
this.workingDir = workingDir;
}
/**
* Resolve absolute path from relative path
*/
resolvePath(filePath) {
return path.isAbsolute(filePath)
? filePath
: path.join(this.workingDir, filePath);
}
/**
* Ensure directory exists
*/
ensureDir(dirPath) {
if (!fs.existsSync(dirPath)) {
fs.mkdirSync(dirPath, { recursive: true });
}
}
/**
* Create a new file with the specified content
*
* @param {string} filePath - Relative or absolute path to the file to create
* @param {string} content - Content to write to the file
* @returns {Promise<Object>} Result object with success status and absolute path
* @throws {Error} If file already exists or write operation fails
*/
async createFile(filePath, content) {
try {
const absolutePath = this.resolvePath(filePath);
const dir = path.dirname(absolutePath);
// Check if file already exists
if (fs.existsSync(absolutePath)) {
throw new Error(`File already exists: ${filePath}`);
}
// Ensure directory exists
this.ensureDir(dir);
// Write file
fs.writeFileSync(absolutePath, content, 'utf8');
ui.displayFileOp('create_file', filePath, 'success');
return { success: true, path: absolutePath };
} catch (error) {
ui.displayFileOp('create_file', filePath, 'error');
throw error;
}
}
/**
* Update an existing file (replace entire content)
*/
async updateFile(filePath, content) {
try {
const absolutePath = this.resolvePath(filePath);
// Check if file exists
if (!fs.existsSync(absolutePath)) {
throw new Error(`File not found: ${filePath}`);
}
// Write file
fs.writeFileSync(absolutePath, content, 'utf8');
ui.displayFileOp('update_file', filePath, 'success');
return { success: true, path: absolutePath };
} catch (error) {
ui.displayFileOp('update_file', filePath, 'error');
throw error;
}
}
/**
* Patch a file (partial update)
*/
async patchFile(filePath, oldContent, newContent) {
try {
const absolutePath = this.resolvePath(filePath);
// Check if file exists
if (!fs.existsSync(absolutePath)) {
throw new Error(`File not found: ${filePath}`);
}
// Read current content
let content = fs.readFileSync(absolutePath, 'utf8');
// Replace old content with new content
if (!content.includes(oldContent)) {
throw new Error(`Pattern not found in file: ${filePath}`);
}
content = content.replace(oldContent, newContent);
// Write back
fs.writeFileSync(absolutePath, content, 'utf8');
ui.displayFileOp('patch_file', filePath, 'success');
return { success: true, path: absolutePath };
} catch (error) {
ui.displayFileOp('patch_file', filePath, 'error');
throw error;
}
}
/**
* Delete a file
*/
async deleteFile(filePath) {
try {
const absolutePath = this.resolvePath(filePath);
// Check if file exists
if (!fs.existsSync(absolutePath)) {
throw new Error(`File not found: ${filePath}`);
}
// Delete file
fs.unlinkSync(absolutePath);
ui.displayFileOp('delete_file', filePath, 'success');
return { success: true, path: absolutePath };
} catch (error) {
ui.displayFileOp('delete_file', filePath, 'error');
throw error;
}
}
/**
* Read a file
*/
async readFile(filePath) {
try {
const absolutePath = this.resolvePath(filePath);
// Check if file exists
if (!fs.existsSync(absolutePath)) {
throw new Error(`File not found: ${filePath}`);
}
// Read file
const content = fs.readFileSync(absolutePath, 'utf8');
ui.displayFileOp('read_file', filePath, 'success');
return { success: true, content, path: absolutePath };
} catch (error) {
ui.displayFileOp('read_file', filePath, 'error');
throw error;
}
}
/**
* Create a new directory
*
* @param {string} dirPath - Relative or absolute path to the directory to create
* @returns {Promise<Object>} Result object with success status and absolute path
* @throws {Error} If directory already exists or creation fails
*/
async createDir(dirPath) {
try {
const absolutePath = this.resolvePath(dirPath);
// Check if directory already exists
if (fs.existsSync(absolutePath)) {
throw new Error(`Directory already exists: ${dirPath}`);
}
// Create directory (recursive)
fs.mkdirSync(absolutePath, { recursive: true });
ui.displayFileOp('create_dir', dirPath, 'success');
return { success: true, path: absolutePath };
} catch (error) {
ui.displayFileOp('create_dir', dirPath, 'error');
throw error;
}
}
/**
* Delete an empty directory
*
* @param {string} dirPath - Relative or absolute path to the directory to delete
* @returns {Promise<Object>} Result object with success status and absolute path
* @throws {Error} If directory not found, not empty, or deletion fails
*/
async deleteDir(dirPath) {
try {
const absolutePath = this.resolvePath(dirPath);
// Check if directory exists
if (!fs.existsSync(absolutePath)) {
throw new Error(`Directory not found: ${dirPath}`);
}
// Check if it's actually a directory
if (!fs.statSync(absolutePath).isDirectory()) {
throw new Error(`Path is not a directory: ${dirPath}`);
}
// Check if directory is empty
const contents = fs.readdirSync(absolutePath);
if (contents.length > 0) {
throw new Error(`Directory not empty: ${dirPath}`);
}
// Delete directory
fs.rmdirSync(absolutePath);
ui.displayFileOp('delete_dir', dirPath, 'success');
return { success: true, path: absolutePath };
} catch (error) {
ui.displayFileOp('delete_dir', dirPath, 'error');
throw error;
}
}
/**
* List contents of a directory
*
* @param {string} dirPath - Relative or absolute path to the directory to list
* @returns {Promise<Object>} Result object with success status, absolute path, and contents array
* @throws {Error} If directory not found or listing fails
*/
async listDir(dirPath) {
try {
const absolutePath = this.resolvePath(dirPath);
// Check if directory exists
if (!fs.existsSync(absolutePath)) {
throw new Error(`Directory not found: ${dirPath}`);
}
// Check if it's actually a directory
if (!fs.statSync(absolutePath).isDirectory()) {
throw new Error(`Path is not a directory: ${dirPath}`);
}
// List contents
const contents = fs.readdirSync(absolutePath);
ui.displayFileOp('list_dir', dirPath, 'success');
return { success: true, path: absolutePath, contents };
} catch (error) {
ui.displayFileOp('list_dir', dirPath, 'error');
throw error;
}
}
/**
* Rename/move a directory
*
* @param {string} oldDirPath - Relative or absolute path to the directory to rename
* @param {string} newDirPath - Relative or absolute path for the new directory name/location
* @returns {Promise<Object>} Result object with success status and absolute paths
* @throws {Error} If source directory not found, destination exists, or rename fails
*/
async renameDir(oldDirPath, newDirPath) {
try {
const oldAbsolutePath = this.resolvePath(oldDirPath);
const newAbsolutePath = this.resolvePath(newDirPath);
// Check if source directory exists
if (!fs.existsSync(oldAbsolutePath)) {
throw new Error(`Directory not found: ${oldDirPath}`);
}
// Check if it's actually a directory
if (!fs.statSync(oldAbsolutePath).isDirectory()) {
throw new Error(`Source path is not a directory: ${oldDirPath}`);
}
// Check if destination already exists
if (fs.existsSync(newAbsolutePath)) {
throw new Error(`Destination already exists: ${newDirPath}`);
}
// Ensure parent directory of destination exists
const newDirParent = path.dirname(newAbsolutePath);
this.ensureDir(newDirParent);
// Rename/move directory
fs.renameSync(oldAbsolutePath, newAbsolutePath);
ui.displayFileOp('rename_dir', `${oldDirPath} -> ${newDirPath}`, 'success');
return { success: true, oldPath: oldAbsolutePath, newPath: newAbsolutePath };
} catch (error) {
ui.displayFileOp('rename_dir', `${oldDirPath} -> ${newDirPath}`, 'error');
throw error;
}
}
/**
* Execute a file or directory operation based on action type
*/
async execute(action) {
switch (action.action) {
case 'create_file':
return await this.createFile(action.path, action.content || '');
case 'update_file':
return await this.updateFile(action.path, action.content || '');
case 'patch_file':
return await this.patchFile(
action.path,
action.oldContent || action.patch?.old || '',
action.newContent || action.patch?.new || action.content || ''
);
case 'delete_file':
return await this.deleteFile(action.path);
case 'read_file':
return await this.readFile(action.path);
case 'create_dir':
return await this.createDir(action.path);
case 'delete_dir':
return await this.deleteDir(action.path);
case 'list_dir':
return await this.listDir(action.path);
case 'rename_dir':
return await this.renameDir(action.oldPath || action.path, action.newPath);
default:
throw new Error(`Unknown action: ${action.action}`);
}
}
}
module.exports = FileOperations;