@thevinci/web 1.0.0
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/README.md +197 -0
- package/bin/cli-entry.js +55 -0
- package/bin/cli-output.js +145 -0
- package/bin/cli.js +4887 -0
- package/bin/cli.test.js +64 -0
- package/dist/apple-touch-icon-120x120.png +0 -0
- package/dist/apple-touch-icon-152x152.png +0 -0
- package/dist/apple-touch-icon-167x167.png +0 -0
- package/dist/apple-touch-icon-180x180.png +0 -0
- package/dist/apple-touch-icon.png +0 -0
- package/dist/apple-touch-icon.svg +528 -0
- package/dist/assets/JsonTreeView-CSm9OzXG.js +1 -0
- package/dist/assets/KaTeX_AMS-Regular-BQhdFMY1.woff2 +0 -0
- package/dist/assets/KaTeX_AMS-Regular-DMm9YOAa.woff +0 -0
- package/dist/assets/KaTeX_AMS-Regular-DRggAlZN.ttf +0 -0
- package/dist/assets/KaTeX_Caligraphic-Bold-ATXxdsX0.ttf +0 -0
- package/dist/assets/KaTeX_Caligraphic-Bold-BEiXGLvX.woff +0 -0
- package/dist/assets/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2 +0 -0
- package/dist/assets/KaTeX_Caligraphic-Regular-CTRA-rTL.woff +0 -0
- package/dist/assets/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2 +0 -0
- package/dist/assets/KaTeX_Caligraphic-Regular-wX97UBjC.ttf +0 -0
- package/dist/assets/KaTeX_Fraktur-Bold-BdnERNNW.ttf +0 -0
- package/dist/assets/KaTeX_Fraktur-Bold-BsDP51OF.woff +0 -0
- package/dist/assets/KaTeX_Fraktur-Bold-CL6g_b3V.woff2 +0 -0
- package/dist/assets/KaTeX_Fraktur-Regular-CB_wures.ttf +0 -0
- package/dist/assets/KaTeX_Fraktur-Regular-CTYiF6lA.woff2 +0 -0
- package/dist/assets/KaTeX_Fraktur-Regular-Dxdc4cR9.woff +0 -0
- package/dist/assets/KaTeX_Main-Bold-Cx986IdX.woff2 +0 -0
- package/dist/assets/KaTeX_Main-Bold-Jm3AIy58.woff +0 -0
- package/dist/assets/KaTeX_Main-Bold-waoOVXN0.ttf +0 -0
- package/dist/assets/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2 +0 -0
- package/dist/assets/KaTeX_Main-BoldItalic-DzxPMmG6.ttf +0 -0
- package/dist/assets/KaTeX_Main-BoldItalic-SpSLRI95.woff +0 -0
- package/dist/assets/KaTeX_Main-Italic-3WenGoN9.ttf +0 -0
- package/dist/assets/KaTeX_Main-Italic-BMLOBm91.woff +0 -0
- package/dist/assets/KaTeX_Main-Italic-NWA7e6Wa.woff2 +0 -0
- package/dist/assets/KaTeX_Main-Regular-B22Nviop.woff2 +0 -0
- package/dist/assets/KaTeX_Main-Regular-Dr94JaBh.woff +0 -0
- package/dist/assets/KaTeX_Main-Regular-ypZvNtVU.ttf +0 -0
- package/dist/assets/KaTeX_Math-BoldItalic-B3XSjfu4.ttf +0 -0
- package/dist/assets/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2 +0 -0
- package/dist/assets/KaTeX_Math-BoldItalic-iY-2wyZ7.woff +0 -0
- package/dist/assets/KaTeX_Math-Italic-DA0__PXp.woff +0 -0
- package/dist/assets/KaTeX_Math-Italic-flOr_0UB.ttf +0 -0
- package/dist/assets/KaTeX_Math-Italic-t53AETM-.woff2 +0 -0
- package/dist/assets/KaTeX_SansSerif-Bold-CFMepnvq.ttf +0 -0
- package/dist/assets/KaTeX_SansSerif-Bold-D1sUS0GD.woff2 +0 -0
- package/dist/assets/KaTeX_SansSerif-Bold-DbIhKOiC.woff +0 -0
- package/dist/assets/KaTeX_SansSerif-Italic-C3H0VqGB.woff2 +0 -0
- package/dist/assets/KaTeX_SansSerif-Italic-DN2j7dab.woff +0 -0
- package/dist/assets/KaTeX_SansSerif-Italic-YYjJ1zSn.ttf +0 -0
- package/dist/assets/KaTeX_SansSerif-Regular-BNo7hRIc.ttf +0 -0
- package/dist/assets/KaTeX_SansSerif-Regular-CS6fqUqJ.woff +0 -0
- package/dist/assets/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2 +0 -0
- package/dist/assets/KaTeX_Script-Regular-C5JkGWo-.ttf +0 -0
- package/dist/assets/KaTeX_Script-Regular-D3wIWfF6.woff2 +0 -0
- package/dist/assets/KaTeX_Script-Regular-D5yQViql.woff +0 -0
- package/dist/assets/KaTeX_Size1-Regular-C195tn64.woff +0 -0
- package/dist/assets/KaTeX_Size1-Regular-Dbsnue_I.ttf +0 -0
- package/dist/assets/KaTeX_Size1-Regular-mCD8mA8B.woff2 +0 -0
- package/dist/assets/KaTeX_Size2-Regular-B7gKUWhC.ttf +0 -0
- package/dist/assets/KaTeX_Size2-Regular-Dy4dx90m.woff2 +0 -0
- package/dist/assets/KaTeX_Size2-Regular-oD1tc_U0.woff +0 -0
- package/dist/assets/KaTeX_Size3-Regular-CTq5MqoE.woff +0 -0
- package/dist/assets/KaTeX_Size3-Regular-DgpXs0kz.ttf +0 -0
- package/dist/assets/KaTeX_Size4-Regular-BF-4gkZK.woff +0 -0
- package/dist/assets/KaTeX_Size4-Regular-DWFBv043.ttf +0 -0
- package/dist/assets/KaTeX_Size4-Regular-Dl5lxZxV.woff2 +0 -0
- package/dist/assets/KaTeX_Typewriter-Regular-C0xS9mPB.woff +0 -0
- package/dist/assets/KaTeX_Typewriter-Regular-CO6r4hn1.woff2 +0 -0
- package/dist/assets/KaTeX_Typewriter-Regular-D3Ib7_Hf.ttf +0 -0
- package/dist/assets/MarkdownRendererImpl-DensKOLc.js +6 -0
- package/dist/assets/MultiRunWindow-Bo7THayo.js +1 -0
- package/dist/assets/OnboardingScreen-BDqmzTVR.js +2 -0
- package/dist/assets/SettingsWindow-coz__Ykw.js +1 -0
- package/dist/assets/TerminalView-DrZ-i3Dr.js +1 -0
- package/dist/assets/ToolOutputDialog-Eglzslt3.js +16 -0
- package/dist/assets/es-4o9ciP61.js +15 -0
- package/dist/assets/ibm-plex-mono-latin-400-normal-CvHOgSBP.woff +0 -0
- package/dist/assets/ibm-plex-mono-latin-400-normal-DMJ8VG8y.woff2 +0 -0
- package/dist/assets/ibm-plex-mono-latin-500-normal-CB9ihrfo.woff +0 -0
- package/dist/assets/ibm-plex-mono-latin-500-normal-DSY6xOcd.woff2 +0 -0
- package/dist/assets/ibm-plex-mono-latin-600-normal-BgSNZQsw.woff2 +0 -0
- package/dist/assets/ibm-plex-mono-latin-600-normal-DWFSQ4vo.woff +0 -0
- package/dist/assets/ibm-plex-sans-latin-400-normal-CDDApCn2.woff2 +0 -0
- package/dist/assets/ibm-plex-sans-latin-400-normal-CYLoc0-x.woff +0 -0
- package/dist/assets/ibm-plex-sans-latin-500-normal-6ng42L7E.woff2 +0 -0
- package/dist/assets/ibm-plex-sans-latin-500-normal-BgVn5rGT.woff +0 -0
- package/dist/assets/ibm-plex-sans-latin-600-normal-Cu4Hd6ag.woff +0 -0
- package/dist/assets/ibm-plex-sans-latin-600-normal-CuJfVYMP.woff2 +0 -0
- package/dist/assets/index-DLTDToSP.css +1 -0
- package/dist/assets/index-DgiFEKGN.js +1 -0
- package/dist/assets/ko-B20imCHE.js +15 -0
- package/dist/assets/main-BV3KOtdA.css +1 -0
- package/dist/assets/main-CDKJj0sH.js +226 -0
- package/dist/assets/main-LC-PSNVM.js +2 -0
- package/dist/assets/miniChat-CQUiG_cr.js +2 -0
- package/dist/assets/modelPrefsAutoSave-Dm799vzR.js +6986 -0
- package/dist/assets/pl-DQJ7LSzj.js +15 -0
- package/dist/assets/pt-BR-OmjHUz9y.js +15 -0
- package/dist/assets/renderElectronMiniChatApp-CARbeW0G.js +2 -0
- package/dist/assets/uk-BNFxOlO4.js +15 -0
- package/dist/assets/vendor--DBfsbEis.css +1 -0
- package/dist/assets/vendor-.bun-B9l0ZNi2.js +4094 -0
- package/dist/assets/wasm-CG6Dc4jp.js +1 -0
- package/dist/assets/wasmSttWorker-Dtlxac_K.js +1 -0
- package/dist/assets/wasmSttWorker-oo7Dm_jy.js +1806 -0
- package/dist/assets/worker-CbT6TVo7.js +155 -0
- package/dist/assets/zh-CN-C6T-Ac7F.js +15 -0
- package/dist/favicon-16.png +0 -0
- package/dist/favicon-32.png +0 -0
- package/dist/favicon.png +0 -0
- package/dist/favicon.svg +528 -0
- package/dist/index.html +607 -0
- package/dist/logo-dark-192x192.png +0 -0
- package/dist/logo-dark-512x512.png +0 -0
- package/dist/logo-dark-512x512.svg +528 -0
- package/dist/logo-light-192x192.png +0 -0
- package/dist/logo-light-512x512.png +0 -0
- package/dist/logo-light-512x512.svg +528 -0
- package/dist/mini-chat.html +16 -0
- package/dist/pwa-192.png +0 -0
- package/dist/pwa-512.png +0 -0
- package/dist/pwa-maskable-192.png +0 -0
- package/dist/pwa-maskable-512.png +0 -0
- package/dist/site.webmanifest +21 -0
- package/dist/sw.js +1 -0
- package/package.json +118 -0
- package/public/apple-touch-icon-120x120.png +0 -0
- package/public/apple-touch-icon-152x152.png +0 -0
- package/public/apple-touch-icon-167x167.png +0 -0
- package/public/apple-touch-icon-180x180.png +0 -0
- package/public/apple-touch-icon.png +0 -0
- package/public/apple-touch-icon.svg +528 -0
- package/public/favicon-16.png +0 -0
- package/public/favicon-32.png +0 -0
- package/public/favicon.png +0 -0
- package/public/favicon.svg +528 -0
- package/public/logo-dark-192x192.png +0 -0
- package/public/logo-dark-512x512.png +0 -0
- package/public/logo-dark-512x512.svg +528 -0
- package/public/logo-light-192x192.png +0 -0
- package/public/logo-light-512x512.png +0 -0
- package/public/logo-light-512x512.svg +528 -0
- package/public/pwa-192.png +0 -0
- package/public/pwa-512.png +0 -0
- package/public/pwa-maskable-192.png +0 -0
- package/public/pwa-maskable-512.png +0 -0
- package/public/site.webmanifest +21 -0
- package/server/TERMINAL_WS_PROTOCOL.md +48 -0
- package/server/index.d.ts +39 -0
- package/server/index.js +1311 -0
- package/server/lib/cloudflare-tunnel.js +650 -0
- package/server/lib/event-stream/DOCUMENTATION.md +61 -0
- package/server/lib/event-stream/directory-ws-bridge.js +185 -0
- package/server/lib/event-stream/global-hub.js +158 -0
- package/server/lib/event-stream/global-hub.test.js +140 -0
- package/server/lib/event-stream/global-ws-bridge.js +206 -0
- package/server/lib/event-stream/index.js +25 -0
- package/server/lib/event-stream/protocol.js +131 -0
- package/server/lib/event-stream/protocol.test.js +182 -0
- package/server/lib/event-stream/runtime.js +180 -0
- package/server/lib/event-stream/runtime.test.js +512 -0
- package/server/lib/event-stream/upstream-reader.js +226 -0
- package/server/lib/event-stream/upstream-reader.test.js +276 -0
- package/server/lib/fs/DOCUMENTATION.md +36 -0
- package/server/lib/fs/routes.js +1040 -0
- package/server/lib/fs/search.js +238 -0
- package/server/lib/git/DOCUMENTATION.md +152 -0
- package/server/lib/git/credentials.js +74 -0
- package/server/lib/git/identity-storage.js +112 -0
- package/server/lib/git/index.js +6 -0
- package/server/lib/git/routes.js +972 -0
- package/server/lib/git/service.js +3432 -0
- package/server/lib/git/service.test.js +39 -0
- package/server/lib/github/DOCUMENTATION.md +171 -0
- package/server/lib/github/auth.js +307 -0
- package/server/lib/github/device-flow.js +50 -0
- package/server/lib/github/index.js +24 -0
- package/server/lib/github/octokit.js +10 -0
- package/server/lib/github/pr-status.js +519 -0
- package/server/lib/github/repo/fork-detection.js +102 -0
- package/server/lib/github/repo/index.js +55 -0
- package/server/lib/github/routes.js +1560 -0
- package/server/lib/magic-prompts/routes.js +63 -0
- package/server/lib/magic-prompts/runtime.js +119 -0
- package/server/lib/notifications/DOCUMENTATION.md +122 -0
- package/server/lib/notifications/emitter-runtime.js +102 -0
- package/server/lib/notifications/index.js +4 -0
- package/server/lib/notifications/message.js +52 -0
- package/server/lib/notifications/message.test.js +34 -0
- package/server/lib/notifications/push-runtime.js +304 -0
- package/server/lib/notifications/routes.js +315 -0
- package/server/lib/notifications/runtime.js +566 -0
- package/server/lib/notifications/template-runtime.js +349 -0
- package/server/lib/notifications/template-runtime.test.js +26 -0
- package/server/lib/opencode/DOCUMENTATION.md +362 -0
- package/server/lib/opencode/agents.js +634 -0
- package/server/lib/opencode/auth-state-runtime.js +88 -0
- package/server/lib/opencode/auth.js +83 -0
- package/server/lib/opencode/bootstrap-runtime.js +131 -0
- package/server/lib/opencode/cli-entry-runtime.js +43 -0
- package/server/lib/opencode/cli-options.js +128 -0
- package/server/lib/opencode/commands.js +339 -0
- package/server/lib/opencode/config-entity-routes.js +370 -0
- package/server/lib/opencode/core-routes.js +500 -0
- package/server/lib/opencode/core-routes.test.js +26 -0
- package/server/lib/opencode/env-config.js +74 -0
- package/server/lib/opencode/env-keys.js +68 -0
- package/server/lib/opencode/env-runtime.js +1162 -0
- package/server/lib/opencode/env-runtime.test.js +116 -0
- package/server/lib/opencode/feature-routes-runtime.js +244 -0
- package/server/lib/opencode/hmr-state-runtime.js +85 -0
- package/server/lib/opencode/index.js +66 -0
- package/server/lib/opencode/lifecycle.js +1019 -0
- package/server/lib/opencode/lifecycle.test.js +240 -0
- package/server/lib/opencode/mcp.js +278 -0
- package/server/lib/opencode/network-runtime.js +104 -0
- package/server/lib/opencode/network-runtime.test.js +37 -0
- package/server/lib/opencode/opencode-resolution-runtime.js +71 -0
- package/server/lib/opencode/path-utils.js +100 -0
- package/server/lib/opencode/path-utils.test.js +71 -0
- package/server/lib/opencode/project-directory-runtime.js +124 -0
- package/server/lib/opencode/project-icon-routes.js +399 -0
- package/server/lib/opencode/project-icon-routes.test.js +107 -0
- package/server/lib/opencode/providers.js +96 -0
- package/server/lib/opencode/proxy.js +445 -0
- package/server/lib/opencode/pwa-manifest-routes.js +257 -0
- package/server/lib/opencode/pwa-manifest-routes.test.js +133 -0
- package/server/lib/opencode/routes.js +541 -0
- package/server/lib/opencode/server-startup-runtime.js +156 -0
- package/server/lib/opencode/server-utils-runtime.js +168 -0
- package/server/lib/opencode/server-utils-runtime.test.js +135 -0
- package/server/lib/opencode/session-runtime.js +356 -0
- package/server/lib/opencode/session-runtime.test.js +151 -0
- package/server/lib/opencode/settings-helpers.js +770 -0
- package/server/lib/opencode/settings-helpers.test.js +109 -0
- package/server/lib/opencode/settings-normalization-runtime.js +428 -0
- package/server/lib/opencode/settings-runtime.js +826 -0
- package/server/lib/opencode/settings-runtime.test.js +85 -0
- package/server/lib/opencode/shared.js +615 -0
- package/server/lib/opencode/shutdown-runtime.js +139 -0
- package/server/lib/opencode/shutdown-runtime.test.js +58 -0
- package/server/lib/opencode/skill-routes.js +701 -0
- package/server/lib/opencode/skills.js +548 -0
- package/server/lib/opencode/startup-pipeline-runtime.js +130 -0
- package/server/lib/opencode/static-routes-runtime.js +65 -0
- package/server/lib/opencode/theme-runtime.js +167 -0
- package/server/lib/opencode/tunnel-auth.js +591 -0
- package/server/lib/opencode/tunnel-wiring-runtime.js +94 -0
- package/server/lib/opencode/vinci-routes.js +76 -0
- package/server/lib/opencode/watcher.js +115 -0
- package/server/lib/opencode/watcher.test.js +239 -0
- package/server/lib/preview/proxy-runtime.js +1333 -0
- package/server/lib/preview/proxy-runtime.test.js +144 -0
- package/server/lib/projects/project-config.js +567 -0
- package/server/lib/projects/project-config.test.js +175 -0
- package/server/lib/projects/project-id.js +13 -0
- package/server/lib/quota/DOCUMENTATION.md +58 -0
- package/server/lib/quota/index.js +25 -0
- package/server/lib/quota/providers/claude.js +107 -0
- package/server/lib/quota/providers/codex.js +113 -0
- package/server/lib/quota/providers/copilot.js +165 -0
- package/server/lib/quota/providers/google/api.js +92 -0
- package/server/lib/quota/providers/google/auth.js +108 -0
- package/server/lib/quota/providers/google/index.js +124 -0
- package/server/lib/quota/providers/google/transforms.js +109 -0
- package/server/lib/quota/providers/index.js +168 -0
- package/server/lib/quota/providers/interface.js +55 -0
- package/server/lib/quota/providers/kimi.js +108 -0
- package/server/lib/quota/providers/minimax-cn-coding-plan.js +140 -0
- package/server/lib/quota/providers/minimax-coding-plan.js +139 -0
- package/server/lib/quota/providers/nanogpt.js +124 -0
- package/server/lib/quota/providers/ollama-cloud.js +112 -0
- package/server/lib/quota/providers/openai.js +91 -0
- package/server/lib/quota/providers/openrouter.js +92 -0
- package/server/lib/quota/providers/zai.js +91 -0
- package/server/lib/quota/providers/zhipuai-coding-plan.js +133 -0
- package/server/lib/quota/providers/zhipuai.js +114 -0
- package/server/lib/quota/routes.js +27 -0
- package/server/lib/quota/utils/auth.js +50 -0
- package/server/lib/quota/utils/formatters.js +85 -0
- package/server/lib/quota/utils/formatters.test.js +54 -0
- package/server/lib/quota/utils/index.js +10 -0
- package/server/lib/quota/utils/transformers.js +55 -0
- package/server/lib/scheduled-tasks/DOCUMENTATION.md +44 -0
- package/server/lib/scheduled-tasks/routes.js +235 -0
- package/server/lib/scheduled-tasks/runtime.js +773 -0
- package/server/lib/scheduled-tasks/runtime.test.js +100 -0
- package/server/lib/security/request-security.js +115 -0
- package/server/lib/session-folders/routes.js +63 -0
- package/server/lib/session-folders/routes.test.js +102 -0
- package/server/lib/skills-catalog/DOCUMENTATION.md +178 -0
- package/server/lib/skills-catalog/cache.js +29 -0
- package/server/lib/skills-catalog/clawdhub/api.js +158 -0
- package/server/lib/skills-catalog/clawdhub/index.js +30 -0
- package/server/lib/skills-catalog/clawdhub/install.js +238 -0
- package/server/lib/skills-catalog/clawdhub/scan.js +113 -0
- package/server/lib/skills-catalog/curated-sources.js +21 -0
- package/server/lib/skills-catalog/git.js +77 -0
- package/server/lib/skills-catalog/index.js +42 -0
- package/server/lib/skills-catalog/install.js +294 -0
- package/server/lib/skills-catalog/scan.js +221 -0
- package/server/lib/skills-catalog/source.js +87 -0
- package/server/lib/terminal/DOCUMENTATION.md +76 -0
- package/server/lib/terminal/index.js +31 -0
- package/server/lib/terminal/output-replay-buffer.js +78 -0
- package/server/lib/terminal/output-replay-buffer.test.js +75 -0
- package/server/lib/terminal/runtime.js +850 -0
- package/server/lib/terminal/runtime.test.js +96 -0
- package/server/lib/terminal/terminal-ws-protocol.js +68 -0
- package/server/lib/terminal/terminal-ws-protocol.test.js +145 -0
- package/server/lib/text/DOCUMENTATION.md +35 -0
- package/server/lib/text/summarization.js +138 -0
- package/server/lib/text/summarization.test.js +34 -0
- package/server/lib/tts/DOCUMENTATION.md +146 -0
- package/server/lib/tts/base-url.js +62 -0
- package/server/lib/tts/capability-runtime.js +31 -0
- package/server/lib/tts/index.js +19 -0
- package/server/lib/tts/routes.js +261 -0
- package/server/lib/tts/routes.test.js +53 -0
- package/server/lib/tts/service.js +178 -0
- package/server/lib/tts/stt.js +75 -0
- package/server/lib/tunnels/DOCUMENTATION.md +18 -0
- package/server/lib/tunnels/index.js +166 -0
- package/server/lib/tunnels/managed-config.js +201 -0
- package/server/lib/tunnels/providers/cloudflare.js +260 -0
- package/server/lib/tunnels/registry.js +51 -0
- package/server/lib/tunnels/routes.js +605 -0
- package/server/lib/tunnels/types.js +219 -0
- package/server/lib/ui-auth/DOCUMENTATION.md +38 -0
- package/server/lib/ui-auth/ui-auth.js +673 -0
- package/server/lib/ui-auth/ui-passkeys.js +545 -0
- package/server/opencode-proxy.test.js +151 -0
- package/server/proxy-headers.js +61 -0
- package/server/proxy-headers.test.js +58 -0
- package/server/sse-routes.test.js +152 -0
|
@@ -0,0 +1,1019 @@
|
|
|
1
|
+
import { spawn, spawnSync } from 'node:child_process';
|
|
2
|
+
import net from 'node:net';
|
|
3
|
+
import os from 'node:os';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { readEnvKeys } from './env-keys.js';
|
|
6
|
+
|
|
7
|
+
const parsePositiveInt = (value, fallback) => {
|
|
8
|
+
const parsed = Number.parseInt(String(value ?? ''), 10);
|
|
9
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
const HEALTH_CHECK_TIMEOUT_MS = parsePositiveInt(process.env.VINCI_OPENCODE_HEALTH_TIMEOUT_MS, 5000);
|
|
13
|
+
const HEALTH_CHECK_MAX_CONSECUTIVE_FAILURES = parsePositiveInt(
|
|
14
|
+
process.env.VINCI_OPENCODE_HEALTH_CONSECUTIVE_FAILURES,
|
|
15
|
+
20
|
|
16
|
+
);
|
|
17
|
+
const HEALTH_CHECK_INTERVAL_OVERRIDE_MS = parsePositiveInt(process.env.VINCI_OPENCODE_HEALTH_INTERVAL_MS, 0);
|
|
18
|
+
const HEALTH_CHECK_RESULT_CACHE_MS = parsePositiveInt(process.env.VINCI_OPENCODE_HEALTH_CACHE_MS, 750);
|
|
19
|
+
|
|
20
|
+
export const createOpenCodeLifecycleRuntime = (deps) => {
|
|
21
|
+
const {
|
|
22
|
+
state,
|
|
23
|
+
env,
|
|
24
|
+
syncToHmrState,
|
|
25
|
+
syncFromHmrState,
|
|
26
|
+
getOpenCodeAuthHeaders,
|
|
27
|
+
buildOpenCodeUrl,
|
|
28
|
+
waitForReady,
|
|
29
|
+
normalizeApiPrefix,
|
|
30
|
+
applyOpencodeBinaryFromSettings,
|
|
31
|
+
ensureOpencodeCliEnv,
|
|
32
|
+
ensureLocalOpenCodeServerPassword,
|
|
33
|
+
buildWslExecArgs,
|
|
34
|
+
resolveWslExecutablePath,
|
|
35
|
+
resolveManagedOpenCodeLaunchSpec,
|
|
36
|
+
setOpenCodePort,
|
|
37
|
+
setDetectedOpenCodeApiPrefix,
|
|
38
|
+
setupProxy,
|
|
39
|
+
ensureOpenCodeApiPrefix,
|
|
40
|
+
clearResolvedOpenCodeBinary,
|
|
41
|
+
buildAugmentedPath,
|
|
42
|
+
buildManagedOpenCodePath,
|
|
43
|
+
getManagedOpenCodeShellEnvSnapshot,
|
|
44
|
+
getActiveSessionCount = () => 0,
|
|
45
|
+
readSettingsFromDiskMigrated,
|
|
46
|
+
} = deps;
|
|
47
|
+
|
|
48
|
+
const killProcessOnPort = (port) => {
|
|
49
|
+
if (!port || process.platform === 'win32') return;
|
|
50
|
+
try {
|
|
51
|
+
const result = spawnSync('lsof', ['-ti', `:${port}`], { encoding: 'utf8', timeout: 5000, windowsHide: true });
|
|
52
|
+
const output = result.stdout || '';
|
|
53
|
+
const myPid = process.pid;
|
|
54
|
+
for (const pidStr of output.split(/\s+/)) {
|
|
55
|
+
const pid = parseInt(pidStr.trim(), 10);
|
|
56
|
+
if (pid && pid !== myPid) {
|
|
57
|
+
try {
|
|
58
|
+
spawnSync('kill', ['-9', String(pid)], { stdio: 'ignore', timeout: 2000 });
|
|
59
|
+
} catch {
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
} catch {
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
const hasChildProcessExited = (child) => !child || child.exitCode !== null || child.signalCode !== null;
|
|
68
|
+
|
|
69
|
+
const isManagedOpenCodeProcessAlive = () => {
|
|
70
|
+
const child = state.openCodeProcess;
|
|
71
|
+
if (!child || hasChildProcessExited(child)) return false;
|
|
72
|
+
if (!child.pid) return true;
|
|
73
|
+
try {
|
|
74
|
+
process.kill(child.pid, 0);
|
|
75
|
+
return true;
|
|
76
|
+
} catch {
|
|
77
|
+
return false;
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
const waitForChildProcessClose = (child, timeoutMs) => new Promise((resolve) => {
|
|
82
|
+
if (!child || hasChildProcessExited(child)) {
|
|
83
|
+
resolve(true);
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
let done = false;
|
|
88
|
+
const finish = (closed) => {
|
|
89
|
+
if (done) return;
|
|
90
|
+
done = true;
|
|
91
|
+
clearTimeout(timer);
|
|
92
|
+
child.off('close', onClose);
|
|
93
|
+
child.off('error', onError);
|
|
94
|
+
resolve(closed);
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
const onClose = () => finish(true);
|
|
98
|
+
const onError = () => finish(hasChildProcessExited(child));
|
|
99
|
+
const timer = setTimeout(() => finish(hasChildProcessExited(child)), timeoutMs);
|
|
100
|
+
|
|
101
|
+
child.once('close', onClose);
|
|
102
|
+
child.once('error', onError);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
const waitForPortRelease = (port, timeoutMs, hostname = env.ENV_CONFIGURED_OPENCODE_HOSTNAME) => {
|
|
106
|
+
if (!port) {
|
|
107
|
+
return Promise.resolve(true);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const probeHost = !hostname || hostname === '0.0.0.0' || hostname === '::' || hostname === '[::]'
|
|
111
|
+
? '127.0.0.1'
|
|
112
|
+
: hostname;
|
|
113
|
+
const deadline = Date.now() + timeoutMs;
|
|
114
|
+
|
|
115
|
+
return new Promise((resolve) => {
|
|
116
|
+
const attempt = () => {
|
|
117
|
+
const socket = net.connect({ port, host: probeHost });
|
|
118
|
+
let settled = false;
|
|
119
|
+
|
|
120
|
+
const finish = (released) => {
|
|
121
|
+
if (settled) return;
|
|
122
|
+
settled = true;
|
|
123
|
+
socket.removeAllListeners();
|
|
124
|
+
socket.destroy();
|
|
125
|
+
if (released || Date.now() >= deadline) {
|
|
126
|
+
resolve(released);
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
setTimeout(attempt, 150);
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
socket.once('connect', () => finish(false));
|
|
133
|
+
socket.once('timeout', () => finish(true));
|
|
134
|
+
socket.once('error', (error) => {
|
|
135
|
+
if (error && typeof error === 'object' && (error.code === 'ECONNREFUSED' || error.code === 'EHOSTUNREACH')) {
|
|
136
|
+
finish(true);
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
finish(false);
|
|
140
|
+
});
|
|
141
|
+
socket.setTimeout(500);
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
attempt();
|
|
145
|
+
});
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
const closeManagedOpenCodeChild = async (child) => {
|
|
149
|
+
if (!child) {
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const pid = child.pid;
|
|
154
|
+
if (!pid || hasChildProcessExited(child)) {
|
|
155
|
+
await waitForChildProcessClose(child, 250);
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (process.platform === 'win32') {
|
|
160
|
+
try {
|
|
161
|
+
child.kill();
|
|
162
|
+
} catch {
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if (await waitForChildProcessClose(child, 800)) {
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
try {
|
|
170
|
+
spawnSync('taskkill', ['/pid', String(pid), '/t'], {
|
|
171
|
+
stdio: 'ignore',
|
|
172
|
+
timeout: 3000,
|
|
173
|
+
windowsHide: true,
|
|
174
|
+
});
|
|
175
|
+
} catch {
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
if (await waitForChildProcessClose(child, 1500)) {
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
try {
|
|
183
|
+
spawnSync('taskkill', ['/pid', String(pid), '/f', '/t'], {
|
|
184
|
+
stdio: 'ignore',
|
|
185
|
+
timeout: 5000,
|
|
186
|
+
windowsHide: true,
|
|
187
|
+
});
|
|
188
|
+
} catch {
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
await waitForChildProcessClose(child, 3000);
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
try {
|
|
196
|
+
child.kill('SIGTERM');
|
|
197
|
+
} catch {
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
if (await waitForChildProcessClose(child, 2500)) {
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
try {
|
|
205
|
+
child.kill('SIGKILL');
|
|
206
|
+
} catch {
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
await waitForChildProcessClose(child, 1000);
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
const formatCapturedOutput = ({ stdout, stderr }) => {
|
|
213
|
+
const parts = [];
|
|
214
|
+
if (stdout.trim()) {
|
|
215
|
+
parts.push(`stdout:\n${stdout.trim()}`);
|
|
216
|
+
}
|
|
217
|
+
if (stderr.trim()) {
|
|
218
|
+
parts.push(`stderr:\n${stderr.trim()}`);
|
|
219
|
+
}
|
|
220
|
+
return parts.length > 0 ? parts.join('\n\n') : 'No stdout/stderr captured';
|
|
221
|
+
};
|
|
222
|
+
|
|
223
|
+
const createManagedOpenCodeServerProcess = async ({ hostname, port, timeout, cwd, env: processEnv, shellEnvKeysCount = 0 }) => {
|
|
224
|
+
let binary = (process.env.OPENCODE_BINARY || 'opencode').trim() || 'opencode';
|
|
225
|
+
let args = ['serve', '--hostname', hostname, '--port', String(port)];
|
|
226
|
+
let launchWrapperType = null;
|
|
227
|
+
|
|
228
|
+
if (process.platform === 'win32' && state.useWslForOpencode) {
|
|
229
|
+
const wslBinary = state.resolvedWslBinary || resolveWslExecutablePath();
|
|
230
|
+
if (!wslBinary) {
|
|
231
|
+
throw new Error('WSL executable not found while attempting to launch OpenCode from WSL');
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
const wslOpencode = state.resolvedWslOpencodePath && state.resolvedWslOpencodePath.trim().length > 0
|
|
235
|
+
? state.resolvedWslOpencodePath.trim()
|
|
236
|
+
: 'opencode';
|
|
237
|
+
const serveHost = hostname === '127.0.0.1' ? '0.0.0.0' : hostname;
|
|
238
|
+
|
|
239
|
+
binary = wslBinary;
|
|
240
|
+
args = buildWslExecArgs([
|
|
241
|
+
wslOpencode,
|
|
242
|
+
'serve',
|
|
243
|
+
'--hostname',
|
|
244
|
+
serveHost,
|
|
245
|
+
'--port',
|
|
246
|
+
String(port),
|
|
247
|
+
], state.resolvedWslDistro);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
if (process.platform === 'win32' && !state.useWslForOpencode) {
|
|
251
|
+
const launchSpec = resolveManagedOpenCodeLaunchSpec(binary);
|
|
252
|
+
if (launchSpec?.binary) {
|
|
253
|
+
if (launchSpec.wrapperType) {
|
|
254
|
+
console.log(`Launching OpenCode via ${launchSpec.wrapperType}: ${launchSpec.binary}`);
|
|
255
|
+
}
|
|
256
|
+
launchWrapperType = launchSpec.wrapperType || null;
|
|
257
|
+
binary = launchSpec.binary;
|
|
258
|
+
args = [...(Array.isArray(launchSpec.args) ? launchSpec.args : []), ...args];
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
const pathValue = typeof processEnv?.PATH === 'string' ? processEnv.PATH : '';
|
|
263
|
+
const pathEntryCount = pathValue ? pathValue.split(process.platform === 'win32' ? ';' : ':').filter(Boolean).length : 0;
|
|
264
|
+
state.lastOpenCodeLaunchDiagnostics = {
|
|
265
|
+
launchedAt: new Date().toISOString(),
|
|
266
|
+
binary,
|
|
267
|
+
args,
|
|
268
|
+
cwd,
|
|
269
|
+
hostname,
|
|
270
|
+
port,
|
|
271
|
+
wrapperType: launchWrapperType,
|
|
272
|
+
pathEntryCount,
|
|
273
|
+
hasShellEnv: shellEnvKeysCount > 0,
|
|
274
|
+
shellEnvKeysCount,
|
|
275
|
+
};
|
|
276
|
+
console.log('[OpenCode] Launching managed server', state.lastOpenCodeLaunchDiagnostics);
|
|
277
|
+
|
|
278
|
+
const child = spawn(binary, args, {
|
|
279
|
+
cwd,
|
|
280
|
+
env: processEnv,
|
|
281
|
+
windowsHide: true,
|
|
282
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
const url = await new Promise((resolve, reject) => {
|
|
286
|
+
let stdout = '';
|
|
287
|
+
let stderr = '';
|
|
288
|
+
let done = false;
|
|
289
|
+
const finish = (handler, value) => {
|
|
290
|
+
if (done) return;
|
|
291
|
+
done = true;
|
|
292
|
+
clearTimeout(timer);
|
|
293
|
+
child.stdout?.off('data', onStdout);
|
|
294
|
+
child.stderr?.off('data', onStderr);
|
|
295
|
+
child.off('exit', onExit);
|
|
296
|
+
child.off('error', onError);
|
|
297
|
+
handler(value);
|
|
298
|
+
};
|
|
299
|
+
|
|
300
|
+
const onStdout = (chunk) => {
|
|
301
|
+
stdout += chunk.toString();
|
|
302
|
+
const lines = stdout.split('\n');
|
|
303
|
+
for (const line of lines) {
|
|
304
|
+
if (!line.startsWith('opencode server listening')) continue;
|
|
305
|
+
const match = line.match(/on\s+(https?:\/\/[^\s]+)/);
|
|
306
|
+
if (!match) {
|
|
307
|
+
finish(reject, new Error(`Failed to parse server url from output: ${line}`));
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
310
|
+
finish(resolve, match[1]);
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
313
|
+
};
|
|
314
|
+
|
|
315
|
+
const onStderr = (chunk) => {
|
|
316
|
+
stderr += chunk.toString();
|
|
317
|
+
};
|
|
318
|
+
|
|
319
|
+
const onExit = (code, signal) => {
|
|
320
|
+
const reason = signal ? `signal ${signal}` : `code ${code}`;
|
|
321
|
+
const appBundleHint = process.platform === 'darwin' && /\/OpenCode\.app\/Contents\/MacOS\/(?:OpenCode|opencode-cli)$/i.test(binary)
|
|
322
|
+
? ' The configured binary appears to point at the macOS desktop app bundle; Vinci needs the standalone opencode CLI.'
|
|
323
|
+
: '';
|
|
324
|
+
finish(reject, new Error(`OpenCode process exited before serving with ${reason}. Binary used: ${binary}.${appBundleHint} ${formatCapturedOutput({ stdout, stderr })}`));
|
|
325
|
+
};
|
|
326
|
+
|
|
327
|
+
const onError = (error) => {
|
|
328
|
+
finish(reject, error);
|
|
329
|
+
};
|
|
330
|
+
|
|
331
|
+
const timer = setTimeout(() => {
|
|
332
|
+
finish(reject, new Error(`Timeout waiting for OpenCode to start after ${timeout}ms`));
|
|
333
|
+
}, timeout);
|
|
334
|
+
|
|
335
|
+
child.stdout?.on('data', onStdout);
|
|
336
|
+
child.stderr?.on('data', onStderr);
|
|
337
|
+
child.on('exit', onExit);
|
|
338
|
+
child.on('error', onError);
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
return {
|
|
342
|
+
url,
|
|
343
|
+
async close() {
|
|
344
|
+
await closeManagedOpenCodeChild(child);
|
|
345
|
+
},
|
|
346
|
+
};
|
|
347
|
+
};
|
|
348
|
+
|
|
349
|
+
const resolveManagedOpenCodePort = async (requestedPort, hostname = '127.0.0.1') => {
|
|
350
|
+
if (typeof requestedPort === 'number' && Number.isFinite(requestedPort) && requestedPort > 0) {
|
|
351
|
+
return requestedPort;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
return await new Promise((resolve, reject) => {
|
|
355
|
+
const server = net.createServer();
|
|
356
|
+
const cleanup = () => {
|
|
357
|
+
server.removeAllListeners('error');
|
|
358
|
+
server.removeAllListeners('listening');
|
|
359
|
+
};
|
|
360
|
+
|
|
361
|
+
server.once('error', (error) => {
|
|
362
|
+
cleanup();
|
|
363
|
+
reject(error);
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
server.once('listening', () => {
|
|
367
|
+
const address = server.address();
|
|
368
|
+
const port = address && typeof address === 'object' ? address.port : 0;
|
|
369
|
+
server.close(() => {
|
|
370
|
+
cleanup();
|
|
371
|
+
if (port > 0) {
|
|
372
|
+
resolve(port);
|
|
373
|
+
return;
|
|
374
|
+
}
|
|
375
|
+
reject(new Error('Failed to allocate OpenCode port'));
|
|
376
|
+
});
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
server.listen(0, hostname);
|
|
380
|
+
});
|
|
381
|
+
};
|
|
382
|
+
|
|
383
|
+
const isOpenCodeProcessHealthy = async () => {
|
|
384
|
+
if (!state.openCodeProcess || !state.openCodePort) {
|
|
385
|
+
return false;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
try {
|
|
389
|
+
const response = await fetch(buildOpenCodeUrl('/global/health', ''), {
|
|
390
|
+
method: 'GET',
|
|
391
|
+
headers: {
|
|
392
|
+
Accept: 'application/json',
|
|
393
|
+
...getOpenCodeAuthHeaders(),
|
|
394
|
+
},
|
|
395
|
+
signal: AbortSignal.timeout(HEALTH_CHECK_TIMEOUT_MS),
|
|
396
|
+
});
|
|
397
|
+
if (!response.ok) return false;
|
|
398
|
+
const body = await response.json().catch(() => null);
|
|
399
|
+
return body?.healthy === true;
|
|
400
|
+
} catch {
|
|
401
|
+
return false;
|
|
402
|
+
}
|
|
403
|
+
};
|
|
404
|
+
|
|
405
|
+
const probeExternalOpenCode = async (port, origin) => {
|
|
406
|
+
if (!port || port <= 0) {
|
|
407
|
+
return false;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
try {
|
|
411
|
+
const controller = new AbortController();
|
|
412
|
+
const timeout = setTimeout(() => controller.abort(), 3000);
|
|
413
|
+
const base = origin ?? `http://127.0.0.1:${port}`;
|
|
414
|
+
const response = await fetch(`${base}/global/health`, {
|
|
415
|
+
method: 'GET',
|
|
416
|
+
headers: {
|
|
417
|
+
Accept: 'application/json',
|
|
418
|
+
...getOpenCodeAuthHeaders(),
|
|
419
|
+
},
|
|
420
|
+
signal: controller.signal,
|
|
421
|
+
});
|
|
422
|
+
clearTimeout(timeout);
|
|
423
|
+
if (!response.ok) return false;
|
|
424
|
+
const body = await response.json().catch(() => null);
|
|
425
|
+
return body?.healthy === true;
|
|
426
|
+
} catch {
|
|
427
|
+
return false;
|
|
428
|
+
}
|
|
429
|
+
};
|
|
430
|
+
|
|
431
|
+
const waitForOpenCodePort = async (timeoutMs = 15000) => {
|
|
432
|
+
if (state.openCodePort !== null) {
|
|
433
|
+
return state.openCodePort;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
const deadline = Date.now() + timeoutMs;
|
|
437
|
+
while (Date.now() < deadline) {
|
|
438
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
439
|
+
if (state.openCodePort !== null) {
|
|
440
|
+
return state.openCodePort;
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
throw new Error('Timed out waiting for OpenCode port');
|
|
445
|
+
};
|
|
446
|
+
|
|
447
|
+
const START_OPEN_CODE_MAX_ATTEMPTS = 2;
|
|
448
|
+
|
|
449
|
+
const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
450
|
+
|
|
451
|
+
const startOpenCodeOnce = async () => {
|
|
452
|
+
const desiredPort = env.ENV_CONFIGURED_OPENCODE_PORT ?? 0;
|
|
453
|
+
|
|
454
|
+
// Kill any orphaned OpenCode process on the desired port BEFORE resolving.
|
|
455
|
+
// This ensures the port is free when resolveManagedOpenCodePort runs.
|
|
456
|
+
if (desiredPort > 0) {
|
|
457
|
+
killProcessOnPort(desiredPort);
|
|
458
|
+
await waitForPortRelease(desiredPort, 3000);
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
const spawnPort = await resolveManagedOpenCodePort(desiredPort, env.ENV_CONFIGURED_OPENCODE_HOSTNAME);
|
|
462
|
+
console.log(
|
|
463
|
+
desiredPort > 0
|
|
464
|
+
? `Starting OpenCode on requested port ${desiredPort}...`
|
|
465
|
+
: `Starting OpenCode on allocated port ${spawnPort}...`
|
|
466
|
+
);
|
|
467
|
+
|
|
468
|
+
await applyOpencodeBinaryFromSettings({ strict: true });
|
|
469
|
+
ensureOpencodeCliEnv();
|
|
470
|
+
const openCodePassword = await ensureLocalOpenCodeServerPassword({ rotateManaged: true });
|
|
471
|
+
const envPath = typeof buildManagedOpenCodePath === 'function'
|
|
472
|
+
? buildManagedOpenCodePath()
|
|
473
|
+
: typeof buildAugmentedPath === 'function'
|
|
474
|
+
? buildAugmentedPath()
|
|
475
|
+
: process.env.PATH;
|
|
476
|
+
const shellEnv = typeof getManagedOpenCodeShellEnvSnapshot === 'function'
|
|
477
|
+
? getManagedOpenCodeShellEnvSnapshot() || {}
|
|
478
|
+
: {};
|
|
479
|
+
|
|
480
|
+
// The OpenCode CLI binary uses the xdg-basedir library (not OPENCODE_DATA_DIR)
|
|
481
|
+
// to resolve its data directory (opencode.db, auth tokens, logs, repos).
|
|
482
|
+
// Set XDG_DATA_HOME and XDG_CONFIG_HOME on the child process so xdg-basedir
|
|
483
|
+
// resolves to the correct isolated paths instead of sharing with other installs.
|
|
484
|
+
//
|
|
485
|
+
// Default to ~/.vinci so the sidecar always isolates data even when env vars
|
|
486
|
+
// are not explicitly set (e.g. running `cli.js serve` directly).
|
|
487
|
+
const vinciHome = process.env.VINCI_DATA_DIR || path.join(os.homedir(), '.vinci');
|
|
488
|
+
const ocConfigDir = process.env.OPENCODE_CONFIG_DIR || path.join(vinciHome, 'opencode');
|
|
489
|
+
const ocDataDir = process.env.OPENCODE_DATA_DIR || path.join(vinciHome, 'data', 'opencode');
|
|
490
|
+
const xdgOverrides = {};
|
|
491
|
+
xdgOverrides.XDG_DATA_HOME = path.dirname(path.resolve(ocDataDir));
|
|
492
|
+
xdgOverrides.XDG_CONFIG_HOME = path.dirname(path.resolve(ocConfigDir));
|
|
493
|
+
xdgOverrides.XDG_CACHE_HOME = path.join(path.resolve(vinciHome), 'cache');
|
|
494
|
+
xdgOverrides.XDG_STATE_HOME = path.join(path.resolve(vinciHome), 'state');
|
|
495
|
+
|
|
496
|
+
console.log('[lifecycle] XDG overrides:', JSON.stringify({
|
|
497
|
+
XDG_DATA_HOME: xdgOverrides.XDG_DATA_HOME || '(not set)',
|
|
498
|
+
XDG_CONFIG_HOME: xdgOverrides.XDG_CONFIG_HOME || '(not set)',
|
|
499
|
+
XDG_CACHE_HOME: xdgOverrides.XDG_CACHE_HOME || '(not set)',
|
|
500
|
+
XDG_STATE_HOME: xdgOverrides.XDG_STATE_HOME || '(not set)',
|
|
501
|
+
OPENCODE_CONFIG_DIR: process.env.OPENCODE_CONFIG_DIR || '(not set)',
|
|
502
|
+
OPENCODE_DATA_DIR: process.env.OPENCODE_DATA_DIR || '(not set)',
|
|
503
|
+
VINCI_DATA_DIR: process.env.VINCI_DATA_DIR || '(not set)',
|
|
504
|
+
}, null, 2));
|
|
505
|
+
|
|
506
|
+
const settings = (await readSettingsFromDiskMigrated?.().catch(() => null)) || {};
|
|
507
|
+
const keyEnv = {};
|
|
508
|
+
if (typeof settings.klipyAppKey === 'string') {
|
|
509
|
+
keyEnv.KLIPY_APP_KEY = settings.klipyAppKey;
|
|
510
|
+
}
|
|
511
|
+
if (typeof settings.imageGenApiKey === 'string') {
|
|
512
|
+
keyEnv.GOOGLE_GENERATIVE_AI_API_KEY = settings.imageGenApiKey;
|
|
513
|
+
}
|
|
514
|
+
if (typeof settings.elevenlabsApiKey === 'string') {
|
|
515
|
+
keyEnv.ELEVENLABS_API_KEY = settings.elevenlabsApiKey;
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
// Also load env keys from env-keys.json (written by PUT /api/config/settings)
|
|
519
|
+
// so they survive server restarts even if settings.json lacks them.
|
|
520
|
+
const envKeysFromFile = readEnvKeys();
|
|
521
|
+
for (const [envVar, value] of Object.entries(envKeysFromFile)) {
|
|
522
|
+
if (typeof value === 'string' && value.length > 0) {
|
|
523
|
+
keyEnv[envVar] = value;
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
try {
|
|
528
|
+
const serverInstance = await createManagedOpenCodeServerProcess({
|
|
529
|
+
hostname: env.ENV_CONFIGURED_OPENCODE_HOSTNAME,
|
|
530
|
+
port: spawnPort,
|
|
531
|
+
timeout: 30000,
|
|
532
|
+
cwd: state.openCodeWorkingDirectory,
|
|
533
|
+
shellEnvKeysCount: Object.keys(shellEnv).length,
|
|
534
|
+
env: {
|
|
535
|
+
...shellEnv,
|
|
536
|
+
...process.env,
|
|
537
|
+
...xdgOverrides,
|
|
538
|
+
...keyEnv,
|
|
539
|
+
PATH: envPath,
|
|
540
|
+
OPENCODE_SERVER_PASSWORD: openCodePassword,
|
|
541
|
+
},
|
|
542
|
+
});
|
|
543
|
+
|
|
544
|
+
if (!serverInstance || !serverInstance.url) {
|
|
545
|
+
throw new Error('OpenCode server started but URL is missing');
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
const url = new URL(serverInstance.url);
|
|
549
|
+
const port = parseInt(url.port, 10);
|
|
550
|
+
const prefix = normalizeApiPrefix(url.pathname);
|
|
551
|
+
|
|
552
|
+
if (await waitForReady(serverInstance.url, 10000)) {
|
|
553
|
+
setOpenCodePort(port);
|
|
554
|
+
setDetectedOpenCodeApiPrefix(prefix);
|
|
555
|
+
|
|
556
|
+
state.isOpenCodeReady = true;
|
|
557
|
+
state.lastOpenCodeError = null;
|
|
558
|
+
state.openCodeNotReadySince = 0;
|
|
559
|
+
|
|
560
|
+
return serverInstance;
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
try {
|
|
564
|
+
await serverInstance.close();
|
|
565
|
+
} catch {
|
|
566
|
+
}
|
|
567
|
+
throw new Error('Server started but health check failed (timeout)');
|
|
568
|
+
} catch (error) {
|
|
569
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
570
|
+
state.lastOpenCodeError = message;
|
|
571
|
+
state.openCodePort = null;
|
|
572
|
+
syncToHmrState();
|
|
573
|
+
console.error(`Failed to start OpenCode: ${message}`);
|
|
574
|
+
throw error;
|
|
575
|
+
}
|
|
576
|
+
};
|
|
577
|
+
|
|
578
|
+
const startOpenCode = async () => {
|
|
579
|
+
let lastError = null;
|
|
580
|
+
for (let attempt = 1; attempt <= START_OPEN_CODE_MAX_ATTEMPTS; attempt += 1) {
|
|
581
|
+
try {
|
|
582
|
+
return await startOpenCodeOnce();
|
|
583
|
+
} catch (error) {
|
|
584
|
+
lastError = error;
|
|
585
|
+
if (error?.code === 'OPENCODE_BINARY_INVALID') {
|
|
586
|
+
break;
|
|
587
|
+
}
|
|
588
|
+
if (attempt >= START_OPEN_CODE_MAX_ATTEMPTS) {
|
|
589
|
+
break;
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
593
|
+
console.warn(`[OpenCode] Managed server startup failed on attempt ${attempt}/${START_OPEN_CODE_MAX_ATTEMPTS}; retrying: ${message}`);
|
|
594
|
+
state.openCodePort = null;
|
|
595
|
+
state.isOpenCodeReady = false;
|
|
596
|
+
state.openCodeNotReadySince = Date.now();
|
|
597
|
+
syncToHmrState();
|
|
598
|
+
await delay(750 * attempt);
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
throw lastError;
|
|
603
|
+
};
|
|
604
|
+
|
|
605
|
+
const restartOpenCode = async () => {
|
|
606
|
+
if (state.isShuttingDown) return;
|
|
607
|
+
if (state.currentRestartPromise) {
|
|
608
|
+
await state.currentRestartPromise;
|
|
609
|
+
return;
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
state.currentRestartPromise = (async () => {
|
|
613
|
+
state.isRestartingOpenCode = true;
|
|
614
|
+
state.isOpenCodeReady = false;
|
|
615
|
+
state.openCodeNotReadySince = Date.now();
|
|
616
|
+
console.log('Restarting OpenCode process...');
|
|
617
|
+
|
|
618
|
+
if (state.isExternalOpenCode) {
|
|
619
|
+
console.log('Re-probing external OpenCode server...');
|
|
620
|
+
const probePort = state.openCodePort || env.ENV_CONFIGURED_OPENCODE_PORT || 4096;
|
|
621
|
+
const probeOrigin = state.openCodeBaseUrl ?? env.ENV_CONFIGURED_OPENCODE_HOST?.origin;
|
|
622
|
+
const healthy = await probeExternalOpenCode(probePort, probeOrigin);
|
|
623
|
+
if (healthy) {
|
|
624
|
+
console.log(`External OpenCode server on port ${probePort} is healthy`);
|
|
625
|
+
setOpenCodePort(probePort);
|
|
626
|
+
state.isOpenCodeReady = true;
|
|
627
|
+
state.lastOpenCodeError = null;
|
|
628
|
+
state.openCodeNotReadySince = 0;
|
|
629
|
+
syncToHmrState();
|
|
630
|
+
} else {
|
|
631
|
+
state.lastOpenCodeError = `External OpenCode server on port ${probePort} is not responding`;
|
|
632
|
+
console.error(state.lastOpenCodeError);
|
|
633
|
+
throw new Error(state.lastOpenCodeError);
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
if (state.expressApp) {
|
|
637
|
+
setupProxy(state.expressApp);
|
|
638
|
+
ensureOpenCodeApiPrefix();
|
|
639
|
+
}
|
|
640
|
+
return;
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
const portToKill = state.openCodePort;
|
|
644
|
+
|
|
645
|
+
if (state.openCodeProcess) {
|
|
646
|
+
console.log('Stopping existing OpenCode process...');
|
|
647
|
+
try {
|
|
648
|
+
await state.openCodeProcess.close();
|
|
649
|
+
} catch (error) {
|
|
650
|
+
console.warn('Error closing OpenCode process:', error);
|
|
651
|
+
}
|
|
652
|
+
state.openCodeProcess = null;
|
|
653
|
+
syncToHmrState();
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
killProcessOnPort(portToKill);
|
|
657
|
+
if (!(await waitForPortRelease(portToKill, 5000))) {
|
|
658
|
+
console.warn(`Timed out waiting for OpenCode port ${portToKill} to be released`);
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
if (env.ENV_CONFIGURED_OPENCODE_PORT) {
|
|
662
|
+
console.log(`Using OpenCode port from environment: ${env.ENV_CONFIGURED_OPENCODE_PORT}`);
|
|
663
|
+
setOpenCodePort(env.ENV_CONFIGURED_OPENCODE_PORT);
|
|
664
|
+
} else {
|
|
665
|
+
state.openCodePort = null;
|
|
666
|
+
syncToHmrState();
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
state.openCodeApiPrefixDetected = true;
|
|
670
|
+
state.openCodeApiPrefix = '';
|
|
671
|
+
if (state.openCodeApiDetectionTimer) {
|
|
672
|
+
clearTimeout(state.openCodeApiDetectionTimer);
|
|
673
|
+
state.openCodeApiDetectionTimer = null;
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
state.lastOpenCodeError = null;
|
|
677
|
+
state.openCodeProcess = await startOpenCode();
|
|
678
|
+
syncToHmrState();
|
|
679
|
+
|
|
680
|
+
if (state.expressApp) {
|
|
681
|
+
setupProxy(state.expressApp);
|
|
682
|
+
ensureOpenCodeApiPrefix();
|
|
683
|
+
}
|
|
684
|
+
})();
|
|
685
|
+
|
|
686
|
+
try {
|
|
687
|
+
await state.currentRestartPromise;
|
|
688
|
+
} catch (error) {
|
|
689
|
+
console.error(`Failed to restart OpenCode: ${error.message}`);
|
|
690
|
+
state.lastOpenCodeError = error.message;
|
|
691
|
+
if (!env.ENV_CONFIGURED_OPENCODE_PORT) {
|
|
692
|
+
state.openCodePort = null;
|
|
693
|
+
syncToHmrState();
|
|
694
|
+
}
|
|
695
|
+
state.openCodeApiPrefixDetected = true;
|
|
696
|
+
state.openCodeApiPrefix = '';
|
|
697
|
+
throw error;
|
|
698
|
+
} finally {
|
|
699
|
+
state.currentRestartPromise = null;
|
|
700
|
+
state.isRestartingOpenCode = false;
|
|
701
|
+
}
|
|
702
|
+
};
|
|
703
|
+
|
|
704
|
+
const waitForOpenCodeReady = async (timeoutMs = 20000, intervalMs = 400) => {
|
|
705
|
+
if (!state.openCodePort) {
|
|
706
|
+
throw new Error('OpenCode port is not available');
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
const deadline = Date.now() + timeoutMs;
|
|
710
|
+
let lastError = null;
|
|
711
|
+
|
|
712
|
+
while (Date.now() < deadline) {
|
|
713
|
+
try {
|
|
714
|
+
const [configResult, agentResult] = await Promise.all([
|
|
715
|
+
fetch(buildOpenCodeUrl('/config', ''), {
|
|
716
|
+
method: 'GET',
|
|
717
|
+
headers: { Accept: 'application/json', ...getOpenCodeAuthHeaders() },
|
|
718
|
+
}).catch((error) => error),
|
|
719
|
+
fetch(buildOpenCodeUrl('/agent', ''), {
|
|
720
|
+
method: 'GET',
|
|
721
|
+
headers: { Accept: 'application/json', ...getOpenCodeAuthHeaders() },
|
|
722
|
+
}).catch((error) => error),
|
|
723
|
+
]);
|
|
724
|
+
|
|
725
|
+
if (configResult instanceof Error) {
|
|
726
|
+
lastError = configResult;
|
|
727
|
+
await new Promise((resolve) => setTimeout(resolve, intervalMs));
|
|
728
|
+
continue;
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
if (!configResult.ok) {
|
|
732
|
+
lastError = new Error(`OpenCode config endpoint responded with status ${configResult.status}`);
|
|
733
|
+
await new Promise((resolve) => setTimeout(resolve, intervalMs));
|
|
734
|
+
continue;
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
await configResult.json().catch(() => null);
|
|
738
|
+
|
|
739
|
+
if (agentResult instanceof Error) {
|
|
740
|
+
lastError = agentResult;
|
|
741
|
+
await new Promise((resolve) => setTimeout(resolve, intervalMs));
|
|
742
|
+
continue;
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
if (!agentResult.ok) {
|
|
746
|
+
lastError = new Error(`Agent endpoint responded with status ${agentResult.status}`);
|
|
747
|
+
await new Promise((resolve) => setTimeout(resolve, intervalMs));
|
|
748
|
+
continue;
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
await agentResult.json().catch(() => []);
|
|
752
|
+
|
|
753
|
+
state.isOpenCodeReady = true;
|
|
754
|
+
state.lastOpenCodeError = null;
|
|
755
|
+
return;
|
|
756
|
+
} catch (error) {
|
|
757
|
+
lastError = error;
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
await new Promise((resolve) => setTimeout(resolve, intervalMs));
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
if (lastError) {
|
|
764
|
+
state.lastOpenCodeError = lastError.message || String(lastError);
|
|
765
|
+
throw lastError;
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
const timeoutError = new Error('Timed out waiting for OpenCode to become ready');
|
|
769
|
+
state.lastOpenCodeError = timeoutError.message;
|
|
770
|
+
throw timeoutError;
|
|
771
|
+
};
|
|
772
|
+
|
|
773
|
+
const waitForAgentPresence = async (agentName, timeoutMs = 15000, intervalMs = 300) => {
|
|
774
|
+
if (!state.openCodePort) {
|
|
775
|
+
throw new Error('OpenCode port is not available');
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
const deadline = Date.now() + timeoutMs;
|
|
779
|
+
while (Date.now() < deadline) {
|
|
780
|
+
try {
|
|
781
|
+
const response = await fetch(buildOpenCodeUrl('/agent'), {
|
|
782
|
+
method: 'GET',
|
|
783
|
+
headers: { Accept: 'application/json', ...getOpenCodeAuthHeaders() },
|
|
784
|
+
});
|
|
785
|
+
|
|
786
|
+
if (response.ok) {
|
|
787
|
+
const agents = await response.json();
|
|
788
|
+
if (Array.isArray(agents) && agents.some((agent) => agent?.name === agentName)) {
|
|
789
|
+
return;
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
} catch {
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
await new Promise((resolve) => setTimeout(resolve, intervalMs));
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
throw new Error(`Agent "${agentName}" not available after OpenCode restart`);
|
|
799
|
+
};
|
|
800
|
+
|
|
801
|
+
const refreshOpenCodeAfterConfigChange = async (reason, options = {}) => {
|
|
802
|
+
const { agentName } = options;
|
|
803
|
+
|
|
804
|
+
console.log(`Refreshing OpenCode after ${reason}`);
|
|
805
|
+
clearResolvedOpenCodeBinary();
|
|
806
|
+
await applyOpencodeBinaryFromSettings();
|
|
807
|
+
|
|
808
|
+
await restartOpenCode();
|
|
809
|
+
|
|
810
|
+
try {
|
|
811
|
+
await waitForOpenCodeReady();
|
|
812
|
+
state.isOpenCodeReady = true;
|
|
813
|
+
state.openCodeNotReadySince = 0;
|
|
814
|
+
|
|
815
|
+
if (agentName) {
|
|
816
|
+
await waitForAgentPresence(agentName);
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
state.isOpenCodeReady = true;
|
|
820
|
+
state.openCodeNotReadySince = 0;
|
|
821
|
+
} catch (error) {
|
|
822
|
+
state.isOpenCodeReady = false;
|
|
823
|
+
state.openCodeNotReadySince = Date.now();
|
|
824
|
+
console.error(`Failed to refresh OpenCode after ${reason}:`, error.message);
|
|
825
|
+
throw error;
|
|
826
|
+
}
|
|
827
|
+
};
|
|
828
|
+
|
|
829
|
+
const bootstrapOpenCodeAtStartup = async () => {
|
|
830
|
+
console.log('[lifecycle] bootstrapOpenCodeAtStartup: beginning');
|
|
831
|
+
console.log('[lifecycle] ENV_EFFECTIVE_PORT:', env.ENV_EFFECTIVE_PORT ?? '(not set)');
|
|
832
|
+
console.log('[lifecycle] ENV_SKIP_OPENCODE_START:', env.ENV_SKIP_OPENCODE_START ?? '(not set)');
|
|
833
|
+
console.log('[lifecycle] ENV_CONFIGURED_OPENCODE_HOST:', env.ENV_CONFIGURED_OPENCODE_HOST?.origin ?? '(not set)');
|
|
834
|
+
console.log('[lifecycle] Current state:', JSON.stringify({
|
|
835
|
+
openCodePort: state.openCodePort,
|
|
836
|
+
isOpenCodeReady: state.isOpenCodeReady,
|
|
837
|
+
isExternalOpenCode: state.isExternalOpenCode,
|
|
838
|
+
openCodeBaseUrl: state.openCodeBaseUrl,
|
|
839
|
+
}));
|
|
840
|
+
try {
|
|
841
|
+
syncFromHmrState();
|
|
842
|
+
if (await isOpenCodeProcessHealthy()) {
|
|
843
|
+
console.log(`[HMR] Reusing existing OpenCode process on port ${state.openCodePort}`);
|
|
844
|
+
} else if (env.ENV_SKIP_OPENCODE_START && env.ENV_EFFECTIVE_PORT) {
|
|
845
|
+
const label = env.ENV_CONFIGURED_OPENCODE_HOST ? env.ENV_CONFIGURED_OPENCODE_HOST.origin : `http://localhost:${env.ENV_EFFECTIVE_PORT}`;
|
|
846
|
+
console.log(`Using external OpenCode server at ${label} (skip-start mode)`);
|
|
847
|
+
state.openCodeBaseUrl = env.ENV_CONFIGURED_OPENCODE_HOST?.origin ?? null;
|
|
848
|
+
setOpenCodePort(env.ENV_EFFECTIVE_PORT);
|
|
849
|
+
state.isOpenCodeReady = true;
|
|
850
|
+
state.isExternalOpenCode = true;
|
|
851
|
+
state.lastOpenCodeError = null;
|
|
852
|
+
state.openCodeNotReadySince = 0;
|
|
853
|
+
syncToHmrState();
|
|
854
|
+
} else {
|
|
855
|
+
if (env.ENV_EFFECTIVE_PORT) {
|
|
856
|
+
console.log(`Using OpenCode port from environment: ${env.ENV_EFFECTIVE_PORT}`);
|
|
857
|
+
setOpenCodePort(env.ENV_EFFECTIVE_PORT);
|
|
858
|
+
} else {
|
|
859
|
+
state.openCodePort = null;
|
|
860
|
+
syncToHmrState();
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
state.lastOpenCodeError = null;
|
|
864
|
+
state.openCodeProcess = await startOpenCode();
|
|
865
|
+
syncToHmrState();
|
|
866
|
+
}
|
|
867
|
+
await waitForOpenCodePort();
|
|
868
|
+
try {
|
|
869
|
+
await waitForOpenCodeReady();
|
|
870
|
+
} catch (error) {
|
|
871
|
+
console.error(`OpenCode readiness check failed: ${error.message}`);
|
|
872
|
+
}
|
|
873
|
+
} catch (error) {
|
|
874
|
+
console.error(`Failed to start OpenCode: ${error.message}`);
|
|
875
|
+
console.log('Continuing without OpenCode integration...');
|
|
876
|
+
state.lastOpenCodeError = error.message;
|
|
877
|
+
}
|
|
878
|
+
};
|
|
879
|
+
|
|
880
|
+
/**
|
|
881
|
+
* Perform an immediate (one-shot) health check and restart OpenCode if it's
|
|
882
|
+
* not healthy. Callers on the SSE / WS proxy path use this to trigger
|
|
883
|
+
* recovery without waiting for the next periodic interval (up to 15 s).
|
|
884
|
+
*
|
|
885
|
+
* Skips restart when sessions are actively busy — a busy server under
|
|
886
|
+
* concurrent load can fail the health check timeout without actually
|
|
887
|
+
* being dead (the health endpoint competes with LLM work).
|
|
888
|
+
* Forces restart if sessions stay "busy" and the server stays unhealthy
|
|
889
|
+
* for over 2 minutes (staleness guard against stuck session state).
|
|
890
|
+
*/
|
|
891
|
+
const STALE_BUSY_GRACE_MS = 2 * 60 * 1000;
|
|
892
|
+
let lastUnhealthyWithBusySessionsAt = 0;
|
|
893
|
+
let consecutiveHealthFailures = 0;
|
|
894
|
+
let healthProbePromise = null;
|
|
895
|
+
let healthCheckCyclePromise = null;
|
|
896
|
+
let lastHealthProbeResult = null;
|
|
897
|
+
|
|
898
|
+
const resetHealthFailureState = () => {
|
|
899
|
+
consecutiveHealthFailures = 0;
|
|
900
|
+
lastUnhealthyWithBusySessionsAt = 0;
|
|
901
|
+
};
|
|
902
|
+
|
|
903
|
+
const probeOpenCodeHealth = async () => {
|
|
904
|
+
const now = Date.now();
|
|
905
|
+
if (lastHealthProbeResult && now - lastHealthProbeResult.at < HEALTH_CHECK_RESULT_CACHE_MS) {
|
|
906
|
+
return lastHealthProbeResult.healthy;
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
if (healthProbePromise) {
|
|
910
|
+
return healthProbePromise;
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
healthProbePromise = isOpenCodeProcessHealthy()
|
|
914
|
+
.then((healthy) => {
|
|
915
|
+
lastHealthProbeResult = { at: Date.now(), healthy };
|
|
916
|
+
return healthy;
|
|
917
|
+
})
|
|
918
|
+
.finally(() => {
|
|
919
|
+
healthProbePromise = null;
|
|
920
|
+
});
|
|
921
|
+
|
|
922
|
+
return healthProbePromise;
|
|
923
|
+
};
|
|
924
|
+
|
|
925
|
+
const shouldSkipRestartForBusySessions = () => {
|
|
926
|
+
const activeCount = getActiveSessionCount();
|
|
927
|
+
if (activeCount === 0) {
|
|
928
|
+
lastUnhealthyWithBusySessionsAt = 0;
|
|
929
|
+
return false;
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
const now = Date.now();
|
|
933
|
+
if (!lastUnhealthyWithBusySessionsAt) {
|
|
934
|
+
lastUnhealthyWithBusySessionsAt = now;
|
|
935
|
+
return true;
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
if (now - lastUnhealthyWithBusySessionsAt >= STALE_BUSY_GRACE_MS) {
|
|
939
|
+
console.warn(
|
|
940
|
+
`[lifecycle] OpenCode unhealthy with ${activeCount} busy session(s) for > 2 min — forcing restart`
|
|
941
|
+
);
|
|
942
|
+
lastUnhealthyWithBusySessionsAt = 0;
|
|
943
|
+
return false;
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
return true;
|
|
947
|
+
};
|
|
948
|
+
|
|
949
|
+
const runHealthCheckCycle = async (source) => {
|
|
950
|
+
if (!state.openCodeProcess || state.isShuttingDown || state.isRestartingOpenCode) return;
|
|
951
|
+
if (healthCheckCyclePromise) return healthCheckCyclePromise;
|
|
952
|
+
|
|
953
|
+
healthCheckCyclePromise = (async () => {
|
|
954
|
+
const healthy = await probeOpenCodeHealth();
|
|
955
|
+
if (!healthy) {
|
|
956
|
+
if (!isManagedOpenCodeProcessAlive()) {
|
|
957
|
+
console.log(`[lifecycle] ${source} health check: OpenCode process exited, restarting...`);
|
|
958
|
+
consecutiveHealthFailures = 0;
|
|
959
|
+
lastHealthProbeResult = null;
|
|
960
|
+
await restartOpenCode();
|
|
961
|
+
return;
|
|
962
|
+
}
|
|
963
|
+
consecutiveHealthFailures += 1;
|
|
964
|
+
console.warn(
|
|
965
|
+
`[lifecycle] ${source} health check failed (${consecutiveHealthFailures}/${HEALTH_CHECK_MAX_CONSECUTIVE_FAILURES})`
|
|
966
|
+
);
|
|
967
|
+
if (consecutiveHealthFailures < HEALTH_CHECK_MAX_CONSECUTIVE_FAILURES) return;
|
|
968
|
+
if (shouldSkipRestartForBusySessions()) return;
|
|
969
|
+
console.log(`[lifecycle] ${source} health check failure threshold reached, restarting OpenCode...`);
|
|
970
|
+
consecutiveHealthFailures = 0;
|
|
971
|
+
lastHealthProbeResult = null;
|
|
972
|
+
await restartOpenCode();
|
|
973
|
+
} else {
|
|
974
|
+
resetHealthFailureState();
|
|
975
|
+
}
|
|
976
|
+
})().finally(() => {
|
|
977
|
+
healthCheckCyclePromise = null;
|
|
978
|
+
});
|
|
979
|
+
|
|
980
|
+
return healthCheckCyclePromise;
|
|
981
|
+
};
|
|
982
|
+
|
|
983
|
+
const triggerHealthCheck = async () => {
|
|
984
|
+
try {
|
|
985
|
+
await runHealthCheckCycle('immediate');
|
|
986
|
+
} catch (error) {
|
|
987
|
+
console.error(`[lifecycle] immediate health check error: ${error.message}`);
|
|
988
|
+
}
|
|
989
|
+
};
|
|
990
|
+
|
|
991
|
+
const startHealthMonitoring = (healthCheckIntervalMs) => {
|
|
992
|
+
if (state.healthCheckInterval) {
|
|
993
|
+
clearInterval(state.healthCheckInterval);
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
const effectiveIntervalMs = HEALTH_CHECK_INTERVAL_OVERRIDE_MS || healthCheckIntervalMs;
|
|
997
|
+
|
|
998
|
+
state.healthCheckInterval = setInterval(async () => {
|
|
999
|
+
try {
|
|
1000
|
+
await runHealthCheckCycle('periodic');
|
|
1001
|
+
} catch (error) {
|
|
1002
|
+
console.error(`Health check error: ${error.message}`);
|
|
1003
|
+
}
|
|
1004
|
+
}, effectiveIntervalMs);
|
|
1005
|
+
};
|
|
1006
|
+
|
|
1007
|
+
return {
|
|
1008
|
+
killProcessOnPort,
|
|
1009
|
+
startOpenCode,
|
|
1010
|
+
restartOpenCode,
|
|
1011
|
+
waitForOpenCodeReady,
|
|
1012
|
+
waitForAgentPresence,
|
|
1013
|
+
refreshOpenCodeAfterConfigChange,
|
|
1014
|
+
bootstrapOpenCodeAtStartup,
|
|
1015
|
+
startHealthMonitoring,
|
|
1016
|
+
triggerHealthCheck,
|
|
1017
|
+
waitForPortRelease,
|
|
1018
|
+
};
|
|
1019
|
+
};
|