mirror of
https://github.com/openai/codex.git
synced 2026-05-20 19:23:21 +00:00
310 lines
7.8 KiB
JavaScript
310 lines
7.8 KiB
JavaScript
#!/usr/bin/env node
|
|
import { createServer } from "node:http";
|
|
import { createReadStream, existsSync } from "node:fs";
|
|
import { dirname, extname, join, resolve } from "node:path";
|
|
import { fileURLToPath } from "node:url";
|
|
import pty from "node-pty";
|
|
import { WebSocketServer } from "ws";
|
|
import { decodeClientFrame } from "./protocol.mjs";
|
|
|
|
const __filename = fileURLToPath(import.meta.url);
|
|
const packageRoot = dirname(__filename);
|
|
const repoRoot = resolve(packageRoot, "..");
|
|
|
|
const MIME_TYPES = {
|
|
".css": "text/css; charset=utf-8",
|
|
".html": "text/html; charset=utf-8",
|
|
".js": "text/javascript; charset=utf-8",
|
|
".json": "application/json; charset=utf-8",
|
|
".map": "application/json; charset=utf-8",
|
|
".svg": "image/svg+xml",
|
|
".wasm": "application/wasm",
|
|
};
|
|
|
|
const SERVER_FLAGS = new Set([
|
|
"--dev",
|
|
"--shell",
|
|
"--host",
|
|
"--port",
|
|
"--cwd",
|
|
"--codex-bin",
|
|
]);
|
|
|
|
export function parseArgs(argv) {
|
|
const options = {
|
|
dev: false,
|
|
host: "127.0.0.1",
|
|
port: 4321,
|
|
cwd: repoRoot,
|
|
codexBin: null,
|
|
shell: false,
|
|
codexArgs: [],
|
|
};
|
|
|
|
for (let index = 0; index < argv.length; index += 1) {
|
|
const arg = argv[index];
|
|
if (arg === "--") {
|
|
if (SERVER_FLAGS.has(argv[index + 1])) {
|
|
continue;
|
|
}
|
|
options.codexArgs = argv.slice(index + 1);
|
|
break;
|
|
}
|
|
if (arg === "--dev") {
|
|
options.dev = true;
|
|
} else if (arg === "--shell") {
|
|
options.shell = true;
|
|
} else if (arg === "--host") {
|
|
options.host = requireValue(argv, (index += 1), arg);
|
|
} else if (arg === "--port") {
|
|
options.port = Number.parseInt(requireValue(argv, (index += 1), arg), 10);
|
|
if (!Number.isFinite(options.port) || options.port <= 0) {
|
|
throw new Error("--port must be a positive integer");
|
|
}
|
|
} else if (arg === "--cwd") {
|
|
options.cwd = resolve(requireValue(argv, (index += 1), arg));
|
|
} else if (arg === "--codex-bin") {
|
|
options.codexBin = resolve(requireValue(argv, (index += 1), arg));
|
|
} else {
|
|
throw new Error(`Unknown argument: ${arg}`);
|
|
}
|
|
}
|
|
|
|
return options;
|
|
}
|
|
|
|
function requireValue(argv, index, flag) {
|
|
const value = argv[index];
|
|
if (!value) {
|
|
throw new Error(`${flag} requires a value`);
|
|
}
|
|
return value;
|
|
}
|
|
|
|
function commandFor(options) {
|
|
if (options.shell) {
|
|
return {
|
|
command: process.env.SHELL || "/bin/sh",
|
|
args: ["-l"],
|
|
};
|
|
}
|
|
|
|
if (options.codexBin) {
|
|
return {
|
|
command: options.codexBin,
|
|
args: options.codexArgs,
|
|
};
|
|
}
|
|
|
|
const debugCodex = join(
|
|
repoRoot,
|
|
"codex-rs",
|
|
"target",
|
|
"debug",
|
|
process.platform === "win32" ? "codex.exe" : "codex",
|
|
);
|
|
if (existsSync(debugCodex)) {
|
|
return {
|
|
command: debugCodex,
|
|
args: options.codexArgs,
|
|
};
|
|
}
|
|
|
|
return {
|
|
command: "cargo",
|
|
args: [
|
|
"run",
|
|
"--manifest-path",
|
|
join(repoRoot, "codex-rs", "Cargo.toml"),
|
|
"--bin",
|
|
"codex",
|
|
"--",
|
|
...options.codexArgs,
|
|
],
|
|
};
|
|
}
|
|
|
|
function createPty(options) {
|
|
const { command, args } = commandFor(options);
|
|
return pty.spawn(command, args, {
|
|
name: "xterm-256color",
|
|
cols: 80,
|
|
rows: 24,
|
|
cwd: options.cwd,
|
|
env: {
|
|
...process.env,
|
|
TERM: "xterm-256color",
|
|
COLORTERM: "truecolor",
|
|
TERM_PROGRAM: "wterm",
|
|
CODEX_TUI_DISABLE_KEYBOARD_ENHANCEMENT: "1",
|
|
},
|
|
});
|
|
}
|
|
|
|
function isAllowedOrigin(request) {
|
|
const origin = request.headers.origin;
|
|
if (!origin) {
|
|
return true;
|
|
}
|
|
|
|
try {
|
|
return new URL(origin).host === request.headers.host;
|
|
} catch {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
function sendHttp(response, status, body, headers = {}) {
|
|
response.writeHead(status, {
|
|
"content-type": "text/plain; charset=utf-8",
|
|
"cache-control": "no-store",
|
|
...headers,
|
|
});
|
|
response.end(body);
|
|
}
|
|
|
|
async function serveStatic(request, response) {
|
|
const distRoot = join(packageRoot, "dist");
|
|
const url = new URL(request.url ?? "/", "http://localhost");
|
|
const pathname = decodeURIComponent(url.pathname);
|
|
const relativePath = pathname === "/" ? "index.html" : pathname.slice(1);
|
|
const resolvedPath = resolve(distRoot, relativePath);
|
|
|
|
if (!resolvedPath.startsWith(`${distRoot}/`) && resolvedPath !== distRoot) {
|
|
sendHttp(response, 403, "Forbidden");
|
|
return;
|
|
}
|
|
|
|
const filePath = existsSync(resolvedPath)
|
|
? resolvedPath
|
|
: join(distRoot, "index.html");
|
|
const contentType =
|
|
MIME_TYPES[extname(filePath)] || "application/octet-stream";
|
|
response.writeHead(200, {
|
|
"content-type": contentType,
|
|
"cache-control": filePath.endsWith("index.html")
|
|
? "no-store"
|
|
: "public, max-age=31536000, immutable",
|
|
});
|
|
createReadStream(filePath).pipe(response);
|
|
}
|
|
|
|
async function main() {
|
|
const options = parseArgs(process.argv.slice(2));
|
|
const server = createServer();
|
|
const wss = new WebSocketServer({ noServer: true });
|
|
|
|
if (options.dev) {
|
|
const { createServer: createViteServer } = await import("vite");
|
|
const vite = await createViteServer({
|
|
root: packageRoot,
|
|
server: { middlewareMode: true },
|
|
appType: "spa",
|
|
});
|
|
server.on("request", (request, response) => {
|
|
vite.middlewares(request, response, () => {
|
|
sendHttp(response, 404, "Not found");
|
|
});
|
|
});
|
|
} else {
|
|
server.on("request", (request, response) => {
|
|
if (request.url === "/healthz") {
|
|
sendHttp(response, 200, "ok");
|
|
return;
|
|
}
|
|
|
|
if (!existsSync(join(packageRoot, "dist", "index.html"))) {
|
|
sendHttp(
|
|
response,
|
|
500,
|
|
"Missing dist/. Run pnpm --filter @openai/codex-webui build first.",
|
|
);
|
|
return;
|
|
}
|
|
|
|
void serveStatic(request, response).catch((error) => {
|
|
sendHttp(
|
|
response,
|
|
500,
|
|
error instanceof Error ? error.message : String(error),
|
|
);
|
|
});
|
|
});
|
|
}
|
|
|
|
server.on("upgrade", (request, socket, head) => {
|
|
const url = new URL(request.url ?? "/", "http://localhost");
|
|
if (url.pathname !== "/api/pty" || !isAllowedOrigin(request)) {
|
|
socket.write("HTTP/1.1 403 Forbidden\r\n\r\n");
|
|
socket.destroy();
|
|
return;
|
|
}
|
|
|
|
wss.handleUpgrade(request, socket, head, (websocket) => {
|
|
wss.emit("connection", websocket, request);
|
|
});
|
|
});
|
|
|
|
wss.on("connection", (websocket) => {
|
|
let child;
|
|
try {
|
|
child = createPty(options);
|
|
} catch (error) {
|
|
const message = error instanceof Error ? error.message : String(error);
|
|
if (websocket.readyState === websocket.OPEN) {
|
|
websocket.send(Buffer.from(`Failed to start PTY: ${message}\r\n`));
|
|
websocket.close();
|
|
}
|
|
return;
|
|
}
|
|
|
|
const output = child.onData((data) => {
|
|
if (websocket.readyState === websocket.OPEN) {
|
|
websocket.send(Buffer.from(data, "utf8"));
|
|
}
|
|
});
|
|
|
|
child.onExit(({ exitCode, signal }) => {
|
|
if (websocket.readyState === websocket.OPEN) {
|
|
websocket.send(
|
|
Buffer.from(
|
|
`\r\n[process exited: ${signal ?? exitCode}]\r\n`,
|
|
"utf8",
|
|
),
|
|
);
|
|
websocket.close();
|
|
}
|
|
});
|
|
|
|
websocket.on("message", (data) => {
|
|
const frame = decodeClientFrame(data);
|
|
if (frame.type === "input") {
|
|
child.write(frame.data);
|
|
} else if (frame.type === "resize") {
|
|
child.resize(frame.cols, frame.rows);
|
|
}
|
|
});
|
|
|
|
websocket.on("close", () => {
|
|
output.dispose();
|
|
child.kill();
|
|
});
|
|
});
|
|
|
|
await new Promise((resolveListen) => {
|
|
server.listen(options.port, options.host, resolveListen);
|
|
});
|
|
|
|
const address = server.address();
|
|
const port =
|
|
typeof address === "object" && address ? address.port : options.port;
|
|
console.log(`Codex web terminal listening on http://${options.host}:${port}`);
|
|
}
|
|
|
|
if (process.argv[1] === __filename) {
|
|
main().catch((error) => {
|
|
console.error(error instanceof Error ? error.message : error);
|
|
process.exit(1);
|
|
});
|
|
}
|