@neuroequalityorg/knightcode
Version:
Knightcode CLI - Your local AI coding assistant using Ollama, LM Studio, and more
517 lines (438 loc) • 14.5 kB
text/typescript
/**
* File Operations Module
*
* Provides utilities for reading, writing, and manipulating files
* with proper error handling and security considerations.
*/
import { promises as fs } from 'fs';
import path from 'path';
import { logger } from '../utils/logger.js';
import { createUserError } from '../errors/formatter.js';
import { ErrorCategory } from '../errors/types.js';
import { ErrnoException } from '../utils/types.js';
/**
* Result of a file operation
*/
interface FileOperationResult {
success: boolean;
error?: Error;
path?: string;
content?: string;
created?: boolean;
}
/**
* File Operations Manager
*/
class FileOperationsManager {
private config: any;
private workspacePath: string;
/**
* Create a new file operations manager
*/
constructor(config: any) {
this.config = config;
this.workspacePath = config.workspacePath || process.cwd();
logger.debug('File operations manager created', {
workspacePath: this.workspacePath
});
}
/**
* Initialize file operations
*/
async initialize(): Promise<void> {
logger.info('Initializing file operations manager');
try {
// Verify workspace directory exists
const stats = await fs.stat(this.workspacePath);
if (!stats.isDirectory()) {
throw createUserError(`Workspace path is not a directory: ${this.workspacePath}`, {
category: ErrorCategory.FILE_SYSTEM
});
}
logger.info('File operations manager initialized');
} catch (error) {
if ((error as ErrnoException).code === 'ENOENT') {
throw createUserError(`Workspace directory does not exist: ${this.workspacePath}`, {
category: ErrorCategory.FILE_SYSTEM,
resolution: 'Please provide a valid workspace path'
});
}
logger.error('Failed to initialize file operations manager', error);
throw createUserError('Failed to initialize file operations', {
cause: error,
category: ErrorCategory.FILE_SYSTEM
});
}
}
/**
* Get absolute path relative to workspace
*/
getAbsolutePath(relativePath: string): string {
// Clean up path to prevent directory traversal attacks
const normalizedPath = path.normalize(relativePath).replace(/^(\.\.(\/|\\|$))+/, '');
return path.resolve(this.workspacePath, normalizedPath);
}
/**
* Get relative path from workspace
*/
getRelativePath(absolutePath: string): string {
return path.relative(this.workspacePath, absolutePath);
}
/**
* Read a file
*/
async readFile(filePath: string): Promise<FileOperationResult> {
const absolutePath = this.getAbsolutePath(filePath);
logger.debug('Reading file', { path: filePath, absolutePath });
try {
// Verify file exists and is a file
const stats = await fs.stat(absolutePath);
if (!stats.isFile()) {
return {
success: false,
error: createUserError(`Not a file: ${filePath}`, {
category: ErrorCategory.FILE_SYSTEM
})
};
}
// Check file size
const maxSizeBytes = this.config.fileOps?.maxReadSizeBytes || 10 * 1024 * 1024; // 10MB default
if (stats.size > maxSizeBytes) {
return {
success: false,
error: createUserError(`File too large to read: ${filePath} (${stats.size} bytes)`, {
category: ErrorCategory.FILE_SYSTEM,
resolution: 'Try reading a smaller file or use a text editor to open this file'
})
};
}
// Read file content
const content = await fs.readFile(absolutePath, 'utf8');
return {
success: true,
path: filePath,
content
};
} catch (error) {
logger.error(`Error reading file: ${filePath}`, error);
const errnoError = error as ErrnoException;
if (errnoError.code === 'ENOENT') {
return {
success: false,
error: createUserError(`File not found: ${filePath}`, {
category: ErrorCategory.FILE_SYSTEM,
resolution: 'Check that the file exists and the path is correct'
})
};
}
if (errnoError.code === 'EACCES') {
return {
success: false,
error: createUserError(`Permission denied reading file: ${filePath}`, {
category: ErrorCategory.FILE_SYSTEM,
resolution: 'Check file permissions or try running with elevated privileges'
})
};
}
return {
success: false,
error: createUserError(`Failed to read file: ${filePath}`, {
cause: error,
category: ErrorCategory.FILE_SYSTEM
})
};
}
}
/**
* Write a file
*/
async writeFile(filePath: string, content: string, options: { createDirs?: boolean } = {}): Promise<FileOperationResult> {
const absolutePath = this.getAbsolutePath(filePath);
logger.debug('Writing file', {
path: filePath,
absolutePath,
contentLength: content.length,
createDirs: options.createDirs
});
try {
// Check if file exists
let fileExists = false;
let isCreating = false;
try {
const stats = await fs.stat(absolutePath);
fileExists = stats.isFile();
} catch (error) {
const errnoError = error as ErrnoException;
if (errnoError.code === 'ENOENT') {
isCreating = true;
// Create directories if requested
if (options.createDirs) {
const dirPath = path.dirname(absolutePath);
await fs.mkdir(dirPath, { recursive: true });
}
} else {
throw error;
}
}
// Write file content
await fs.writeFile(absolutePath, content, 'utf8');
return {
success: true,
path: filePath,
created: isCreating
};
} catch (error) {
logger.error(`Error writing file: ${filePath}`, error);
const errnoError = error as ErrnoException;
if (errnoError.code === 'ENOENT') {
return {
success: false,
error: createUserError(`Directory does not exist: ${path.dirname(filePath)}`, {
category: ErrorCategory.FILE_SYSTEM,
resolution: 'Use the createDirs option to create parent directories'
})
};
}
if (errnoError.code === 'EACCES') {
return {
success: false,
error: createUserError(`Permission denied writing file: ${filePath}`, {
category: ErrorCategory.FILE_SYSTEM,
resolution: 'Check file permissions or try running with elevated privileges'
})
};
}
return {
success: false,
error: createUserError(`Failed to write file: ${filePath}`, {
cause: error,
category: ErrorCategory.FILE_SYSTEM
})
};
}
}
/**
* Delete a file
*/
async deleteFile(filePath: string): Promise<FileOperationResult> {
const absolutePath = this.getAbsolutePath(filePath);
logger.debug('Deleting file', { path: filePath, absolutePath });
try {
// Verify file exists and is a file
const stats = await fs.stat(absolutePath);
if (!stats.isFile()) {
return {
success: false,
error: createUserError(`Not a file: ${filePath}`, {
category: ErrorCategory.FILE_SYSTEM
})
};
}
// Delete file
await fs.unlink(absolutePath);
return {
success: true,
path: filePath
};
} catch (error) {
logger.error(`Error deleting file: ${filePath}`, error);
const errnoError = error as ErrnoException;
if (errnoError.code === 'ENOENT') {
return {
success: false,
error: createUserError(`File not found: ${filePath}`, {
category: ErrorCategory.FILE_SYSTEM
})
};
}
if (errnoError.code === 'EACCES') {
return {
success: false,
error: createUserError(`Permission denied deleting file: ${filePath}`, {
category: ErrorCategory.FILE_SYSTEM,
resolution: 'Check file permissions or try running with elevated privileges'
})
};
}
return {
success: false,
error: createUserError(`Failed to delete file: ${filePath}`, {
cause: error,
category: ErrorCategory.FILE_SYSTEM
})
};
}
}
/**
* Check if a file exists
*/
async fileExists(filePath: string): Promise<boolean> {
const absolutePath = this.getAbsolutePath(filePath);
try {
const stats = await fs.stat(absolutePath);
return stats.isFile();
} catch (error) {
return false;
}
}
/**
* Create a directory
*/
async createDirectory(dirPath: string, options: { recursive?: boolean } = {}): Promise<FileOperationResult> {
const absolutePath = this.getAbsolutePath(dirPath);
logger.debug('Creating directory', {
path: dirPath,
absolutePath,
recursive: options.recursive
});
try {
// Create directory
await fs.mkdir(absolutePath, { recursive: options.recursive !== false });
return {
success: true,
path: dirPath
};
} catch (error) {
logger.error(`Error creating directory: ${dirPath}`, error);
const errnoError = error as ErrnoException;
if (errnoError.code === 'EEXIST') {
return {
success: false,
error: createUserError(`Directory already exists: ${dirPath}`, {
category: ErrorCategory.FILE_SYSTEM
})
};
}
if (errnoError.code === 'EACCES') {
return {
success: false,
error: createUserError(`Permission denied creating directory: ${dirPath}`, {
category: ErrorCategory.FILE_SYSTEM,
resolution: 'Check file permissions or try running with elevated privileges'
})
};
}
return {
success: false,
error: createUserError(`Failed to create directory: ${dirPath}`, {
cause: error,
category: ErrorCategory.FILE_SYSTEM
})
};
}
}
/**
* List directory contents
*/
async listDirectory(dirPath: string): Promise<FileOperationResult & { files?: string[] }> {
const absolutePath = this.getAbsolutePath(dirPath);
logger.debug('Listing directory', { path: dirPath, absolutePath });
try {
// Verify directory exists and is a directory
const stats = await fs.stat(absolutePath);
if (!stats.isDirectory()) {
return {
success: false,
error: createUserError(`Not a directory: ${dirPath}`, {
category: ErrorCategory.FILE_SYSTEM
})
};
}
// Read directory contents
const files = await fs.readdir(absolutePath);
return {
success: true,
path: dirPath,
files
};
} catch (error) {
logger.error(`Error listing directory: ${dirPath}`, error);
const errnoError = error as ErrnoException;
if (errnoError.code === 'ENOENT') {
return {
success: false,
error: createUserError(`Directory not found: ${dirPath}`, {
category: ErrorCategory.FILE_SYSTEM
})
};
}
if (errnoError.code === 'EACCES') {
return {
success: false,
error: createUserError(`Permission denied listing directory: ${dirPath}`, {
category: ErrorCategory.FILE_SYSTEM,
resolution: 'Check directory permissions or try running with elevated privileges'
})
};
}
return {
success: false,
error: createUserError(`Failed to list directory: ${dirPath}`, {
cause: error,
category: ErrorCategory.FILE_SYSTEM
})
};
}
}
/**
* Generate a diff between two strings
*/
generateDiff(original: string, modified: string): string {
// A simple line-by-line diff implementation
// In a real implementation, this would use a proper diff library
const originalLines = original.split('\n');
const modifiedLines = modified.split('\n');
const diff: string[] = [];
let i = 0, j = 0;
while (i < originalLines.length || j < modifiedLines.length) {
if (i >= originalLines.length) {
// All remaining lines in modified are additions
diff.push(`+ ${modifiedLines[j]}`);
j++;
} else if (j >= modifiedLines.length) {
// All remaining lines in original are deletions
diff.push(`- ${originalLines[i]}`);
i++;
} else if (originalLines[i] === modifiedLines[j]) {
// Lines are the same
diff.push(` ${originalLines[i]}`);
i++;
j++;
} else {
// Lines differ
// Simple approach: treat as a deletion and addition
// A more sophisticated diff would detect changes within lines
diff.push(`- ${originalLines[i]}`);
diff.push(`+ ${modifiedLines[j]}`);
i++;
j++;
}
}
return diff.join('\n');
}
/**
* Apply a patch to a file
*/
async applyPatch(filePath: string, patch: string): Promise<FileOperationResult> {
// In a real implementation, this would parse and apply a unified diff
// For simplicity, we'll just write the patched content directly
return this.writeFile(filePath, patch);
}
}
/**
* Initialize the file operations system
*/
export async function initFileOperations(config: any): Promise<FileOperationsManager> {
logger.info('Initializing file operations system');
try {
const fileOps = new FileOperationsManager(config);
await fileOps.initialize();
logger.info('File operations system initialized successfully');
return fileOps;
} catch (error) {
logger.error('Failed to initialize file operations system', error);
// Create a basic file operations manager even if initialization failed
return new FileOperationsManager(config);
}
}
export default FileOperationsManager;