154 lines · 4.1 KB · TypeScript
RawDownload
import { spawn, spawnSync, type ChildProcessWithoutNullStreams } from "node:child_process";import readline from "node:readline";import { readCodexAccountSnapshot, type CodexAccountSnapshot } from "./codexAccount"; interface JsonRpcProbeResponse {  readonly id?: unknown;  readonly result?: unknown;  readonly error?: {    readonly message?: unknown;  };} function readErrorMessage(response: JsonRpcProbeResponse): string | undefined {  return typeof response.error?.message === "string" ? response.error.message : undefined;} export function buildCodexInitializeParams() {  return {    clientInfo: {      name: "t3code_desktop",      title: "T3 Code Desktop",      version: "0.1.0",    },    capabilities: {      experimentalApi: true,    },  } as const;} export function killCodexChildProcess(child: ChildProcessWithoutNullStreams): void {  if (process.platform === "win32" && child.pid !== undefined) {    try {      spawnSync("taskkill", ["/pid", String(child.pid), "/T", "/F"], { stdio: "ignore" });      return;    } catch {      // Fall through to direct kill when taskkill is unavailable.    }  }   child.kill();} export async function probeCodexAccount(input: {  readonly binaryPath: string;  readonly homePath?: string;  readonly signal?: AbortSignal;}): Promise<CodexAccountSnapshot> {  return await new Promise((resolve, reject) => {    const child = spawn(input.binaryPath, ["app-server"], {      env: {        ...process.env,        ...(input.homePath ? { CODEX_HOME: input.homePath } : {}),      },      stdio: ["pipe", "pipe", "pipe"],      shell: process.platform === "win32",    });    const output = readline.createInterface({ input: child.stdout });     let completed = false;     const cleanup = () => {      output.removeAllListeners();      output.close();      child.removeAllListeners();      if (!child.killed) {        killCodexChildProcess(child);      }    };     const finish = (callback: () => void) => {      if (completed) return;      completed = true;      cleanup();      callback();    };     const fail = (error: unknown) =>      finish(() =>        reject(          error instanceof Error            ? error            : new Error(`Codex account probe failed: ${String(error)}.`),        ),      );     if (input.signal?.aborted) {      fail(new Error("Codex account probe aborted."));      return;    }    input.signal?.addEventListener("abort", () => fail(new Error("Codex account probe aborted.")));     const writeMessage = (message: unknown) => {      if (!child.stdin.writable) {        fail(new Error("Cannot write to codex app-server stdin."));        return;      }       child.stdin.write(`${JSON.stringify(message)}\n`);    };     output.on("line", (line) => {      let parsed: unknown;      try {        parsed = JSON.parse(line);      } catch {        fail(new Error("Received invalid JSON from codex app-server during account probe."));        return;      }       if (!parsed || typeof parsed !== "object") {        return;      }       const response = parsed as JsonRpcProbeResponse;      if (response.id === 1) {        const errorMessage = readErrorMessage(response);        if (errorMessage) {          fail(new Error(`initialize failed: ${errorMessage}`));          return;        }         writeMessage({ method: "initialized" });        writeMessage({ id: 2, method: "account/read", params: {} });        return;      }       if (response.id === 2) {        const errorMessage = readErrorMessage(response);        if (errorMessage) {          fail(new Error(`account/read failed: ${errorMessage}`));          return;        }         finish(() => resolve(readCodexAccountSnapshot(response.result)));      }    });     child.once("error", fail);    child.once("exit", (code, signal) => {      if (completed) return;      fail(        new Error(          `codex app-server exited before probe completed (code=${code ?? "null"}, signal=${signal ?? "null"}).`,        ),      );    });     writeMessage({      id: 1,      method: "initialize",      params: buildCodexInitializeParams(),    });  });}