|
1 | 1 | import fs from "node:fs"; |
| 2 | +import { spawn } from "node:child_process"; |
2 | 3 | import path from "node:path"; |
3 | 4 | import process from "node:process"; |
4 | 5 | import { repairNodePtySpawnHelperPermissions } from "../runner/ptyPreflight.js"; |
| 6 | +import { extractCodexExecResponse } from "../bot/formatter.js"; |
5 | 7 |
|
6 | 8 | function makeCheck(name, status, detail) { |
7 | 9 | return { name, status, detail }; |
@@ -68,6 +70,96 @@ function checkWritableDirectory(name, directoryPath) { |
68 | 70 | } |
69 | 71 | } |
70 | 72 |
|
| 73 | +function runCliCodexLiveCheck(config) { |
| 74 | + return new Promise((resolve, reject) => { |
| 75 | + const prompt = "Reply with exactly: HEALTHCHECK_OK"; |
| 76 | + const proc = spawn( |
| 77 | + config.runner.command, |
| 78 | + [...(config.runner.args || []), "exec", prompt], |
| 79 | + { |
| 80 | + cwd: config.runner.cwd, |
| 81 | + env: process.env |
| 82 | + } |
| 83 | + ); |
| 84 | + |
| 85 | + let stdout = ""; |
| 86 | + let stderr = ""; |
| 87 | + |
| 88 | + proc.stdout?.on("data", (chunk) => { |
| 89 | + stdout += String(chunk || ""); |
| 90 | + }); |
| 91 | + proc.stderr?.on("data", (chunk) => { |
| 92 | + stderr += String(chunk || ""); |
| 93 | + }); |
| 94 | + proc.on("error", reject); |
| 95 | + proc.on("close", (code, signal) => { |
| 96 | + const output = extractCodexExecResponse(`${stdout}\n${stderr}`).trim(); |
| 97 | + if (code !== 0) { |
| 98 | + reject( |
| 99 | + new Error( |
| 100 | + `CLI health check exited with code ${code}, signal ${signal || "none"}` |
| 101 | + ) |
| 102 | + ); |
| 103 | + return; |
| 104 | + } |
| 105 | + |
| 106 | + if (output !== "HEALTHCHECK_OK") { |
| 107 | + reject( |
| 108 | + new Error(`Unexpected CLI response: ${output || "(empty output)"}`) |
| 109 | + ); |
| 110 | + return; |
| 111 | + } |
| 112 | + |
| 113 | + resolve({ |
| 114 | + backend: "cli", |
| 115 | + output |
| 116 | + }); |
| 117 | + }); |
| 118 | + }); |
| 119 | +} |
| 120 | + |
| 121 | +async function runSdkCodexLiveCheck(config) { |
| 122 | + const { Codex } = await import("@openai/codex-sdk"); |
| 123 | + const codex = new Codex({ |
| 124 | + config: config.runner.sdkConfig |
| 125 | + }); |
| 126 | + const thread = codex.startThread({ |
| 127 | + workingDirectory: config.runner.cwd, |
| 128 | + skipGitRepoCheck: config.runner.sdkThreadOptions.skipGitRepoCheck, |
| 129 | + approvalPolicy: config.runner.sdkThreadOptions.approvalPolicy, |
| 130 | + sandboxMode: config.runner.sdkThreadOptions.sandboxMode, |
| 131 | + modelReasoningEffort: config.runner.sdkThreadOptions.modelReasoningEffort, |
| 132 | + networkAccessEnabled: config.runner.sdkThreadOptions.networkAccessEnabled, |
| 133 | + webSearchMode: config.runner.sdkThreadOptions.webSearchMode, |
| 134 | + additionalDirectories: config.runner.sdkThreadOptions.additionalDirectories |
| 135 | + }); |
| 136 | + const turn = await thread.run("Reply with exactly: HEALTHCHECK_OK"); |
| 137 | + |
| 138 | + if (turn.finalResponse.trim() !== "HEALTHCHECK_OK") { |
| 139 | + throw new Error( |
| 140 | + `Unexpected SDK response: ${turn.finalResponse.trim() || "(empty output)"}` |
| 141 | + ); |
| 142 | + } |
| 143 | + |
| 144 | + return { |
| 145 | + backend: "sdk", |
| 146 | + threadId: thread.id, |
| 147 | + output: turn.finalResponse.trim() |
| 148 | + }; |
| 149 | +} |
| 150 | + |
| 151 | +async function runCodexLiveCheck(config, options = {}) { |
| 152 | + if (typeof options.codexLiveRunner === "function") { |
| 153 | + return options.codexLiveRunner(config); |
| 154 | + } |
| 155 | + |
| 156 | + if (config.runner.backend === "sdk") { |
| 157 | + return runSdkCodexLiveCheck(config); |
| 158 | + } |
| 159 | + |
| 160 | + return runCliCodexLiveCheck(config); |
| 161 | +} |
| 162 | + |
71 | 163 | export async function runHealthcheck(config, options = {}) { |
72 | 164 | const strict = Boolean(options.strict); |
73 | 165 | const env = options.env || process.env; |
@@ -144,6 +236,30 @@ export async function runHealthcheck(config, options = {}) { |
144 | 236 | } |
145 | 237 | } |
146 | 238 |
|
| 239 | + const codexLiveCheck = Boolean(options.codexLiveCheck); |
| 240 | + if (codexLiveCheck) { |
| 241 | + try { |
| 242 | + const result = await runCodexLiveCheck(config, options); |
| 243 | + checks.push( |
| 244 | + makeCheck( |
| 245 | + "codex live", |
| 246 | + "pass", |
| 247 | + `${result.backend} backend responded with ${result.output}${ |
| 248 | + result.threadId ? ` (thread ${result.threadId})` : "" |
| 249 | + }` |
| 250 | + ) |
| 251 | + ); |
| 252 | + } catch (error) { |
| 253 | + checks.push( |
| 254 | + makeCheck( |
| 255 | + "codex live", |
| 256 | + "fail", |
| 257 | + error instanceof Error ? error.message : String(error) |
| 258 | + ) |
| 259 | + ); |
| 260 | + } |
| 261 | + } |
| 262 | + |
147 | 263 | const failed = checks.filter((check) => check.status === "fail"); |
148 | 264 | const warned = checks.filter((check) => check.status === "warn"); |
149 | 265 |
|
|
0 commit comments