baa-conductor


baa-conductor / apps / status-api / src
im_wower  ·  2026-03-25

cli.ts

  1import {
  2  getDefaultStatusApiHost,
  3  getDefaultStatusApiPort,
  4  runStatusApiSmokeCheck,
  5  startStatusApiServer,
  6} from "./host.js";
  7import type { StatusApiEnvironment } from "./contracts.js";
  8
  9type StatusApiCliAction = "help" | "serve" | "smoke";
 10
 11export interface StatusApiTextWriter {
 12  write(chunk: string): unknown;
 13}
 14
 15export interface RunStatusApiCliOptions {
 16  argv?: readonly string[];
 17  env?: StatusApiEnvironment;
 18  stderr?: StatusApiTextWriter;
 19  stdout?: StatusApiTextWriter;
 20}
 21
 22interface StatusApiCliCommand {
 23  action: StatusApiCliAction;
 24  host?: string;
 25  port?: number;
 26}
 27
 28export async function runStatusApiCli(options: RunStatusApiCliOptions = {}): Promise<number> {
 29  const stdout = toTextWriter(options.stdout, console.log);
 30  const stderr = toTextWriter(options.stderr, console.error);
 31  const command = parseStatusApiCliCommand(options.argv ?? process?.argv ?? [], options.env ?? process?.env ?? {});
 32
 33  switch (command.action) {
 34    case "help":
 35      stdout.write(renderStatusApiCliHelp());
 36      return 0;
 37
 38    case "serve": {
 39      const server = await startStatusApiServer({
 40        env: options.env ?? process?.env ?? {},
 41        host: command.host,
 42        port: command.port
 43      });
 44      const baseUrl =
 45        server.getBaseUrl() ??
 46        `http://${command.host ?? getDefaultStatusApiHost()}:${command.port ?? getDefaultStatusApiPort()}`;
 47
 48      stdout.write(`Status API listening on ${baseUrl}\n`);
 49
 50      for (const route of server.describeSurface()) {
 51        stdout.write(`- ${route}\n`);
 52      }
 53
 54      return 0;
 55    }
 56
 57    case "smoke": {
 58      const result = await runStatusApiSmokeCheck({
 59        env: options.env ?? process?.env ?? {},
 60        host: command.host ?? getDefaultStatusApiHost(),
 61        port: command.port ?? 0
 62      });
 63
 64      stdout.write(`Smoke OK on ${result.baseUrl}\n`);
 65
 66      for (const check of result.checks) {
 67        stdout.write(`- ${check.status} ${check.path} ${check.detail}\n`);
 68      }
 69
 70      return 0;
 71    }
 72  }
 73}
 74
 75function parseStatusApiCliCommand(argv: readonly string[], env: StatusApiEnvironment): StatusApiCliCommand {
 76  const tokens = argv.slice(2);
 77  let action: StatusApiCliAction = "serve";
 78  let actionSet = false;
 79  let host: string | undefined;
 80  let port: number | undefined;
 81
 82  for (let index = 0; index < tokens.length; index += 1) {
 83    const token = tokens[index];
 84
 85    if (token == null) {
 86      continue;
 87    }
 88
 89    if (token === "--help" || token === "-h" || token === "help") {
 90      return { action: "help" };
 91    }
 92
 93    if (token === "--host") {
 94      host = readCliValue(tokens, index, "--host");
 95      index += 1;
 96      continue;
 97    }
 98
 99    if (token === "--port") {
100      port = parseCliPort(readCliValue(tokens, index, "--port"));
101      index += 1;
102      continue;
103    }
104
105    if (token.startsWith("--")) {
106      throw new Error(`Unknown status-api flag "${token}".`);
107    }
108
109    if (actionSet) {
110      throw new Error(`Unexpected extra status-api argument "${token}".`);
111    }
112
113    if (!isStatusApiCliAction(token)) {
114      throw new Error(`Unknown status-api action "${token}".`);
115    }
116
117    action = token;
118    actionSet = true;
119  }
120
121  return {
122    action,
123    host: host ?? env.BAA_STATUS_API_HOST ?? env.HOST,
124    port: port
125  };
126}
127
128function isStatusApiCliAction(value: string): value is Exclude<StatusApiCliAction, "help"> {
129  return value === "serve" || value === "smoke";
130}
131
132function readCliValue(tokens: readonly string[], index: number, flag: string): string {
133  const value = tokens[index + 1];
134
135  if (value == null || value.startsWith("--")) {
136    throw new Error(`Missing value for ${flag}.`);
137  }
138
139  return value;
140}
141
142function parseCliPort(value: string): number {
143  const port = Number(value);
144
145  if (!Number.isInteger(port) || port < 0 || port > 65_535) {
146    throw new Error(`Invalid --port value "${value}".`);
147  }
148
149  return port;
150}
151
152function renderStatusApiCliHelp(): string {
153  return [
154    "Usage: node apps/status-api/dist/index.js [serve|smoke|help] [--host <host>] [--port <port>]",
155    "",
156    `Default listen address: http://${getDefaultStatusApiHost()}:${getDefaultStatusApiPort()}`,
157    "Default truth source: BAA_CONDUCTOR_LOCAL_API or http://100.71.210.78:4317",
158    "Legacy ad-hoc override: BAA_CONTROL_API_BASE",
159    "Routes:",
160    "- GET /healthz",
161    "- GET /describe",
162    "- GET /v1/status",
163    "- GET /v1/status/ui",
164    "",
165    "Examples:",
166    "- pnpm --filter @baa-conductor/status-api serve",
167    "- pnpm --filter @baa-conductor/status-api smoke",
168    "- node apps/status-api/dist/index.js --host 127.0.0.1 --port 4318"
169  ].join("\n") + "\n";
170}
171
172function toTextWriter(
173  writer: StatusApiTextWriter | undefined,
174  fallback: (message?: unknown, ...optionalParams: unknown[]) => void
175): StatusApiTextWriter {
176  if (writer != null) {
177    return writer;
178  }
179
180  return {
181    write(chunk: string) {
182      fallback(chunk.endsWith("\n") ? chunk.slice(0, -1) : chunk);
183    }
184  };
185}