@vantran-se/openchamber-web 1.24.4 → 2.0.2
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 +2 -8
- package/bin/lib/commands-connect-url.js +3 -8
- package/bin/lib/commands-tunnel.js +3 -0
- package/dist/assets/{CodeMirrorCommentWidgets-B4a0JkUz.js → CodeMirrorCommentWidgets-B82Oye3V.js} +1 -1
- package/dist/assets/DiffView-DJLmWn6Z.js +4 -0
- package/dist/assets/FilesView-DjksmZRo.js +17 -0
- package/dist/assets/{GitView-Byu2DY7s.js → GitView-DiUsUIgr.js} +4 -4
- package/dist/assets/{GuestAttachDialog-B808E0-d.js → GuestAttachDialog-CYwWYLr3.js} +1 -1
- package/dist/assets/GuestSurfacePane-CmctyRI4.js +1 -0
- package/dist/assets/{JsonTreeView-VW_7czo3.js → JsonTreeView-DOHHcbgJ.js} +1 -1
- package/dist/assets/LinearIssuesView-DaqnylcV.js +1 -0
- package/dist/assets/MarkdownImageGallery-HJNvWt7L.js +1 -0
- package/dist/assets/MarkdownRendererImpl-nDZRGg2I.js +12 -0
- package/dist/assets/OnboardingScreen-C2B5Bsof.js +1 -0
- package/dist/assets/{PatchDiff-DCjGhlYV.js → PatchDiff-C4FLBjpt.js} +1 -1
- package/dist/assets/{PierreDiffViewer-CEDZNkU2.js → PierreDiffViewer-C_1S9c9n.js} +1 -1
- package/dist/assets/PlanView-_eZvb-G5.js +5 -0
- package/dist/assets/PluginPane-sM8fcZIV.js +3 -0
- package/dist/assets/SettingsView-BRfr46hb.js +29 -0
- package/dist/assets/SettingsWindow-CcUDwQHg.js +1 -0
- package/dist/assets/SubmoduleDiffSummary-CrN1ZGwl.js +1 -0
- package/dist/assets/ToolOutputDialog-Cij4jI09.js +15 -0
- package/dist/assets/ToolPartDiffPreview-BXzwQ-JY.js +8 -0
- package/dist/assets/{VirtualizedFileDiff-NU0FHFhT.js → VirtualizedFileDiff-DSWTLJNH.js} +4 -4
- package/dist/assets/{WalkthroughView-BZyjhqHv.js → WalkthroughView-CpUUgY8H.js} +2 -2
- package/dist/assets/appThemeRegistry-B5tHTkR8.js +1 -0
- package/dist/assets/{commitAndPush-BDftrjr6.js → commitAndPush-D7D8qV8p.js} +1 -1
- package/dist/assets/{computeVirtualFileMetrics-DlWlEM3x.js → computeVirtualFileMetrics-cq9uQ30i.js} +1 -1
- package/dist/assets/de-CUO1-KqF.js +10 -0
- package/dist/assets/es-BlbC6sGJ.js +16 -0
- package/dist/assets/fr-D2nwe_0W.js +14 -0
- package/dist/assets/index-BQOoYIOu.js +1 -0
- package/dist/assets/index-CLcM8HKx.css +1 -0
- package/dist/assets/index-CvLgnuNI.js +41 -0
- package/dist/assets/{isDiffPlainText-CO4e5J_S.js → isDiffPlainText-D0obJ2p8.js} +1 -1
- package/dist/assets/{isFilePlainText-ue6kCou7.js → isFilePlainText-DB3_rjyW.js} +1 -1
- package/dist/assets/ja-CA7vnDsZ.js +16 -0
- package/dist/assets/ko-Chy6a9l-.js +16 -0
- package/dist/assets/main-B9JFK3vF.js +896 -0
- package/dist/assets/main-Bhiqnrhd.js +2 -0
- package/dist/assets/{markdownCore-CK48LG0z.js → markdownCore-tfqOFaBd.js} +4 -4
- package/dist/assets/miniChat-B0UOL5zA.js +2 -0
- package/dist/assets/mobile-BJppFIEo.js +2 -0
- package/dist/assets/{patchFileDiff-D7QVlovD.js → patchFileDiff-Db6KuwCP.js} +3 -3
- package/dist/assets/pl-CTR5A_1P.js +16 -0
- package/dist/assets/pt-BR-BE1O1KEx.js +16 -0
- package/dist/assets/renderElectronMiniChatApp-Yz5sfoFh.js +2 -0
- package/dist/assets/renderMobileApp-QQ-yB8aY.js +25 -0
- package/dist/assets/runtimeEndpointReset-DKDdzRzm.js +27 -0
- package/dist/assets/scheduledTasksApi-C5sCuNqf.js +1 -0
- package/dist/assets/{shikiHighlight-BPu3JiQs.js → shikiHighlight-xURoo5t_.js} +1 -1
- package/dist/assets/{textMateThemeFromAppTheme-DWqRPo9A.js → textMateThemeFromAppTheme-BZLGuYTU.js} +1 -1
- package/dist/assets/tr-Bh5vK6cW.js +7 -0
- package/dist/assets/uk-CLDkZFy_.js +16 -0
- package/dist/assets/useAppFontEffects-BxrtCnfR.js +7337 -0
- package/dist/assets/{useInlineCommentController-CgS0fnw_.js → useInlineCommentController-CeXuR-SR.js} +2 -2
- package/dist/assets/useRootScrollLock-DgWxezc1.js +2 -0
- package/dist/assets/{vendor-codemirror-language-data-Bx2EHc1O.js → vendor-codemirror-language-data-DOuaQUZ0.js} +2 -2
- package/dist/assets/vendor-cron-parser-CjvYVigk.js +2 -0
- package/dist/assets/vendor-opencode-client-_BAHQo4R.js +12 -0
- package/dist/assets/{vendor-remixicon-react-BDmGmFCA.js → vendor-remixicon-react-D6dZ0UwF.js} +1 -1
- package/dist/assets/{vendor-zod-CgAdXWZO.js → vendor-zod-Gea9uBlY.js} +1 -1
- package/dist/assets/{web-components-DxX8bZ1o.js → web-components-63H1h21j.js} +2 -2
- package/dist/assets/zh-CN-DODgcmYj.js +16 -0
- package/dist/assets/zh-TW-ByNfIwLA.js +16 -0
- package/dist/index.html +13 -7
- package/dist/mini-chat.html +5 -5
- package/dist/mobile.html +5 -5
- package/package.json +6 -5
- package/server/index.d.ts +1 -0
- package/server/index.js +246 -91
- package/server/lib/agent-tool/DOCUMENTATION.md +32 -15
- package/server/lib/agent-tool/runtime.js +135 -54
- package/server/lib/agent-tool/runtime.test.js +155 -87
- package/server/lib/browser-control/DOCUMENTATION.md +37 -2
- package/server/lib/browser-control/provider.js +217 -0
- package/server/lib/browser-control/provider.test.js +251 -0
- package/server/lib/context-obligatory/runtime.js +61 -50
- package/server/lib/context-obligatory/runtime.test.js +62 -162
- package/server/lib/event-stream/DOCUMENTATION.md +12 -1
- package/server/lib/event-stream/delta-coalescer.js +0 -0
- package/server/lib/event-stream/delta-coalescer.test.js +68 -57
- package/server/lib/event-stream/directory-ws-bridge.js +1 -1
- package/server/lib/event-stream/global-hub.js +10 -1
- package/server/lib/event-stream/global-hub.test.js +27 -26
- package/server/lib/event-stream/index.js +6 -0
- package/server/lib/event-stream/protocol.js +26 -19
- package/server/lib/event-stream/protocol.test.js +18 -7
- package/server/lib/event-stream/rebind.test.js +5 -5
- package/server/lib/event-stream/runtime.test.js +17 -15
- package/server/lib/event-stream/translate-v2.js +313 -0
- package/server/lib/event-stream/translate-v2.test.js +209 -0
- package/server/lib/event-stream/upstream-reader.test.js +16 -16
- package/server/lib/fs/DOCUMENTATION.md +4 -2
- package/server/lib/fs/byte-range.js +35 -0
- package/server/lib/fs/byte-range.test.js +31 -0
- package/server/lib/fs/routes.js +47 -42
- package/server/lib/fs/routes.test.js +123 -18
- package/server/lib/git/service.js +12 -0
- package/server/lib/github/DOCUMENTATION.md +1 -1
- package/server/lib/github/pr-status.js +22 -2
- package/server/lib/github/pr-status.test.js +27 -14
- package/server/lib/guests/DOCUMENTATION.md +13 -0
- package/server/lib/guests/catalog.js +12 -0
- package/server/lib/guests/catalog.test.js +33 -0
- package/server/lib/guests/routes.js +10 -1
- package/server/lib/guests/service.js +192 -14
- package/server/lib/guests/service.test.js +201 -1
- package/server/lib/guests/surface.js +630 -0
- package/server/lib/guests/surface.test.js +486 -0
- package/server/lib/markdown-image-grants/routes.js +18 -5
- package/server/lib/markdown-image-grants/routes.test.js +9 -6
- package/server/lib/message-queue/DOCUMENTATION.md +33 -22
- package/server/lib/message-queue/runtime.js +155 -100
- package/server/lib/message-queue/runtime.test.js +168 -82
- package/server/lib/notifications/DOCUMENTATION.md +28 -0
- package/server/lib/notifications/control-stream.http.test.js +68 -0
- package/server/lib/notifications/emitter-runtime.js +15 -0
- package/server/lib/notifications/emitter-runtime.test.js +32 -0
- package/server/lib/notifications/push-runtime.js +5 -1
- package/server/lib/notifications/push-runtime.test.js +2 -2
- package/server/lib/notifications/routes.js +1 -1
- package/server/lib/notifications/runtime.js +115 -26
- package/server/lib/notifications/template-runtime.js +13 -15
- package/server/lib/notifications/template-runtime.test.js +16 -9
- package/server/lib/notifications/trigger-runtime.test.js +187 -0
- package/server/lib/openchamber-control/DOCUMENTATION.md +10 -0
- package/server/lib/openchamber-control/actions.js +3 -2
- package/server/lib/openchamber-control/error.js +1 -1
- package/server/lib/openchamber-control/file-open.js +59 -0
- package/server/lib/openchamber-control/file-open.test.js +57 -0
- package/server/lib/openchamber-control/routes.test.js +12 -0
- package/server/lib/openchamber-control/service.js +102 -49
- package/server/lib/openchamber-control/service.test.js +84 -44
- package/server/lib/openchamber-sessions/archive-store.js +224 -0
- package/server/lib/openchamber-sessions/archive-store.test.js +187 -0
- package/server/lib/openchamber-sessions/opencode-client.js +22 -0
- package/server/lib/openchamber-sessions/routes.js +398 -328
- package/server/lib/openchamber-sessions/routes.test.js +777 -740
- package/server/lib/openchamber-sessions/session-metadata-store.js +285 -0
- package/server/lib/openchamber-sessions/session-metadata-store.test.js +259 -0
- package/server/lib/opencode/DOCUMENTATION.md +449 -29
- package/server/lib/opencode/agents.js +233 -327
- package/server/lib/opencode/auth.js +41 -44
- package/server/lib/opencode/auth.test.js +92 -0
- package/server/lib/opencode/cli-upgrade.d.ts +4 -0
- package/server/lib/opencode/cli-upgrade.js +18 -0
- package/server/lib/opencode/cli-upgrade.test.js +36 -0
- package/server/lib/opencode/commands.js +164 -145
- package/server/lib/opencode/compatibility.d.ts +16 -0
- package/server/lib/opencode/compatibility.js +88 -0
- package/server/lib/opencode/compatibility.test.js +62 -0
- package/server/lib/opencode/config-entity-routes.js +58 -29
- package/server/lib/opencode/config-entity-v2.test.js +290 -0
- package/server/lib/opencode/config-mutation-response.js +17 -7
- package/server/lib/opencode/config-mutation-response.test.js +4 -7
- package/server/lib/opencode/config-paths.test.js +2 -2
- package/server/lib/opencode/config-v2.d.ts +183 -0
- package/server/lib/opencode/config-v2.js +801 -0
- package/server/lib/opencode/config-v2.test.js +466 -0
- package/server/lib/opencode/credential-db.js +144 -0
- package/server/lib/opencode/credential-db.test.js +104 -0
- package/server/lib/opencode/env-runtime.js +48 -9
- package/server/lib/opencode/env-runtime.test.js +45 -0
- package/server/lib/opencode/feature-routes-runtime.js +15 -5
- package/server/lib/opencode/lifecycle-process.test.js +7 -4
- package/server/lib/opencode/lifecycle.js +85 -30
- package/server/lib/opencode/lifecycle.test.js +80 -8
- package/server/lib/opencode/login-shell-env.js +27 -0
- package/server/lib/opencode/managed-config-file.js +119 -0
- package/server/lib/opencode/managed-config-file.test.js +152 -0
- package/server/lib/opencode/managed-plugin-config.js +33 -13
- package/server/lib/opencode/mcp.js +43 -149
- package/server/lib/opencode/network-runtime.js +6 -7
- package/server/lib/opencode/openchamber-routes.test.js +2 -5
- package/server/lib/opencode/plugin-routes.js +3 -3
- package/server/lib/opencode/plugin-routes.test.js +6 -24
- package/server/lib/opencode/plugins.js +91 -40
- package/server/lib/opencode/plugins.test.js +28 -15
- package/server/lib/opencode/providers.js +63 -83
- package/server/lib/opencode/providers.test.js +128 -59
- package/server/lib/opencode/proxy-agent-wiring.test.js +4 -2
- package/server/lib/opencode/proxy.js +179 -129
- package/server/lib/opencode/pwa-manifest-routes.js +4 -2
- package/server/lib/opencode/pwa-manifest-routes.test.js +3 -2
- package/server/lib/opencode/response-envelope.js +14 -0
- package/server/lib/opencode/response-envelope.test.js +23 -0
- package/server/lib/opencode/routes-upgrade.test.js +73 -116
- package/server/lib/opencode/routes.js +121 -428
- package/server/lib/opencode/server-utils-runtime.js +10 -4
- package/server/lib/opencode/session-activity.js +78 -0
- package/server/lib/opencode/session-runtime.js +21 -10
- package/server/lib/opencode/session-runtime.test.js +9 -9
- package/server/lib/opencode/settings-helpers.js +9 -9
- package/server/lib/opencode/settings-helpers.test.js +7 -12
- package/server/lib/opencode/settings-registry.json +6 -8
- package/server/lib/opencode/settings-runtime.js +18 -1
- package/server/lib/opencode/settings-runtime.test.js +31 -2
- package/server/lib/opencode/shared.js +54 -22
- package/server/lib/opencode/shared.test.js +8 -4
- package/server/lib/opencode/shutdown-runtime.js +54 -7
- package/server/lib/opencode/shutdown-runtime.test.js +224 -30
- package/server/lib/opencode/skill-routes.js +23 -24
- package/server/lib/opencode/skills.js +7 -5
- package/server/lib/opencode/tunnel-wiring-runtime.js +2 -1
- package/server/lib/opencode/upgrade-capability.test.js +1 -1
- package/server/lib/opencode/v1-migration-topup.js +288 -0
- package/server/lib/opencode/v1-migration-topup.test.js +266 -0
- package/server/lib/opencode/v2-install.d.ts +8 -0
- package/server/lib/opencode/v2-install.js +158 -0
- package/server/lib/opencode/v2-install.test.js +124 -0
- package/server/lib/opencode/watcher.js +10 -3
- package/server/lib/opencode/watcher.test.js +17 -21
- package/server/lib/opencode/websearch-config.js +66 -0
- package/server/lib/package-manager.js +55 -68
- package/server/lib/package-manager.test.js +241 -75
- package/server/lib/permission-auto-accept/runtime.js +38 -22
- package/server/lib/permission-auto-accept/runtime.test.js +39 -17
- package/server/lib/quota/DOCUMENTATION.md +4 -0
- package/server/lib/quota/providers/xai.js +4 -4
- package/server/lib/relay/tunnel-host.js +4 -1
- package/server/lib/relay/tunnel-host.test.js +7 -0
- package/server/lib/routing/DOCUMENTATION.md +52 -21
- package/server/lib/routing/defaults.js +23 -6
- package/server/lib/routing/jev.js +17 -7
- package/server/lib/routing/routes.http.test.js +68 -33
- package/server/lib/routing/routes.js +65 -22
- package/server/lib/routing/runtime.js +145 -48
- package/server/lib/routing/runtime.test.js +79 -55
- package/server/lib/scheduled-tasks/DOCUMENTATION.md +4 -0
- package/server/lib/scheduled-tasks/issue-2710-double-execution.test.js +51 -46
- package/server/lib/scheduled-tasks/runtime.js +81 -74
- package/server/lib/scheduled-tasks/runtime.test.js +53 -1
- package/server/lib/session-assist/DOCUMENTATION.md +8 -3
- package/server/lib/session-assist/context.js +122 -46
- package/server/lib/session-assist/context.test.js +117 -49
- package/server/lib/session-assist/runtime.js +52 -22
- package/server/lib/session-assist/runtime.test.js +145 -173
- package/server/lib/session-goal/DOCUMENTATION.md +23 -15
- package/server/lib/session-goal/create.js +9 -12
- package/server/lib/session-goal/create.test.js +27 -28
- package/server/lib/session-goal/runtime.js +164 -77
- package/server/lib/session-goal/runtime.test.js +271 -519
- package/server/lib/session-knowledge/DOCUMENTATION.md +5 -0
- package/server/lib/session-knowledge/runtime.js +59 -41
- package/server/lib/session-knowledge/runtime.test.js +90 -20
- package/server/lib/small-model/DOCUMENTATION.md +154 -208
- package/server/lib/small-model/client.js +113 -0
- package/server/lib/small-model/index.js +296 -158
- package/server/lib/small-model/index.test.js +423 -256
- package/server/lib/small-model/routes.js +1 -1
- package/server/lib/spaces/DOCUMENTATION.md +807 -0
- package/server/lib/spaces/code-in-bait.js +185 -0
- package/server/lib/spaces/code-in.js +641 -0
- package/server/lib/spaces/code-in.test.js +1048 -0
- package/server/lib/spaces/errors.js +8 -0
- package/server/lib/spaces/exec-http.js +94 -0
- package/server/lib/spaces/gatekeeper-channel.js +202 -0
- package/server/lib/spaces/gatekeeper-channel.test.js +215 -0
- package/server/lib/spaces/gatekeeper-program.cjs +914 -0
- package/server/lib/spaces/gatekeeper-program.test.js +1391 -0
- package/server/lib/spaces/hardening.js +457 -0
- package/server/lib/spaces/hardening.test.js +368 -0
- package/server/lib/spaces/host-git.js +127 -0
- package/server/lib/spaces/host-git.test.js +137 -0
- package/server/lib/spaces/labels.js +234 -0
- package/server/lib/spaces/labels.test.js +219 -0
- package/server/lib/spaces/layout.js +184 -0
- package/server/lib/spaces/layout.test.js +30 -0
- package/server/lib/spaces/manager.js +82 -0
- package/server/lib/spaces/manager.test.js +114 -0
- package/server/lib/spaces/places/code-in.docker.live.test.js +216 -0
- package/server/lib/spaces/places/contract-suite.js +156 -0
- package/server/lib/spaces/places/contract.docker.live.test.js +7 -0
- package/server/lib/spaces/places/contract.test.js +6 -0
- package/server/lib/spaces/places/docker-engine.js +126 -0
- package/server/lib/spaces/places/docker-live-support.js +202 -0
- package/server/lib/spaces/places/docker-tools.js +186 -0
- package/server/lib/spaces/places/docker.js +587 -0
- package/server/lib/spaces/places/docker.test.js +1610 -0
- package/server/lib/spaces/places/escape-suite.js +1018 -0
- package/server/lib/spaces/places/escape.docker.live.test.js +7 -0
- package/server/lib/spaces/places/fake-docker.js +375 -0
- package/server/lib/spaces/places/memory-place.js +103 -0
- package/server/lib/spaces/places/packed.docker.live.test.js +77 -0
- package/server/lib/spaces/places/registry.js +43 -0
- package/server/lib/spaces/places/registry.test.js +50 -0
- package/server/lib/spaces/places/server.docker.live.test.js +225 -0
- package/server/lib/spaces/run-command.js +261 -0
- package/server/lib/spaces/run-command.test.js +396 -0
- package/server/lib/spaces/space-server.js +190 -0
- package/server/lib/spaces/space-server.test.js +335 -0
- package/server/lib/spaces/tools-filler.js +82 -0
- package/server/lib/spaces/tools-filler.test.js +148 -0
- package/server/lib/spaces/tools-pack.js +49 -0
- package/server/lib/spaces/tools-pack.test.js +57 -0
- package/server/lib/spaces/tools.js +138 -0
- package/server/lib/spaces/tools.test.js +153 -0
- package/server/lib/tts/openai-sdk.js +11 -0
- package/server/lib/tts/service.js +7 -5
- package/server/lib/tts/stt.js +3 -2
- package/server/lib/tunnels/DOCUMENTATION.md +2 -0
- package/server/lib/tunnels/routes.js +7 -0
- package/server/lib/tunnels/routes.test.js +55 -0
- package/server/lib/ui-auth/ui-auth.js +38 -2
- package/server/lib/ui-auth/ui-auth.test.js +42 -0
- package/server/lib/ui-auth/ui-passkeys.js +12 -10
- package/server/lib/walkthrough/DOCUMENTATION.md +10 -1
- package/server/lib/walkthrough/pull-request.js +61 -8
- package/server/lib/walkthrough/pull-request.test.js +54 -1
- package/server/lib/walkthrough/routes.js +20 -0
- package/server/lib/walkthrough/routes.test.js +19 -0
- package/server/opencode-proxy.test.js +190 -166
- package/server/opencode-worktree-readiness.test.js +5 -4
- package/dist/assets/DiffView-B2CD9wC8.js +0 -4
- package/dist/assets/FilesView-C4vMFclc.js +0 -13
- package/dist/assets/LinearIssuesView-EzGNCGrd.js +0 -1
- package/dist/assets/MarkdownImageGallery-B2YUDFaB.js +0 -1
- package/dist/assets/MarkdownRendererImpl-B4ikIzR3.js +0 -12
- package/dist/assets/OnboardingScreen-CQHBSTEi.js +0 -1
- package/dist/assets/PlanView-D3x-pTkZ.js +0 -5
- package/dist/assets/PluginPane-DEcgDQ9h.js +0 -3
- package/dist/assets/SettingsView-CSinuH-1.js +0 -29
- package/dist/assets/SettingsWindow-B5CHoJFZ.js +0 -1
- package/dist/assets/SubmoduleDiffSummary-oxeACCWs.js +0 -1
- package/dist/assets/ToolOutputDialog-tLNWmfcW.js +0 -15
- package/dist/assets/ToolPartDiffPreview-CflzmK-x.js +0 -8
- package/dist/assets/appThemeRegistry-DLicw4dH.js +0 -1
- package/dist/assets/de-Bro6_zyl.js +0 -10
- package/dist/assets/es-Bb9XQZvd.js +0 -16
- package/dist/assets/fr-Dgfzn-IL.js +0 -14
- package/dist/assets/index-CtQNmqUL.js +0 -1
- package/dist/assets/index-DqaMWNrC.css +0 -1
- package/dist/assets/index-Dqc6USl3.js +0 -40
- package/dist/assets/ja-BYsakGFh.js +0 -16
- package/dist/assets/ko-BSaQz9aN.js +0 -16
- package/dist/assets/main-B62_pjL5.js +0 -2
- package/dist/assets/main-BEdcOI2P.js +0 -896
- package/dist/assets/miniChat-LT8Rqofr.js +0 -2
- package/dist/assets/mobile-CqarEynh.js +0 -2
- package/dist/assets/pl-OIviPdoj.js +0 -16
- package/dist/assets/pt-BR-Cltg-E6s.js +0 -16
- package/dist/assets/renderElectronMiniChatApp-COyLTtZx.js +0 -2
- package/dist/assets/renderMobileApp-BC1MHg6g.js +0 -25
- package/dist/assets/runtimeEndpointReset-DdrazOVH.js +0 -25
- package/dist/assets/scheduledTasksApi-B2SWMGEU.js +0 -1
- package/dist/assets/tr-CQI-KPre.js +0 -7
- package/dist/assets/uk-CqW7R4DA.js +0 -16
- package/dist/assets/useAppFontEffects-ne84JBVL.js +0 -7333
- package/dist/assets/useRootScrollLock-Cpf1qoGp.js +0 -2
- package/dist/assets/vendor-cron-parser-CUcP7OfX.js +0 -2
- package/dist/assets/vendor-opencode-sdk-BJ73u-bH.js +0 -7
- package/dist/assets/zh-CN-CVZG_o_J.js +0 -16
- package/dist/assets/zh-TW-D5ZxzCZp.js +0 -16
- package/server/lib/opencode/mcp-oauth-callback.test.js +0 -111
- package/server/lib/routing/feature-flag.js +0 -11
- package/server/lib/small-model/call.js +0 -851
- package/server/lib/small-model/call.test.js +0 -1292
- package/server/lib/small-model/catalog.js +0 -13
- package/server/lib/small-model/resolve.js +0 -131
- package/server/lib/small-model/resolve.test.js +0 -197
- package/server/lib/small-model/runtime-providers.js +0 -164
- package/server/lib/small-model/runtime-providers.test.js +0 -118
- package/server/lib/system-prompt/DOCUMENTATION.md +0 -36
- package/server/lib/system-prompt/runtime.js +0 -49
- package/server/lib/system-prompt/runtime.test.js +0 -76
- package/server/lib/walkthrough/reproduce-2607.test.js +0 -148
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
// The server inside a space, on a real Docker daemon. Runs only with OPENCHAMBER_TEST_DOCKER=1.
|
|
2
|
+
// The host reaches the server only through `exec`, as stage 1b does.
|
|
3
|
+
|
|
4
|
+
import { afterAll, beforeAll, describe, expect, it } from 'vitest';
|
|
5
|
+
|
|
6
|
+
import { createSpaceId, hashProjectDirectory, toolsKeyFromVolumeName } from '../labels.js';
|
|
7
|
+
import { SPACE_SERVER_PORT, SPACE_TOKEN_PATH, TOOLS_MOUNT_PATH } from '../layout.js';
|
|
8
|
+
import { createSpaceServerChannel } from '../space-server.js';
|
|
9
|
+
import { createRegistryToolsSource, readHostToolVersions } from '../tools.js';
|
|
10
|
+
import { LIVE_DOCKER_ENABLED, createLiveDockerPlace } from './docker-live-support.js';
|
|
11
|
+
|
|
12
|
+
const CREATE_TIMEOUT_MS = 25 * 60_000;
|
|
13
|
+
const JSON_HEADERS = { 'Content-Type': 'application/json' };
|
|
14
|
+
|
|
15
|
+
// Runs inside the space. It logs in with the token file, opens a terminal, types two
|
|
16
|
+
// commands into its WebSocket and prints one JSON line. `ws` comes out of the tools volume.
|
|
17
|
+
// The protocol is the one in server/lib/terminal: a cookie, an Origin whose host equals
|
|
18
|
+
// Host, and binary frames of 0x01 plus JSON.
|
|
19
|
+
const TERMINAL_PROBE = `
|
|
20
|
+
const fs = require('node:fs');
|
|
21
|
+
const WebSocket = require('${TOOLS_MOUNT_PATH}/node_modules/ws');
|
|
22
|
+
const [tokenPath, port, cwd] = process.argv.slice(2);
|
|
23
|
+
const base = 'http://127.0.0.1:' + port;
|
|
24
|
+
const frame = (payload) => Buffer.concat([Buffer.from([1]), Buffer.from(JSON.stringify(payload), 'utf8')]);
|
|
25
|
+
const finish = (result) => { console.log(JSON.stringify(result)); process.exit(0); };
|
|
26
|
+
setTimeout(() => finish({ error: 'timeout' }), 30000);
|
|
27
|
+
(async () => {
|
|
28
|
+
const password = fs.readFileSync(tokenPath, 'utf8').trim();
|
|
29
|
+
const login = await fetch(base + '/auth/session', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ password }) });
|
|
30
|
+
const cookie = (login.headers.get('set-cookie') || '').split(';')[0];
|
|
31
|
+
const withoutCookie = await fetch(base + '/api/terminal/sessions');
|
|
32
|
+
const created = await fetch(base + '/api/terminal/create', { method: 'POST', headers: { 'Content-Type': 'application/json', Cookie: cookie }, body: JSON.stringify({ cwd, cols: 80, rows: 24 }) });
|
|
33
|
+
const { sessionId } = await created.json();
|
|
34
|
+
const socket = new WebSocket('ws://127.0.0.1:' + port + '/api/terminal/ws', { headers: { Cookie: cookie, Origin: base } });
|
|
35
|
+
let output = '';
|
|
36
|
+
let backend = null;
|
|
37
|
+
socket.on('open', () => socket.send(frame({ t: 'attach', v: 3, s: sessionId })));
|
|
38
|
+
socket.on('error', (error) => finish({ error: String(error) }));
|
|
39
|
+
socket.on('message', async (raw) => {
|
|
40
|
+
const message = JSON.parse(raw.subarray(1).toString('utf8'));
|
|
41
|
+
if (message.t === 'snapshot') {
|
|
42
|
+
backend = message.ptyBackend;
|
|
43
|
+
socket.send(frame({ t: 'write', v: 3, s: sessionId, d: 'echo space-$((40+2)); id -u\\r' }));
|
|
44
|
+
}
|
|
45
|
+
if (message.t === 'output') output += message.d;
|
|
46
|
+
if (/space-42[\\s\\S]*\\n1000\\r?\\n/.test(output)) {
|
|
47
|
+
await fetch(base + '/api/terminal/' + sessionId, { method: 'DELETE', headers: { Cookie: cookie } });
|
|
48
|
+
finish({ loginStatus: login.status, withoutCookieStatus: withoutCookie.status, createStatus: created.status, backend, output });
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
})().catch((error) => finish({ error: String(error) }));
|
|
52
|
+
`;
|
|
53
|
+
|
|
54
|
+
const HELLO_TOOL = `import { tool } from "@opencode/plugin"
|
|
55
|
+
export default tool({
|
|
56
|
+
description: "Says hello",
|
|
57
|
+
args: { name: tool.schema.string() },
|
|
58
|
+
async execute(args) { return "hello " + args.name },
|
|
59
|
+
})
|
|
60
|
+
`;
|
|
61
|
+
|
|
62
|
+
describe.skipIf(!LIVE_DOCKER_ENABLED)('server inside a space: docker (live)', () => {
|
|
63
|
+
const spec = {
|
|
64
|
+
id: createSpaceId(),
|
|
65
|
+
name: 'Server inside',
|
|
66
|
+
project: hashProjectDirectory('/server/inside/project'),
|
|
67
|
+
created: new Date().toISOString(),
|
|
68
|
+
memoryBytes: 2 * 1024 * 1024 * 1024,
|
|
69
|
+
};
|
|
70
|
+
const repo = `/spaces/${spec.id}/repo`;
|
|
71
|
+
const link = `/spaces/${spec.id}/link-to-repo`;
|
|
72
|
+
let place;
|
|
73
|
+
let placeWith;
|
|
74
|
+
let host;
|
|
75
|
+
let owner;
|
|
76
|
+
let server;
|
|
77
|
+
let cookie;
|
|
78
|
+
let dispose = async () => {};
|
|
79
|
+
|
|
80
|
+
const shell = async (script, stdin = '') => {
|
|
81
|
+
const result = await place.exec(spec.id, ['sh', '-c', script], { stdin });
|
|
82
|
+
expect(result.code, `${script}: ${result.stderr}`).toBe(0);
|
|
83
|
+
return result.stdout;
|
|
84
|
+
};
|
|
85
|
+
const logIn = async () => {
|
|
86
|
+
const login = await server.request(spec.id, { method: 'POST', path: '/auth/session', headers: JSON_HEADERS, body: JSON.stringify({ password: await server.readToken(spec.id) }) });
|
|
87
|
+
expect(login.status).toBe(200);
|
|
88
|
+
return login.headers['set-cookie'][0].split(';')[0];
|
|
89
|
+
};
|
|
90
|
+
const health = async () => JSON.parse((await server.request(spec.id, { path: '/health' })).body);
|
|
91
|
+
|
|
92
|
+
beforeAll(async () => {
|
|
93
|
+
({ place, placeWith, host, owner, dispose } = createLiveDockerPlace());
|
|
94
|
+
server = createSpaceServerChannel({ exec: place.exec });
|
|
95
|
+
await place.create(spec);
|
|
96
|
+
cookie = await logIn();
|
|
97
|
+
}, CREATE_TIMEOUT_MS);
|
|
98
|
+
|
|
99
|
+
afterAll(async () => {
|
|
100
|
+
await dispose();
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it('answers /health and reports OpenCode ready as soon as create resolves', async () => {
|
|
104
|
+
expect(await health()).toMatchObject({ isOpenCodeReady: true, openCodeRunning: true });
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it('refuses the API without the token, and the cookie name carries the port inside', async () => {
|
|
108
|
+
expect((await server.request(spec.id, { path: '/api/terminal/sessions' })).status).toBe(401);
|
|
109
|
+
expect(cookie).toMatch(new RegExp(`^oc_ui_session_${SPACE_SERVER_PORT}=`));
|
|
110
|
+
expect((await server.request(spec.id, { path: '/api/terminal/sessions', headers: { Cookie: cookie } })).status).toBe(200);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it('keeps the token in a private file, and not in the container environment', async () => {
|
|
114
|
+
expect(await shell(`stat -c '%a %u' ${SPACE_TOKEN_PATH} "$(dirname ${SPACE_TOKEN_PATH})"`)).toBe('600 1000\n700 1000\n');
|
|
115
|
+
expect(await shell('tr "\\0" "\\n" < /proc/1/environ')).not.toContain('OPENCHAMBER_UI_PASSWORD');
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
// The open question of stage 0: does OpenCode report the space path unchanged?
|
|
119
|
+
it('reports the directory of a session byte for byte as the space path', async () => {
|
|
120
|
+
await shell(`mkdir -p ${repo} && cd ${repo} && git init -q . && git -c user.email=space@example.invalid -c user.name=Space commit -q --allow-empty -m init`);
|
|
121
|
+
|
|
122
|
+
const created = await server.request(spec.id, { method: 'POST', path: `/api/session?directory=${encodeURIComponent(repo)}`, headers: { ...JSON_HEADERS, Cookie: cookie }, body: '{}' });
|
|
123
|
+
expect(created.status).toBe(200);
|
|
124
|
+
const session = JSON.parse(created.body);
|
|
125
|
+
expect(session.directory).toBe(repo);
|
|
126
|
+
|
|
127
|
+
const readBack = await server.request(spec.id, { path: `/api/session/${session.id}?directory=${encodeURIComponent(repo)}`, headers: { Cookie: cookie } });
|
|
128
|
+
expect(JSON.parse(readBack.body).directory).toBe(repo);
|
|
129
|
+
|
|
130
|
+
const listed = await server.request(spec.id, { path: `/api/session?directory=${encodeURIComponent(repo)}`, headers: { Cookie: cookie } });
|
|
131
|
+
expect(JSON.parse(listed.body).map((entry) => entry.directory)).toEqual([repo]);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
// Recorded, not wished for: OpenCode 1.18.31 resolves a symlink and reports the real path.
|
|
135
|
+
// The dispatcher stage must therefore hand OpenCode real paths only.
|
|
136
|
+
it('reports the real path for a directory that was reached through a symlink', async () => {
|
|
137
|
+
await shell(`ln -sfn ${repo} ${link}`);
|
|
138
|
+
|
|
139
|
+
const created = await server.request(spec.id, { method: 'POST', path: `/api/session?directory=${encodeURIComponent(link)}`, headers: { ...JSON_HEADERS, Cookie: cookie }, body: '{}' });
|
|
140
|
+
expect(created.status).toBe(200);
|
|
141
|
+
expect(JSON.parse(created.body).directory).toBe(repo);
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it('lists a project tool that imports @opencode/plugin, without waiting for a download that cannot happen', async () => {
|
|
145
|
+
await shell(`mkdir -p ${repo}/.opencode/tool && cat > ${repo}/.opencode/tool/hello.ts`, HELLO_TOOL);
|
|
146
|
+
// Only the plugin is linked above the projects, not the whole tools node_modules.
|
|
147
|
+
expect(await shell(`ls /spaces/${spec.id}/node_modules /spaces/${spec.id}/node_modules/@opencode`)).toBe(`/spaces/${spec.id}/node_modules:\n@opencode\n\n/spaces/${spec.id}/node_modules/@opencode:\nplugin\n`);
|
|
148
|
+
// The instance of the earlier tests loaded before the tool existed.
|
|
149
|
+
await server.request(spec.id, { method: 'POST', path: `/api/instance/dispose?directory=${encodeURIComponent(repo)}`, headers: { Cookie: cookie } });
|
|
150
|
+
|
|
151
|
+
const started = Date.now();
|
|
152
|
+
const answer = await server.request(spec.id, { path: `/api/experimental/tool/ids?directory=${encodeURIComponent(repo)}`, headers: { Cookie: cookie } });
|
|
153
|
+
expect(answer.status).toBe(200);
|
|
154
|
+
expect(JSON.parse(answer.body)).toContain('hello');
|
|
155
|
+
// Without npm_config_fetch_retries=0 this took 131 seconds.
|
|
156
|
+
expect(Date.now() - started).toBeLessThan(15_000);
|
|
157
|
+
}, 60_000);
|
|
158
|
+
|
|
159
|
+
it('runs commands in a terminal as uid 1000, through the terminal WebSocket', async () => {
|
|
160
|
+
await shell('cat > /tmp/terminal-probe.cjs', TERMINAL_PROBE);
|
|
161
|
+
const result = await place.exec(spec.id, ['node', '/tmp/terminal-probe.cjs', SPACE_TOKEN_PATH, String(SPACE_SERVER_PORT), `/spaces/${spec.id}`], { timeoutMs: 60_000 });
|
|
162
|
+
|
|
163
|
+
expect(result.code, result.stderr).toBe(0);
|
|
164
|
+
const probe = JSON.parse(result.stdout);
|
|
165
|
+
expect(probe).toMatchObject({ loginStatus: 200, withoutCookieStatus: 401, createStatus: 200, backend: 'node-pty' });
|
|
166
|
+
expect(probe.output).toMatch(/space-42\r?\n1000\r?\n/);
|
|
167
|
+
}, 90_000);
|
|
168
|
+
|
|
169
|
+
it('comes back healthy after stop and start, with the same token and the same files', async () => {
|
|
170
|
+
const tokenBefore = await server.readToken(spec.id);
|
|
171
|
+
await place.stop(spec.id);
|
|
172
|
+
await place.start(spec.id);
|
|
173
|
+
|
|
174
|
+
expect(await health()).toMatchObject({ isOpenCodeReady: true });
|
|
175
|
+
expect(await server.readToken(spec.id)).toBe(tokenBefore);
|
|
176
|
+
expect(await shell(`cat ${repo}/.opencode/tool/hello.ts`)).toBe(HELLO_TOOL);
|
|
177
|
+
expect(await place.verify(spec.id)).toEqual([]);
|
|
178
|
+
}, 5 * 60_000);
|
|
179
|
+
|
|
180
|
+
it('moves a stopped space to new tools at its next start, and drops the old tools volume', async () => {
|
|
181
|
+
// The same packages under another key, as a host with other tools would ask for.
|
|
182
|
+
const updatedPlace = placeWith(createRegistryToolsSource({ ...readHostToolVersions(), revision: 'live-test-second-fill' }));
|
|
183
|
+
const toolsOf = async () => JSON.parse(await host.spaceMetadata(spec.id))[0].Mounts.find((mount) => mount.Destination === TOOLS_MOUNT_PATH).Name;
|
|
184
|
+
const before = await toolsOf();
|
|
185
|
+
const tokenBefore = await server.readToken(spec.id);
|
|
186
|
+
|
|
187
|
+
// A running space is never touched.
|
|
188
|
+
await updatedPlace.start(spec.id);
|
|
189
|
+
expect(await toolsOf()).toBe(before);
|
|
190
|
+
|
|
191
|
+
await place.stop(spec.id);
|
|
192
|
+
await updatedPlace.start(spec.id);
|
|
193
|
+
|
|
194
|
+
const after = await toolsOf();
|
|
195
|
+
expect(after).not.toBe(before);
|
|
196
|
+
expect(toolsKeyFromVolumeName(after, owner)).toMatch(/^[0-9a-f]{16}$/);
|
|
197
|
+
expect(await updatedPlace.verify(spec.id)).toEqual([]);
|
|
198
|
+
expect(await health()).toMatchObject({ isOpenCodeReady: true });
|
|
199
|
+
expect(await server.readToken(spec.id)).toBe(tokenBefore);
|
|
200
|
+
expect(await shell(`cat ${repo}/.opencode/tool/hello.ts`)).toBe(HELLO_TOOL);
|
|
201
|
+
expect(await updatedPlace.list()).toMatchObject([{ id: spec.id, state: 'running', damaged: false }]);
|
|
202
|
+
// No container mounts the old tools volume any more, so it is gone.
|
|
203
|
+
expect((await host.volumes()).filter((name) => name.startsWith('openchamber-tools-'))).toEqual([after]);
|
|
204
|
+
}, CREATE_TIMEOUT_MS);
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
describe.skipIf(!LIVE_DOCKER_ENABLED)('failed tools fill: docker (live)', () => {
|
|
208
|
+
it('rejects the create and leaves no container, network or volume', async () => {
|
|
209
|
+
const neverPublished = createRegistryToolsSource({ ...readHostToolVersions(), webVersion: '0.0.0-never-published' });
|
|
210
|
+
const { place, dispose, host } = createLiveDockerPlace({ toolsSource: neverPublished });
|
|
211
|
+
const spec = { id: createSpaceId(), name: 'Failed fill', project: hashProjectDirectory('/failed/fill'), created: new Date().toISOString(), memoryBytes: 1024 * 1024 * 1024 };
|
|
212
|
+
|
|
213
|
+
try {
|
|
214
|
+
const error = await place.create(spec).catch((caught) => caught);
|
|
215
|
+
expect(error.code).toBe('tools_fill_failed');
|
|
216
|
+
expect(error.message).toMatch(/0\.0\.0-never-published/);
|
|
217
|
+
expect(error.details).toMatchObject({ rollbackFailures: [], uncertain: false });
|
|
218
|
+
expect(await place.list()).toEqual([]);
|
|
219
|
+
expect(await host.volumes()).toEqual([]);
|
|
220
|
+
} finally {
|
|
221
|
+
// Asserts that nothing with this owner's label is left.
|
|
222
|
+
await dispose();
|
|
223
|
+
}
|
|
224
|
+
}, CREATE_TIMEOUT_MS);
|
|
225
|
+
});
|
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
import { spawn, spawnSync } from 'node:child_process';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
|
|
4
|
+
import { SpaceError } from './errors.js';
|
|
5
|
+
|
|
6
|
+
const DEFAULT_TIMEOUT_MS = 60_000;
|
|
7
|
+
const DEFAULT_MAX_OUTPUT_BYTES = 4 * 1024 * 1024;
|
|
8
|
+
// How long a tree kill waits for `taskkill` and for the child to close before it gives up waiting.
|
|
9
|
+
const KILL_WAIT_MS = 5_000;
|
|
10
|
+
|
|
11
|
+
// Children started with `killTree` whose own process has not exited yet. A `killTree` child leads a
|
|
12
|
+
// process group of its own on POSIX, so a Ctrl-C that reaches the server's group does not reach it,
|
|
13
|
+
// and measured, it outlived the server. When this process exits, each of these trees is killed
|
|
14
|
+
// synchronously. No signal handler is installed here: the server has its own, which end in
|
|
15
|
+
// `process.exit`, and those decide how it shuts down.
|
|
16
|
+
//
|
|
17
|
+
// Windows needs this too. libuv puts each child it starts into a job object that kills its members
|
|
18
|
+
// when this Node process goes, whatever way it goes, but the job is created with silent breakaway:
|
|
19
|
+
// only the processes libuv adds are members, "and *their* subprocesses are not", as libuv's
|
|
20
|
+
// src/win/process.c says. git is a member and dies with us; the `docker exec` git starts is not.
|
|
21
|
+
//
|
|
22
|
+
// An entry leaves the set when the child itself exits, not when its pipes close: an escaped
|
|
23
|
+
// grandchild can hold the pipes for ever, and a pid that exited long ago may belong to somebody else.
|
|
24
|
+
const liveTrees = new Set();
|
|
25
|
+
let exitHandlerInstalled = false;
|
|
26
|
+
|
|
27
|
+
/** How many `killTree` children are live, for the tests. */
|
|
28
|
+
export const liveTreeCount = () => liveTrees.size;
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Kills each tree synchronously, refusing any pid that is not an integer greater than 1, and never
|
|
32
|
+
* throws: it runs from `process.on('exit')`, where nothing asynchronous finishes. `children` defaults
|
|
33
|
+
* to the live set, which is emptied; the rest is injectable for the tests.
|
|
34
|
+
*/
|
|
35
|
+
export function killLiveTrees({
|
|
36
|
+
children = liveTrees,
|
|
37
|
+
platform = process.platform,
|
|
38
|
+
kill = (pid, signal) => process.kill(pid, signal),
|
|
39
|
+
spawnSyncProcess = spawnSync,
|
|
40
|
+
systemRoot = process.env.SystemRoot,
|
|
41
|
+
} = {}) {
|
|
42
|
+
for (const child of children) {
|
|
43
|
+
const pid = child?.pid;
|
|
44
|
+
if (!Number.isInteger(pid) || pid <= 1) continue;
|
|
45
|
+
try {
|
|
46
|
+
if (platform === 'win32') {
|
|
47
|
+
if (systemRoot) {
|
|
48
|
+
spawnSyncProcess(path.win32.join(systemRoot, 'System32', 'taskkill.exe'), ['/T', '/F', '/PID', String(pid)], {
|
|
49
|
+
shell: false, windowsHide: true, stdio: 'ignore', timeout: KILL_WAIT_MS,
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
} else {
|
|
53
|
+
kill(-pid, 'SIGKILL');
|
|
54
|
+
}
|
|
55
|
+
} catch {
|
|
56
|
+
// Already gone, or nothing more can be done from an exit handler.
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
if (children === liveTrees) liveTrees.clear();
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const killLiveTreesAtExit = () => killLiveTrees();
|
|
63
|
+
|
|
64
|
+
const trackTree = (child) => {
|
|
65
|
+
if (!exitHandlerInstalled) {
|
|
66
|
+
exitHandlerInstalled = true;
|
|
67
|
+
// First in line: a listener registered earlier that throws would otherwise stop ours.
|
|
68
|
+
process.prependListener('exit', killLiveTreesAtExit);
|
|
69
|
+
}
|
|
70
|
+
liveTrees.add(child);
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Runs one executable with an argument array and resolves `{ code, stdout, stderr }`
|
|
75
|
+
* for any exit code. The executable is spawned directly, never through a shell.
|
|
76
|
+
*
|
|
77
|
+
* `options.stdin` is a string or a Buffer. A Buffer goes to the child byte for byte, which is
|
|
78
|
+
* how the tarballs of a development build reach the tools filler. `options.cwd` is the
|
|
79
|
+
* working directory of the child, and `options.env` its whole environment when given.
|
|
80
|
+
*
|
|
81
|
+
* `options.killTree: true` makes a kill end the child's whole process tree instead of the child
|
|
82
|
+
* alone. It is for a child that starts children of its own that hold a connection open: a
|
|
83
|
+
* `git push` over `ext::` runs `docker exec`, and measured on three machines, killing only the
|
|
84
|
+
* `git` process left that `docker exec` running on the host. See `killProcessTree`. The rejection
|
|
85
|
+
* then waits until the kill has done its work and the child has closed, at most five seconds, so a
|
|
86
|
+
* caller that cleans up afterwards does not race processes that still hold its files. Measured on
|
|
87
|
+
* Windows: without the wait, removing the temporary folder failed with EBUSY and hid the timeout.
|
|
88
|
+
* `options.killWaitMs` changes that bound, for the tests. While a `killTree` child runs, an exit of
|
|
89
|
+
* this process kills its tree too, see `liveTrees`.
|
|
90
|
+
*
|
|
91
|
+
* Rejects with a SpaceError when the process cannot start (`command_spawn_failed`),
|
|
92
|
+
* runs past `timeoutMs` (`command_timeout`), prints more than `maxOutputBytes`
|
|
93
|
+
* (`command_output_too_large`), or dies from a signal (`command_killed`). The child
|
|
94
|
+
* is killed in the first three cases.
|
|
95
|
+
*/
|
|
96
|
+
export function runCommand(file, args, options = {}) {
|
|
97
|
+
const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
98
|
+
const maxOutputBytes = options.maxOutputBytes ?? DEFAULT_MAX_OUTPUT_BYTES;
|
|
99
|
+
const stdin = options.stdin ?? '';
|
|
100
|
+
const killTree = options.killTree === true;
|
|
101
|
+
const killWaitMs = options.killWaitMs ?? KILL_WAIT_MS;
|
|
102
|
+
|
|
103
|
+
return new Promise((resolve, reject) => {
|
|
104
|
+
let child;
|
|
105
|
+
try {
|
|
106
|
+
child = spawn(file, args, {
|
|
107
|
+
shell: false,
|
|
108
|
+
windowsHide: true,
|
|
109
|
+
cwd: options.cwd,
|
|
110
|
+
env: options.env,
|
|
111
|
+
// On POSIX the child leads a process group of its own, so the kill can reach its children.
|
|
112
|
+
// Never on Windows: there `detached` gives the child a console of its own.
|
|
113
|
+
detached: killTree && process.platform !== 'win32',
|
|
114
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
115
|
+
});
|
|
116
|
+
} catch (error) {
|
|
117
|
+
reject(new SpaceError('command_spawn_failed', `Could not start ${file}: ${error.message}`, { errno: error.code ?? null }));
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (killTree) trackTree(child);
|
|
122
|
+
|
|
123
|
+
const stdoutChunks = [];
|
|
124
|
+
const stderrChunks = [];
|
|
125
|
+
let capturedBytes = 0;
|
|
126
|
+
let settled = false;
|
|
127
|
+
let closed = false;
|
|
128
|
+
const closeWaiters = [];
|
|
129
|
+
const waitForClose = () => new Promise((resolveWait) => {
|
|
130
|
+
if (closed) {
|
|
131
|
+
resolveWait();
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
const bound = setTimeout(resolveWait, killWaitMs);
|
|
135
|
+
closeWaiters.push(() => { clearTimeout(bound); resolveWait(); });
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
const fail = (error) => {
|
|
139
|
+
if (settled) return;
|
|
140
|
+
settled = true;
|
|
141
|
+
clearTimeout(timer);
|
|
142
|
+
if (!killTree) {
|
|
143
|
+
child.kill('SIGKILL');
|
|
144
|
+
reject(error);
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
killProcessTree(child, { waitMs: killWaitMs }).then(waitForClose).then(() => reject(error));
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
const timer = setTimeout(() => {
|
|
151
|
+
fail(new SpaceError('command_timeout', `${file} ${args[0] ?? ''} did not finish within ${timeoutMs} ms and was stopped`));
|
|
152
|
+
}, timeoutMs);
|
|
153
|
+
|
|
154
|
+
const capture = (chunks) => (chunk) => {
|
|
155
|
+
capturedBytes += chunk.length;
|
|
156
|
+
if (capturedBytes > maxOutputBytes) {
|
|
157
|
+
fail(new SpaceError('command_output_too_large', `${file} ${args[0] ?? ''} printed more than ${maxOutputBytes} bytes and was stopped`));
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
chunks.push(chunk);
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
child.stdout.on('data', capture(stdoutChunks));
|
|
164
|
+
child.stderr.on('data', capture(stderrChunks));
|
|
165
|
+
|
|
166
|
+
child.on('error', (error) => {
|
|
167
|
+
liveTrees.delete(child);
|
|
168
|
+
fail(new SpaceError('command_spawn_failed', `Could not start ${file}: ${error.message}`, { errno: error.code ?? null }));
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
child.on('exit', () => {
|
|
172
|
+
liveTrees.delete(child);
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
child.on('close', (code, signal) => {
|
|
176
|
+
closed = true;
|
|
177
|
+
for (const wake of closeWaiters.splice(0)) wake();
|
|
178
|
+
if (settled) return;
|
|
179
|
+
settled = true;
|
|
180
|
+
clearTimeout(timer);
|
|
181
|
+
if (code === null) {
|
|
182
|
+
reject(new SpaceError('command_killed', `${file} ${args[0] ?? ''} was stopped by signal ${signal}`));
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
resolve({
|
|
186
|
+
code,
|
|
187
|
+
stdout: Buffer.concat(stdoutChunks).toString('utf8'),
|
|
188
|
+
stderr: Buffer.concat(stderrChunks).toString('utf8'),
|
|
189
|
+
});
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
// A child that exits without reading its input closes the pipe first.
|
|
193
|
+
child.stdin.on('error', () => {});
|
|
194
|
+
child.stdin.end(stdin);
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Kills a child and every process it started. Resolves true once the kill is under way and done
|
|
200
|
+
* as far as we can tell, false when it refused, and never rejects.
|
|
201
|
+
*
|
|
202
|
+
* POSIX: the child was spawned with `detached: true`, so it leads a process group, and the
|
|
203
|
+
* signal goes to that group. Windows: `taskkill.exe /T /F`, named by its absolute path under
|
|
204
|
+
* `SystemRoot` and spawned directly, hidden, with no shell.
|
|
205
|
+
*
|
|
206
|
+
* A pid that is not an integer greater than 1 is refused and nothing is signalled. A fake process
|
|
207
|
+
* with pid 1 once became `kill(-1)` and closed every program on a developer's machine, and a pid
|
|
208
|
+
* of 0 would signal our own process group. Tests hand in fakes with `pid: null`.
|
|
209
|
+
*
|
|
210
|
+
* On Windows it resolves when `taskkill` exits, or after `waitMs` if it does not.
|
|
211
|
+
*
|
|
212
|
+
* `platform`, `kill`, `spawnProcess`, `systemRoot` and `waitMs` are injectable for the tests.
|
|
213
|
+
*/
|
|
214
|
+
export async function killProcessTree(child, {
|
|
215
|
+
platform = process.platform,
|
|
216
|
+
kill = (pid, signal) => process.kill(pid, signal),
|
|
217
|
+
spawnProcess = spawn,
|
|
218
|
+
systemRoot = process.env.SystemRoot,
|
|
219
|
+
waitMs = KILL_WAIT_MS,
|
|
220
|
+
} = {}) {
|
|
221
|
+
const pid = child?.pid;
|
|
222
|
+
if (!Number.isInteger(pid) || pid <= 1) {
|
|
223
|
+
return false;
|
|
224
|
+
}
|
|
225
|
+
const killChildOnly = () => {
|
|
226
|
+
try {
|
|
227
|
+
child.kill('SIGKILL');
|
|
228
|
+
} catch {
|
|
229
|
+
// Already gone.
|
|
230
|
+
}
|
|
231
|
+
};
|
|
232
|
+
if (platform === 'win32') {
|
|
233
|
+
if (!systemRoot) {
|
|
234
|
+
killChildOnly();
|
|
235
|
+
return true;
|
|
236
|
+
}
|
|
237
|
+
await new Promise((resolveKill) => {
|
|
238
|
+
const bound = setTimeout(resolveKill, waitMs);
|
|
239
|
+
const done = () => { clearTimeout(bound); resolveKill(); };
|
|
240
|
+
try {
|
|
241
|
+
const killer = spawnProcess(path.win32.join(systemRoot, 'System32', 'taskkill.exe'), ['/T', '/F', '/PID', String(pid)], {
|
|
242
|
+
shell: false,
|
|
243
|
+
windowsHide: true,
|
|
244
|
+
stdio: 'ignore',
|
|
245
|
+
});
|
|
246
|
+
killer.once('error', () => { killChildOnly(); done(); });
|
|
247
|
+
killer.once('exit', done);
|
|
248
|
+
} catch {
|
|
249
|
+
killChildOnly();
|
|
250
|
+
done();
|
|
251
|
+
}
|
|
252
|
+
});
|
|
253
|
+
return true;
|
|
254
|
+
}
|
|
255
|
+
try {
|
|
256
|
+
kill(-pid, 'SIGKILL');
|
|
257
|
+
} catch {
|
|
258
|
+
killChildOnly();
|
|
259
|
+
}
|
|
260
|
+
return true;
|
|
261
|
+
}
|