baa-conductor


baa-conductor / apps / codexd / src
im_wower  ·  2026-03-22

cli.ts

  1import {
  2  formatCodexdConfigText,
  3  getCodexdUsageText,
  4  parseCodexdCliRequest
  5} from "./config.js";
  6import {
  7  CodexdDaemon,
  8  runCodexdSmoke,
  9  type CodexdDaemonOptions
 10} from "./daemon.js";
 11import { CodexdLocalService, type CodexdLocalServiceStatus } from "./local-service.js";
 12import type {
 13  CodexdEnvironment,
 14  CodexdSmokeResult,
 15  CodexdStatusSnapshot
 16} from "./contracts.js";
 17import { CodexdStateStore } from "./state-store.js";
 18
 19export interface CodexdTextWriter {
 20  write(chunk: string): unknown;
 21}
 22
 23export interface CodexdProcessLike {
 24  argv: string[];
 25  cwd?(): string;
 26  env: CodexdEnvironment;
 27  execPath?: string;
 28  exitCode?: number;
 29  off?(event: string, listener: () => void): unknown;
 30  on?(event: string, listener: () => void): unknown;
 31  pid?: number;
 32}
 33
 34export interface RunCodexdCliOptions extends CodexdDaemonOptions {
 35  argv?: readonly string[];
 36  env?: CodexdEnvironment;
 37  processLike?: CodexdProcessLike;
 38  stderr?: CodexdTextWriter;
 39  stdout?: CodexdTextWriter;
 40}
 41
 42type CodexdOutputWriter = CodexdTextWriter | typeof console;
 43
 44export async function runCodexdCli(options: RunCodexdCliOptions = {}): Promise<number> {
 45  const processLike = options.processLike ?? getProcessLike();
 46  const stdout = options.stdout ?? console;
 47  const stderr = options.stderr ?? console;
 48  const argv = options.argv ?? processLike?.argv ?? [];
 49  const env = options.env ?? processLike?.env ?? {};
 50  const request = parseCodexdCliRequest(argv, env);
 51
 52  if (request.action === "help") {
 53    writeLine(stdout, getCodexdUsageText());
 54    return 0;
 55  }
 56
 57  if (request.action === "config") {
 58    if (request.printJson) {
 59      writeLine(stdout, JSON.stringify(request.config, null, 2));
 60    } else {
 61      writeLine(stdout, formatCodexdConfigText(request.config));
 62    }
 63
 64    return 0;
 65  }
 66
 67  if (request.action === "status") {
 68    const store = new CodexdStateStore(request.config, {
 69      processId: () => processLike?.pid ?? null
 70    });
 71    const snapshot = await store.initialize();
 72
 73    if (request.printJson) {
 74      writeLine(stdout, JSON.stringify(snapshot, null, 2));
 75    } else {
 76      writeLine(stdout, formatCodexdStatusText(snapshot));
 77    }
 78
 79    return 0;
 80  }
 81
 82  if (request.action === "smoke") {
 83    const result = await runCodexdSmoke(request.config, options);
 84
 85    if (request.printJson) {
 86      writeLine(stdout, JSON.stringify(result, null, 2));
 87    } else {
 88      writeLine(stdout, formatCodexdSmokeText(result));
 89    }
 90
 91    return result.checks.every((check) => check.status === "ok") ? 0 : 1;
 92  }
 93
 94  if (request.action !== "start") {
 95    throw new Error(`Unsupported codexd request action "${request.action}".`);
 96  }
 97
 98  const service = new CodexdLocalService(request.config, {
 99    ...options,
100    env
101  });
102  const started = await service.start();
103
104  if (!request.runOnce) {
105    if (request.printJson) {
106      writeLine(stdout, JSON.stringify(started, null, 2));
107    } else {
108      writeLine(stdout, formatCodexdLocalServiceText(started));
109    }
110
111    const signal = await waitForShutdownSignal(processLike);
112    const stopped = await service.stop();
113
114    if (!request.printJson) {
115      writeLine(stdout, `codexd stopped${signal ? ` after ${signal}` : ""}`);
116      writeLine(stdout, formatCodexdLocalServiceText(stopped));
117    }
118
119    return 0;
120  }
121
122  await sleep(request.lifetimeMs);
123  const stopped = await service.stop();
124
125  if (request.printJson) {
126    writeLine(stdout, JSON.stringify(stopped, null, 2));
127  } else {
128    writeLine(stdout, formatCodexdLocalServiceText(stopped));
129  }
130
131  return 0;
132}
133
134function formatCodexdSmokeText(result: CodexdSmokeResult): string {
135  return [
136    `smoke daemon=${result.snapshot.identity.daemonId}`,
137    ...result.checks.map((check) => `- ${check.status} ${check.name}: ${check.detail}`),
138    formatCodexdStatusText(result.snapshot)
139  ].join("\n");
140}
141
142function formatCodexdStatusText(snapshot: CodexdStatusSnapshot): string {
143  return [
144    `identity=${snapshot.identity.daemonId}`,
145    `node=${snapshot.identity.nodeId}`,
146    `daemon=${snapshot.daemon.started ? "running" : "stopped"}`,
147    `child=${snapshot.daemon.child.status}`,
148    `strategy=${snapshot.config.server.childStrategy}`,
149    `mode=${snapshot.config.server.mode}`,
150    `endpoint=${snapshot.config.server.endpoint}`,
151    `local_api_base=${snapshot.config.service.localApiBase}`,
152    `event_stream_path=${snapshot.config.service.eventStreamPath}`,
153    `sessions=${snapshot.sessionRegistry.sessions.length}`,
154    `runs=${snapshot.runRegistry.runs.length}`,
155    `recent_events=${snapshot.recentEvents.events.length}`,
156    `logs_dir=${snapshot.config.paths.logsDir}`,
157    `state_dir=${snapshot.config.paths.stateDir}`
158  ].join(" ");
159}
160
161function formatCodexdLocalServiceText(status: CodexdLocalServiceStatus): string {
162  return [
163    formatCodexdStatusText(status.snapshot),
164    `resolved_base=${status.service.resolvedBaseUrl ?? "not-listening"}`,
165    `ws_url=${status.service.eventStreamUrl ?? "not-listening"}`
166  ].join(" ");
167}
168
169function getProcessLike(): CodexdProcessLike | undefined {
170  return (globalThis as { process?: CodexdProcessLike }).process;
171}
172
173function sleep(ms: number): Promise<void> {
174  return new Promise((resolve) => {
175    setTimeout(resolve, ms);
176  });
177}
178
179async function waitForShutdownSignal(processLike: CodexdProcessLike | undefined): Promise<string | null> {
180  const subscribe = processLike?.on;
181
182  if (!subscribe || !processLike) {
183    return null;
184  }
185
186  return new Promise((resolve) => {
187    const signals = ["SIGINT", "SIGTERM"] as const;
188    const listeners: Partial<Record<(typeof signals)[number], () => void>> = {};
189    const cleanup = () => {
190      if (!processLike.off) {
191        return;
192      }
193
194      for (const signal of signals) {
195        const listener = listeners[signal];
196
197        if (listener) {
198          processLike.off(signal, listener);
199        }
200      }
201    };
202
203    for (const signal of signals) {
204      const listener = () => {
205        cleanup();
206        resolve(signal);
207      };
208
209      listeners[signal] = listener;
210      subscribe.call(processLike, signal, listener);
211    }
212  });
213}
214
215function writeLine(writer: CodexdOutputWriter, line: string): void {
216  if ("write" in writer) {
217    writer.write(`${line}\n`);
218    return;
219  }
220
221  writer.log(line);
222}