| 188 | // finishes in <2s. The bump to 300s covers CI cold-start without papering over |
| 189 | // genuine readiness failures — 5 minutes is still far above any realistic boot. |
| 190 | async function waitForReady(url: string, timeoutMs = 300_000): Promise<void> { |
| 191 | const deadline = Date.now() + timeoutMs; |
| 192 | const FETCH_TIMEOUT_MS = 2_000; |
| 193 | let lastFetchErr: unknown = null; |
| 194 | let fetchAttempts = 0; |
| 195 | |
| 196 | while (Date.now() < deadline) { |
| 197 | try { |
| 198 | fetchAttempts++; |
| 199 | const res = await fetch(`${url}/doc`, { |
| 200 | method: "GET", |
| 201 | signal: AbortSignal.timeout(FETCH_TIMEOUT_MS), |
| 202 | }); |
| 203 | if (res.ok || res.status === 404 || res.status === 401) { |
| 204 | // Server is responding — any HTTP response means it booted. |
| 205 | return; |
| 206 | } |
| 207 | } catch (err) { |
| 208 | lastFetchErr = err; |
| 209 | } |
| 210 | await Bun.sleep(200); |
| 211 | } |
| 212 | throw new Error( |
| 213 | `opencode serve did not become ready in ${timeoutMs}ms.\n` + |
| 214 | ` url=${url}/doc\n` + |
| 215 | ` fetchAttempts=${fetchAttempts}\n` + |
| 216 | ` fetchLastErr=${String(lastFetchErr)}`, |
| 217 | ); |
| 218 | } |
| 219 | |
| 220 | export async function spawnOpencode(opts: SpawnOptions): Promise<SpawnedOpencode> { |
| 221 | const env = createIsolatedEnv(); |