baa-conductor


baa-conductor / apps / claude-coded / src
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}