@vantran-se/openchamber-web 1.24.3 → 2.0.1
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-BjJHrzmU.js → CodeMirrorCommentWidgets-ntXIbhV2.js} +1 -1
- package/dist/assets/DiffView-CjxUovbM.js +4 -0
- package/dist/assets/FilesView-DFWD0RQR.js +17 -0
- package/dist/assets/{GitView-CDOJy5Ay.js → GitView-BWa7e5vw.js} +4 -4
- package/dist/assets/{GuestAttachDialog-D3S0Rjic.js → GuestAttachDialog-DdOuyZzp.js} +1 -1
- package/dist/assets/GuestSurfacePane-DDfzid3U.js +1 -0
- package/dist/assets/{JsonTreeView-D0C9nU7K.js → JsonTreeView-3sGwqexh.js} +1 -1
- package/dist/assets/LinearIssuesView-C6dpIdjw.js +1 -0
- package/dist/assets/MarkdownImageGallery-Q7YR_tl4.js +1 -0
- package/dist/assets/MarkdownRendererImpl-DxDV56bL.js +12 -0
- package/dist/assets/OnboardingScreen-rK4Stg__.js +1 -0
- package/dist/assets/{PatchDiff-DnEhjXcC.js → PatchDiff-DS-IMU1j.js} +1 -1
- package/dist/assets/{PierreDiffViewer-DyxZVVVR.js → PierreDiffViewer-DW23KWKZ.js} +1 -1
- package/dist/assets/PlanView-BCk_VobP.js +5 -0
- package/dist/assets/PluginPane-BPP1dbck.js +3 -0
- package/dist/assets/SettingsView-kMmrpn43.js +29 -0
- package/dist/assets/SettingsWindow-BpvD6IYQ.js +1 -0
- package/dist/assets/SubmoduleDiffSummary-BLttSS1X.js +1 -0
- package/dist/assets/ToolOutputDialog-CtqDqbyr.js +15 -0
- package/dist/assets/ToolPartDiffPreview-5FcKdyRy.js +8 -0
- package/dist/assets/{VirtualizedFileDiff-0pyr_mHY.js → VirtualizedFileDiff-C763hNCR.js} +4 -4
- package/dist/assets/{WalkthroughView-yNj4nOP5.js → WalkthroughView-lap5btCF.js} +2 -2
- package/dist/assets/appThemeRegistry-u4zw1l0B.js +1 -0
- package/dist/assets/{commitAndPush-DvJKa3Ks.js → commitAndPush-CwGJfJG-.js} +1 -1
- package/dist/assets/{computeVirtualFileMetrics-DxTBMuu-.js → computeVirtualFileMetrics-DW5jjX4n.js} +1 -1
- package/dist/assets/de-CndEg67T.js +10 -0
- package/dist/assets/es-DglvPlaC.js +16 -0
- package/dist/assets/fr-DejPCpbS.js +14 -0
- package/dist/assets/index-C2r64QfD.js +1 -0
- package/dist/assets/index-CLcM8HKx.css +1 -0
- package/dist/assets/index-CvLgnuNI.js +41 -0
- package/dist/assets/{isDiffPlainText-D7FkBMP6.js → isDiffPlainText-D47a6hMP.js} +1 -1
- package/dist/assets/{isFilePlainText-CL2vsrIZ.js → isFilePlainText-B-mGgeKl.js} +1 -1
- package/dist/assets/ja-CVpQawCd.js +16 -0
- package/dist/assets/ko-Bw446h9j.js +16 -0
- package/dist/assets/main-Bz4WWXbr.js +2 -0
- package/dist/assets/main-oreqDsOV.js +896 -0
- package/dist/assets/{markdownCore-Dh7WEsdM.js → markdownCore-D2e5sp9y.js} +4 -4
- package/dist/assets/miniChat-D1v2gsBI.js +2 -0
- package/dist/assets/mobile-DBj3lCf2.js +2 -0
- package/dist/assets/{patchFileDiff-Bsba8KFp.js → patchFileDiff-BpeUOV91.js} +3 -3
- package/dist/assets/pl-BYNlPdhm.js +16 -0
- package/dist/assets/pt-BR-CL-Yfv9H.js +16 -0
- package/dist/assets/renderElectronMiniChatApp-DVtfEB2z.js +2 -0
- package/dist/assets/renderMobileApp-DFp9-xbG.js +25 -0
- package/dist/assets/runtimeEndpointReset-CsIpUep1.js +27 -0
- package/dist/assets/scheduledTasksApi-B5OHVpPM.js +1 -0
- package/dist/assets/{shikiHighlight-8_3csmr6.js → shikiHighlight-DNm2Z_Jc.js} +1 -1
- package/dist/assets/{textMateThemeFromAppTheme-CHo2fCgW.js → textMateThemeFromAppTheme-DGF2kG7g.js} +1 -1
- package/dist/assets/tr-DzmbxLy-.js +7 -0
- package/dist/assets/uk-C7vQnwro.js +16 -0
- package/dist/assets/useAppFontEffects-Ci3FEaPf.js +7337 -0
- package/dist/assets/{useInlineCommentController-DkQ_gqpn.js → useInlineCommentController-CuUuakrt.js} +2 -2
- package/dist/assets/useRootScrollLock-CQIsyERq.js +2 -0
- package/dist/assets/{vendor-codemirror-language-data-CdzUmUyZ.js → vendor-codemirror-language-data-CQa7_1cr.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-B8IcVQAA.js → web-components-B6tKQyJN.js} +2 -2
- package/dist/assets/zh-CN-Cl0srS-z.js +16 -0
- package/dist/assets/zh-TW-DtII5bDb.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-COVQhWGd.js +0 -4
- package/dist/assets/FilesView-B1V5JxFX.js +0 -13
- package/dist/assets/LinearIssuesView-D7FKg1XJ.js +0 -1
- package/dist/assets/MarkdownImageGallery-BrFjAVEf.js +0 -1
- package/dist/assets/MarkdownRendererImpl-DWepTafh.js +0 -12
- package/dist/assets/OnboardingScreen-Dyxxiqdt.js +0 -1
- package/dist/assets/PlanView-LW2z_EAP.js +0 -5
- package/dist/assets/PluginPane-B4tKno2h.js +0 -3
- package/dist/assets/SettingsView-DEaMulN9.js +0 -29
- package/dist/assets/SettingsWindow-FLDJXOgQ.js +0 -1
- package/dist/assets/SubmoduleDiffSummary-CvVwD-Vi.js +0 -1
- package/dist/assets/ToolOutputDialog-BiP0x2Rw.js +0 -15
- package/dist/assets/ToolPartDiffPreview-CkR4msKP.js +0 -8
- package/dist/assets/appThemeRegistry-BuUKhbYQ.js +0 -1
- package/dist/assets/de-DgxgFyGG.js +0 -10
- package/dist/assets/es-BFsZj5F_.js +0 -16
- package/dist/assets/fr-jdZ5KKQi.js +0 -14
- package/dist/assets/index-CKHjPKbq.js +0 -1
- package/dist/assets/index-Cbci4i1W.js +0 -40
- package/dist/assets/index-DqaMWNrC.css +0 -1
- package/dist/assets/ja-C5kKrhO4.js +0 -16
- package/dist/assets/ko-DXDTT_0O.js +0 -16
- package/dist/assets/main-CZALtVek.js +0 -896
- package/dist/assets/main-CgXMBNvG.js +0 -2
- package/dist/assets/miniChat-CwTmDixi.js +0 -2
- package/dist/assets/mobile-CLf2vCtO.js +0 -2
- package/dist/assets/pl-D-7x4Y8Q.js +0 -16
- package/dist/assets/pt-BR-CSBZrHa8.js +0 -16
- package/dist/assets/renderElectronMiniChatApp-C5gAFBk0.js +0 -2
- package/dist/assets/renderMobileApp-V4hYd32H.js +0 -25
- package/dist/assets/runtimeEndpointReset-DhlHCn-2.js +0 -25
- package/dist/assets/scheduledTasksApi-DzjbLd8P.js +0 -1
- package/dist/assets/tr-Dr-oJ22L.js +0 -7
- package/dist/assets/uk-9Nx83UcI.js +0 -16
- package/dist/assets/useAppFontEffects-khZXpFL_.js +0 -7333
- package/dist/assets/useRootScrollLock-DdvHf9Pt.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-Bxy_Hj3r.js +0 -16
- package/dist/assets/zh-TW-0BB4gpHM.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
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import fs from 'fs';
|
|
2
2
|
import path from 'path';
|
|
3
3
|
import {
|
|
4
|
+
getAncestors,
|
|
5
|
+
findWorktreeRoot,
|
|
4
6
|
CONFIG_FILE,
|
|
5
7
|
OPENCODE_CONFIG_DIR,
|
|
6
8
|
COMMAND_DIR,
|
|
@@ -11,47 +13,60 @@ import {
|
|
|
11
13
|
readConfigLayers,
|
|
12
14
|
writeConfig,
|
|
13
15
|
getJsonEntrySource,
|
|
14
|
-
getJsonWriteTarget,
|
|
15
16
|
isPromptFileReference,
|
|
16
17
|
resolvePromptFilePath,
|
|
17
18
|
writePromptFile,
|
|
18
19
|
} from './shared.js';
|
|
20
|
+
import {
|
|
21
|
+
toCommandEntity,
|
|
22
|
+
fromCommandEntity,
|
|
23
|
+
isLegacyCommandFrontmatter,
|
|
24
|
+
writeSectionEntry,
|
|
25
|
+
deleteSectionEntry,
|
|
26
|
+
parseModelSelection,
|
|
27
|
+
formatModelSelection,
|
|
28
|
+
isRecord,
|
|
29
|
+
} from './config-v2.js';
|
|
19
30
|
|
|
20
31
|
// ============== COMMAND SCOPE HELPERS ==============
|
|
32
|
+
//
|
|
33
|
+
// OpenCode 2 discovers commands from `command/` and `commands/`. OpenChamber
|
|
34
|
+
// reads both and writes only `commands/`; a command already living in the v1
|
|
35
|
+
// directory is rewritten at its own path in v2 shape.
|
|
21
36
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
37
|
+
const USER_COMMAND_DIRS = [COMMAND_DIR, path.join(OPENCODE_CONFIG_DIR, 'command')];
|
|
38
|
+
const PROJECT_COMMAND_DIR_NAMES = ['commands', 'command'];
|
|
39
|
+
|
|
40
|
+
/** Ensure the v2 project command directory exists. */
|
|
25
41
|
function ensureProjectCommandDir(workingDirectory) {
|
|
26
42
|
const projectCommandDir = path.join(workingDirectory, '.opencode', 'commands');
|
|
27
43
|
if (!fs.existsSync(projectCommandDir)) {
|
|
28
44
|
fs.mkdirSync(projectCommandDir, { recursive: true });
|
|
29
45
|
}
|
|
30
|
-
const legacyProjectCommandDir = path.join(workingDirectory, '.opencode', 'command');
|
|
31
|
-
if (!fs.existsSync(legacyProjectCommandDir)) {
|
|
32
|
-
fs.mkdirSync(legacyProjectCommandDir, { recursive: true });
|
|
33
|
-
}
|
|
34
46
|
return projectCommandDir;
|
|
35
47
|
}
|
|
36
48
|
|
|
37
|
-
/**
|
|
38
|
-
* Get project-level command path
|
|
39
|
-
*/
|
|
40
49
|
function getProjectCommandPath(workingDirectory, commandName) {
|
|
41
|
-
const
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
50
|
+
const preferred = path.join(workingDirectory, '.opencode', 'commands', `${commandName}.md`);
|
|
51
|
+
// Same walk as agents: every `.opencode` from the working directory up to
|
|
52
|
+
// the project root is a source; nested names (`team/review`) map onto paths.
|
|
53
|
+
const worktreeRoot = findWorktreeRoot(workingDirectory) || path.resolve(workingDirectory);
|
|
54
|
+
for (const base of getAncestors(workingDirectory, worktreeRoot)) {
|
|
55
|
+
for (const dirName of PROJECT_COMMAND_DIR_NAMES) {
|
|
56
|
+
const candidate = path.join(base, '.opencode', dirName, `${commandName}.md`);
|
|
57
|
+
if (fs.existsSync(candidate)) return candidate;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return preferred;
|
|
45
61
|
}
|
|
46
62
|
|
|
47
|
-
/**
|
|
48
|
-
* Get user-level command path
|
|
49
|
-
*/
|
|
50
63
|
function getUserCommandPath(commandName) {
|
|
51
|
-
const
|
|
52
|
-
const
|
|
53
|
-
|
|
54
|
-
|
|
64
|
+
const preferred = path.join(COMMAND_DIR, `${commandName}.md`);
|
|
65
|
+
for (const dir of USER_COMMAND_DIRS) {
|
|
66
|
+
const candidate = path.join(dir, `${commandName}.md`);
|
|
67
|
+
if (fs.existsSync(candidate)) return candidate;
|
|
68
|
+
}
|
|
69
|
+
return preferred;
|
|
55
70
|
}
|
|
56
71
|
|
|
57
72
|
/**
|
|
@@ -65,12 +80,12 @@ function getCommandScope(commandName, workingDirectory) {
|
|
|
65
80
|
return { scope: COMMAND_SCOPE.PROJECT, path: projectPath };
|
|
66
81
|
}
|
|
67
82
|
}
|
|
68
|
-
|
|
83
|
+
|
|
69
84
|
const userPath = getUserCommandPath(commandName);
|
|
70
85
|
if (fs.existsSync(userPath)) {
|
|
71
86
|
return { scope: COMMAND_SCOPE.USER, path: userPath };
|
|
72
87
|
}
|
|
73
|
-
|
|
88
|
+
|
|
74
89
|
return { scope: null, path: null };
|
|
75
90
|
}
|
|
76
91
|
|
|
@@ -78,41 +93,40 @@ function getCommandScope(commandName, workingDirectory) {
|
|
|
78
93
|
* Get the path where a command should be written based on scope
|
|
79
94
|
*/
|
|
80
95
|
function getCommandWritePath(commandName, workingDirectory, requestedScope) {
|
|
81
|
-
// For updates: check existing location first (project takes precedence)
|
|
82
96
|
const existing = getCommandScope(commandName, workingDirectory);
|
|
83
97
|
if (existing.path) {
|
|
84
98
|
return existing;
|
|
85
99
|
}
|
|
86
|
-
|
|
87
|
-
// For new commands or built-in overrides: use requested scope or default to user
|
|
100
|
+
|
|
88
101
|
const scope = requestedScope || COMMAND_SCOPE.USER;
|
|
89
102
|
if (scope === COMMAND_SCOPE.PROJECT && workingDirectory) {
|
|
90
|
-
return {
|
|
91
|
-
scope: COMMAND_SCOPE.PROJECT,
|
|
92
|
-
path: getProjectCommandPath(workingDirectory, commandName)
|
|
103
|
+
return {
|
|
104
|
+
scope: COMMAND_SCOPE.PROJECT,
|
|
105
|
+
path: getProjectCommandPath(workingDirectory, commandName),
|
|
93
106
|
};
|
|
94
107
|
}
|
|
95
|
-
|
|
96
|
-
return {
|
|
97
|
-
scope: COMMAND_SCOPE.USER,
|
|
98
|
-
path: getUserCommandPath(commandName)
|
|
108
|
+
|
|
109
|
+
return {
|
|
110
|
+
scope: COMMAND_SCOPE.USER,
|
|
111
|
+
path: getUserCommandPath(commandName),
|
|
99
112
|
};
|
|
100
113
|
}
|
|
101
114
|
|
|
115
|
+
// ============== READ ==============
|
|
116
|
+
|
|
102
117
|
function getCommandSources(commandName, workingDirectory) {
|
|
103
118
|
const projectPath = workingDirectory ? getProjectCommandPath(workingDirectory, commandName) : null;
|
|
104
|
-
const projectExists = projectPath && fs.existsSync(projectPath);
|
|
119
|
+
const projectExists = Boolean(projectPath) && fs.existsSync(projectPath);
|
|
105
120
|
|
|
106
121
|
const userPath = getUserCommandPath(commandName);
|
|
107
122
|
const userExists = fs.existsSync(userPath);
|
|
108
123
|
|
|
109
124
|
const mdPath = projectExists ? projectPath : (userExists ? userPath : null);
|
|
110
|
-
const mdExists =
|
|
125
|
+
const mdExists = Boolean(mdPath);
|
|
111
126
|
const mdScope = projectExists ? COMMAND_SCOPE.PROJECT : (userExists ? COMMAND_SCOPE.USER : null);
|
|
112
127
|
|
|
113
128
|
const layers = readConfigLayers(workingDirectory);
|
|
114
|
-
const jsonSource = getJsonEntrySource(layers, '
|
|
115
|
-
const jsonSection = jsonSource.section;
|
|
129
|
+
const jsonSource = getJsonEntrySource(layers, 'commands', commandName);
|
|
116
130
|
const jsonPath = jsonSource.path || layers.paths.customPath || layers.paths.projectPath || layers.paths.userPath;
|
|
117
131
|
const jsonScope = jsonSource.path === layers.paths.projectPath ? COMMAND_SCOPE.PROJECT : COMMAND_SCOPE.USER;
|
|
118
132
|
|
|
@@ -121,39 +135,77 @@ function getCommandSources(commandName, workingDirectory) {
|
|
|
121
135
|
exists: mdExists,
|
|
122
136
|
path: mdPath,
|
|
123
137
|
scope: mdScope,
|
|
124
|
-
|
|
138
|
+
legacy: false,
|
|
139
|
+
fields: [],
|
|
125
140
|
},
|
|
126
141
|
json: {
|
|
127
142
|
exists: jsonSource.exists,
|
|
128
143
|
path: jsonPath,
|
|
129
144
|
scope: jsonSource.exists ? jsonScope : null,
|
|
130
|
-
|
|
145
|
+
sectionKey: jsonSource.sectionKey,
|
|
146
|
+
legacy: Boolean(jsonSource.legacy),
|
|
147
|
+
fields: [],
|
|
131
148
|
},
|
|
132
149
|
projectMd: {
|
|
133
150
|
exists: projectExists,
|
|
134
|
-
path: projectPath
|
|
151
|
+
path: projectPath,
|
|
135
152
|
},
|
|
136
153
|
userMd: {
|
|
137
154
|
exists: userExists,
|
|
138
|
-
path: userPath
|
|
139
|
-
}
|
|
155
|
+
path: userPath,
|
|
156
|
+
},
|
|
140
157
|
};
|
|
141
158
|
|
|
142
159
|
if (mdExists) {
|
|
143
160
|
const { frontmatter, body } = parseMdFile(mdPath);
|
|
144
|
-
sources.md.
|
|
145
|
-
|
|
146
|
-
sources.md.fields.push('template');
|
|
147
|
-
}
|
|
161
|
+
sources.md.legacy = isLegacyCommandFrontmatter(frontmatter);
|
|
162
|
+
sources.md.fields = Object.keys(toCommandEntity(frontmatter, body));
|
|
148
163
|
}
|
|
149
164
|
|
|
150
|
-
if (
|
|
151
|
-
sources.json.fields = Object.keys(
|
|
165
|
+
if (jsonSource.exists) {
|
|
166
|
+
sources.json.fields = Object.keys(toCommandEntity(jsonSource.section));
|
|
152
167
|
}
|
|
153
168
|
|
|
154
169
|
return sources;
|
|
155
170
|
}
|
|
156
171
|
|
|
172
|
+
/** Canonical v2 command entity plus where it came from. */
|
|
173
|
+
function getCommandConfig(commandName, workingDirectory) {
|
|
174
|
+
const { path: mdPath, scope } = getCommandScope(commandName, workingDirectory);
|
|
175
|
+
if (mdPath) {
|
|
176
|
+
const { frontmatter, body } = parseMdFile(mdPath);
|
|
177
|
+
return {
|
|
178
|
+
source: 'md',
|
|
179
|
+
scope,
|
|
180
|
+
path: mdPath,
|
|
181
|
+
legacy: isLegacyCommandFrontmatter(frontmatter),
|
|
182
|
+
config: toCommandEntity(frontmatter, body),
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const layers = readConfigLayers(workingDirectory);
|
|
187
|
+
const jsonSource = getJsonEntrySource(layers, 'commands', commandName);
|
|
188
|
+
if (jsonSource.exists) {
|
|
189
|
+
return {
|
|
190
|
+
source: 'json',
|
|
191
|
+
scope: jsonSource.path === layers.paths.projectPath ? COMMAND_SCOPE.PROJECT : COMMAND_SCOPE.USER,
|
|
192
|
+
path: jsonSource.path,
|
|
193
|
+
legacy: Boolean(jsonSource.legacy),
|
|
194
|
+
config: toCommandEntity(jsonSource.section),
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
return { source: 'none', scope: null, path: null, legacy: false, config: {} };
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// ============== WRITE ==============
|
|
202
|
+
|
|
203
|
+
function writeCommandMd(targetPath, entity) {
|
|
204
|
+
const { fields, template } = fromCommandEntity(entity);
|
|
205
|
+
fs.mkdirSync(path.dirname(targetPath), { recursive: true });
|
|
206
|
+
writeMdFile(targetPath, fields, template);
|
|
207
|
+
}
|
|
208
|
+
|
|
157
209
|
function createCommand(commandName, config, workingDirectory, scope) {
|
|
158
210
|
ensureDirs();
|
|
159
211
|
|
|
@@ -169,8 +221,7 @@ function createCommand(commandName, config, workingDirectory, scope) {
|
|
|
169
221
|
}
|
|
170
222
|
|
|
171
223
|
const layers = readConfigLayers(workingDirectory);
|
|
172
|
-
|
|
173
|
-
if (jsonSource.exists) {
|
|
224
|
+
if (getJsonEntrySource(layers, 'commands', commandName).exists) {
|
|
174
225
|
throw new Error(`Command ${commandName} already exists in opencode.json`);
|
|
175
226
|
}
|
|
176
227
|
|
|
@@ -186,110 +237,78 @@ function createCommand(commandName, config, workingDirectory, scope) {
|
|
|
186
237
|
targetScope = COMMAND_SCOPE.USER;
|
|
187
238
|
}
|
|
188
239
|
|
|
189
|
-
const {
|
|
190
|
-
|
|
191
|
-
writeMdFile(targetPath, frontmatter, template || '');
|
|
240
|
+
const { scope: _ignoredScope, ...entity } = isRecord(config) ? config : {};
|
|
241
|
+
writeCommandMd(targetPath, entity);
|
|
192
242
|
console.log(`Created new command: ${commandName} (scope: ${targetScope}, path: ${targetPath})`);
|
|
243
|
+
return { scope: targetScope, path: targetPath };
|
|
193
244
|
}
|
|
194
245
|
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
246
|
+
// Clearing a v1 field has to reach its v2 location: `subtask` is `subagent`,
|
|
247
|
+
// and `variant` is the suffix on `model`.
|
|
248
|
+
function deleteCommandField(entity, field) {
|
|
249
|
+
if (field === 'variant') {
|
|
250
|
+
const parsed = parseModelSelection(entity.model);
|
|
251
|
+
if (!parsed) return;
|
|
252
|
+
const stripped = formatModelSelection({ providerID: parsed.providerID, modelID: parsed.modelID });
|
|
253
|
+
if (stripped) entity.model = stripped;
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
delete entity[field === 'subtask' ? 'subagent' : field];
|
|
257
|
+
}
|
|
200
258
|
|
|
201
|
-
|
|
202
|
-
const
|
|
203
|
-
const
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
259
|
+
function applyCommandUpdates(entity, updates) {
|
|
260
|
+
const next = { ...entity };
|
|
261
|
+
for (const [field, value] of Object.entries(isRecord(updates) ? updates : {})) {
|
|
262
|
+
if (field === 'scope' || value === undefined) continue;
|
|
263
|
+
if (value === null) {
|
|
264
|
+
deleteCommandField(next, field);
|
|
265
|
+
continue;
|
|
266
|
+
}
|
|
267
|
+
// `subtask` is the v1 spelling of `subagent`; accept it from older clients.
|
|
268
|
+
next[field === 'subtask' ? 'subagent' : field] = value;
|
|
269
|
+
}
|
|
270
|
+
return toCommandEntity(next);
|
|
271
|
+
}
|
|
209
272
|
|
|
210
|
-
|
|
273
|
+
function updateCommand(commandName, updates, workingDirectory) {
|
|
274
|
+
ensureDirs();
|
|
211
275
|
|
|
212
|
-
|
|
213
|
-
|
|
276
|
+
const current = getCommandConfig(commandName, workingDirectory);
|
|
277
|
+
const entity = applyCommandUpdates(current.config, updates);
|
|
214
278
|
|
|
215
|
-
if (
|
|
216
|
-
|
|
217
|
-
|
|
279
|
+
if (current.source === 'md') {
|
|
280
|
+
writeCommandMd(current.path, entity);
|
|
281
|
+
console.log(`Updated command: ${commandName} (md: ${current.path})`);
|
|
282
|
+
return { source: 'md', scope: current.scope, path: current.path };
|
|
218
283
|
}
|
|
219
284
|
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
if (mdExists || creatingNewMd) {
|
|
231
|
-
if (mdData) {
|
|
232
|
-
mdData.body = normalizedValue;
|
|
233
|
-
mdModified = true;
|
|
234
|
-
}
|
|
235
|
-
continue;
|
|
236
|
-
} else if (isPromptFileReference(jsonSection?.template)) {
|
|
237
|
-
const templateFilePath = resolvePromptFilePath(jsonSection.template);
|
|
238
|
-
if (!templateFilePath) {
|
|
239
|
-
throw new Error(`Invalid template file reference for command ${commandName}`);
|
|
240
|
-
}
|
|
241
|
-
writePromptFile(templateFilePath, normalizedValue);
|
|
242
|
-
continue;
|
|
243
|
-
} else if (isPromptFileReference(normalizedValue)) {
|
|
244
|
-
if (!config.command) config.command = {};
|
|
245
|
-
if (!config.command[commandName]) config.command[commandName] = {};
|
|
246
|
-
config.command[commandName].template = normalizedValue;
|
|
247
|
-
jsonModified = true;
|
|
248
|
-
continue;
|
|
285
|
+
if (current.source === 'json') {
|
|
286
|
+
const layers = readConfigLayers(workingDirectory);
|
|
287
|
+
const jsonSource = getJsonEntrySource(layers, 'commands', commandName);
|
|
288
|
+
const config = jsonSource.config || {};
|
|
289
|
+
const rawTemplate = jsonSource.section?.template;
|
|
290
|
+
if (isPromptFileReference(rawTemplate)) {
|
|
291
|
+
const templateFilePath = resolvePromptFilePath(rawTemplate);
|
|
292
|
+
if (!templateFilePath) {
|
|
293
|
+
throw new Error(`Invalid template file reference for command ${commandName}`);
|
|
249
294
|
}
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
if (!config.command[commandName]) config.command[commandName] = {};
|
|
253
|
-
config.command[commandName].template = normalizedValue;
|
|
254
|
-
jsonModified = true;
|
|
255
|
-
continue;
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
const inMd = mdData?.frontmatter?.[field] !== undefined;
|
|
259
|
-
const inJson = jsonSection?.[field] !== undefined;
|
|
260
|
-
|
|
261
|
-
if (inJson) {
|
|
262
|
-
if (!config.command) config.command = {};
|
|
263
|
-
if (!config.command[commandName]) config.command[commandName] = {};
|
|
264
|
-
config.command[commandName][field] = value;
|
|
265
|
-
jsonModified = true;
|
|
266
|
-
} else if (inMd || creatingNewMd) {
|
|
267
|
-
if (mdData) {
|
|
268
|
-
mdData.frontmatter[field] = value;
|
|
269
|
-
mdModified = true;
|
|
270
|
-
}
|
|
271
|
-
} else {
|
|
272
|
-
if ((mdExists || creatingNewMd) && mdData) {
|
|
273
|
-
mdData.frontmatter[field] = value;
|
|
274
|
-
mdModified = true;
|
|
275
|
-
} else {
|
|
276
|
-
if (!config.command) config.command = {};
|
|
277
|
-
if (!config.command[commandName]) config.command[commandName] = {};
|
|
278
|
-
config.command[commandName][field] = value;
|
|
279
|
-
jsonModified = true;
|
|
295
|
+
if (entity.template !== current.config.template) {
|
|
296
|
+
writePromptFile(templateFilePath, entity.template ?? '');
|
|
280
297
|
}
|
|
298
|
+
entity.template = rawTemplate;
|
|
281
299
|
}
|
|
300
|
+
writeSectionEntry(config, 'commands', commandName, entity);
|
|
301
|
+
const targetPath = jsonSource.path || CONFIG_FILE;
|
|
302
|
+
writeConfig(config, targetPath);
|
|
303
|
+
console.log(`Updated command: ${commandName} (json: ${targetPath})`);
|
|
304
|
+
return { source: 'json', scope: current.scope, path: targetPath };
|
|
282
305
|
}
|
|
283
306
|
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
writeConfig(config, jsonTarget.path || CONFIG_FILE);
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
console.log(`Updated command: ${commandName} (scope: ${targetScope}, md: ${mdModified}, json: ${jsonModified})`);
|
|
307
|
+
// Built-in override: materialize a user-level v2 markdown command.
|
|
308
|
+
const { scope, path: targetPath } = getCommandWritePath(commandName, workingDirectory, COMMAND_SCOPE.USER);
|
|
309
|
+
writeCommandMd(targetPath, entity);
|
|
310
|
+
console.log(`Created command override: ${commandName} (scope: ${scope}, path: ${targetPath})`);
|
|
311
|
+
return { source: 'md', scope, path: targetPath };
|
|
293
312
|
}
|
|
294
313
|
|
|
295
314
|
function deleteCommand(commandName, workingDirectory) {
|
|
@@ -312,10 +331,9 @@ function deleteCommand(commandName, workingDirectory) {
|
|
|
312
331
|
}
|
|
313
332
|
|
|
314
333
|
const layers = readConfigLayers(workingDirectory);
|
|
315
|
-
const jsonSource = getJsonEntrySource(layers, '
|
|
316
|
-
if (jsonSource.exists && jsonSource.config && jsonSource.path
|
|
317
|
-
|
|
318
|
-
delete jsonSource.config.command[commandName];
|
|
334
|
+
const jsonSource = getJsonEntrySource(layers, 'commands', commandName);
|
|
335
|
+
if (jsonSource.exists && jsonSource.config && jsonSource.path
|
|
336
|
+
&& deleteSectionEntry(jsonSource.config, 'commands', commandName)) {
|
|
319
337
|
writeConfig(jsonSource.config, jsonSource.path);
|
|
320
338
|
console.log(`Removed command from opencode.json: ${commandName}`);
|
|
321
339
|
deleted = true;
|
|
@@ -328,6 +346,7 @@ function deleteCommand(commandName, workingDirectory) {
|
|
|
328
346
|
|
|
329
347
|
export {
|
|
330
348
|
getCommandSources,
|
|
349
|
+
getCommandConfig,
|
|
331
350
|
createCommand,
|
|
332
351
|
updateCommand,
|
|
333
352
|
deleteCommand,
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export type OpenCodeCompatibility = {
|
|
2
|
+
state: 'unavailable' | 'compatible' | 'incompatible';
|
|
3
|
+
version: string | null;
|
|
4
|
+
installation: 'managed' | 'external' | 'bundled';
|
|
5
|
+
minimumVersion: string;
|
|
6
|
+
canInstall: boolean;
|
|
7
|
+
};
|
|
8
|
+
type Launch = { binary: string; args: string[] };
|
|
9
|
+
type CliOptions = { cwd?: string; env?: NodeJS.ProcessEnv };
|
|
10
|
+
export function isSupportedOpenCodeVersion(version: string): boolean;
|
|
11
|
+
export function readOpenCodeInfo(response: Response): Promise<{ version: string } | null>;
|
|
12
|
+
export function readOpenCodeCliVersion(launch: Launch, options?: CliOptions): Promise<string>;
|
|
13
|
+
export class UnsupportedOpenCodeVersionError extends Error { version: string; constructor(version: string); }
|
|
14
|
+
export function requireOpenCodeV2(launch: Launch, options?: CliOptions): Promise<string>;
|
|
15
|
+
export function readExternalOpenCodeVersion(baseUrl: string, headers: Record<string, string>, fetchImpl?: typeof fetch): Promise<string | null>;
|
|
16
|
+
export function describeOpenCodeCompatibility(version: string | null, installation: OpenCodeCompatibility['installation'], canInstall: boolean): OpenCodeCompatibility;
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { execFile } from 'node:child_process';
|
|
2
|
+
import { promisify } from 'node:util';
|
|
3
|
+
import { z } from 'zod';
|
|
4
|
+
|
|
5
|
+
const execute = promisify(execFile);
|
|
6
|
+
const versionSchema = z.string().regex(/^\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?$/);
|
|
7
|
+
const infoSchema = z.object({ version: versionSchema });
|
|
8
|
+
const legacyHealthSchema = z.object({ version: versionSchema, healthy: z.boolean() });
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Oldest OpenCode OpenChamber runs against. 2.0.15 added `PATCH /api/session`
|
|
12
|
+
* metadata, which now holds every OpenChamber per-session record.
|
|
13
|
+
*/
|
|
14
|
+
const MINIMUM_OPENCODE_VERSION = '2.0.15';
|
|
15
|
+
|
|
16
|
+
const releaseParts = (version) => version.split(/[-+]/, 1)[0].split('.').map(Number);
|
|
17
|
+
|
|
18
|
+
const compareRelease = (left, right) => {
|
|
19
|
+
const a = releaseParts(left);
|
|
20
|
+
const b = releaseParts(right);
|
|
21
|
+
for (let index = 0; index < 3; index += 1) {
|
|
22
|
+
if (a[index] !== b[index]) return a[index] - b[index];
|
|
23
|
+
}
|
|
24
|
+
return 0;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const isOlderThanMinimum = (version) => versionSchema.safeParse(version).success
|
|
28
|
+
&& compareRelease(version, MINIMUM_OPENCODE_VERSION) < 0;
|
|
29
|
+
|
|
30
|
+
/** 2.x at or above the minimum. A future major is a contract OpenChamber has not met yet. */
|
|
31
|
+
export const isSupportedOpenCodeVersion = (version) => versionSchema.safeParse(version).success
|
|
32
|
+
&& releaseParts(version)[0] === 2
|
|
33
|
+
&& !isOlderThanMinimum(version);
|
|
34
|
+
|
|
35
|
+
export const readOpenCodeInfo = async (response) => {
|
|
36
|
+
if (!response.ok) return null;
|
|
37
|
+
const parsed = infoSchema.safeParse(await response.json().catch(() => null));
|
|
38
|
+
return parsed.success ? parsed.data : null;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
export const readOpenCodeCliVersion = async (launch, options = {}) => {
|
|
42
|
+
const { stdout } = await execute(launch.binary, [...launch.args, '--version'], {
|
|
43
|
+
...options, encoding: 'utf8', timeout: 15_000, maxBuffer: 16 * 1024, windowsHide: true,
|
|
44
|
+
});
|
|
45
|
+
const match = /^(?:opencode\s+v?)?(\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?)\s*$/.exec(stdout.trim());
|
|
46
|
+
if (!match) throw new Error('Could not determine the installed OpenCode version.');
|
|
47
|
+
return match[1];
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
export class UnsupportedOpenCodeVersionError extends Error {
|
|
51
|
+
constructor(version) {
|
|
52
|
+
super(`OpenCode ${version} is installed. OpenChamber requires OpenCode ${MINIMUM_OPENCODE_VERSION} or newer.`);
|
|
53
|
+
this.name = 'UnsupportedOpenCodeVersionError';
|
|
54
|
+
this.version = version;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export const requireOpenCodeV2 = async (launch, options) => {
|
|
59
|
+
const version = await readOpenCodeCliVersion(launch, options);
|
|
60
|
+
if (!isSupportedOpenCodeVersion(version)) throw new UnsupportedOpenCodeVersionError(version);
|
|
61
|
+
return version;
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
// External URLs have no local executable. A legacy probe identifies v1 only
|
|
65
|
+
// from its JSON contract; neither HTML fallbacks nor auth failures imply v1.
|
|
66
|
+
export const readExternalOpenCodeVersion = async (baseUrl, headers, fetchImpl = fetch) => {
|
|
67
|
+
const request = (pathname) => fetchImpl(new URL(pathname, baseUrl), {
|
|
68
|
+
headers: { ...headers, Accept: 'application/json' }, redirect: 'error', signal: AbortSignal.timeout(5000),
|
|
69
|
+
});
|
|
70
|
+
const response = await request('/api/info');
|
|
71
|
+
if (response.status === 401 || response.status === 403) return null;
|
|
72
|
+
const info = await readOpenCodeInfo(response);
|
|
73
|
+
if (info) return info.version;
|
|
74
|
+
const legacy = await request('/global/health');
|
|
75
|
+
if (!legacy.ok) return null;
|
|
76
|
+
const parsed = legacyHealthSchema.safeParse(await legacy.json().catch(() => null));
|
|
77
|
+
return parsed.success && parsed.data.version.startsWith('1.') ? parsed.data.version : null;
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
export const describeOpenCodeCompatibility = (version, installation, canInstall) => ({
|
|
81
|
+
state: version === null ? 'unavailable' : isSupportedOpenCodeVersion(version) ? 'compatible' : 'incompatible',
|
|
82
|
+
version,
|
|
83
|
+
installation,
|
|
84
|
+
minimumVersion: MINIMUM_OPENCODE_VERSION,
|
|
85
|
+
// The installer fetches the latest release, which clears both a 1.x CLI and
|
|
86
|
+
// a 2.x one older than the minimum.
|
|
87
|
+
canInstall: version !== null && isOlderThanMinimum(version) && installation === 'managed' && canInstall,
|
|
88
|
+
});
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { describeOpenCodeCompatibility, readOpenCodeCliVersion, readExternalOpenCodeVersion, readOpenCodeInfo, requireOpenCodeV2, UnsupportedOpenCodeVersionError } from './compatibility.js';
|
|
3
|
+
|
|
4
|
+
const cli = (output, code = 0) => ({ binary: process.execPath, args: ['-e', `console.log(${JSON.stringify(output)}); process.exit(${code})`, '--'] });
|
|
5
|
+
|
|
6
|
+
describe('OpenCode compatibility', () => {
|
|
7
|
+
it('recognizes both CLI version formats before launching a server', async () => {
|
|
8
|
+
expect(await readOpenCodeCliVersion(cli('1.18.32'))).toBe('1.18.32');
|
|
9
|
+
expect(await requireOpenCodeV2(cli('opencode v2.0.15'))).toBe('2.0.15');
|
|
10
|
+
await expect(requireOpenCodeV2(cli('2.0.14'))).rejects.toBeInstanceOf(UnsupportedOpenCodeVersionError);
|
|
11
|
+
await expect(requireOpenCodeV2(cli('1.18.32'))).rejects.toBeInstanceOf(UnsupportedOpenCodeVersionError);
|
|
12
|
+
await expect(readOpenCodeCliVersion(cli('2.0.14', 1))).rejects.toThrow();
|
|
13
|
+
await expect(readOpenCodeCliVersion(cli('error, requires 2.0.14'))).rejects.toThrow();
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it('does not accept HTTP 200 HTML or malformed JSON as readiness', async () => {
|
|
17
|
+
expect(await readOpenCodeInfo(new Response('<html>OpenCode</html>'))).toBeNull();
|
|
18
|
+
expect(await readOpenCodeInfo(Response.json({ healthy: true }))).toBeNull();
|
|
19
|
+
expect(await readOpenCodeInfo(Response.json({ version: 'not a version' }))).toBeNull();
|
|
20
|
+
expect(await readOpenCodeInfo(Response.json({ version: '2.0.14' }))).toEqual({ version: '2.0.14' });
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it('identifies external v1 through its legacy health contract', async () => {
|
|
24
|
+
const requests = [];
|
|
25
|
+
const fetchImpl = async (url) => {
|
|
26
|
+
requests.push(url.pathname);
|
|
27
|
+
return url.pathname === '/api/info' ? new Response('<html/>') : Response.json({ healthy: true, version: '1.18.32' });
|
|
28
|
+
};
|
|
29
|
+
expect(await readExternalOpenCodeVersion('http://localhost:4096', {}, fetchImpl)).toBe('1.18.32');
|
|
30
|
+
expect(requests).toEqual(['/api/info', '/global/health']);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it.each([401, 403])('does not call an auth failure v1 (%s)', async (status) => {
|
|
34
|
+
let requests = 0;
|
|
35
|
+
expect(await readExternalOpenCodeVersion('http://localhost:4096', {}, async () => {
|
|
36
|
+
requests += 1;
|
|
37
|
+
return new Response(null, { status });
|
|
38
|
+
})).toBeNull();
|
|
39
|
+
expect(requests).toBe(1);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('accepts 2.x from the minimum on and nothing older or of another major', () => {
|
|
43
|
+
for (const version of ['2.0.15', '2.0.16', '2.1.0', '2.0.15-beta.1']) {
|
|
44
|
+
expect(describeOpenCodeCompatibility(version, 'managed', true).state).toBe('compatible');
|
|
45
|
+
}
|
|
46
|
+
for (const version of ['1.18.32', '2.0.14', '2.0.9', '3.0.0']) {
|
|
47
|
+
expect(describeOpenCodeCompatibility(version, 'managed', true).state).toBe('incompatible');
|
|
48
|
+
}
|
|
49
|
+
expect(describeOpenCodeCompatibility('2.0.14', 'managed', true).minimumVersion).toBe('2.0.15');
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('only offers installation for a known managed CLI older than the minimum', () => {
|
|
53
|
+
expect(describeOpenCodeCompatibility('1.18.32', 'managed', true)).toMatchObject({ state: 'incompatible', canInstall: true });
|
|
54
|
+
expect(describeOpenCodeCompatibility('2.0.14', 'managed', true)).toMatchObject({ state: 'incompatible', canInstall: true });
|
|
55
|
+
for (const installation of ['external', 'bundled']) {
|
|
56
|
+
expect(describeOpenCodeCompatibility('1.18.32', installation, true).canInstall).toBe(false);
|
|
57
|
+
}
|
|
58
|
+
for (const version of [null, '2.0.15', '3.0.0']) {
|
|
59
|
+
expect(describeOpenCodeCompatibility(version, 'managed', true).canInstall).toBe(false);
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
});
|