Files
codex/app/main.js
T

194 lines
4.6 KiB
JavaScript

#!/usr/bin/env node
const fs = require("fs");
const os = require("os");
const path = require("path");
const { spawn } = require("child_process");
const DEFAULT_PROMPT = "請自我介紹";
const createdPaths = new Set();
function removeIfCreated(filePath) {
if (!filePath || !createdPaths.has(filePath)) {
return;
}
try {
fs.rmSync(filePath, { force: true });
} catch {
// Best-effort cleanup only.
}
}
function cleanup() {
for (const filePath of Array.from(createdPaths).reverse()) {
removeIfCreated(filePath);
}
}
function makeTempFile(dir, prefix) {
const random = `${Date.now()}-${process.pid}-${Math.random().toString(16).slice(2)}`;
const filePath = path.join(dir, `${prefix}.${random}`);
const fd = fs.openSync(filePath, "wx", 0o600);
fs.closeSync(fd);
createdPaths.add(filePath);
return filePath;
}
function appendGithubOutput(status, output) {
const outputFile = process.env.GITHUB_OUTPUT;
if (!outputFile) {
return;
}
let delimiter;
do {
delimiter = `CODEX_OUTPUT_${Math.random().toString(36).slice(2)}${Date.now()}`;
} while (output.includes(delimiter));
fs.appendFileSync(
outputFile,
`status=${status}\noutput<<${delimiter}\n${output}${output.endsWith("\n") ? "" : "\n"}${delimiter}\n`,
{ encoding: "utf8", mode: 0o600 },
);
}
function fail(message, code = 1) {
console.error(message);
appendGithubOutput("failed", message);
cleanup();
process.exit(code);
}
function validateAuth(encodedAuth, authFile) {
const decoded = Buffer.from(encodedAuth, "base64");
if (decoded.length === 0 && encodedAuth.length > 0) {
fail("OAUTH must be valid base64 encoded Codex auth.json.");
}
const normalized = encodedAuth.replace(/\s+/g, "");
if (decoded.toString("base64").replace(/=+$/, "") !== normalized.replace(/=+$/, "")) {
fail("OAUTH must be valid base64 encoded Codex auth.json.");
}
fs.writeFileSync(authFile, decoded, { mode: 0o600 });
let parsed;
try {
parsed = JSON.parse(decoded.toString("utf8"));
} catch {
fail("Decoded OAUTH must be a JSON object.");
}
if (!parsed || Array.isArray(parsed) || typeof parsed !== "object") {
fail("Decoded OAUTH must be a JSON object.");
}
}
function runCodex(model, prompt) {
return new Promise((resolve) => {
const child = spawn(
"codex",
[
"exec",
"--dangerously-bypass-approvals-and-sandbox",
"--skip-git-repo-check",
"--model",
model,
prompt,
],
{ stdio: ["ignore", "pipe", "pipe"] },
);
let output = "";
child.stdout.on("data", (chunk) => {
process.stdout.write(chunk);
output += chunk.toString();
});
child.stderr.on("data", (chunk) => {
process.stdout.write(chunk);
output += chunk.toString();
});
child.on("error", (error) => {
output += `${error.message}\n`;
resolve({ status: 1, output });
});
child.on("close", (code) => {
resolve({ status: code ?? 1, output });
});
});
}
async function main() {
process.on("exit", cleanup);
process.on("SIGINT", () => {
cleanup();
process.exit(130);
});
process.on("SIGTERM", () => {
cleanup();
process.exit(143);
});
const oauth = process.env.OAUTH || "";
const model = process.env.MODEL || "";
const codexHome = process.env.CODEX_HOME || "/root/.codex";
const prompt = process.env.PROMPT || DEFAULT_PROMPT;
if (!oauth) {
fail("OAUTH is required: provide base64 encoded Codex auth.json.");
}
if (!model) {
fail("MODEL is required.");
}
try {
fs.mkdirSync(codexHome, { recursive: true, mode: 0o700 });
} catch {
fail("Unable to create CODEX_HOME.");
}
const authFile = makeTempFile(codexHome, "auth");
const authPath = path.join(codexHome, "auth.json");
const lockPath = path.join(os.tmpdir(), `codex-auth-${Buffer.from(codexHome).toString("hex")}.lock`);
let lockHandle;
try {
lockHandle = fs.openSync(lockPath, "wx", 0o600);
createdPaths.add(lockPath);
} catch {
fail("Unable to lock Codex auth.json.");
}
validateAuth(oauth, authFile);
if (fs.existsSync(authPath)) {
fail("Refusing to overwrite existing Codex auth.json.");
}
fs.copyFileSync(authFile, authPath);
fs.chmodSync(authPath, 0o600);
createdPaths.add(authPath);
removeIfCreated(authFile);
const result = await runCodex(model, prompt);
appendGithubOutput(result.status === 0 ? "completed" : "failed", result.output);
if (lockHandle !== undefined) {
fs.closeSync(lockHandle);
}
cleanup();
process.exit(result.status);
}
main().catch((error) => {
fail(error instanceof Error ? error.message : String(error));
});