diff --git a/app/main.js b/app/main.js index 2f3323a..543a620 100644 --- a/app/main.js +++ b/app/main.js @@ -39,11 +39,11 @@ class TempFileRegistry { } cleanup() { - for (const filePath of Array.from(this.files).reverse()) { + for (const filePath of this.files) { this.removeFile(filePath); } - for (const dirPath of Array.from(this.dirs).reverse()) { + for (const dirPath of this.dirs) { try { fs.rmSync(dirPath, { force: true, recursive: true }); this.dirs.delete(dirPath); @@ -56,6 +56,37 @@ class TempFileRegistry { const tempFiles = new TempFileRegistry(); +class OutputCollector { + constructor(maxBytes) { + this.maxBytes = maxBytes; + this.chunks = []; + this.size = 0; + this.truncated = false; + } + + append(chunk) { + const available = this.maxBytes - this.size; + + if (available <= 0) { + this.truncated = true; + return; + } + + const storedChunk = chunk.length > available ? chunk.subarray(0, available) : chunk; + this.chunks.push(storedChunk); + this.size += storedChunk.length; + + if (storedChunk.length < chunk.length) { + this.truncated = true; + } + } + + toString() { + const truncationMessage = this.truncated ? "\n[Output truncated]\n" : ""; + return `${Buffer.concat(this.chunks, this.size).toString()}${truncationMessage}`; + } +} + function cleanup() { tempFiles.cleanup(); } @@ -101,13 +132,16 @@ function fail(message, code = 1) { process.exit(code); } -function validateAuth(encodedAuth, authFile) { - const normalizedAuth = encodedAuth.replace(/\s+/g, ""); - const decoded = Buffer.from(encodedAuth, "base64"); - const normalizedDecoded = decoded.toString("base64").replace(/=+$/, ""); - const normalizedInput = normalizedAuth.replace(/=+$/, ""); +function normalizeBase64(value) { + return value.replace(/\s+/g, "").replace(/=+$/, ""); +} - if (decoded.length === 0 && normalizedAuth.length > 0) { +function validateAuth(encodedAuth, authFile) { + const decoded = Buffer.from(encodedAuth, "base64"); + const normalizedDecoded = normalizeBase64(decoded.toString("base64")); + const normalizedInput = normalizeBase64(encodedAuth); + + if (decoded.length === 0 && normalizedInput.length > 0) { fail("OAUTH must be valid base64 encoded Codex auth.json."); } @@ -134,18 +168,6 @@ function parsePositiveInteger(value, fallback) { return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback; } -function truncateOutput(chunks, nextChunk, maxBytes) { - const currentSize = chunks.reduce((total, chunk) => total + chunk.length, 0); - const available = maxBytes - currentSize; - - if (available <= 0) { - return false; - } - - chunks.push(nextChunk.length > available ? nextChunk.subarray(0, available) : nextChunk); - return nextChunk.length <= available; -} - function runCodex(model, prompt) { return new Promise((resolve) => { const timeoutMs = parsePositiveInteger(process.env.CODEX_TIMEOUT_MS, DEFAULT_CODEX_TIMEOUT_MS); @@ -167,46 +189,39 @@ function runCodex(model, prompt) { { cwd: workspace, stdio: ["ignore", "pipe", "pipe"] }, ); - const outputChunks = []; - let outputTruncated = false; - const appendOutput = (chunk) => { - if (!truncateOutput(outputChunks, chunk, outputLimitBytes)) { - outputTruncated = true; - } - return Buffer.concat(outputChunks).toString(); - }; + const output = new OutputCollector(outputLimitBytes); const timeout = setTimeout(() => { child.kill("SIGTERM"); - appendOutput(Buffer.from(`Codex execution timed out after ${timeoutMs} ms.\n`)); + output.append(Buffer.from(`Codex execution timed out after ${timeoutMs} ms.\n`)); }, timeoutMs); child.stdout.pipe(process.stdout); child.stderr.pipe(process.stdout); child.stdout.on("data", (chunk) => { - if (!truncateOutput(outputChunks, chunk, outputLimitBytes)) { - outputTruncated = true; - } + output.append(chunk); }); child.stderr.on("data", (chunk) => { - if (!truncateOutput(outputChunks, chunk, outputLimitBytes)) { - outputTruncated = true; - } + output.append(chunk); }); child.on("error", (error) => { clearTimeout(timeout); - const output = appendOutput(Buffer.from(`${error.message}\n`)); - resolve({ status: 1, output }); + const message = error.code === "ENOENT" ? "Unable to find codex command.\n" : `${error.message}\n`; + output.append(Buffer.from(message)); + resolve({ status: 1, output: output.toString() }); }); - child.on("close", (code) => { + child.on("close", (code, signal) => { clearTimeout(timeout); - const truncationMessage = outputTruncated ? "\n[Output truncated]\n" : ""; - const output = `${Buffer.concat(outputChunks).toString()}${truncationMessage}`; - resolve({ status: code ?? 1, output }); + + if (code === null && signal) { + output.append(Buffer.from(`Codex process terminated by signal ${signal}.\n`)); + } + + resolve({ status: code ?? 1, output: output.toString() }); }); }); } @@ -243,6 +258,8 @@ function readConfig() { function setupAuth(oauth, codexHome) { try { fs.mkdirSync(codexHome, { recursive: true, mode: DIR_MODE_PRIVATE }); + fs.chmodSync(codexHome, DIR_MODE_PRIVATE); + fs.accessSync(codexHome, fs.constants.R_OK | fs.constants.W_OK | fs.constants.X_OK); } catch { fail("Unable to create CODEX_HOME."); }