mstro-app 0.5.1 → 0.5.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/PRIVACY.md +9 -9
- package/README.md +71 -28
- package/bin/commands/config.js +1 -1
- package/bin/mstro.js +55 -4
- package/dist/server/cli/eta-estimator.d.ts +55 -0
- package/dist/server/cli/eta-estimator.d.ts.map +1 -0
- package/dist/server/cli/eta-estimator.js +222 -0
- package/dist/server/cli/eta-estimator.js.map +1 -0
- package/dist/server/cli/headless/claude-invoker-process.d.ts.map +1 -1
- package/dist/server/cli/headless/claude-invoker-process.js +9 -1
- package/dist/server/cli/headless/claude-invoker-process.js.map +1 -1
- package/dist/server/cli/headless/mcp-config.d.ts +22 -5
- package/dist/server/cli/headless/mcp-config.d.ts.map +1 -1
- package/dist/server/cli/headless/mcp-config.js +7 -5
- package/dist/server/cli/headless/mcp-config.js.map +1 -1
- package/dist/server/cli/headless/runner.d.ts.map +1 -1
- package/dist/server/cli/headless/runner.js +19 -0
- package/dist/server/cli/headless/runner.js.map +1 -1
- package/dist/server/cli/headless/stall-assessor.d.ts +50 -0
- package/dist/server/cli/headless/stall-assessor.d.ts.map +1 -1
- package/dist/server/cli/headless/stall-assessor.js +64 -9
- package/dist/server/cli/headless/stall-assessor.js.map +1 -1
- package/dist/server/cli/headless/tool-watchdog.d.ts +21 -0
- package/dist/server/cli/headless/tool-watchdog.d.ts.map +1 -1
- package/dist/server/cli/headless/tool-watchdog.js +19 -12
- package/dist/server/cli/headless/tool-watchdog.js.map +1 -1
- package/dist/server/cli/headless/types.d.ts +16 -1
- package/dist/server/cli/headless/types.d.ts.map +1 -1
- package/dist/server/cli/improvisation-history-store.d.ts.map +1 -1
- package/dist/server/cli/improvisation-history-store.js +5 -1
- package/dist/server/cli/improvisation-history-store.js.map +1 -1
- package/dist/server/cli/improvisation-output-queue.d.ts +5 -1
- package/dist/server/cli/improvisation-output-queue.d.ts.map +1 -1
- package/dist/server/cli/improvisation-output-queue.js +30 -7
- package/dist/server/cli/improvisation-output-queue.js.map +1 -1
- package/dist/server/cli/improvisation-session-manager.d.ts +35 -0
- package/dist/server/cli/improvisation-session-manager.d.ts.map +1 -1
- package/dist/server/cli/improvisation-session-manager.js +58 -1
- package/dist/server/cli/improvisation-session-manager.js.map +1 -1
- package/dist/server/cli/improvisation-types.d.ts +9 -0
- package/dist/server/cli/improvisation-types.d.ts.map +1 -1
- package/dist/server/cli/improvisation-types.js.map +1 -1
- package/dist/server/cli/retry/retry-runner-factory.d.ts.map +1 -1
- package/dist/server/cli/retry/retry-runner-factory.js +1 -0
- package/dist/server/cli/retry/retry-runner-factory.js.map +1 -1
- package/dist/server/engines/EngineEvent.d.ts +126 -0
- package/dist/server/engines/EngineEvent.d.ts.map +1 -0
- package/dist/server/engines/EngineEvent.js +11 -0
- package/dist/server/engines/EngineEvent.js.map +1 -0
- package/dist/server/engines/claude/ClaudeCodeEngine.d.ts +47 -0
- package/dist/server/engines/claude/ClaudeCodeEngine.d.ts.map +1 -0
- package/dist/server/engines/claude/ClaudeCodeEngine.js +338 -0
- package/dist/server/engines/claude/ClaudeCodeEngine.js.map +1 -0
- package/dist/server/engines/factory.d.ts +21 -0
- package/dist/server/engines/factory.d.ts.map +1 -0
- package/dist/server/engines/factory.js +152 -0
- package/dist/server/engines/factory.js.map +1 -0
- package/dist/server/engines/opencode/OpenCodeEngine.d.ts +148 -0
- package/dist/server/engines/opencode/OpenCodeEngine.d.ts.map +1 -0
- package/dist/server/engines/opencode/OpenCodeEngine.js +630 -0
- package/dist/server/engines/opencode/OpenCodeEngine.js.map +1 -0
- package/dist/server/engines/opencode/OpenCodeServerManager.d.ts +172 -0
- package/dist/server/engines/opencode/OpenCodeServerManager.d.ts.map +1 -0
- package/dist/server/engines/opencode/OpenCodeServerManager.js +390 -0
- package/dist/server/engines/opencode/OpenCodeServerManager.js.map +1 -0
- package/dist/server/engines/opencode/model-catalog.d.ts +94 -0
- package/dist/server/engines/opencode/model-catalog.d.ts.map +1 -0
- package/dist/server/engines/opencode/model-catalog.js +141 -0
- package/dist/server/engines/opencode/model-catalog.js.map +1 -0
- package/dist/server/engines/types.d.ts +146 -0
- package/dist/server/engines/types.d.ts.map +1 -0
- package/dist/server/engines/types.js +4 -0
- package/dist/server/engines/types.js.map +1 -0
- package/dist/server/index.js +9 -2
- package/dist/server/index.js.map +1 -1
- package/dist/server/mcp/bouncer-haiku.d.ts +17 -4
- package/dist/server/mcp/bouncer-haiku.d.ts.map +1 -1
- package/dist/server/mcp/bouncer-haiku.js +8 -124
- package/dist/server/mcp/bouncer-haiku.js.map +1 -1
- package/dist/server/mcp/bouncer-integration.d.ts +45 -0
- package/dist/server/mcp/bouncer-integration.d.ts.map +1 -1
- package/dist/server/mcp/bouncer-integration.js +69 -5
- package/dist/server/mcp/bouncer-integration.js.map +1 -1
- package/dist/server/mcp/classifier/BouncerClassifier.d.ts +34 -0
- package/dist/server/mcp/classifier/BouncerClassifier.d.ts.map +1 -0
- package/dist/server/mcp/classifier/BouncerClassifier.js +4 -0
- package/dist/server/mcp/classifier/BouncerClassifier.js.map +1 -0
- package/dist/server/mcp/classifier/ClaudeBouncerClassifier.d.ts +17 -0
- package/dist/server/mcp/classifier/ClaudeBouncerClassifier.d.ts.map +1 -0
- package/dist/server/mcp/classifier/ClaudeBouncerClassifier.js +142 -0
- package/dist/server/mcp/classifier/ClaudeBouncerClassifier.js.map +1 -0
- package/dist/server/mcp/classifier/OpenCodeBouncerClassifier.d.ts +68 -0
- package/dist/server/mcp/classifier/OpenCodeBouncerClassifier.d.ts.map +1 -0
- package/dist/server/mcp/classifier/OpenCodeBouncerClassifier.js +182 -0
- package/dist/server/mcp/classifier/OpenCodeBouncerClassifier.js.map +1 -0
- package/dist/server/mcp/classifier/factory.d.ts +70 -0
- package/dist/server/mcp/classifier/factory.d.ts.map +1 -0
- package/dist/server/mcp/classifier/factory.js +155 -0
- package/dist/server/mcp/classifier/factory.js.map +1 -0
- package/dist/server/mcp/server.js +52 -0
- package/dist/server/mcp/server.js.map +1 -1
- package/dist/server/routes/index.d.ts +1 -0
- package/dist/server/routes/index.d.ts.map +1 -1
- package/dist/server/routes/index.js +1 -0
- package/dist/server/routes/index.js.map +1 -1
- package/dist/server/routes/internal.d.ts +16 -0
- package/dist/server/routes/internal.d.ts.map +1 -0
- package/dist/server/routes/internal.js +94 -0
- package/dist/server/routes/internal.js.map +1 -0
- package/dist/server/services/plan/agent-resolver.d.ts +26 -0
- package/dist/server/services/plan/agent-resolver.d.ts.map +1 -0
- package/dist/server/services/plan/agent-resolver.js +102 -0
- package/dist/server/services/plan/agent-resolver.js.map +1 -0
- package/dist/server/services/plan/composer.d.ts.map +1 -1
- package/dist/server/services/plan/composer.js +59 -11
- package/dist/server/services/plan/composer.js.map +1 -1
- package/dist/server/services/plan/executor.d.ts.map +1 -1
- package/dist/server/services/plan/executor.js +3 -1
- package/dist/server/services/plan/executor.js.map +1 -1
- package/dist/server/services/plan/issue-prompt-builder.d.ts.map +1 -1
- package/dist/server/services/plan/issue-prompt-builder.js +33 -1
- package/dist/server/services/plan/issue-prompt-builder.js.map +1 -1
- package/dist/server/services/plan/parser-core.d.ts.map +1 -1
- package/dist/server/services/plan/parser-core.js +1 -0
- package/dist/server/services/plan/parser-core.js.map +1 -1
- package/dist/server/services/plan/types.d.ts +1 -0
- package/dist/server/services/plan/types.d.ts.map +1 -1
- package/dist/server/services/runtime-info.d.ts +3 -0
- package/dist/server/services/runtime-info.d.ts.map +1 -0
- package/dist/server/services/runtime-info.js +21 -0
- package/dist/server/services/runtime-info.js.map +1 -0
- package/dist/server/services/settings.d.ts +76 -2
- package/dist/server/services/settings.d.ts.map +1 -1
- package/dist/server/services/settings.js +127 -4
- package/dist/server/services/settings.js.map +1 -1
- package/dist/server/services/websocket/ask-user-question-bridge.d.ts +32 -0
- package/dist/server/services/websocket/ask-user-question-bridge.d.ts.map +1 -0
- package/dist/server/services/websocket/ask-user-question-bridge.js +115 -0
- package/dist/server/services/websocket/ask-user-question-bridge.js.map +1 -0
- package/dist/server/services/websocket/git-branch-handlers.d.ts.map +1 -1
- package/dist/server/services/websocket/git-branch-handlers.js +19 -6
- package/dist/server/services/websocket/git-branch-handlers.js.map +1 -1
- package/dist/server/services/websocket/handler.d.ts +25 -1
- package/dist/server/services/websocket/handler.d.ts.map +1 -1
- package/dist/server/services/websocket/handler.js +84 -2
- package/dist/server/services/websocket/handler.js.map +1 -1
- package/dist/server/services/websocket/quality-complexity.d.ts.map +1 -1
- package/dist/server/services/websocket/quality-complexity.js +78 -26
- package/dist/server/services/websocket/quality-complexity.js.map +1 -1
- package/dist/server/services/websocket/quality-eta.d.ts +47 -0
- package/dist/server/services/websocket/quality-eta.d.ts.map +1 -0
- package/dist/server/services/websocket/quality-eta.js +110 -0
- package/dist/server/services/websocket/quality-eta.js.map +1 -0
- package/dist/server/services/websocket/quality-grading.d.ts +27 -4
- package/dist/server/services/websocket/quality-grading.d.ts.map +1 -1
- package/dist/server/services/websocket/quality-grading.js +369 -201
- package/dist/server/services/websocket/quality-grading.js.map +1 -1
- package/dist/server/services/websocket/quality-handlers.d.ts.map +1 -1
- package/dist/server/services/websocket/quality-handlers.js +145 -7
- package/dist/server/services/websocket/quality-handlers.js.map +1 -1
- package/dist/server/services/websocket/quality-operations.d.ts +34 -0
- package/dist/server/services/websocket/quality-operations.d.ts.map +1 -0
- package/dist/server/services/websocket/quality-operations.js +47 -0
- package/dist/server/services/websocket/quality-operations.js.map +1 -0
- package/dist/server/services/websocket/quality-persistence.d.ts +9 -0
- package/dist/server/services/websocket/quality-persistence.d.ts.map +1 -1
- package/dist/server/services/websocket/quality-persistence.js +10 -0
- package/dist/server/services/websocket/quality-persistence.js.map +1 -1
- package/dist/server/services/websocket/quality-review-agent.d.ts +1 -1
- package/dist/server/services/websocket/quality-review-agent.d.ts.map +1 -1
- package/dist/server/services/websocket/quality-review-agent.js +105 -56
- package/dist/server/services/websocket/quality-review-agent.js.map +1 -1
- package/dist/server/services/websocket/quality-service.d.ts +9 -1
- package/dist/server/services/websocket/quality-service.d.ts.map +1 -1
- package/dist/server/services/websocket/quality-service.js +334 -14
- package/dist/server/services/websocket/quality-service.js.map +1 -1
- package/dist/server/services/websocket/quality-tools.d.ts +21 -0
- package/dist/server/services/websocket/quality-tools.d.ts.map +1 -1
- package/dist/server/services/websocket/quality-tools.js +49 -0
- package/dist/server/services/websocket/quality-tools.js.map +1 -1
- package/dist/server/services/websocket/quality-types.d.ts +35 -2
- package/dist/server/services/websocket/quality-types.d.ts.map +1 -1
- package/dist/server/services/websocket/quality-types.js +1 -1
- package/dist/server/services/websocket/quality-types.js.map +1 -1
- package/dist/server/services/websocket/session-handlers.d.ts +3 -1
- package/dist/server/services/websocket/session-handlers.d.ts.map +1 -1
- package/dist/server/services/websocket/session-handlers.js +60 -9
- package/dist/server/services/websocket/session-handlers.js.map +1 -1
- package/dist/server/services/websocket/session-history.js +3 -0
- package/dist/server/services/websocket/session-history.js.map +1 -1
- package/dist/server/services/websocket/session-initialization.d.ts.map +1 -1
- package/dist/server/services/websocket/session-initialization.js +158 -42
- package/dist/server/services/websocket/session-initialization.js.map +1 -1
- package/dist/server/services/websocket/session-registry.d.ts +25 -0
- package/dist/server/services/websocket/session-registry.d.ts.map +1 -1
- package/dist/server/services/websocket/session-registry.js +19 -0
- package/dist/server/services/websocket/session-registry.js.map +1 -1
- package/dist/server/services/websocket/settings-handlers.d.ts +1 -1
- package/dist/server/services/websocket/settings-handlers.d.ts.map +1 -1
- package/dist/server/services/websocket/settings-handlers.js +35 -4
- package/dist/server/services/websocket/settings-handlers.js.map +1 -1
- package/dist/server/services/websocket/tab-broadcast.d.ts +7 -2
- package/dist/server/services/websocket/tab-broadcast.d.ts.map +1 -1
- package/dist/server/services/websocket/tab-broadcast.js +10 -2
- package/dist/server/services/websocket/tab-broadcast.js.map +1 -1
- package/dist/server/services/websocket/tab-event-buffer.d.ts +97 -8
- package/dist/server/services/websocket/tab-event-buffer.d.ts.map +1 -1
- package/dist/server/services/websocket/tab-event-buffer.js +138 -12
- package/dist/server/services/websocket/tab-event-buffer.js.map +1 -1
- package/dist/server/services/websocket/tab-event-replay.d.ts +29 -13
- package/dist/server/services/websocket/tab-event-replay.d.ts.map +1 -1
- package/dist/server/services/websocket/tab-event-replay.js +55 -2
- package/dist/server/services/websocket/tab-event-replay.js.map +1 -1
- package/dist/server/services/websocket/tab-handlers.d.ts +9 -1
- package/dist/server/services/websocket/tab-handlers.d.ts.map +1 -1
- package/dist/server/services/websocket/tab-handlers.js +47 -2
- package/dist/server/services/websocket/tab-handlers.js.map +1 -1
- package/dist/server/services/websocket/types.d.ts +67 -7
- package/dist/server/services/websocket/types.d.ts.map +1 -1
- package/dist/server/services/websocket/types.js +12 -6
- package/dist/server/services/websocket/types.js.map +1 -1
- package/package.json +5 -3
- package/server/cli/eta-estimator.ts +249 -0
- package/server/cli/headless/claude-invoker-process.ts +9 -1
- package/server/cli/headless/mcp-config.ts +30 -5
- package/server/cli/headless/runner.ts +21 -0
- package/server/cli/headless/stall-assessor.ts +93 -0
- package/server/cli/headless/tool-watchdog.ts +21 -0
- package/server/cli/headless/types.ts +16 -1
- package/server/cli/improvisation-history-store.ts +4 -1
- package/server/cli/improvisation-output-queue.ts +29 -7
- package/server/cli/improvisation-session-manager.ts +63 -1
- package/server/cli/improvisation-types.ts +9 -0
- package/server/cli/retry/retry-runner-factory.ts +1 -0
- package/server/engines/EngineEvent.ts +156 -0
- package/server/engines/claude/ClaudeCodeEngine.ts +404 -0
- package/server/engines/factory.ts +176 -0
- package/server/engines/opencode/OpenCodeEngine.ts +786 -0
- package/server/engines/opencode/OpenCodeServerManager.ts +577 -0
- package/server/engines/opencode/model-catalog.ts +217 -0
- package/server/engines/types.ts +173 -0
- package/server/index.ts +9 -1
- package/server/mcp/bouncer-haiku.ts +21 -145
- package/server/mcp/bouncer-integration.ts +107 -5
- package/server/mcp/classifier/BouncerClassifier.ts +40 -0
- package/server/mcp/classifier/ClaudeBouncerClassifier.ts +189 -0
- package/server/mcp/classifier/OpenCodeBouncerClassifier.ts +305 -0
- package/server/mcp/classifier/factory.ts +195 -0
- package/server/mcp/server.ts +57 -0
- package/server/routes/index.ts +1 -0
- package/server/routes/internal.ts +112 -0
- package/server/services/plan/agent-resolver.ts +115 -0
- package/server/services/plan/agents/code-review.md +38 -8
- package/server/services/plan/composer.ts +63 -11
- package/server/services/plan/executor.ts +3 -1
- package/server/services/plan/issue-prompt-builder.ts +39 -1
- package/server/services/plan/parser-core.ts +1 -0
- package/server/services/plan/types.ts +4 -0
- package/server/services/runtime-info.ts +24 -0
- package/server/services/settings.ts +161 -4
- package/server/services/websocket/ask-user-question-bridge.ts +148 -0
- package/server/services/websocket/git-branch-handlers.ts +20 -6
- package/server/services/websocket/handler.ts +89 -2
- package/server/services/websocket/quality-complexity.ts +80 -26
- package/server/services/websocket/quality-eta.ts +155 -0
- package/server/services/websocket/quality-grading.ts +445 -222
- package/server/services/websocket/quality-handlers.ts +153 -7
- package/server/services/websocket/quality-operations.ts +72 -0
- package/server/services/websocket/quality-persistence.ts +17 -0
- package/server/services/websocket/quality-review-agent.ts +154 -64
- package/server/services/websocket/quality-service.ts +361 -13
- package/server/services/websocket/quality-tools.ts +51 -0
- package/server/services/websocket/quality-types.ts +41 -2
- package/server/services/websocket/session-handlers.ts +67 -10
- package/server/services/websocket/session-history.ts +3 -0
- package/server/services/websocket/session-initialization.ts +189 -46
- package/server/services/websocket/session-registry.ts +37 -0
- package/server/services/websocket/settings-handlers.ts +41 -4
- package/server/services/websocket/tab-broadcast.ts +10 -2
- package/server/services/websocket/tab-event-buffer.ts +143 -11
- package/server/services/websocket/tab-event-replay.ts +70 -3
- package/server/services/websocket/tab-handlers.ts +53 -5
- package/server/services/websocket/types.ts +85 -7
|
@@ -0,0 +1,577 @@
|
|
|
1
|
+
// Copyright (c) 2025-present Mstro, Inc. All rights reserved.
|
|
2
|
+
// Licensed under the MIT License. See LICENSE file for details.
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* OpenCodeServerManager — lifecycle owner for a single `opencode serve`
|
|
6
|
+
* subprocess per CLI instance.
|
|
7
|
+
*
|
|
8
|
+
* Responsibilities:
|
|
9
|
+
* - Lazy start: the subprocess is spawned on the first `start()` call.
|
|
10
|
+
* - Idempotent start: concurrent callers share a single subprocess and a
|
|
11
|
+
* single in-flight start Promise.
|
|
12
|
+
* - Health-gated readiness: `start()` resolves only after the subprocess has
|
|
13
|
+
* both (a) printed its listening URL and (b) responded to an HTTP probe.
|
|
14
|
+
* - Crash detection with exponential backoff: an unexpected process exit
|
|
15
|
+
* triggers automatic restart; after `maxRestartAttempts` failures the
|
|
16
|
+
* manager transitions to a terminal `failed` state and further calls
|
|
17
|
+
* reject.
|
|
18
|
+
* - Clean shutdown: `shutdown()` kills the subprocess, prevents further
|
|
19
|
+
* restarts, and (when registerProcessHandlers is set) is wired to CLI
|
|
20
|
+
* exit signals so no orphan process survives the parent.
|
|
21
|
+
*
|
|
22
|
+
* Callers obtain a typed `OpencodeClient` via `getClient()`. The client is
|
|
23
|
+
* stable across restarts — its baseUrl is updated in place when the
|
|
24
|
+
* subprocess is restarted on the same hostname/port.
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
import type { ChildProcess, SpawnOptions } from 'node:child_process'
|
|
28
|
+
import { spawn as nodeSpawn } from 'node:child_process'
|
|
29
|
+
import { EventEmitter } from 'node:events'
|
|
30
|
+
|
|
31
|
+
import {
|
|
32
|
+
createOpencodeClient,
|
|
33
|
+
type OpencodeClient,
|
|
34
|
+
} from '@opencode-ai/sdk'
|
|
35
|
+
|
|
36
|
+
/** Spawn signature accepted by the manager — matches node's `child_process.spawn`. */
|
|
37
|
+
export type SpawnFn = (
|
|
38
|
+
command: string,
|
|
39
|
+
args: readonly string[],
|
|
40
|
+
options: SpawnOptions,
|
|
41
|
+
) => ChildProcess
|
|
42
|
+
|
|
43
|
+
/** Events emitted by the manager for observability and tests. */
|
|
44
|
+
export type OpenCodeServerEvent =
|
|
45
|
+
| { kind: 'starting'; attempt: number }
|
|
46
|
+
| { kind: 'ready'; url: string; pid: number | undefined }
|
|
47
|
+
| { kind: 'crash'; code: number | null; signal: NodeJS.Signals | null }
|
|
48
|
+
| { kind: 'restart-scheduled'; attempt: number; delayMs: number }
|
|
49
|
+
| { kind: 'failed'; error: Error }
|
|
50
|
+
| { kind: 'shutdown' }
|
|
51
|
+
|
|
52
|
+
/** Construction options for {@link OpenCodeServerManager}. */
|
|
53
|
+
export interface OpenCodeServerManagerOptions {
|
|
54
|
+
/** Host the opencode server binds to. Defaults to `127.0.0.1`. */
|
|
55
|
+
hostname?: string
|
|
56
|
+
/** Port the opencode server binds to. Defaults to `4096`. */
|
|
57
|
+
port?: number
|
|
58
|
+
/** Binary to invoke. Defaults to `opencode`. Tests override with a shim. */
|
|
59
|
+
command?: string
|
|
60
|
+
/**
|
|
61
|
+
* Extra args appended to the spawned command. The manager always passes
|
|
62
|
+
* `serve --hostname=<h> --port=<p>` first; overrides may supply the
|
|
63
|
+
* entire arg list by setting `overrideArgs`.
|
|
64
|
+
*/
|
|
65
|
+
extraArgs?: string[]
|
|
66
|
+
/**
|
|
67
|
+
* If set, the manager uses exactly these args instead of the default
|
|
68
|
+
* `serve --hostname --port` trio. Tests use this to drive shims that
|
|
69
|
+
* don't implement the opencode CLI.
|
|
70
|
+
*/
|
|
71
|
+
overrideArgs?: string[]
|
|
72
|
+
/** Additional env vars merged into the subprocess environment. */
|
|
73
|
+
env?: Record<string, string>
|
|
74
|
+
/**
|
|
75
|
+
* Milliseconds to wait for the subprocess to emit its readiness line
|
|
76
|
+
* before giving up on an attempt. Defaults to 10_000.
|
|
77
|
+
*/
|
|
78
|
+
startTimeoutMs?: number
|
|
79
|
+
/**
|
|
80
|
+
* Milliseconds between HTTP readiness probes after the readiness line
|
|
81
|
+
* has been seen. Defaults to 50.
|
|
82
|
+
*/
|
|
83
|
+
healthPollIntervalMs?: number
|
|
84
|
+
/**
|
|
85
|
+
* Overall budget for the HTTP readiness probe phase. Defaults to
|
|
86
|
+
* 5_000.
|
|
87
|
+
*/
|
|
88
|
+
healthTimeoutMs?: number
|
|
89
|
+
/**
|
|
90
|
+
* Path used for the HTTP readiness probe. Defaults to `/config`.
|
|
91
|
+
*/
|
|
92
|
+
healthPath?: string
|
|
93
|
+
/** Cap on restart attempts before the manager transitions to `failed`. */
|
|
94
|
+
maxRestartAttempts?: number
|
|
95
|
+
/** Base backoff for restarts. Doubles each attempt. Defaults to 500ms. */
|
|
96
|
+
initialBackoffMs?: number
|
|
97
|
+
/** Ceiling for exponential backoff. Defaults to 10_000ms. */
|
|
98
|
+
maxBackoffMs?: number
|
|
99
|
+
/**
|
|
100
|
+
* If true, the manager registers SIGINT/SIGTERM/exit handlers on the
|
|
101
|
+
* parent process and calls `shutdown()` when they fire. Defaults to
|
|
102
|
+
* false (opt-in at the call site that owns the process-level policy).
|
|
103
|
+
*/
|
|
104
|
+
registerProcessHandlers?: boolean
|
|
105
|
+
/** Inject a fake spawn function for tests. Defaults to node's spawn. */
|
|
106
|
+
spawnFn?: SpawnFn
|
|
107
|
+
/**
|
|
108
|
+
* Inject a fake fetch for HTTP readiness probes. Defaults to global
|
|
109
|
+
* `fetch`. Primarily used to decouple tests from network timing.
|
|
110
|
+
*/
|
|
111
|
+
fetchFn?: typeof fetch
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** Lifecycle state for the manager. */
|
|
115
|
+
export type OpenCodeServerStatus =
|
|
116
|
+
| 'idle'
|
|
117
|
+
| 'starting'
|
|
118
|
+
| 'ready'
|
|
119
|
+
| 'restarting'
|
|
120
|
+
| 'failed'
|
|
121
|
+
| 'shutdown'
|
|
122
|
+
|
|
123
|
+
const DEFAULTS = {
|
|
124
|
+
hostname: '127.0.0.1',
|
|
125
|
+
port: 4096,
|
|
126
|
+
command: 'opencode',
|
|
127
|
+
startTimeoutMs: 10_000,
|
|
128
|
+
healthPollIntervalMs: 50,
|
|
129
|
+
healthTimeoutMs: 5_000,
|
|
130
|
+
healthPath: '/config',
|
|
131
|
+
maxRestartAttempts: 5,
|
|
132
|
+
initialBackoffMs: 500,
|
|
133
|
+
maxBackoffMs: 10_000,
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Thrown when the manager has been shut down or has exceeded its restart
|
|
138
|
+
* budget. Callers should construct a new manager to retry.
|
|
139
|
+
*/
|
|
140
|
+
export class OpenCodeServerManagerClosedError extends Error {
|
|
141
|
+
constructor(message: string) {
|
|
142
|
+
super(message)
|
|
143
|
+
this.name = 'OpenCodeServerManagerClosedError'
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export class OpenCodeServerManager extends EventEmitter {
|
|
148
|
+
private readonly opts: Required<
|
|
149
|
+
Omit<
|
|
150
|
+
OpenCodeServerManagerOptions,
|
|
151
|
+
'extraArgs' | 'overrideArgs' | 'env' | 'spawnFn' | 'fetchFn'
|
|
152
|
+
>
|
|
153
|
+
> & {
|
|
154
|
+
extraArgs: string[]
|
|
155
|
+
overrideArgs: string[] | undefined
|
|
156
|
+
env: Record<string, string>
|
|
157
|
+
spawnFn: SpawnFn
|
|
158
|
+
fetchFn: typeof fetch
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
private status: OpenCodeServerStatus = 'idle'
|
|
162
|
+
private proc: ChildProcess | null = null
|
|
163
|
+
private url: string | null = null
|
|
164
|
+
private startPromise: Promise<void> | null = null
|
|
165
|
+
private restartTimer: NodeJS.Timeout | null = null
|
|
166
|
+
private restartAttempts = 0
|
|
167
|
+
private failureReason: Error | null = null
|
|
168
|
+
/** Intentional shutdown flag — prevents crash handler from restarting. */
|
|
169
|
+
private isShuttingDown = false
|
|
170
|
+
/** Cached SDK client; its baseUrl is rotated on each successful start. */
|
|
171
|
+
private cachedClient: OpencodeClient | null = null
|
|
172
|
+
private lastBaseUrl: string | null = null
|
|
173
|
+
private processHandlersRegistered = false
|
|
174
|
+
private readonly boundProcessHandler: () => void
|
|
175
|
+
|
|
176
|
+
constructor(options: OpenCodeServerManagerOptions = {}) {
|
|
177
|
+
super()
|
|
178
|
+
this.opts = OpenCodeServerManager.resolveOptions(options)
|
|
179
|
+
|
|
180
|
+
this.boundProcessHandler = () => {
|
|
181
|
+
void this.shutdown().catch(() => {})
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
if (this.opts.registerProcessHandlers) {
|
|
185
|
+
this.attachProcessHandlers()
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
private static resolveOptions(
|
|
190
|
+
options: OpenCodeServerManagerOptions,
|
|
191
|
+
): OpenCodeServerManager['opts'] {
|
|
192
|
+
return {
|
|
193
|
+
hostname: options.hostname ?? DEFAULTS.hostname,
|
|
194
|
+
port: options.port ?? DEFAULTS.port,
|
|
195
|
+
command: options.command ?? DEFAULTS.command,
|
|
196
|
+
extraArgs: options.extraArgs ?? [],
|
|
197
|
+
overrideArgs: options.overrideArgs,
|
|
198
|
+
env: options.env ?? {},
|
|
199
|
+
startTimeoutMs: options.startTimeoutMs ?? DEFAULTS.startTimeoutMs,
|
|
200
|
+
healthPollIntervalMs:
|
|
201
|
+
options.healthPollIntervalMs ?? DEFAULTS.healthPollIntervalMs,
|
|
202
|
+
healthTimeoutMs: options.healthTimeoutMs ?? DEFAULTS.healthTimeoutMs,
|
|
203
|
+
healthPath: options.healthPath ?? DEFAULTS.healthPath,
|
|
204
|
+
maxRestartAttempts:
|
|
205
|
+
options.maxRestartAttempts ?? DEFAULTS.maxRestartAttempts,
|
|
206
|
+
initialBackoffMs: options.initialBackoffMs ?? DEFAULTS.initialBackoffMs,
|
|
207
|
+
maxBackoffMs: options.maxBackoffMs ?? DEFAULTS.maxBackoffMs,
|
|
208
|
+
registerProcessHandlers: options.registerProcessHandlers ?? false,
|
|
209
|
+
spawnFn: options.spawnFn ?? (nodeSpawn as unknown as SpawnFn),
|
|
210
|
+
fetchFn: options.fetchFn ?? fetch,
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/** Current lifecycle state. */
|
|
215
|
+
getStatus(): OpenCodeServerStatus {
|
|
216
|
+
return this.status
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/** URL of the running subprocess, or `null` if not ready. */
|
|
220
|
+
getUrl(): string | null {
|
|
221
|
+
return this.url
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/** True once `start()` has resolved and the subprocess is alive. */
|
|
225
|
+
isRunning(): boolean {
|
|
226
|
+
return this.status === 'ready' && this.proc !== null
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Return a typed SDK client bound to the currently running server.
|
|
231
|
+
* Throws if the manager has not been started or has entered a terminal
|
|
232
|
+
* state.
|
|
233
|
+
*/
|
|
234
|
+
getClient(): OpencodeClient {
|
|
235
|
+
if (this.status === 'shutdown' || this.status === 'failed') {
|
|
236
|
+
throw new OpenCodeServerManagerClosedError(
|
|
237
|
+
`OpenCodeServerManager is ${this.status}` +
|
|
238
|
+
(this.failureReason ? `: ${this.failureReason.message}` : ''),
|
|
239
|
+
)
|
|
240
|
+
}
|
|
241
|
+
if (!this.url) {
|
|
242
|
+
throw new OpenCodeServerManagerClosedError(
|
|
243
|
+
'OpenCodeServerManager.start() has not completed; no client available',
|
|
244
|
+
)
|
|
245
|
+
}
|
|
246
|
+
if (!this.cachedClient || this.lastBaseUrl !== this.url) {
|
|
247
|
+
this.cachedClient = createOpencodeClient({ baseUrl: this.url })
|
|
248
|
+
this.lastBaseUrl = this.url
|
|
249
|
+
}
|
|
250
|
+
return this.cachedClient
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Idempotent: first call spawns the subprocess; concurrent calls share
|
|
255
|
+
* the same in-flight Promise. Returns after the subprocess is both
|
|
256
|
+
* listening (stdout readiness line seen) and answering HTTP probes.
|
|
257
|
+
*/
|
|
258
|
+
start(): Promise<void> {
|
|
259
|
+
if (this.status === 'shutdown' || this.status === 'failed') {
|
|
260
|
+
return Promise.reject(
|
|
261
|
+
new OpenCodeServerManagerClosedError(
|
|
262
|
+
`OpenCodeServerManager is ${this.status}` +
|
|
263
|
+
(this.failureReason ? `: ${this.failureReason.message}` : ''),
|
|
264
|
+
),
|
|
265
|
+
)
|
|
266
|
+
}
|
|
267
|
+
if (this.status === 'ready' && this.proc) {
|
|
268
|
+
return Promise.resolve()
|
|
269
|
+
}
|
|
270
|
+
if (this.startPromise) {
|
|
271
|
+
return this.startPromise
|
|
272
|
+
}
|
|
273
|
+
this.startPromise = this.spawnOnce(1)
|
|
274
|
+
.then(() => {
|
|
275
|
+
this.startPromise = null
|
|
276
|
+
})
|
|
277
|
+
.catch((err) => {
|
|
278
|
+
this.startPromise = null
|
|
279
|
+
throw err
|
|
280
|
+
})
|
|
281
|
+
return this.startPromise
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* Terminate the subprocess, cancel any pending restart, and move the
|
|
286
|
+
* manager to the terminal `shutdown` state. Idempotent.
|
|
287
|
+
*/
|
|
288
|
+
async shutdown(): Promise<void> {
|
|
289
|
+
if (this.status === 'shutdown') return
|
|
290
|
+
this.isShuttingDown = true
|
|
291
|
+
this.status = 'shutdown'
|
|
292
|
+
if (this.restartTimer) {
|
|
293
|
+
clearTimeout(this.restartTimer)
|
|
294
|
+
this.restartTimer = null
|
|
295
|
+
}
|
|
296
|
+
const proc = this.proc
|
|
297
|
+
this.proc = null
|
|
298
|
+
this.url = null
|
|
299
|
+
this.cachedClient = null
|
|
300
|
+
this.lastBaseUrl = null
|
|
301
|
+
if (proc && proc.exitCode === null && proc.signalCode === null) {
|
|
302
|
+
await killAndWait(proc)
|
|
303
|
+
}
|
|
304
|
+
this.detachProcessHandlers()
|
|
305
|
+
this.emitEvent({ kind: 'shutdown' })
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
private emitEvent(event: OpenCodeServerEvent): void {
|
|
309
|
+
this.emit('event', event)
|
|
310
|
+
this.emit(event.kind, event)
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
private buildArgs(): string[] {
|
|
314
|
+
if (this.opts.overrideArgs) return [...this.opts.overrideArgs]
|
|
315
|
+
return [
|
|
316
|
+
'serve',
|
|
317
|
+
`--hostname=${this.opts.hostname}`,
|
|
318
|
+
`--port=${this.opts.port}`,
|
|
319
|
+
...this.opts.extraArgs,
|
|
320
|
+
]
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
private async spawnOnce(attempt: number): Promise<void> {
|
|
324
|
+
if (this.isShuttingDown || this.status === 'shutdown') {
|
|
325
|
+
throw new OpenCodeServerManagerClosedError(
|
|
326
|
+
'OpenCodeServerManager is shutdown',
|
|
327
|
+
)
|
|
328
|
+
}
|
|
329
|
+
this.status = attempt === 1 ? 'starting' : 'restarting'
|
|
330
|
+
this.emitEvent({ kind: 'starting', attempt })
|
|
331
|
+
|
|
332
|
+
const args = this.buildArgs()
|
|
333
|
+
const proc = this.opts.spawnFn(this.opts.command, args, {
|
|
334
|
+
env: { ...process.env, ...this.opts.env },
|
|
335
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
336
|
+
})
|
|
337
|
+
this.proc = proc
|
|
338
|
+
|
|
339
|
+
const readinessUrl = await this.runStartupPhase(proc, () =>
|
|
340
|
+
this.awaitReadinessLine(proc),
|
|
341
|
+
)
|
|
342
|
+
await this.runStartupPhase(proc, () => this.awaitHealthy(readinessUrl))
|
|
343
|
+
|
|
344
|
+
this.url = readinessUrl
|
|
345
|
+
this.status = 'ready'
|
|
346
|
+
this.restartAttempts = 0
|
|
347
|
+
this.wireCrashHandler(proc)
|
|
348
|
+
this.emitEvent({ kind: 'ready', url: readinessUrl, pid: proc.pid })
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
private async runStartupPhase<T>(
|
|
352
|
+
proc: ChildProcess,
|
|
353
|
+
phase: () => Promise<T>,
|
|
354
|
+
): Promise<T> {
|
|
355
|
+
try {
|
|
356
|
+
return await phase()
|
|
357
|
+
} catch (err) {
|
|
358
|
+
// Make sure the subprocess is not left dangling if a phase failed.
|
|
359
|
+
if (proc.exitCode === null && proc.signalCode === null) {
|
|
360
|
+
try {
|
|
361
|
+
proc.kill()
|
|
362
|
+
} catch {
|
|
363
|
+
// ignore
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
this.proc = null
|
|
367
|
+
throw err
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
private awaitReadinessLine(proc: ChildProcess): Promise<string> {
|
|
372
|
+
return new Promise<string>((resolve, reject) => {
|
|
373
|
+
let buffered = ''
|
|
374
|
+
let settled = false
|
|
375
|
+
|
|
376
|
+
const onStdout = (chunk: Buffer | string) => {
|
|
377
|
+
if (settled) return
|
|
378
|
+
buffered += chunk.toString()
|
|
379
|
+
const lines = buffered.split('\n')
|
|
380
|
+
for (const line of lines) {
|
|
381
|
+
if (line.startsWith('opencode server listening')) {
|
|
382
|
+
const match = line.match(/on\s+(https?:\/\/[^\s]+)/)
|
|
383
|
+
if (!match) {
|
|
384
|
+
finish(
|
|
385
|
+
reject,
|
|
386
|
+
new Error(
|
|
387
|
+
`Failed to parse opencode readiness line: ${line}`,
|
|
388
|
+
),
|
|
389
|
+
)
|
|
390
|
+
return
|
|
391
|
+
}
|
|
392
|
+
finish(resolve, match[1])
|
|
393
|
+
return
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
const onStderr = (chunk: Buffer | string) => {
|
|
398
|
+
buffered += chunk.toString()
|
|
399
|
+
}
|
|
400
|
+
const onExit = (code: number | null, signal: NodeJS.Signals | null) => {
|
|
401
|
+
const tail = buffered.trim()
|
|
402
|
+
finish(
|
|
403
|
+
reject,
|
|
404
|
+
new Error(
|
|
405
|
+
`opencode subprocess exited before readiness (code=${code}, signal=${signal})` +
|
|
406
|
+
(tail ? `\nOutput: ${tail}` : ''),
|
|
407
|
+
),
|
|
408
|
+
)
|
|
409
|
+
}
|
|
410
|
+
const onError = (err: Error) => finish(reject, err)
|
|
411
|
+
const onTimeout = () => {
|
|
412
|
+
finish(
|
|
413
|
+
reject,
|
|
414
|
+
new Error(
|
|
415
|
+
`Timed out after ${this.opts.startTimeoutMs}ms waiting for opencode to announce readiness`,
|
|
416
|
+
),
|
|
417
|
+
)
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
const timer = setTimeout(onTimeout, this.opts.startTimeoutMs)
|
|
421
|
+
|
|
422
|
+
function finish(
|
|
423
|
+
cb: (value: never) => void,
|
|
424
|
+
value: unknown,
|
|
425
|
+
): void {
|
|
426
|
+
if (settled) return
|
|
427
|
+
settled = true
|
|
428
|
+
clearTimeout(timer)
|
|
429
|
+
proc.stdout?.off('data', onStdout)
|
|
430
|
+
proc.stderr?.off('data', onStderr)
|
|
431
|
+
proc.off('exit', onExit)
|
|
432
|
+
proc.off('error', onError)
|
|
433
|
+
;(cb as (v: unknown) => void)(value)
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
proc.stdout?.on('data', onStdout)
|
|
437
|
+
proc.stderr?.on('data', onStderr)
|
|
438
|
+
proc.on('exit', onExit)
|
|
439
|
+
proc.on('error', onError)
|
|
440
|
+
})
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
private async awaitHealthy(baseUrl: string): Promise<void> {
|
|
444
|
+
const url = baseUrl.replace(/\/$/, '') + this.opts.healthPath
|
|
445
|
+
const deadline = Date.now() + this.opts.healthTimeoutMs
|
|
446
|
+
let lastError: unknown = null
|
|
447
|
+
while (Date.now() < deadline) {
|
|
448
|
+
try {
|
|
449
|
+
const res = await this.opts.fetchFn(url, { method: 'GET' })
|
|
450
|
+
// Any 2xx/3xx/4xx response means the server is up and routing;
|
|
451
|
+
// only network failures / 5xx indicate it is not yet ready.
|
|
452
|
+
if (res.status < 500) return
|
|
453
|
+
lastError = new Error(`Health probe status ${res.status}`)
|
|
454
|
+
} catch (err) {
|
|
455
|
+
lastError = err
|
|
456
|
+
}
|
|
457
|
+
await sleep(this.opts.healthPollIntervalMs)
|
|
458
|
+
}
|
|
459
|
+
throw new Error(
|
|
460
|
+
`opencode server did not pass health check at ${url} within ` +
|
|
461
|
+
`${this.opts.healthTimeoutMs}ms` +
|
|
462
|
+
(lastError instanceof Error ? `: ${lastError.message}` : ''),
|
|
463
|
+
)
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
private wireCrashHandler(proc: ChildProcess): void {
|
|
467
|
+
const onExit = (
|
|
468
|
+
code: number | null,
|
|
469
|
+
signal: NodeJS.Signals | null,
|
|
470
|
+
): void => {
|
|
471
|
+
if (this.proc !== proc) return
|
|
472
|
+
if (this.isShuttingDown || this.status === 'shutdown') return
|
|
473
|
+
this.proc = null
|
|
474
|
+
this.url = null
|
|
475
|
+
this.cachedClient = null
|
|
476
|
+
this.lastBaseUrl = null
|
|
477
|
+
this.emitEvent({ kind: 'crash', code, signal })
|
|
478
|
+
this.scheduleRestart()
|
|
479
|
+
}
|
|
480
|
+
proc.once('exit', onExit)
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
private scheduleRestart(): void {
|
|
484
|
+
if (this.isShuttingDown || this.status === 'shutdown') return
|
|
485
|
+
this.restartAttempts += 1
|
|
486
|
+
if (this.restartAttempts > this.opts.maxRestartAttempts) {
|
|
487
|
+
const err = new Error(
|
|
488
|
+
`opencode subprocess exceeded ${this.opts.maxRestartAttempts} restart attempts`,
|
|
489
|
+
)
|
|
490
|
+
this.failureReason = err
|
|
491
|
+
this.status = 'failed'
|
|
492
|
+
this.emitEvent({ kind: 'failed', error: err })
|
|
493
|
+
return
|
|
494
|
+
}
|
|
495
|
+
const delay = Math.min(
|
|
496
|
+
this.opts.initialBackoffMs * 2 ** (this.restartAttempts - 1),
|
|
497
|
+
this.opts.maxBackoffMs,
|
|
498
|
+
)
|
|
499
|
+
this.status = 'restarting'
|
|
500
|
+
this.emitEvent({
|
|
501
|
+
kind: 'restart-scheduled',
|
|
502
|
+
attempt: this.restartAttempts,
|
|
503
|
+
delayMs: delay,
|
|
504
|
+
})
|
|
505
|
+
this.restartTimer = setTimeout(() => {
|
|
506
|
+
this.restartTimer = null
|
|
507
|
+
if (this.isShuttingDown || this.status === 'shutdown') return
|
|
508
|
+
const attempt = this.restartAttempts + 1
|
|
509
|
+
// spawnOnce uses `attempt` purely for events; we pass the 1-based
|
|
510
|
+
// restart attempt so observers can correlate.
|
|
511
|
+
this.startPromise = this.spawnOnce(attempt)
|
|
512
|
+
.then(() => {
|
|
513
|
+
this.startPromise = null
|
|
514
|
+
})
|
|
515
|
+
.catch((err) => {
|
|
516
|
+
this.startPromise = null
|
|
517
|
+
if (this.isShuttingDown || this.status === 'shutdown') return
|
|
518
|
+
this.emitEvent({
|
|
519
|
+
kind: 'crash',
|
|
520
|
+
code: null,
|
|
521
|
+
signal: null,
|
|
522
|
+
})
|
|
523
|
+
// A failure to spawn counts the same as a crash — reschedule.
|
|
524
|
+
this.scheduleRestart()
|
|
525
|
+
// Surface the immediate error via the failed handler chain
|
|
526
|
+
// when we eventually give up in scheduleRestart above.
|
|
527
|
+
this.failureReason = err instanceof Error ? err : new Error(String(err))
|
|
528
|
+
})
|
|
529
|
+
}, delay)
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
private attachProcessHandlers(): void {
|
|
533
|
+
if (this.processHandlersRegistered) return
|
|
534
|
+
this.processHandlersRegistered = true
|
|
535
|
+
process.on('SIGINT', this.boundProcessHandler)
|
|
536
|
+
process.on('SIGTERM', this.boundProcessHandler)
|
|
537
|
+
process.on('beforeExit', this.boundProcessHandler)
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
private detachProcessHandlers(): void {
|
|
541
|
+
if (!this.processHandlersRegistered) return
|
|
542
|
+
this.processHandlersRegistered = false
|
|
543
|
+
process.off('SIGINT', this.boundProcessHandler)
|
|
544
|
+
process.off('SIGTERM', this.boundProcessHandler)
|
|
545
|
+
process.off('beforeExit', this.boundProcessHandler)
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
function sleep(ms: number): Promise<void> {
|
|
550
|
+
return new Promise((resolve) => setTimeout(resolve, ms))
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
async function killAndWait(proc: ChildProcess): Promise<void> {
|
|
554
|
+
if (proc.exitCode !== null || proc.signalCode !== null) return
|
|
555
|
+
const exited = new Promise<void>((resolve) => {
|
|
556
|
+
proc.once('exit', () => resolve())
|
|
557
|
+
})
|
|
558
|
+
try {
|
|
559
|
+
proc.kill('SIGTERM')
|
|
560
|
+
} catch {
|
|
561
|
+
// already exited or permission denied — fall through
|
|
562
|
+
}
|
|
563
|
+
const timer = setTimeout(() => {
|
|
564
|
+
if (proc.exitCode === null && proc.signalCode === null) {
|
|
565
|
+
try {
|
|
566
|
+
proc.kill('SIGKILL')
|
|
567
|
+
} catch {
|
|
568
|
+
// ignore
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
}, 1_000)
|
|
572
|
+
try {
|
|
573
|
+
await exited
|
|
574
|
+
} finally {
|
|
575
|
+
clearTimeout(timer)
|
|
576
|
+
}
|
|
577
|
+
}
|