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}