(state: ServerState, command: string, args: string[], retries = 0)
| 525 | |
| 526 | // ─── Command Dispatch ────────────────────────────────────────── |
| 527 | async function sendCommand(state: ServerState, command: string, args: string[], retries = 0): Promise<void> { |
| 528 | // Precedence: CLI --tab-id flag > BROWSE_TAB env var. |
| 529 | // make-pdf always passes --tab-id; human users typically rely on BROWSE_TAB |
| 530 | // (set by sidebar-agent per-tab) or the active tab. |
| 531 | const extracted = extractTabId(args); |
| 532 | args = extracted.args; |
| 533 | const envTab = process.env.BROWSE_TAB; |
| 534 | const tabId = extracted.tabId ?? (envTab ? parseInt(envTab, 10) : undefined); |
| 535 | const body = JSON.stringify({ command, args, ...(tabId !== undefined && !isNaN(tabId) ? { tabId } : {}) }); |
| 536 | |
| 537 | try { |
| 538 | const resp = await fetch(`http://127.0.0.1:${state.port}/command`, { |
| 539 | method: 'POST', |
| 540 | headers: { |
| 541 | 'Content-Type': 'application/json', |
| 542 | 'Authorization': `Bearer ${state.token}`, |
| 543 | }, |
| 544 | body, |
| 545 | signal: AbortSignal.timeout(30000), |
| 546 | }); |
| 547 | |
| 548 | if (resp.status === 401) { |
| 549 | // Token mismatch — server may have restarted |
| 550 | console.error('[browse] Auth failed — server may have restarted. Retrying...'); |
| 551 | const newState = readState(); |
| 552 | if (newState && newState.token !== state.token) { |
| 553 | return sendCommand(newState, command, args); |
| 554 | } |
| 555 | throw new Error('Authentication failed'); |
| 556 | } |
| 557 | |
| 558 | const text = await resp.text(); |
| 559 | |
| 560 | if (resp.ok) { |
| 561 | process.stdout.write(text); |
| 562 | if (!text.endsWith('\n')) process.stdout.write('\n'); |
| 563 | } else { |
| 564 | // Try to parse as JSON error |
| 565 | try { |
| 566 | const err = JSON.parse(text); |
| 567 | console.error(err.error || text); |
| 568 | if (err.hint) console.error(err.hint); |
| 569 | } catch { |
| 570 | console.error(text); |
| 571 | } |
| 572 | process.exit(1); |
| 573 | } |
| 574 | } catch (err: any) { |
| 575 | if (err.name === 'AbortError') { |
| 576 | // #1781: a 30s timeout on a heavy page usually means busy, not dead. |
| 577 | // Don't kill a live server (that's what triggered the crash-loop) — report |
| 578 | // and exit so the user can retry rather than losing their (headed) window. |
| 579 | const ts = readState(); |
| 580 | const alive = ts?.pid ? isProcessAlive(ts.pid) : false; |
| 581 | console.error(alive |
| 582 | ? '[browse] Command timed out after 30s (server still alive — busy, not restarting). Retry, or raise load.' |
| 583 | : '[browse] Command timed out after 30s'); |
| 584 | process.exit(1); |
no test coverage detected