authorgravatar for julius0216@outlook.comjulius <julius0216@outlook.com> 2026-03-04 04:34:08-08:00
committergravatar for jmarminge@gmail.comJulius Marminge <jmarminge@gmail.com> 2026-03-03 21:03:16-08:00
log64f2c3a46565d8cd401351072b5b11494b5eccd0
tree9dd2209c33d372b9bc48fbacc671d2155298da55
parent0a1233344896376ec8b31b79ee65500eeef3a09e

effectify


2 files changed, 347 insertions(+), 246 deletions(-)

apps/server/src/provider/Layers/ProviderHealth.test.ts+185-117
......@@ -1,132 +1,200 @@
1import { describe, expect, it } from "vitest";
1import { assert, it } from "@effect/vitest";
2import { Effect, Layer, Sink, Stream } from "effect";
3import * as PlatformError from "effect/PlatformError";
4import { ChildProcessSpawner } from "effect/unstable/process";
5import { expect } from "vitest";
26
3import type { ProcessRunResult } from "../../processRunner";
4import { checkProviderStatusesOnStartup } from "./ProviderHealth";
7import { checkCodexProviderStatus, parseAuthStatusFromOutput } from "./ProviderHealth";
58
6function result(input: Partial<ProcessRunResult>): ProcessRunResult {
7 return {
8 stdout: "",
9 stderr: "",
10 code: 0,
11 signal: null,
12 timedOut: false,
13 stdoutTruncated: false,
14 stderrTruncated: false,
15 ...input,
16 };
9// ── Test helpers ────────────────────────────────────────────────────
10
11const encoder = new TextEncoder();
12
13function mockHandle(result: { stdout: string; stderr: string; code: number }) {
14 return ChildProcessSpawner.makeHandle({
15 pid: ChildProcessSpawner.ProcessId(1),
16 exitCode: Effect.succeed(ChildProcessSpawner.ExitCode(result.code)),
17 isRunning: Effect.succeed(false),
18 kill: () => Effect.void,
19 stdin: Sink.drain,
20 stdout: Stream.make(encoder.encode(result.stdout)),
21 stderr: Stream.make(encoder.encode(result.stderr)),
22 all: Stream.empty,
23 getInputFd: () => Sink.drain,
24 getOutputFd: () => Stream.empty,
25 });
26}
27
28function mockSpawnerLayer(
29 handler: (args: ReadonlyArray<string>) => { stdout: string; stderr: string; code: number },
30) {
31 return Layer.succeed(
32 ChildProcessSpawner.ChildProcessSpawner,
33 ChildProcessSpawner.make((command) => {
34 const cmd = command as unknown as { args: ReadonlyArray<string> };
35 return Effect.succeed(mockHandle(handler(cmd.args)));
36 }),
37 );
38}
39
40function failingSpawnerLayer(description: string) {
41 return Layer.succeed(
42 ChildProcessSpawner.ChildProcessSpawner,
43 ChildProcessSpawner.make(() =>
44 Effect.fail(
45 PlatformError.systemError({
46 _tag: "NotFound",
47 module: "ChildProcess",
48 method: "spawn",
49 description,
50 }),
51 ),
52 ),
53 );
1754}
1855
19describe("checkProviderStatusesOnStartup", () => {
20 it("returns ready when codex is installed and authenticated", async () => {
21 const status = await checkProviderStatusesOnStartup(async (_command, args) => {
22 if (args.join(" ") === "--version") {
23 return result({ stdout: "codex 1.0.0\n" });
24 }
25 if (args.join(" ") === "login status") {
26 return result({ stdout: "Logged in\n" });
27 }
28 throw new Error(`Unexpected command: ${args.join(" ")}`);
29 });
30
31 expect(status).toEqual([
32 expect.objectContaining({
33 provider: "codex",
34 status: "ready",
35 available: true,
36 authStatus: "authenticated",
56// ── Tests ───────────────────────────────────────────────────────────
57
58it.effect("returns ready when codex is installed and authenticated", () =>
59 checkCodexProviderStatus.pipe(
60 Effect.map((status) => {
61 expect(status).toEqual(
62 expect.objectContaining({
63 provider: "codex",
64 status: "ready",
65 available: true,
66 authStatus: "authenticated",
67 }),
68 );
69 }),
70 Effect.provide(
71 mockSpawnerLayer((args) => {
72 const joined = args.join(" ");
73 if (joined === "--version") return { stdout: "codex 1.0.0\n", stderr: "", code: 0 };
74 if (joined === "login status") return { stdout: "Logged in\n", stderr: "", code: 0 };
75 throw new Error(`Unexpected args: ${joined}`);
3776 }),
38 ]);
39 });
77 ),
78 ),
79);
80
81it.effect("returns unavailable when codex is missing", () =>
82 checkCodexProviderStatus.pipe(
83 Effect.map((status) => {
84 expect(status).toEqual(
85 expect.objectContaining({
86 provider: "codex",
87 status: "error",
88 available: false,
89 authStatus: "unknown",
90 message: "Codex CLI (`codex`) is not installed or not on PATH.",
91 }),
92 );
93 }),
94 Effect.provide(failingSpawnerLayer("spawn codex ENOENT")),
95 ),
96);
4097
41 it("returns unavailable when codex is missing", async () => {
42 const status = await checkProviderStatusesOnStartup(async () => {
43 throw new Error("Command not found: codex");
44 });
45
46 expect(status).toEqual([
47 expect.objectContaining({
48 provider: "codex",
49 status: "error",
50 available: false,
51 authStatus: "unknown",
52 message: "Codex CLI (`codex`) is not installed or not on PATH.",
98it.effect("returns unauthenticated when auth probe reports login required", () =>
99 checkCodexProviderStatus.pipe(
100 Effect.map((status) => {
101 expect(status).toEqual(
102 expect.objectContaining({
103 provider: "codex",
104 status: "error",
105 available: true,
106 authStatus: "unauthenticated",
107 message: "Codex CLI is not authenticated. Run `codex login` and try again.",
108 }),
109 );
110 }),
111 Effect.provide(
112 mockSpawnerLayer((args) => {
113 const joined = args.join(" ");
114 if (joined === "--version") return { stdout: "codex 1.0.0\n", stderr: "", code: 0 };
115 if (joined === "login status") {
116 return { stdout: "", stderr: "Not logged in. Run codex login.", code: 1 };
117 }
118 throw new Error(`Unexpected args: ${joined}`);
53119 }),
54 ]);
55 });
120 ),
121 ),
122);
56123
57 it("returns unauthenticated when auth probe reports login required", async () => {
58 const status = await checkProviderStatusesOnStartup(async (_command, args) => {
59 if (args.join(" ") === "--version") {
60 return result({ stdout: "codex 1.0.0\n" });
61 }
62 if (args.join(" ") === "login status") {
63 return result({
64 code: 1,
65 stderr: "Not logged in. Run codex login.",
66 });
67 }
68 throw new Error(`Unexpected command: ${args.join(" ")}`);
69 });
70
71 expect(status).toEqual([
72 expect.objectContaining({
73 provider: "codex",
74 status: "error",
75 available: true,
76 authStatus: "unauthenticated",
77 message: "Codex CLI is not authenticated. Run `codex login` and try again.",
124it.effect("returns unauthenticated when login status output includes 'not logged in'", () =>
125 checkCodexProviderStatus.pipe(
126 Effect.map((status) => {
127 expect(status).toEqual(
128 expect.objectContaining({
129 provider: "codex",
130 status: "error",
131 available: true,
132 authStatus: "unauthenticated",
133 message: "Codex CLI is not authenticated. Run `codex login` and try again.",
134 }),
135 );
136 }),
137 Effect.provide(
138 mockSpawnerLayer((args) => {
139 const joined = args.join(" ");
140 if (joined === "--version") return { stdout: "codex 1.0.0\n", stderr: "", code: 0 };
141 if (joined === "login status") return { stdout: "Not logged in\n", stderr: "", code: 1 };
142 throw new Error(`Unexpected args: ${joined}`);
78143 }),
79 ]);
80 });
144 ),
145 ),
146);
81147
82 it("returns unauthenticated when login status output includes 'not logged in'", async () => {
83 const status = await checkProviderStatusesOnStartup(async (_command, args) => {
84 if (args.join(" ") === "--version") {
85 return result({ stdout: "codex 1.0.0\n" });
86 }
87 if (args.join(" ") === "login status") {
88 return result({
89 code: 1,
90 stdout: "Not logged in\n",
91 });
92 }
93 throw new Error(`Unexpected command: ${args.join(" ")}`);
94 });
95
96 expect(status).toEqual([
97 expect.objectContaining({
98 provider: "codex",
99 status: "error",
100 available: true,
101 authStatus: "unauthenticated",
102 message: "Codex CLI is not authenticated. Run `codex login` and try again.",
148it.effect("returns warning when login status command is unsupported", () =>
149 checkCodexProviderStatus.pipe(
150 Effect.map((status) => {
151 expect(status).toEqual(
152 expect.objectContaining({
153 provider: "codex",
154 status: "warning",
155 available: true,
156 authStatus: "unknown",
157 message: "Codex CLI authentication status command is unavailable in this Codex version.",
158 }),
159 );
160 }),
161 Effect.provide(
162 mockSpawnerLayer((args) => {
163 const joined = args.join(" ");
164 if (joined === "--version") return { stdout: "codex 1.0.0\n", stderr: "", code: 0 };
165 if (joined === "login status") {
166 return { stdout: "", stderr: "error: unknown command 'login'", code: 2 };
167 }
168 throw new Error(`Unexpected args: ${joined}`);
103169 }),
104 ]);
170 ),
171 ),
172);
173
174// ── Pure function tests ─────────────────────────────────────────────
175
176it("parseAuthStatusFromOutput: exit code 0 with no auth markers is ready", () => {
177 const parsed = parseAuthStatusFromOutput({ stdout: "OK\n", stderr: "", code: 0 });
178 assert.strictEqual(parsed.status, "ready");
179 assert.strictEqual(parsed.authStatus, "authenticated");
180});
181
182it("parseAuthStatusFromOutput: JSON with authenticated=false is unauthenticated", () => {
183 const parsed = parseAuthStatusFromOutput({
184 stdout: '[{"authenticated":false}]\n',
185 stderr: "",
186 code: 0,
105187 });
188 assert.strictEqual(parsed.status, "error");
189 assert.strictEqual(parsed.authStatus, "unauthenticated");
190});
106191
107 it("returns warning when login status command is unsupported", async () => {
108 const status = await checkProviderStatusesOnStartup(async (_command, args) => {
109 if (args.join(" ") === "--version") {
110 return result({ stdout: "codex 1.0.0\n" });
111 }
112 if (args.join(" ") === "login status") {
113 return result({
114 code: 2,
115 stderr: "error: unknown command 'login'",
116 });
117 }
118 throw new Error(`Unexpected command: ${args.join(" ")}`);
119 });
120
121 expect(status).toEqual([
122 expect.objectContaining({
123 provider: "codex",
124 status: "warning",
125 available: true,
126 authStatus: "unknown",
127 message:
128 "Codex CLI authentication status command is unavailable in this Codex version.",
129 }),
130 ]);
192it("parseAuthStatusFromOutput: JSON without auth marker is warning", () => {
193 const parsed = parseAuthStatusFromOutput({
194 stdout: '[{"ok":true}]\n',
195 stderr: "",
196 code: 0,
131197 });
198 assert.strictEqual(parsed.status, "warning");
199 assert.strictEqual(parsed.authStatus, "unknown");
132200});
apps/server/src/provider/Layers/ProviderHealth.ts+162-129
......@@ -4,6 +4,8 @@
44 * Performs one-time provider readiness probes when the server starts and
55 * keeps the resulting snapshot in memory for `server.getConfig`.
66 *
7 * Uses effect's ChildProcessSpawner to run CLI probes natively.
8 *
79 * @module ProviderHealthLive
810 */
911import type {
......@@ -11,43 +13,48 @@ import type {
1113 ServerProviderStatus,
1214 ServerProviderStatusState,
1315} from "@t3tools/contracts";
14import { Effect, Layer } from "effect";
16import { Effect, Layer, Option, Result, Stream } from "effect";
17import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process";
1518
16import { type ProcessRunOptions, type ProcessRunResult, runProcess } from "../../processRunner";
1719import { ProviderHealth, type ProviderHealthShape } from "../Services/ProviderHealth";
1820
19type CommandRunner = (
20 command: string,
21 args: readonly string[],
22 options?: ProcessRunOptions,
23) => Promise<ProcessRunResult>;
24
2521const DEFAULT_TIMEOUT_MS = 4_000;
2622const CODEX_PROVIDER = "codex" as const;
2723
28function nonEmptyMessage(value: string | undefined): string | undefined {
24// ── Pure helpers ────────────────────────────────────────────────────
25
26export interface CommandResult {
27 readonly stdout: string;
28 readonly stderr: string;
29 readonly code: number;
30}
31
32function nonEmptyTrimmed(value: string | undefined): string | undefined {
2933 if (!value) return undefined;
3034 const trimmed = value.trim();
3135 return trimmed.length > 0 ? trimmed : undefined;
3236}
3337
34function isCommandMissingError(error: unknown): boolean {
38function isCommandMissingCause(error: unknown): boolean {
3539 if (!(error instanceof Error)) return false;
3640 const lower = error.message.toLowerCase();
3741 return (
3842 lower.includes("command not found: codex") ||
3943 lower.includes("spawn codex enoent") ||
40 lower.includes("enoent")
44 lower.includes("enoent") ||
45 lower.includes("notfound")
4146 );
4247}
4348
44function detailFromResult(result: ProcessRunResult): string | undefined {
49function detailFromResult(
50 result: CommandResult & { readonly timedOut?: boolean },
51): string | undefined {
4552 if (result.timedOut) return "Timed out while running command.";
46 const stderr = nonEmptyMessage(result.stderr);
53 const stderr = nonEmptyTrimmed(result.stderr);
4754 if (stderr) return stderr;
48 const stdout = nonEmptyMessage(result.stdout);
55 const stdout = nonEmptyTrimmed(result.stdout);
4956 if (stdout) return stdout;
50 if (result.code !== null && result.code !== 0) {
57 if (result.code !== 0) {
5158 return `Command exited with code ${result.code}.`;
5259 }
5360 return undefined;
......@@ -57,70 +64,50 @@ function extractAuthBoolean(value: unknown): boolean | undefined {
5764 if (Array.isArray(value)) {
5865 for (const entry of value) {
5966 const nested = extractAuthBoolean(entry);
60 if (nested !== undefined) {
61 return nested;
62 }
67 if (nested !== undefined) return nested;
6368 }
6469 return undefined;
6570 }
6671
67 if (!value || typeof value !== "object") {
68 return undefined;
69 }
72 if (!value || typeof value !== "object") return undefined;
7073
7174 const record = value as Record<string, unknown>;
72 const directCandidates = [
73 "authenticated",
74 "isAuthenticated",
75 "loggedIn",
76 "isLoggedIn",
77 ] as const;
78 for (const key of directCandidates) {
79 const candidate = record[key];
80 if (typeof candidate === "boolean") {
81 return candidate;
82 }
75 for (const key of ["authenticated", "isAuthenticated", "loggedIn", "isLoggedIn"] as const) {
76 if (typeof record[key] === "boolean") return record[key];
8377 }
84
85 const nestedCandidates = ["auth", "status", "session", "account"] as const;
86 for (const key of nestedCandidates) {
78 for (const key of ["auth", "status", "session", "account"] as const) {
8779 const nested = extractAuthBoolean(record[key]);
88 if (nested !== undefined) {
89 return nested;
90 }
80 if (nested !== undefined) return nested;
9181 }
92
9382 return undefined;
9483}
9584
96function parseAuthStatusFromOutput(
97 result: ProcessRunResult,
98): {
85export function parseAuthStatusFromOutput(result: CommandResult): {
9986 readonly status: ServerProviderStatusState;
10087 readonly authStatus: ServerProviderAuthStatus;
10188 readonly message?: string;
10289} {
10390 const lowerOutput = `${result.stdout}\n${result.stderr}`.toLowerCase();
104 const unknownCommand =
91
92 if (
10593 lowerOutput.includes("unknown command") ||
10694 lowerOutput.includes("unrecognized command") ||
107 lowerOutput.includes("unexpected argument");
108 if (unknownCommand) {
95 lowerOutput.includes("unexpected argument")
96 ) {
10997 return {
11098 status: "warning",
11199 authStatus: "unknown",
112 message:
113 "Codex CLI authentication status command is unavailable in this Codex version.",
100 message: "Codex CLI authentication status command is unavailable in this Codex version.",
114101 };
115102 }
116103
117 const unauthenticatedSignal =
104 if (
118105 lowerOutput.includes("not logged in") ||
119106 lowerOutput.includes("login required") ||
120107 lowerOutput.includes("authentication required") ||
121108 lowerOutput.includes("run `codex login`") ||
122 lowerOutput.includes("run codex login");
123 if (unauthenticatedSignal) {
109 lowerOutput.includes("run codex login")
110 ) {
124111 return {
125112 status: "error",
126113 authStatus: "unauthenticated",
......@@ -128,27 +115,20 @@ function parseAuthStatusFromOutput(
128115 };
129116 }
130117
131 const parsedAuth =
132 (() => {
133 const trimmed = result.stdout.trim();
134 if (!trimmed || (!trimmed.startsWith("{") && !trimmed.startsWith("["))) {
135 return {
136 attemptedJsonParse: false as const,
137 auth: undefined as boolean | undefined,
138 };
139 }
140 try {
141 return {
142 attemptedJsonParse: true as const,
143 auth: extractAuthBoolean(JSON.parse(trimmed)),
144 };
145 } catch {
146 return {
147 attemptedJsonParse: false as const,
148 auth: undefined as boolean | undefined,
149 };
150 }
151 })();
118 const parsedAuth = (() => {
119 const trimmed = result.stdout.trim();
120 if (!trimmed || (!trimmed.startsWith("{") && !trimmed.startsWith("["))) {
121 return { attemptedJsonParse: false as const, auth: undefined as boolean | undefined };
122 }
123 try {
124 return {
125 attemptedJsonParse: true as const,
126 auth: extractAuthBoolean(JSON.parse(trimmed)),
127 };
128 } catch {
129 return { attemptedJsonParse: false as const, auth: undefined as boolean | undefined };
130 }
131 })();
152132
153133 if (parsedAuth.auth === true) {
154134 return { status: "ready", authStatus: "authenticated" };
......@@ -160,7 +140,6 @@ function parseAuthStatusFromOutput(
160140 message: "Codex CLI is not authenticated. Run `codex login` and try again.",
161141 };
162142 }
163
164143 if (parsedAuth.attemptedJsonParse) {
165144 return {
166145 status: "warning",
......@@ -169,9 +148,7 @@ function parseAuthStatusFromOutput(
169148 "Could not verify Codex authentication status from JSON output (missing auth marker).",
170149 };
171150 }
172
173151 if (result.code === 0) {
174 // Successful command with no explicit auth marker is still a pass.
175152 return { status: "ready", authStatus: "authenticated" };
176153 }
177154
......@@ -179,70 +156,110 @@ function parseAuthStatusFromOutput(
179156 return {
180157 status: "warning",
181158 authStatus: "unknown",
182 ...(detail
183 ? { message: `Could not verify Codex authentication status. ${detail}` }
184 : { message: "Could not verify Codex authentication status." }),
159 message: detail
160 ? `Could not verify Codex authentication status. ${detail}`
161 : "Could not verify Codex authentication status.",
185162 };
186163}
187164
188async function checkCodexProviderStatus(run: CommandRunner): Promise<ServerProviderStatus> {
189 const checkedAt = new Date().toISOString();
165// ── Effect-native command execution ─────────────────────────────────
166
167const collectStreamAsString = <E>(stream: Stream.Stream<Uint8Array, E>): Effect.Effect<string, E> =>
168 Stream.runFold(
169 stream,
170 () => "",
171 (acc, chunk) => acc + new TextDecoder().decode(chunk),
172 );
190173
191 try {
192 const version = await run("codex", ["--version"], {
193 timeoutMs: DEFAULT_TIMEOUT_MS,
194 allowNonZeroExit: true,
195 outputMode: "truncate",
174const runCodexCommand = (args: ReadonlyArray<string>) =>
175 Effect.gen(function* () {
176 const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;
177 const command = ChildProcess.make("codex", [...args], {
178 shell: process.platform === "win32",
196179 });
197180
198 if (version.code !== 0 || version.timedOut) {
199 const detail = detailFromResult(version);
200 return {
201 provider: CODEX_PROVIDER,
202 status: "error",
203 available: false,
204 authStatus: "unknown",
205 checkedAt,
206 message:
207 detail !== undefined
208 ? `Codex CLI is installed but failed to run. ${detail}`
209 : "Codex CLI is installed but failed to run.",
210 };
211 }
212 } catch (error) {
181 const child = yield* spawner.spawn(command);
182
183 const [stdout, stderr, exitCode] = yield* Effect.all(
184 [
185 collectStreamAsString(child.stdout),
186 collectStreamAsString(child.stderr),
187 child.exitCode.pipe(Effect.map(Number)),
188 ],
189 { concurrency: "unbounded" },
190 );
191
192 return { stdout, stderr, code: exitCode } satisfies CommandResult;
193 }).pipe(Effect.scoped);
194
195// ── Health check ────────────────────────────────────────────────────
196
197export const checkCodexProviderStatus: Effect.Effect<
198 ServerProviderStatus,
199 never,
200 ChildProcessSpawner.ChildProcessSpawner
201> = Effect.gen(function* () {
202 const checkedAt = new Date().toISOString();
203
204 // Probe 1: `codex --version` — is the CLI reachable?
205 const versionProbe = yield* runCodexCommand(["--version"]).pipe(
206 Effect.timeoutOption(DEFAULT_TIMEOUT_MS),
207 Effect.result,
208 );
209
210 if (Result.isFailure(versionProbe)) {
211 const error = versionProbe.failure;
213212 return {
214213 provider: CODEX_PROVIDER,
215 status: "error",
214 status: "error" as const,
216215 available: false,
217 authStatus: "unknown",
216 authStatus: "unknown" as const,
218217 checkedAt,
219 message: isCommandMissingError(error)
218 message: isCommandMissingCause(error)
220219 ? "Codex CLI (`codex`) is not installed or not on PATH."
221220 : `Failed to execute Codex CLI health check: ${error instanceof Error ? error.message : String(error)}.`,
222221 };
223222 }
224223
225 try {
226 const auth = await run("codex", ["login", "status"], {
227 timeoutMs: DEFAULT_TIMEOUT_MS,
228 allowNonZeroExit: true,
229 outputMode: "truncate",
230 });
231 const parsed = parseAuthStatusFromOutput(auth);
224 if (Option.isNone(versionProbe.success)) {
232225 return {
233226 provider: CODEX_PROVIDER,
234 status: parsed.status,
235 available: true,
236 authStatus: parsed.authStatus,
227 status: "error" as const,
228 available: false,
229 authStatus: "unknown" as const,
237230 checkedAt,
238 ...(parsed.message ? { message: parsed.message } : {}),
231 message: "Codex CLI is installed but failed to run. Timed out while running command.",
239232 };
240 } catch (error) {
233 }
234
235 const version = versionProbe.success.value;
236 if (version.code !== 0) {
237 const detail = detailFromResult(version);
241238 return {
242239 provider: CODEX_PROVIDER,
243 status: "warning",
240 status: "error" as const,
241 available: false,
242 authStatus: "unknown" as const,
243 checkedAt,
244 message: detail
245 ? `Codex CLI is installed but failed to run. ${detail}`
246 : "Codex CLI is installed but failed to run.",
247 };
248 }
249
250 // Probe 2: `codex login status` — is the user authenticated?
251 const authProbe = yield* runCodexCommand(["login", "status"]).pipe(
252 Effect.timeoutOption(DEFAULT_TIMEOUT_MS),
253 Effect.result,
254 );
255
256 if (Result.isFailure(authProbe)) {
257 const error = authProbe.failure;
258 return {
259 provider: CODEX_PROVIDER,
260 status: "warning" as const,
244261 available: true,
245 authStatus: "unknown",
262 authStatus: "unknown" as const,
246263 checkedAt,
247264 message:
248265 error instanceof Error
......@@ -250,21 +267,37 @@ async function checkCodexProviderStatus(run: CommandRunner): Promise<ServerProvi
250267 : "Could not verify Codex authentication status.",
251268 };
252269 }
253}
254270
255export async function checkProviderStatusesOnStartup(
256 run: CommandRunner = runProcess,
257): Promise<ReadonlyArray<ServerProviderStatus>> {
258 return [await checkCodexProviderStatus(run)];
259}
271 if (Option.isNone(authProbe.success)) {
272 return {
273 provider: CODEX_PROVIDER,
274 status: "warning" as const,
275 available: true,
276 authStatus: "unknown" as const,
277 checkedAt,
278 message: "Could not verify Codex authentication status. Timed out while running command.",
279 };
280 }
260281
261const makeProviderHealth = (run: CommandRunner = runProcess) =>
262 Effect.gen(function* () {
263 const statuses = yield* Effect.promise(() => checkProviderStatusesOnStartup(run));
282 const parsed = parseAuthStatusFromOutput(authProbe.success.value);
283 return {
284 provider: CODEX_PROVIDER,
285 status: parsed.status,
286 available: true,
287 authStatus: parsed.authStatus,
288 checkedAt,
289 ...(parsed.message ? { message: parsed.message } : {}),
290 } satisfies ServerProviderStatus;
291});
292
293// ── Layer ───────────────────────────────────────────────────────────
264294
295export const ProviderHealthLive = Layer.effect(
296 ProviderHealth,
297 Effect.gen(function* () {
298 const codexStatus = yield* checkCodexProviderStatus;
265299 return {
266 getStatuses: Effect.succeed(statuses),
300 getStatuses: Effect.succeed([codexStatus]),
267301 } satisfies ProviderHealthShape;
268 });
269
270export const ProviderHealthLive = Layer.effect(ProviderHealth, makeProviderHealth());
302 }),
303);