im_wower
·
2026-03-28
cli.ts
1import {
2 formatClaudeCodedConfigText,
3 getClaudeCodedUsageText,
4 parseClaudeCodedCliRequest
5} from "./config.js";
6import { ClaudeCodedDaemon } from "./daemon.js";
7import { ClaudeCodedLocalService, type ClaudeCodedLocalServiceStatus } from "./local-service.js";
8import type {
9 ClaudeCodedEnvironment,
10 ClaudeCodedStatusSnapshot
11} from "./contracts.js";
12
13export interface ClaudeCodedTextWriter {
14 write(chunk: string): unknown;
15}
16
17export interface ClaudeCodedProcessLike {
18 argv: string[];
19 cwd?(): string;
20 env: ClaudeCodedEnvironment;
21 execPath?: string;
22 exitCode?: number;
23 off?(event: string, listener: () => void): unknown;
24 on?(event: string, listener: () => void): unknown;
25 pid?: number;
26}
27
28export interface RunClaudeCodedCliOptions {
29 argv?: readonly string[];
30 env?: ClaudeCodedEnvironment;
31 processLike?: ClaudeCodedProcessLike;
32 stderr?: ClaudeCodedTextWriter;
33 stdout?: ClaudeCodedTextWriter;
34}
35
36type ClaudeCodedOutputWriter = ClaudeCodedTextWriter | typeof console;
37
38export async function runClaudeCodedCli(options: RunClaudeCodedCliOptions = {}): Promise<number> {
39 const processLike = options.processLike ?? getProcessLike();
40 const stdout = options.stdout ?? console;
41 const stderr = options.stderr ?? console;
42 const argv = options.argv ?? processLike?.argv ?? [];
43 const env = options.env ?? processLike?.env ?? {};
44 const request = parseClaudeCodedCliRequest(argv, env);
45
46 if (request.action === "help") {
47 writeLine(stdout, getClaudeCodedUsageText());
48 return 0;
49 }
50
51 if (request.action === "config") {
52 if (request.printJson) {
53 writeLine(stdout, JSON.stringify(request.config, null, 2));
54 } else {
55 writeLine(stdout, formatClaudeCodedConfigText(request.config));
56 }
57
58 return 0;
59 }
60
61 if (request.action === "status") {
62 const snapshot = await readStoredStatus(request.config);
63
64 if (request.printJson) {
65 writeLine(stdout, JSON.stringify(snapshot, null, 2));
66 } else {
67 writeLine(stdout, formatClaudeCodedStatusText(snapshot));
68 }
69
70 return 0;
71 }
72
73 if (request.action !== "start") {
74 throw new Error(`Unsupported claude-coded request action "${request.action}".`);
75 }
76
77 const service = new ClaudeCodedLocalService(request.config, {
78 env
79 });
80 const started = await service.start();
81
82 if (request.printJson) {
83 writeLine(stdout, JSON.stringify(started, null, 2));
84 } else {
85 writeLine(stdout, formatClaudeCodedLocalServiceText(started));
86 }
87
88 const signal = await waitForShutdownSignal(processLike);
89 const stopped = await service.stop();
90
91 if (!request.printJson) {
92 writeLine(stdout, `claude-coded stopped${signal ? ` after ${signal}` : ""}`);
93 writeLine(stdout, formatClaudeCodedLocalServiceText(stopped));
94 }
95
96 return 0;
97}
98
99async function readStoredStatus(
100 config: import("./contracts.js").ClaudeCodedResolvedConfig
101): Promise<ClaudeCodedStatusSnapshot | null> {
102 try {
103 const { readFile } = await import("node:fs/promises");
104 const daemonState = JSON.parse(await readFile(config.paths.daemonStatePath, "utf8"));
105 const identity = JSON.parse(await readFile(config.paths.identityPath, "utf8"));
106 return {
107 config,
108 identity,
109 daemon: daemonState,
110 recentEvents: { maxEntries: config.eventCacheSize, updatedAt: null, events: [] }
111 };
112 } catch {
113 return null;
114 }
115}
116
117function formatClaudeCodedStatusText(snapshot: ClaudeCodedStatusSnapshot | null): string {
118 if (snapshot == null) {
119 return "claude-coded: no state found (not yet started?)";
120 }
121
122 return [
123 `identity=${snapshot.identity.daemonId}`,
124 `node=${snapshot.identity.nodeId}`,
125 `daemon=${snapshot.daemon.started ? "running" : "stopped"}`,
126 `child=${snapshot.daemon.child.status}`,
127 `child_command=${snapshot.config.child.command}`,
128 `local_api_base=${snapshot.config.service.localApiBase}`,
129 `logs_dir=${snapshot.config.paths.logsDir}`,
130 `state_dir=${snapshot.config.paths.stateDir}`
131 ].join(" ");
132}
133
134function formatClaudeCodedLocalServiceText(status: ClaudeCodedLocalServiceStatus): string {
135 const snapshot = status.snapshot;
136
137 return [
138 `identity=${snapshot.identity.daemonId}`,
139 `node=${snapshot.identity.nodeId}`,
140 `daemon=${snapshot.daemon.started ? "running" : "stopped"}`,
141 `child=${snapshot.daemon.child.status}`,
142 `resolved_base=${status.service.resolvedBaseUrl ?? "not-listening"}`
143 ].join(" ");
144}
145
146function getProcessLike(): ClaudeCodedProcessLike | undefined {
147 return (globalThis as { process?: ClaudeCodedProcessLike }).process;
148}
149
150async function waitForShutdownSignal(processLike: ClaudeCodedProcessLike | undefined): Promise<string | null> {
151 const subscribe = processLike?.on;
152
153 if (!subscribe || !processLike) {
154 return null;
155 }
156
157 return new Promise((resolve) => {
158 const signals = ["SIGINT", "SIGTERM"] as const;
159 const listeners: Partial<Record<(typeof signals)[number], () => void>> = {};
160 const cleanup = () => {
161 if (!processLike.off) {
162 return;
163 }
164
165 for (const signal of signals) {
166 const listener = listeners[signal];
167
168 if (listener) {
169 processLike.off(signal, listener);
170 }
171 }
172 };
173
174 for (const signal of signals) {
175 const listener = () => {
176 cleanup();
177 resolve(signal);
178 };
179
180 listeners[signal] = listener;
181 subscribe.call(processLike, signal, listener);
182 }
183 });
184}
185
186function writeLine(writer: ClaudeCodedOutputWriter, line: string): void {
187 if ("write" in writer) {
188 writer.write(`${line}\n`);
189 return;
190 }
191
192 writer.log(line);
193}