import { promises as fs } from 'node:fs'; import path from 'node:path'; import { execFile } from 'node:child_process'; export class WorkspaceManager { constructor(workspace) { this.workspace = path.resolve(workspace); } resolve(relativePath) { if (!relativePath || path.isAbsolute(relativePath)) throw new Error('workspace-relative path required'); const target = path.resolve(this.workspace, relativePath); if (!target.startsWith(`${this.workspace}${path.sep}`)) throw new Error('path escaped workspace'); return target; } async read(relativePath) { return fs.readFile(this.resolve(relativePath), 'utf8'); } async write(relativePath, content, approved) { if (approved !== true) throw new Error('explicit approval required'); const target = this.resolve(relativePath); await fs.mkdir(path.dirname(target), { recursive: true }); const temporary = `${target}.aide-tmp-${process.pid}`; await fs.writeFile(temporary, String(content), { mode: 0o600 }); await fs.rename(temporary, target); return { path: relativePath, bytes: Buffer.byteLength(String(content)) }; } async applyPatch(patch, approved) { if (approved !== true) throw new Error('explicit approval required'); if (typeof patch !== 'string' || !patch.startsWith('diff --git ')) throw new Error('unified diff required'); if (patch.length > 200_000) throw new Error('patch exceeds size limit'); const temporary = path.join(this.workspace, `.aide-patch-${process.pid}.diff`); await fs.writeFile(temporary, patch, { mode: 0o600 }); try { await this.runGit(['apply', '--check', '--whitespace=error', temporary]); await this.runGit(['apply', '--whitespace=error', temporary]); return { applied: true, bytes: Buffer.byteLength(patch) }; } finally { await fs.rm(temporary, { force: true }); } } runGit(args) { return new Promise((resolve, reject) => { execFile('git', args, { cwd: this.workspace, timeout: 15000, maxBuffer: 256 * 1024 }, (error, stdout, stderr) => { if (error) reject(new Error(stderr.trim() || error.message)); else resolve(stdout); }); }); } }