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}