@openchambery/web 2.0.0-beta.3 → 2.0.0-beta.31
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 +14 -0
- package/dist/assets/{AgentAvatar-B_JQoreg.js → AgentAvatar-O2x_1cFU.js} +1 -1
- package/dist/assets/AssistantView-D_vWSFzf.js +3 -0
- package/dist/assets/{DiagramView-BNnda8qO.js → DiagramView-B_hiA-Tg.js} +1 -1
- package/dist/assets/{FadeInOnReveal-VYQTSAkr.js → FadeInOnReveal-CMBd5BTd.js} +1 -1
- package/dist/assets/Icon-BN87CyS4.js +1 -0
- package/dist/assets/MarkdownRenderer--4f1RVU1.js +2 -0
- package/dist/assets/{MarkdownRendererImpl-DwrHFGUp.js → MarkdownRendererImpl-Blh7LZm7.js} +3 -3
- package/dist/assets/MarkstreamRendererImpl-CstRN3R1.js +3 -0
- package/dist/assets/{MobileDetailNavigation-BXr6AGLT.js → MobileDetailNavigation-DR6wUqeI.js} +1 -1
- package/dist/assets/{MobileShareBridge-D7OE9OP_.js → MobileShareBridge-CTFAmi5i.js} +3 -3
- package/dist/assets/MobileSurface-B5XmnUTI.js +2 -0
- package/dist/assets/{MultiRunWindow-DF3iIFCd.js → MultiRunWindow-ccv_K6Mt.js} +1 -1
- package/dist/assets/{OnboardingScreen-BjHmlwlw.js → OnboardingScreen-BLjZIBOs.js} +1 -1
- package/dist/assets/{OverlayScrollbar-DLtWO_Gp.js → OverlayScrollbar-CoZ4wtau.js} +1 -1
- package/dist/assets/SettingsView-LYoifZgH.js +25 -0
- package/dist/assets/{SettingsWindow-BI5swspb.js → SettingsWindow-CnDiFgGl.js} +1 -1
- package/dist/assets/TerminalView-DRhXa_u9.js +8 -0
- package/dist/assets/{ToolOutputDialog-BEWhBcRd.js → ToolOutputDialog-cQm6YPbu.js} +1 -1
- package/dist/assets/appThemeRegistry-DvJG4vb9.js +1 -0
- package/dist/assets/{assistant-attachment-cache-C0aRnFkj.js → assistant-attachment-cache-sG8YQsVs.js} +1 -1
- package/dist/assets/{button-DKuz3L8l.js → button-Ck8ipzBm.js} +1 -1
- package/dist/assets/collapsible-nU99prkD.js +1 -0
- package/dist/assets/context-menu-DMzZN1oi.js +1 -0
- package/dist/assets/{desktopHosts-CoDkOnyS.js → desktopHosts-DuftEj8B.js} +1 -1
- package/dist/assets/desktopRelayRestore-g378BdZf.js +1 -0
- package/dist/assets/{diagram-D7T8HQgj.js → diagram-CvO4P8ds.js} +1 -1
- package/dist/assets/dialog-ByGUkS3c.js +1 -0
- package/dist/assets/dropdown-menu.styles-ayLWk2gf.js +1 -0
- package/dist/assets/es-DCjl24dV.js +15 -0
- package/dist/assets/fileMentionTouchSelection-B4QTB6Tq.js +2 -0
- package/dist/assets/{fileQueries-JrpcsOCs.js → fileQueries-DuF89nFO.js} +1 -1
- package/dist/assets/fr-DfJ5j0t5.js +13 -0
- package/dist/assets/{gitApi-CM42qUed.js → gitApi-ChE0ftqF.js} +3 -3
- package/dist/assets/{imageSource-CIl0FL10.js → imageSource-BpUsiwFd.js} +1 -1
- package/dist/assets/{input-CzQy8UhV.js → input-CXhFmHcq.js} +1 -1
- package/dist/assets/insertionBoundaries-BP5YmZFb.js +1 -0
- package/dist/assets/ja-B_9_w2Xf.js +15 -0
- package/dist/assets/ko-DlGbo3kV.js +15 -0
- package/dist/assets/main-B0y8N4sq.js +2 -0
- package/dist/assets/main-D83L0SQU.js +169 -0
- package/dist/assets/{markdownHeightCache-Cj1I5NDw.js → markdownHeightCache-g_UsWzbA.js} +1 -1
- package/dist/assets/markdownTheme-DM5h1i18.js +1 -0
- package/dist/assets/{mcpQueries-D5mQymgM.js → mcpQueries-BtSRn19d.js} +1 -1
- package/dist/assets/miniChat-s8g2Otyu.js +2 -0
- package/dist/assets/mobile-B6toR_NR.js +2 -0
- package/dist/assets/multirun-BqZ3MZrK.js +3 -0
- package/dist/assets/persistence-Dnj1QOdX.js +3 -0
- package/dist/assets/pl-DjUsS8Ei.js +15 -0
- package/dist/assets/platform-X3Y3GyM5.js +6 -0
- package/dist/assets/projectMeta-DKjtarC_.js +1 -0
- package/dist/assets/pt-BR-B8sV7qo1.js +15 -0
- package/dist/assets/{relay-image-stream-DM7atqJs.js → relay-image-stream-D2XooXKZ.js} +1 -1
- package/dist/assets/renderElectronMiniChatApp-Bk98yyKC.js +2 -0
- package/dist/assets/renderMobileApp-B_qgjDlv.js +28 -0
- package/dist/assets/runtime-fetch-CiqQd__Y.js +1 -0
- package/dist/assets/runtime-switch-BJM2k_uH.js +1 -0
- package/dist/assets/runtimeConfig-BMSsd_sn.js +2 -0
- package/dist/assets/runtimeEndpointReset-DsuP5FPT.css +1 -0
- package/dist/assets/runtimeEndpointReset-HsKwHFWr.js +148 -0
- package/dist/assets/sessionChangeSummary-C8OSNmpS.js +2 -0
- package/dist/assets/{sessionLookup-myHe5ltu.js → sessionLookup-DuNsPLgr.js} +1 -1
- package/dist/assets/{sortable-tabs-strip-BQtmHx9r.js → sortable-tabs-strip-T-dk_L69.js} +1 -1
- package/dist/assets/src-BmSX_CVj.css +2 -0
- package/dist/assets/store-Cuktref-.js +15 -0
- package/dist/assets/{terminalApi-BxcdiduX.js → terminalApi-C_4joKUG.js} +1 -1
- package/dist/assets/textarea-biAqiqnJ.js +1 -0
- package/dist/assets/{tooltip-Cx5gTbdY.js → tooltip-BKL7J5hx.js} +1 -1
- package/dist/assets/uk-Bh7HXyLp.js +15 -0
- package/dist/assets/{url-CDHnjlsm.js → url-DNRMq_0R.js} +1 -1
- package/dist/assets/{useAppFontEffects-CNeo0aZs.js → useAppFontEffects-oEf92G32.js} +57 -52
- package/dist/assets/{useAssistantUIStore-asNG6K7n.js → useAssistantUIStore-DBwfg9nX.js} +1 -1
- package/dist/assets/useDesktopWindowControlsLayout-CD-czj1j.js +1 -0
- package/dist/assets/{useEffectiveDirectory-B4LvR8Gt.js → useEffectiveDirectory-BmrftoaF.js} +1 -1
- package/dist/assets/{useFontPreferences-CUnQ4TVm.js → useFontPreferences-CMrbTA0r.js} +1 -1
- package/dist/assets/useMobileNavigationStore-Bzu_5Aa7.js +415 -0
- package/dist/assets/{useRuntimeAPIs-DwLtO0c_.js → useRuntimeAPIs-DSLXJybW.js} +1 -1
- package/dist/assets/useSessionAutoCleanup-CHvstjnp.js +2 -0
- package/dist/assets/{useThemeSystem-BlNUWH47.js → useThemeSystem-DMzWVJaW.js} +1 -1
- package/dist/assets/useUIStore-Dfx6TKvi.js +1 -0
- package/dist/assets/{useWorkerHighlightedLines-CJZbJDW3.js → useWorkerHighlightedLines-Cm51EGlD.js} +1 -1
- package/dist/assets/{vendor-base-ui-CDjyofeD.js → vendor-base-ui-CnHfUmDN.js} +1 -1
- package/dist/assets/{vendor-cmdk-BFH0UpXX.js → vendor-cmdk-CeAGfrLo.js} +1 -1
- package/dist/assets/{vendor-dnd-kit-accessibility-BrR3AKvM.js → vendor-dnd-kit-accessibility-BY5LhhUB.js} +1 -1
- package/dist/assets/{vendor-dnd-kit-core-CNfdvk6H.js → vendor-dnd-kit-core-CvWxUPdb.js} +1 -1
- package/dist/assets/{vendor-dnd-kit-sortable-DQUaTeGj.js → vendor-dnd-kit-sortable-hHdP9lLb.js} +1 -1
- package/dist/assets/{vendor-framer-motion-4dogsUk3.js → vendor-framer-motion-DFy_BSVd.js} +1 -1
- package/dist/assets/{vendor-legendapp-list-BU7MdPZR.js → vendor-legendapp-list-WE32lwNk.js} +1 -1
- package/dist/assets/{vendor-markstream-react-qHC2FykU.js → vendor-markstream-react-DX55XLGO.js} +3 -3
- package/dist/assets/vendor-opencode-client-MF2GBrFx.js +12 -0
- package/dist/assets/{vendor-pierre-diffs-CUfqC003.js → vendor-pierre-diffs-DX7XTPG3.js} +1 -1
- package/dist/assets/{vendor-react-C94A0AHl.js → vendor-react-DLPElpoQ.js} +1 -1
- package/dist/assets/{vendor-react-drawio-FVlWqKVE.js → vendor-react-drawio-ECtEwvbc.js} +1 -1
- package/dist/assets/{vendor-reactuses-core-C2A3EjUi.js → vendor-reactuses-core-Dq5ocTF8.js} +1 -1
- package/dist/assets/{vendor-sonner-DNqHmUQs.js → vendor-sonner-DVzioF3d.js} +1 -1
- package/dist/assets/{vendor-stream-diffs-CwbKsmlv.js → vendor-stream-diffs-DWy3xSmp.js} +2 -2
- package/dist/assets/{vendor-tanstack-react-query-D0iFK57C.js → vendor-tanstack-react-query-D7pys_c9.js} +1 -1
- package/dist/assets/{vendor-tanstack-react-virtual-B2kV6cnT.js → vendor-tanstack-react-virtual-tlDgH6PG.js} +1 -1
- package/dist/assets/{vendor-zustand-BqLKf6ZY.js → vendor-zustand-DzRqcymP.js} +2 -2
- package/dist/assets/zh-CN-Uzia0gk4.js +15 -0
- package/dist/assets/zh-TW-CiI7DLdB.js +15 -0
- package/dist/index.html +15 -15
- package/dist/mini-chat.html +15 -15
- package/dist/mobile.html +15 -15
- package/package.json +3 -2
- package/server/index.js +46 -27
- package/server/lib/browser-provider/DOCUMENTATION.md +141 -0
- package/server/lib/browser-provider/contract.js +312 -0
- package/server/lib/browser-provider/host.js +421 -0
- package/server/lib/browser-provider/host.test.js +304 -0
- package/server/lib/browser-provider/routes.js +122 -0
- package/server/lib/browser-provider/routes.test.js +120 -0
- package/server/lib/browser-provider/selection-store.js +43 -0
- package/server/lib/browser-provider/selection-store.test.js +35 -0
- package/server/lib/event-stream/DOCUMENTATION.md +3 -1
- package/server/lib/event-stream/reasoning-projection.js +16 -1
- package/server/lib/event-stream/reasoning-projection.test.js +15 -0
- package/server/lib/git/DOCUMENTATION.md +1 -0
- package/server/lib/git/routes.js +7 -4
- package/server/lib/git/routes.test.js +34 -0
- package/server/lib/git/service.js +3 -1
- package/server/lib/llm/DOCUMENTATION.md +6 -4
- package/server/lib/llm/generate.js +8 -9
- package/server/lib/llm/generate.test.js +33 -6
- package/server/lib/message-queue/service.js +1 -1
- package/server/lib/notifications/DOCUMENTATION.md +3 -1
- package/server/lib/notifications/runtime.js +128 -31
- package/server/lib/notifications/runtime.test.js +103 -0
- package/server/lib/notifications/template-runtime.js +130 -38
- package/server/lib/notifications/template-runtime.test.js +96 -0
- package/server/lib/opencode/DOCUMENTATION.md +36 -19
- package/server/lib/opencode/agents.js +13 -22
- package/server/lib/opencode/auth-state-runtime.js +3 -3
- package/server/lib/opencode/auth-state-runtime.test.js +59 -0
- package/server/lib/opencode/auth.d.ts +13 -0
- package/server/lib/opencode/auth.js +74 -64
- package/server/lib/opencode/auth.test.js +57 -0
- package/server/lib/opencode/commands.js +1 -1
- package/server/lib/opencode/config-entities-layering.test.js +72 -0
- package/server/lib/opencode/config-entity-routes.js +16 -124
- package/server/lib/opencode/config-entity-routes.test.js +48 -11
- package/server/lib/opencode/config-files.d.ts +17 -0
- package/server/lib/opencode/config-files.js +187 -0
- package/server/lib/opencode/config-files.test.js +201 -0
- package/server/lib/opencode/core-routes.js +0 -1
- package/server/lib/opencode/core-routes.test.js +0 -15
- package/server/lib/opencode/credential-boundary.d.ts +1 -0
- package/server/lib/opencode/credential-boundary.js +11 -0
- package/server/lib/opencode/ensure-cli.d.ts +8 -0
- package/server/lib/opencode/ensure-cli.js +4 -1
- package/server/lib/opencode/env-config.js +4 -1
- package/server/lib/opencode/env-config.test.js +28 -0
- package/server/lib/opencode/env-runtime.test.js +18 -1
- package/server/lib/opencode/feature-routes-runtime.js +19 -7
- package/server/lib/opencode/feature-routes-runtime.test.js +7 -0
- package/server/lib/opencode/hmr-state-runtime.js +4 -4
- package/server/lib/opencode/lifecycle.js +1 -49
- package/server/lib/opencode/lifecycle.test.js +30 -4
- package/server/lib/opencode/location-release.d.ts +8 -0
- package/server/lib/opencode/location-release.js +32 -0
- package/server/lib/opencode/location-release.test.js +43 -0
- package/server/lib/opencode/mcp.js +1 -1
- package/server/lib/opencode/opencode2-pin.js +2 -1
- package/server/lib/opencode/owned-runtime-upgrade.test.js +5 -5
- package/server/lib/opencode/plugin-routes.test.js +4 -2
- package/server/lib/opencode/plugins.js +12 -60
- package/server/lib/opencode/plugins.test.js +5 -3
- package/server/lib/opencode/providers.js +6 -3
- package/server/lib/opencode/proxy.js +13 -1
- package/server/lib/opencode/proxy.test.js +31 -3
- package/server/lib/opencode/remote-upgrade.integration.test.js +66 -0
- package/server/lib/opencode/routes.js +40 -8
- package/server/lib/opencode/routes.test.js +51 -10
- package/server/lib/opencode/runtime-contract.js +3 -2
- package/server/lib/opencode/runtime-contract.test.js +12 -12
- package/server/lib/opencode/shared.js +6 -175
- package/server/lib/opencode/update-discovery.js +39 -0
- package/server/lib/opencode/update-discovery.test.js +39 -0
- package/server/lib/opencode/upgrade-screen.d.ts +30 -0
- package/server/lib/opencode/upgrade-screen.js +292 -0
- package/server/lib/opencode/upgrade-screen.test.js +196 -0
- package/server/lib/opencode/version-alignment.test.js +40 -0
- package/server/lib/question-auto-delegate/DOCUMENTATION.md +21 -6
- package/server/lib/question-auto-delegate/core.d.ts +5 -1
- package/server/lib/question-auto-delegate/core.js +21 -13
- package/server/lib/question-auto-delegate/form-io.d.ts +6 -0
- package/server/lib/question-auto-delegate/form-io.js +39 -0
- package/server/lib/question-auto-delegate/runtime.js +4 -24
- package/server/lib/question-auto-delegate/runtime.test.js +138 -0
- package/server/lib/quota/DOCUMENTATION.md +9 -0
- package/server/lib/quota/provider-credentials.test.js +29 -0
- package/server/lib/quota/providers/claude.js +4 -4
- package/server/lib/quota/providers/codex.js +4 -4
- package/server/lib/quota/providers/codex.test.js +1 -1
- package/server/lib/quota/providers/copilot.js +5 -5
- package/server/lib/quota/providers/google/auth.js +3 -3
- package/server/lib/quota/providers/google/index.js +2 -2
- package/server/lib/quota/providers/index.js +3 -7
- package/server/lib/quota/providers/index.test.js +2 -2
- package/server/lib/quota/providers/kimi.js +4 -4
- package/server/lib/quota/providers/minimax-shared.js +4 -4
- package/server/lib/quota/providers/nanogpt.js +4 -4
- package/server/lib/quota/providers/openai.js +4 -4
- package/server/lib/quota/providers/openrouter.js +4 -4
- package/server/lib/quota/providers/wafer.js +4 -4
- package/server/lib/quota/providers/zai.js +4 -4
- package/server/lib/quota/providers/zhipuai-coding-plan.js +6 -20
- package/server/lib/quota/routes.js +1 -1
- package/server/lib/relay/DOCUMENTATION.md +3 -1
- package/server/lib/relay/service.js +1 -0
- package/server/lib/relay/service.test.js +13 -0
- package/server/lib/scheduled-tasks/DOCUMENTATION.md +8 -1
- package/server/lib/scheduled-tasks/runtime.js +12 -4
- package/server/lib/scheduled-tasks/runtime.test.js +81 -6
- package/server/lib/session-goal/DOCUMENTATION.md +11 -0
- package/server/lib/session-goal/runtime.js +4 -4
- package/server/lib/session-goal/runtime.test.js +32 -3
- package/server/lib/session-index/DOCUMENTATION.md +7 -0
- package/server/lib/session-index/event-ingest.js +13 -0
- package/server/lib/session-index/event-ingest.test.js +7 -0
- package/server/lib/session-index/service.js +45 -0
- package/server/lib/session-index/service.test.js +47 -0
- package/server/lib/session-metadata/DOCUMENTATION.md +33 -17
- package/server/lib/session-metadata/opencode-session-record.d.ts +57 -0
- package/server/lib/session-metadata/opencode-session-record.js +316 -0
- package/server/lib/session-metadata/opencode-session-record.test.js +242 -0
- package/server/lib/session-metadata/routes.js +5 -1
- package/server/lib/session-metadata/session-archive.js +1 -1
- package/server/lib/session-metadata/session-metadata-store.d.ts +2 -0
- package/server/lib/session-metadata/session-metadata-store.js +63 -16
- package/server/lib/session-metadata/session-metadata-store.test.js +23 -0
- package/server/lib/session-metadata/session-projection.d.ts +1 -0
- package/server/lib/session-metadata/session-projection.js +49 -2
- package/server/lib/session-metadata/session-projection.test.js +35 -0
- package/server/lib/session-title/DOCUMENTATION.md +52 -14
- package/server/lib/session-title/native-runtime.test.js +59 -5
- package/server/lib/session-title/runtime.js +65 -19
- package/server/lib/session-title/session-access.js +108 -3
- package/server/lib/session-title/session-access.test.js +125 -7
- package/server/lib/tts/DOCUMENTATION.md +7 -5
- package/server/lib/tts/credential-source.test.js +23 -0
- package/server/lib/tts/routes.js +2 -2
- package/server/lib/tts/service.js +16 -37
- package/dist/assets/AssistantView-ClQAfGOT.js +0 -3
- package/dist/assets/Icon-BiEQEcih.js +0 -1
- package/dist/assets/MarkdownRenderer-CJHSsWin.js +0 -2
- package/dist/assets/MarkstreamRendererImpl-CSnbQMu3.js +0 -3
- package/dist/assets/MobileSurface-D2jUkX9l.js +0 -2
- package/dist/assets/SettingsView-BRxELjow.js +0 -25
- package/dist/assets/TerminalView-BU3x8-fr.js +0 -8
- package/dist/assets/appThemeRegistry-4MR0F7Jz.js +0 -1
- package/dist/assets/collapsible-DXbDynN4.js +0 -3
- package/dist/assets/context-menu-CqolsLCm.js +0 -1
- package/dist/assets/desktopRelayRestore-DDlVKyOE.js +0 -1
- package/dist/assets/dialog-DQYl3W_o.js +0 -1
- package/dist/assets/dropdown-menu.styles-Bzo6L3FH.js +0 -1
- package/dist/assets/es-CN3E_zXj.js +0 -15
- package/dist/assets/fileMentionTouchSelection-BZ6ovBYH.js +0 -4
- package/dist/assets/fr-9zZvQZD2.js +0 -13
- package/dist/assets/insertionBoundaries-CtkZ9720.js +0 -1
- package/dist/assets/ja-Dnjk_82N.js +0 -15
- package/dist/assets/ko-BxCEE99C.js +0 -15
- package/dist/assets/main-BILgBI3B.js +0 -169
- package/dist/assets/main-BeveiW6F.js +0 -2
- package/dist/assets/markdownTheme-CKac3wKC.js +0 -1
- package/dist/assets/miniChat-BgrzM3u7.js +0 -2
- package/dist/assets/mobile-DtYoQE8C.js +0 -2
- package/dist/assets/multirun-jy8dEI96.js +0 -4
- package/dist/assets/persistence-C9LQpiOS.js +0 -3
- package/dist/assets/pl-CNNsWnPM.js +0 -15
- package/dist/assets/platform-BQghHRfp.js +0 -6
- package/dist/assets/pt-BR-BZB2DJMp.js +0 -15
- package/dist/assets/renderElectronMiniChatApp-BGrrmeME.js +0 -2
- package/dist/assets/renderMobileApp-u3c4GtFC.js +0 -28
- package/dist/assets/runtime-fetch-zIalUnbb.js +0 -1
- package/dist/assets/runtime-switch-LvWDU-SL.js +0 -1
- package/dist/assets/runtimeConfig-BIGBRGQI.js +0 -2
- package/dist/assets/runtimeEndpointReset-D2savkuw.js +0 -147
- package/dist/assets/sessionChangeSummary-eIbpSfrO.js +0 -2
- package/dist/assets/src-CLN7Z-zx.css +0 -2
- package/dist/assets/store-DLCiZaA6.js +0 -15
- package/dist/assets/textarea-CakKvqFw.js +0 -1
- package/dist/assets/uk-rwQrxeYM.js +0 -15
- package/dist/assets/useDesktopWindowControlsLayout-DBJ56qIo.js +0 -1
- package/dist/assets/useMobileNavigationStore-ZHJPxlHt.js +0 -414
- package/dist/assets/useSessionAutoCleanup-BIVmrnYU.js +0 -2
- package/dist/assets/useUIStore-CQYKJnrg.js +0 -1
- package/dist/assets/vendor-opencode-client-BOe2qpQb.js +0 -12
- package/dist/assets/zh-CN-BvmBDnkD.js +0 -15
- package/dist/assets/zh-TW-9wcHTX0y.js +0 -15
- package/server/lib/opencode/snippets.js +0 -242
- package/server/lib/opencode/snippets.test.js +0 -97
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Startup upgrade screen for OpenCode below the runtime contract minimum.
|
|
3
|
+
*
|
|
4
|
+
* This module does not own process lifecycle. It classifies the version the
|
|
5
|
+
* existing startup path already detected, and installs through the owned-cache
|
|
6
|
+
* installer (`installPinnedOpenCode2Cli`) plus the caller's restart hook.
|
|
7
|
+
* Success is a verified running version, never a download that has not started.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import {
|
|
11
|
+
PINNED_OPENCODE2_VERSION,
|
|
12
|
+
MINIMUM_OPENCODE2_VERSION,
|
|
13
|
+
isAcceptableOpenCode2HealthVersion,
|
|
14
|
+
isOpenCode1xVersion,
|
|
15
|
+
isOpenCode2VersionAtLeast,
|
|
16
|
+
npmPackageForOpenCode2,
|
|
17
|
+
} from './opencode2-pin.js';
|
|
18
|
+
|
|
19
|
+
/** Oldest OpenCode this OpenChamber build will enter a session against. */
|
|
20
|
+
export const REQUIRED_OPENCODE_VERSION = MINIMUM_OPENCODE2_VERSION;
|
|
21
|
+
|
|
22
|
+
const normalizeVersion = (value) => {
|
|
23
|
+
if (typeof value !== 'string') return null;
|
|
24
|
+
const trimmed = value.trim().replace(/^v/i, '');
|
|
25
|
+
return trimmed || null;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Install target is the pin when the pin already satisfies the screen, otherwise
|
|
30
|
+
* the screen minimum. A pin still at 2.0.12 must not be what this screen installs.
|
|
31
|
+
* @param {string} [pin]
|
|
32
|
+
*/
|
|
33
|
+
export function resolveUpgradeScreenTarget(pin = PINNED_OPENCODE2_VERSION) {
|
|
34
|
+
if (isOpenCode2VersionAtLeast(pin, REQUIRED_OPENCODE_VERSION)) return pin;
|
|
35
|
+
return REQUIRED_OPENCODE_VERSION;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** 2.x at or above the screen minimum. A future major is not this contract. */
|
|
39
|
+
function isUpgradeScreenSatisfied(version) {
|
|
40
|
+
const normalized = normalizeVersion(version);
|
|
41
|
+
if (!normalized || !isAcceptableOpenCode2HealthVersion(normalized)) return false;
|
|
42
|
+
return isOpenCode2VersionAtLeast(normalized, REQUIRED_OPENCODE_VERSION);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function isOlderThanUpgradeScreenMinimum(version) {
|
|
46
|
+
const normalized = normalizeVersion(version);
|
|
47
|
+
if (!normalized) return false;
|
|
48
|
+
if (isOpenCode1xVersion(normalized)) return true;
|
|
49
|
+
if (!isAcceptableOpenCode2HealthVersion(normalized)) return false;
|
|
50
|
+
return !isOpenCode2VersionAtLeast(normalized, REQUIRED_OPENCODE_VERSION);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* @param {string | null | undefined} ownership
|
|
55
|
+
* @returns {'managed' | 'external' | 'bundled'}
|
|
56
|
+
*/
|
|
57
|
+
function classifyUpgradeScreenInstallation(ownership) {
|
|
58
|
+
if (ownership === 'external-serve') return 'external';
|
|
59
|
+
if (ownership === 'bundled') return 'bundled';
|
|
60
|
+
return 'managed';
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function platformSupportsUpgradeScreenInstall(platform = process.platform, arch = process.arch) {
|
|
64
|
+
try {
|
|
65
|
+
npmPackageForOpenCode2(platform, arch);
|
|
66
|
+
return true;
|
|
67
|
+
} catch {
|
|
68
|
+
return false;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* @param {{
|
|
74
|
+
* version?: string | null,
|
|
75
|
+
* installation?: 'managed' | 'external' | 'bundled',
|
|
76
|
+
* platformCanInstall?: boolean,
|
|
77
|
+
* guidance?: string | null,
|
|
78
|
+
* pin?: string,
|
|
79
|
+
* }} input
|
|
80
|
+
*/
|
|
81
|
+
export function describeUpgradeScreen(input = {}) {
|
|
82
|
+
const version = normalizeVersion(input.version);
|
|
83
|
+
const installation = input.installation === 'external' || input.installation === 'bundled'
|
|
84
|
+
? input.installation
|
|
85
|
+
: 'managed';
|
|
86
|
+
const state = version == null
|
|
87
|
+
? 'unavailable'
|
|
88
|
+
: isUpgradeScreenSatisfied(version)
|
|
89
|
+
? 'compatible'
|
|
90
|
+
: 'incompatible';
|
|
91
|
+
const older = version != null && isOlderThanUpgradeScreenMinimum(version);
|
|
92
|
+
// External processes are not replaced. Bundled and managed locals can receive
|
|
93
|
+
// an owned-cache install; that does not overwrite the user's other binaries.
|
|
94
|
+
const canInstall = state === 'incompatible'
|
|
95
|
+
&& older
|
|
96
|
+
&& installation !== 'external'
|
|
97
|
+
&& input.platformCanInstall === true;
|
|
98
|
+
const guidance = canInstall
|
|
99
|
+
? null
|
|
100
|
+
: (typeof input.guidance === 'string' && input.guidance.trim() ? input.guidance.trim() : null);
|
|
101
|
+
return {
|
|
102
|
+
state,
|
|
103
|
+
version,
|
|
104
|
+
installation,
|
|
105
|
+
minimumVersion: REQUIRED_OPENCODE_VERSION,
|
|
106
|
+
targetVersion: resolveUpgradeScreenTarget(input.pin),
|
|
107
|
+
canInstall,
|
|
108
|
+
guidance,
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* A running serve below the minimum, or a missing version, is not an upgrade.
|
|
114
|
+
* @param {{ targetVersion?: string | null, serveVersion?: string | null }} input
|
|
115
|
+
*/
|
|
116
|
+
export function evaluateUpgradeScreenInstallResult(input = {}) {
|
|
117
|
+
const targetVersion = resolveUpgradeScreenTarget(input.targetVersion || REQUIRED_OPENCODE_VERSION);
|
|
118
|
+
const version = normalizeVersion(input.serveVersion);
|
|
119
|
+
if (!version || !isUpgradeScreenSatisfied(version) || !isOpenCode2VersionAtLeast(version, targetVersion)) {
|
|
120
|
+
const found = version || 'unknown';
|
|
121
|
+
return {
|
|
122
|
+
ok: false,
|
|
123
|
+
upgraded: false,
|
|
124
|
+
version,
|
|
125
|
+
targetVersion,
|
|
126
|
+
errorCode: 'UPGRADE_SCREEN_NOT_VERIFIED',
|
|
127
|
+
error: `OpenCode is still ${found}. OpenChamber requires ${REQUIRED_OPENCODE_VERSION} or newer.`,
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
return {
|
|
131
|
+
ok: true,
|
|
132
|
+
upgraded: true,
|
|
133
|
+
version,
|
|
134
|
+
targetVersion,
|
|
135
|
+
error: null,
|
|
136
|
+
errorCode: null,
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
async function readUpgradeScreenStatus(deps) {
|
|
141
|
+
const ownership = typeof deps.resolveOwnership === 'function'
|
|
142
|
+
? await deps.resolveOwnership()
|
|
143
|
+
: { ownership: 'unknown', guidance: null };
|
|
144
|
+
const installation = classifyUpgradeScreenInstallation(ownership?.ownership);
|
|
145
|
+
const serveVersion = typeof deps.readServeVersion === 'function'
|
|
146
|
+
? await Promise.resolve(deps.readServeVersion()).catch(() => null)
|
|
147
|
+
: null;
|
|
148
|
+
const cliVersion = typeof deps.readCliVersion === 'function'
|
|
149
|
+
? await Promise.resolve(deps.readCliVersion()).catch(() => null)
|
|
150
|
+
: null;
|
|
151
|
+
return describeUpgradeScreen({
|
|
152
|
+
version: serveVersion || (typeof deps.readServeVersion === 'function' ? null : cliVersion),
|
|
153
|
+
installation,
|
|
154
|
+
platformCanInstall: typeof deps.platformCanInstall === 'boolean'
|
|
155
|
+
? deps.platformCanInstall
|
|
156
|
+
: platformSupportsUpgradeScreenInstall(deps.platform, deps.arch),
|
|
157
|
+
guidance: ownership?.guidance ?? null,
|
|
158
|
+
pin: deps.pin,
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const failure = (message, code, status = 500) => {
|
|
163
|
+
const error = new Error(message);
|
|
164
|
+
error.code = code;
|
|
165
|
+
error.status = status;
|
|
166
|
+
return error;
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Install the screen target into the owned cache and restart through the
|
|
171
|
+
* caller's existing lifecycle hooks. Restores the previous binary selection
|
|
172
|
+
* when verification fails. Never returns upgraded:true for a failed install.
|
|
173
|
+
* @param {object} deps
|
|
174
|
+
*/
|
|
175
|
+
export async function installRequiredOpenCode(deps) {
|
|
176
|
+
const status = await readUpgradeScreenStatus(deps);
|
|
177
|
+
if (status.state !== 'incompatible' || status.canInstall !== true) {
|
|
178
|
+
throw failure(
|
|
179
|
+
status.guidance || 'Automatic OpenCode installation is unavailable for this runtime.',
|
|
180
|
+
'UPGRADE_SCREEN_UNAVAILABLE',
|
|
181
|
+
409,
|
|
182
|
+
);
|
|
183
|
+
}
|
|
184
|
+
const target = status.targetVersion;
|
|
185
|
+
const previousBinary = typeof deps.getResolvedBinary === 'function' ? deps.getResolvedBinary() : null;
|
|
186
|
+
const previousSource = typeof deps.getResolvedBinarySource === 'function' ? deps.getResolvedBinarySource() : null;
|
|
187
|
+
const previousSetting = typeof deps.readConfiguredBinary === 'function'
|
|
188
|
+
? await deps.readConfiguredBinary()
|
|
189
|
+
: undefined;
|
|
190
|
+
let persisted = false;
|
|
191
|
+
try {
|
|
192
|
+
if (typeof deps.install !== 'function') {
|
|
193
|
+
throw failure('OpenCode install is not configured.', 'UPGRADE_SCREEN_INSTALL_UNAVAILABLE', 500);
|
|
194
|
+
}
|
|
195
|
+
const installedPath = await deps.install({ version: target });
|
|
196
|
+
const diskVersion = typeof deps.readBinaryVersion === 'function'
|
|
197
|
+
? await Promise.resolve(deps.readBinaryVersion(installedPath))
|
|
198
|
+
: null;
|
|
199
|
+
if (diskVersion !== target) {
|
|
200
|
+
throw failure(
|
|
201
|
+
`Installed OpenCode version mismatch: expected ${target}, got ${diskVersion || 'unknown'}`,
|
|
202
|
+
'UPGRADE_SCREEN_BINARY_MISMATCH',
|
|
203
|
+
500,
|
|
204
|
+
);
|
|
205
|
+
}
|
|
206
|
+
if (typeof deps.persistBinary === 'function') {
|
|
207
|
+
await deps.persistBinary(installedPath);
|
|
208
|
+
persisted = true;
|
|
209
|
+
}
|
|
210
|
+
if (typeof deps.forceBinary === 'function') {
|
|
211
|
+
deps.forceBinary(installedPath, 'installed');
|
|
212
|
+
}
|
|
213
|
+
if (typeof deps.restart === 'function') {
|
|
214
|
+
const shared = typeof deps.isSharedService === 'function' && deps.isSharedService() === true;
|
|
215
|
+
await deps.restart(shared ? { binaryPath: installedPath } : undefined);
|
|
216
|
+
}
|
|
217
|
+
if (typeof deps.waitReady === 'function') {
|
|
218
|
+
await deps.waitReady();
|
|
219
|
+
}
|
|
220
|
+
const serveVersion = typeof deps.readServeVersion === 'function'
|
|
221
|
+
? await deps.readServeVersion()
|
|
222
|
+
: null;
|
|
223
|
+
const verification = evaluateUpgradeScreenInstallResult({ targetVersion: target, serveVersion });
|
|
224
|
+
if (!verification.ok) {
|
|
225
|
+
throw failure(verification.error, verification.errorCode, 500);
|
|
226
|
+
}
|
|
227
|
+
return verification;
|
|
228
|
+
} catch (error) {
|
|
229
|
+
if (persisted && typeof deps.persistBinary === 'function') {
|
|
230
|
+
try {
|
|
231
|
+
await deps.persistBinary(typeof previousSetting === 'string' ? previousSetting : '');
|
|
232
|
+
} catch {
|
|
233
|
+
// Keep the install error. A failed restore must not become success.
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
if (previousBinary && typeof deps.forceBinary === 'function') {
|
|
237
|
+
try {
|
|
238
|
+
deps.forceBinary(previousBinary, previousSource || 'discovered');
|
|
239
|
+
} catch {
|
|
240
|
+
// Same: report the install failure, not a restored-success.
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
throw error;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
export function registerUpgradeScreenRoutes(app, deps) {
|
|
248
|
+
app.get('/api/opencode/compatibility', async (_req, res) => {
|
|
249
|
+
try {
|
|
250
|
+
res.json(await readUpgradeScreenStatus(deps));
|
|
251
|
+
} catch (error) {
|
|
252
|
+
res.status(503).json({
|
|
253
|
+
error: error instanceof Error ? error.message : 'Could not check OpenCode compatibility',
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
let inFlight = null;
|
|
259
|
+
app.post('/api/opencode/install-required', async (_req, res) => {
|
|
260
|
+
try {
|
|
261
|
+
if (!inFlight) {
|
|
262
|
+
inFlight = installRequiredOpenCode(deps).finally(() => {
|
|
263
|
+
inFlight = null;
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
const result = await inFlight;
|
|
267
|
+
if (!result || result.ok !== true || result.upgraded !== true || !result.version) {
|
|
268
|
+
return res.status(500).json({
|
|
269
|
+
success: false,
|
|
270
|
+
upgraded: false,
|
|
271
|
+
error: result?.error || 'OpenCode installation did not verify a new version',
|
|
272
|
+
errorCode: result?.errorCode || 'UPGRADE_SCREEN_NOT_VERIFIED',
|
|
273
|
+
version: result?.version ?? null,
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
return res.json({
|
|
277
|
+
success: true,
|
|
278
|
+
upgraded: true,
|
|
279
|
+
version: result.version,
|
|
280
|
+
targetVersion: result.targetVersion,
|
|
281
|
+
});
|
|
282
|
+
} catch (error) {
|
|
283
|
+
const status = Number.isInteger(error?.status) ? error.status : 500;
|
|
284
|
+
return res.status(status).json({
|
|
285
|
+
success: false,
|
|
286
|
+
upgraded: false,
|
|
287
|
+
error: error instanceof Error ? error.message : 'OpenCode installation failed',
|
|
288
|
+
errorCode: error?.code || 'UPGRADE_SCREEN_FAILED',
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
});
|
|
292
|
+
}
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import express from 'express';
|
|
2
|
+
import request from 'supertest';
|
|
3
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
4
|
+
import {
|
|
5
|
+
REQUIRED_OPENCODE_VERSION,
|
|
6
|
+
describeUpgradeScreen,
|
|
7
|
+
evaluateUpgradeScreenInstallResult,
|
|
8
|
+
installRequiredOpenCode,
|
|
9
|
+
registerUpgradeScreenRoutes,
|
|
10
|
+
resolveUpgradeScreenTarget,
|
|
11
|
+
} from './upgrade-screen.js';
|
|
12
|
+
|
|
13
|
+
describe('upgrade screen version gate', () => {
|
|
14
|
+
it('uses the floor even when the runtime pin is older', () => {
|
|
15
|
+
expect(REQUIRED_OPENCODE_VERSION).toBe('2.0.20');
|
|
16
|
+
expect(resolveUpgradeScreenTarget('2.0.12')).toBe('2.0.20');
|
|
17
|
+
expect(resolveUpgradeScreenTarget('2.0.23')).toBe('2.0.23');
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it('does not open the gate at or above 2.0.20', () => {
|
|
21
|
+
expect(describeUpgradeScreen({
|
|
22
|
+
version: '2.0.20',
|
|
23
|
+
installation: 'managed',
|
|
24
|
+
platformCanInstall: true,
|
|
25
|
+
})).toMatchObject({ state: 'compatible', canInstall: false, minimumVersion: '2.0.20' });
|
|
26
|
+
expect(describeUpgradeScreen({
|
|
27
|
+
version: 'v2.0.23',
|
|
28
|
+
installation: 'managed',
|
|
29
|
+
platformCanInstall: true,
|
|
30
|
+
}).state).toBe('compatible');
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('opens the gate below 2.0.20 and offers install only when this runtime can switch', () => {
|
|
34
|
+
expect(describeUpgradeScreen({
|
|
35
|
+
version: '2.0.14',
|
|
36
|
+
installation: 'managed',
|
|
37
|
+
platformCanInstall: true,
|
|
38
|
+
})).toMatchObject({ state: 'incompatible', canInstall: true, version: '2.0.14' });
|
|
39
|
+
expect(describeUpgradeScreen({
|
|
40
|
+
version: '1.18.30',
|
|
41
|
+
installation: 'managed',
|
|
42
|
+
platformCanInstall: true,
|
|
43
|
+
})).toMatchObject({ state: 'incompatible', canInstall: true });
|
|
44
|
+
expect(describeUpgradeScreen({
|
|
45
|
+
version: '2.0.14',
|
|
46
|
+
installation: 'external',
|
|
47
|
+
platformCanInstall: true,
|
|
48
|
+
}).canInstall).toBe(false);
|
|
49
|
+
expect(describeUpgradeScreen({
|
|
50
|
+
version: '2.0.14',
|
|
51
|
+
installation: 'bundled',
|
|
52
|
+
platformCanInstall: true,
|
|
53
|
+
})).toMatchObject({ state: 'incompatible', canInstall: true, installation: 'bundled' });
|
|
54
|
+
expect(describeUpgradeScreen({
|
|
55
|
+
version: '2.0.14',
|
|
56
|
+
installation: 'managed',
|
|
57
|
+
platformCanInstall: false,
|
|
58
|
+
}).canInstall).toBe(false);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('does not treat a missing version as an old install', () => {
|
|
62
|
+
expect(describeUpgradeScreen({
|
|
63
|
+
version: null,
|
|
64
|
+
installation: 'managed',
|
|
65
|
+
platformCanInstall: true,
|
|
66
|
+
}).state).toBe('unavailable');
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('does not report an unverified install as upgraded', () => {
|
|
70
|
+
expect(evaluateUpgradeScreenInstallResult({
|
|
71
|
+
targetVersion: '2.0.23',
|
|
72
|
+
serveVersion: '2.0.14',
|
|
73
|
+
})).toMatchObject({ ok: false, upgraded: false, version: '2.0.14' });
|
|
74
|
+
expect(evaluateUpgradeScreenInstallResult({
|
|
75
|
+
targetVersion: '2.0.23',
|
|
76
|
+
serveVersion: null,
|
|
77
|
+
}).upgraded).toBe(false);
|
|
78
|
+
expect(evaluateUpgradeScreenInstallResult({
|
|
79
|
+
targetVersion: '2.0.23',
|
|
80
|
+
serveVersion: '2.0.23',
|
|
81
|
+
})).toMatchObject({ ok: true, upgraded: true, version: '2.0.23' });
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
describe('installRequiredOpenCode', () => {
|
|
86
|
+
const createDeps = (overrides = {}) => {
|
|
87
|
+
const calls = [];
|
|
88
|
+
const deps = {
|
|
89
|
+
resolveOwnership: async () => ({ ownership: 'global-cli', guidance: null }),
|
|
90
|
+
readServeVersion: async () => '2.0.14',
|
|
91
|
+
readCliVersion: () => '2.0.14',
|
|
92
|
+
platformCanInstall: true,
|
|
93
|
+
install: vi.fn(async () => {
|
|
94
|
+
calls.push('install');
|
|
95
|
+
return '/cache/opencode';
|
|
96
|
+
}),
|
|
97
|
+
readBinaryVersion: vi.fn(() => '2.0.23'),
|
|
98
|
+
persistBinary: vi.fn(async () => {
|
|
99
|
+
calls.push('persist');
|
|
100
|
+
}),
|
|
101
|
+
forceBinary: vi.fn(() => {
|
|
102
|
+
calls.push('force');
|
|
103
|
+
}),
|
|
104
|
+
restart: vi.fn(async () => {
|
|
105
|
+
calls.push('restart');
|
|
106
|
+
}),
|
|
107
|
+
waitReady: vi.fn(async () => {
|
|
108
|
+
calls.push('ready');
|
|
109
|
+
}),
|
|
110
|
+
readConfiguredBinary: async () => null,
|
|
111
|
+
isSharedService: () => false,
|
|
112
|
+
...overrides,
|
|
113
|
+
};
|
|
114
|
+
return { deps, calls };
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
it('installs 2.0.23 and only succeeds after the running serve is verified', async () => {
|
|
118
|
+
const { deps, calls } = createDeps({
|
|
119
|
+
readServeVersion: vi.fn()
|
|
120
|
+
.mockResolvedValueOnce('2.0.14')
|
|
121
|
+
.mockResolvedValueOnce('2.0.23'),
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
await expect(installRequiredOpenCode(deps)).resolves.toMatchObject({
|
|
125
|
+
ok: true,
|
|
126
|
+
upgraded: true,
|
|
127
|
+
version: '2.0.23',
|
|
128
|
+
targetVersion: '2.0.23',
|
|
129
|
+
});
|
|
130
|
+
expect(deps.install).toHaveBeenCalledWith({ version: '2.0.23' });
|
|
131
|
+
expect(calls).toEqual(['install', 'persist', 'force', 'restart', 'ready']);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it('returns the failure reason and does not claim an upgrade when verification fails', async () => {
|
|
135
|
+
const { deps } = createDeps({
|
|
136
|
+
readServeVersion: vi.fn()
|
|
137
|
+
.mockResolvedValueOnce('1.18.30')
|
|
138
|
+
.mockResolvedValueOnce('1.18.30'),
|
|
139
|
+
readCliVersion: () => '1.18.30',
|
|
140
|
+
readBinaryVersion: () => '2.0.23',
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
await expect(installRequiredOpenCode(deps)).rejects.toThrow(/still 1\.18\.30/);
|
|
144
|
+
expect(deps.persistBinary).toHaveBeenCalledWith('');
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it('does not install when the runtime cannot switch', async () => {
|
|
148
|
+
const { deps } = createDeps({
|
|
149
|
+
resolveOwnership: async () => ({
|
|
150
|
+
ownership: 'external-serve',
|
|
151
|
+
guidance: 'Restart that server yourself.',
|
|
152
|
+
}),
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
await expect(installRequiredOpenCode(deps)).rejects.toMatchObject({
|
|
156
|
+
status: 409,
|
|
157
|
+
message: 'Restart that server yourself.',
|
|
158
|
+
});
|
|
159
|
+
expect(deps.install).not.toHaveBeenCalled();
|
|
160
|
+
});
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
describe('upgrade screen routes', () => {
|
|
164
|
+
it('hides the gate for 2.0.20 and reports install failure without upgraded:true', async () => {
|
|
165
|
+
const app = express();
|
|
166
|
+
app.use(express.json());
|
|
167
|
+
const readServeVersion = vi.fn()
|
|
168
|
+
.mockResolvedValueOnce('2.0.20')
|
|
169
|
+
.mockResolvedValueOnce('2.0.12')
|
|
170
|
+
.mockResolvedValueOnce('2.0.12');
|
|
171
|
+
registerUpgradeScreenRoutes(app, {
|
|
172
|
+
resolveOwnership: async () => ({ ownership: 'owned-cache', guidance: null }),
|
|
173
|
+
readServeVersion,
|
|
174
|
+
readCliVersion: () => '2.0.12',
|
|
175
|
+
platformCanInstall: true,
|
|
176
|
+
install: vi.fn(async () => {
|
|
177
|
+
throw new Error('registry unreachable');
|
|
178
|
+
}),
|
|
179
|
+
readBinaryVersion: () => '2.0.23',
|
|
180
|
+
persistBinary: vi.fn(),
|
|
181
|
+
forceBinary: vi.fn(),
|
|
182
|
+
restart: vi.fn(),
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
const compatible = await request(app).get('/api/opencode/compatibility').expect(200);
|
|
186
|
+
expect(compatible.body).toMatchObject({ state: 'compatible', version: '2.0.20', minimumVersion: '2.0.20' });
|
|
187
|
+
|
|
188
|
+
const failed = await request(app).post('/api/opencode/install-required').expect(500);
|
|
189
|
+
expect(failed.body).toMatchObject({
|
|
190
|
+
success: false,
|
|
191
|
+
upgraded: false,
|
|
192
|
+
error: 'registry unreachable',
|
|
193
|
+
});
|
|
194
|
+
expect(failed.body.upgraded).not.toBe(true);
|
|
195
|
+
});
|
|
196
|
+
});
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import { MINIMUM_OPENCODE2_VERSION, PINNED_OPENCODE2_VERSION } from './opencode2-pin.js';
|
|
3
|
+
import { evaluateRuntimeContract } from './runtime-contract.js';
|
|
4
|
+
import { describeUpgradeScreen, installRequiredOpenCode, resolveUpgradeScreenTarget } from './upgrade-screen.js';
|
|
5
|
+
import { ensurePinnedOpenCode2Cli } from './ensure-cli.js';
|
|
6
|
+
|
|
7
|
+
describe('v2 version alignment', () => {
|
|
8
|
+
it.each(['1.18.30', '2.0.19', null, 'unknown'])('blocks execution and startup for %s', (version) => {
|
|
9
|
+
expect(evaluateRuntimeContract({ serveVersion: version, cliVersion: '2.0.23', reachable: true, authenticated: true }).executionAllowed).toBe(false);
|
|
10
|
+
expect(describeUpgradeScreen({ version }).state).not.toBe('compatible');
|
|
11
|
+
});
|
|
12
|
+
it.each(['2.0.20', '2.0.23', '2.1.0'])('admits running %s independently of CLI pin', (version) => {
|
|
13
|
+
expect(evaluateRuntimeContract({ serveVersion: version, cliVersion: '2.0.15', reachable: true, authenticated: true })).toMatchObject({ executionAllowed: true, versionMismatch: true, minVerifiedVersion: '2.0.20' });
|
|
14
|
+
expect(describeUpgradeScreen({ version }).state).toBe('compatible');
|
|
15
|
+
});
|
|
16
|
+
it('installs the audited pin on first install and preserves newer discovered CLIs', async () => {
|
|
17
|
+
expect(MINIMUM_OPENCODE2_VERSION).toBe('2.0.20');
|
|
18
|
+
expect(PINNED_OPENCODE2_VERSION).toBe('2.0.23');
|
|
19
|
+
expect(resolveUpgradeScreenTarget()).toBe('2.0.23');
|
|
20
|
+
const install = vi.fn(async () => '/owned/opencode');
|
|
21
|
+
await expect(ensurePinnedOpenCode2Cli({ readVersion: (path) => path === '/owned/opencode' ? '2.0.23' : '', install })).resolves.toMatchObject({ version: '2.0.23', installed: true });
|
|
22
|
+
expect(install).toHaveBeenCalledWith(expect.objectContaining({ version: '2.0.23' }));
|
|
23
|
+
install.mockClear();
|
|
24
|
+
await expect(ensurePinnedOpenCode2Cli({ discoveredPath: '/global/opencode', readVersion: () => '2.1.0', install })).resolves.toMatchObject({ version: '2.1.0', installed: false });
|
|
25
|
+
expect(install).not.toHaveBeenCalled();
|
|
26
|
+
});
|
|
27
|
+
it('does not infer a successful installation from an unreadable binary', async () => {
|
|
28
|
+
await expect(ensurePinnedOpenCode2Cli({ readVersion: () => '', install: async () => '/owned/opencode' })).rejects.toThrow('got unknown');
|
|
29
|
+
});
|
|
30
|
+
it('verifies the live service after explicit install, and rolls selection back on failure', async () => {
|
|
31
|
+
const persistBinary = vi.fn();
|
|
32
|
+
const restart = vi.fn();
|
|
33
|
+
const deps = { resolveOwnership: async () => ({ ownership: 'shared-service' }), readServeVersion: vi.fn().mockResolvedValueOnce('2.0.19').mockResolvedValueOnce('2.0.23'), readCliVersion: () => '2.0.23', platformCanInstall: true, install: async () => '/owned/opencode', readBinaryVersion: () => '2.0.23', persistBinary, restart, isSharedService: () => true };
|
|
34
|
+
await expect(installRequiredOpenCode(deps)).resolves.toMatchObject({ upgraded: true, version: '2.0.23' });
|
|
35
|
+
expect(restart).toHaveBeenCalledWith({ binaryPath: '/owned/opencode' });
|
|
36
|
+
deps.readServeVersion.mockResolvedValue('2.0.19');
|
|
37
|
+
await expect(installRequiredOpenCode(deps)).rejects.toMatchObject({ code: 'UPGRADE_SCREEN_NOT_VERIFIED' });
|
|
38
|
+
expect(persistBinary).toHaveBeenLastCalledWith('');
|
|
39
|
+
});
|
|
40
|
+
});
|
|
@@ -2,12 +2,15 @@
|
|
|
2
2
|
|
|
3
3
|
## Purpose
|
|
4
4
|
|
|
5
|
-
Server-owned automatic handling of OpenCode
|
|
5
|
+
Server-owned automatic handling of OpenCode 2 question-tool forms (`form.created`,
|
|
6
|
+
`data.form.metadata.kind === "question"`). When
|
|
6
7
|
enabled (default), pending questions are answered after 30 seconds with a fixed
|
|
7
8
|
continuation text so agents (including background/subagent sessions) can proceed
|
|
8
9
|
without a connected UI. Manual reply/reject still use the official OpenCode
|
|
9
|
-
|
|
10
|
-
|
|
10
|
+
form schema. The Host adds a single-flight claim for timer, delegate, and
|
|
11
|
+
compatibility question routes. Native manual form replies settle via
|
|
12
|
+
`form.replied` / `form.cancelled`; upstream form settlement arbitrates any race
|
|
13
|
+
with a native reply already in flight.
|
|
11
14
|
|
|
12
15
|
Permission auto-accept is intentionally out of scope — this module never
|
|
13
16
|
replies to permission requests.
|
|
@@ -42,6 +45,7 @@ Auto-submit requires **settings ready + enabled + authoritative directory**.
|
|
|
42
45
|
|---|---|
|
|
43
46
|
| `core.js` / `core.d.ts` | Platform-agnostic state machine (IO/timer injected). Shared contract for VS Code. |
|
|
44
47
|
| `runtime.js` | Web adapter: OpenCode upstream fetch, global event hub, settings, SSE tips, project + session-index directories. |
|
|
48
|
+
| `form-io.js` / `form-io.d.ts` | Shared Web/VS Code native form list, schema lookup, answer mapping and cancellation. |
|
|
45
49
|
| `routes.js` | OpenChamber routes + precise `/api/question/:id/reply\|reject` intercepts. |
|
|
46
50
|
|
|
47
51
|
## Snapshot
|
|
@@ -62,21 +66,31 @@ Tip event (not a data payload):
|
|
|
62
66
|
| `POST` | `.../requests/:id/pause` | `{ sessionID, directory, reason: 'interaction'\|'user' }` — bound identity conflicts → 409. Directory may be empty; pause still cancels the auto timer. |
|
|
63
67
|
| `POST` | `.../requests/:id/delegate` | recovers identity via scoped list; allowed when toggle off |
|
|
64
68
|
| `POST` | `/api/question/:id/reply` | manual: `answers` must be an array (incl. `[]`); never auto text |
|
|
65
|
-
| `POST` | `/api/question/:id/reject` |
|
|
69
|
+
| `POST` | `/api/question/:id/reject` | Compatibility route mapped to native form cancellation |
|
|
66
70
|
|
|
67
71
|
Concurrent claim losers: HTTP **409** + `code: "question_submission_claimed"`.
|
|
68
72
|
|
|
69
73
|
## Claim / upstream rules
|
|
70
74
|
|
|
75
|
+
OpenCode 2.0.15 upstream uses `GET /api/form?location[directory]=...` for
|
|
76
|
+
pending forms and `GET /api/session/:sessionID/form/:formID` before submitting
|
|
77
|
+
`POST .../reply` with `{ answer }`. Only question-tool forms are eligible.
|
|
78
|
+
The fresh schema owns field keys, option values and string/multiselect shape.
|
|
79
|
+
Failed schema reads are `notSent` and pause for retry. Cancellation uses
|
|
80
|
+
`POST .../cancel`. Session directory recovery reads `info.location.directory`;
|
|
81
|
+
live events read `payload.location.directory` and `payload.data`.
|
|
82
|
+
Web, Electron and mobile hosts use the Web adapter; VS Code imports the same
|
|
83
|
+
form IO and core, restoring `/api` for origin-only upstream URLs.
|
|
84
|
+
|
|
71
85
|
1. One authority owns at most one upstream POST per `requestID` (sync claim).
|
|
72
86
|
2. **Definitive reject** (400/409/422) or pre-send throw → **release claim**, pause for human retry. **No immediate auto retry.**
|
|
73
87
|
3. **Unclear post-dispatch result** (timeout/5xx/`uncertain`) → `uncertain` tombstone; no retry; still **blocks** goal.
|
|
74
88
|
4. **Settled** must not be downgraded by a late POST failure.
|
|
75
89
|
5. Successful **scoped** empty list may converge `uncertain`/pending → `external` settled **only for that directory**.
|
|
76
|
-
6.
|
|
90
|
+
6. Compatibility manual reply passes `answers` (including `[]`) to the form adapter, which maps them by authoritative fields. Fixed text is auto/delegate only.
|
|
77
91
|
7. Client `sessionID`/`directory` hints never override a bound identity (conflict → 409).
|
|
78
92
|
8. Auto-submit requires an authoritative directory binding (from event, session cache, or `getSession` write-back). Missing directory → no timer.
|
|
79
|
-
9. Unknown pause/delegate recovers via scoped `
|
|
93
|
+
9. Unknown pause/delegate recovers via scoped `form.list`, filtered to question-tool forms — never invents `questionCount=1`.
|
|
80
94
|
|
|
81
95
|
## Reconcile
|
|
82
96
|
|
|
@@ -119,6 +133,7 @@ Concurrent claim losers: HTTP **409** + `code: "question_submission_claimed"`.
|
|
|
119
133
|
```sh
|
|
120
134
|
bunx vitest run --project @openchamber/web \
|
|
121
135
|
packages/web/server/lib/question-auto-delegate/core.test.js \
|
|
136
|
+
packages/web/server/lib/question-auto-delegate/runtime.test.js \
|
|
122
137
|
packages/web/server/lib/question-auto-delegate/routes.test.js \
|
|
123
138
|
packages/web/server/lib/session-goal/runtime.test.js
|
|
124
139
|
```
|
|
@@ -114,6 +114,8 @@ export interface QuestionAutoDelegatePendingQuestion {
|
|
|
114
114
|
sessionID: string;
|
|
115
115
|
directory?: string | null;
|
|
116
116
|
questions?: unknown[] | null;
|
|
117
|
+
fields?: unknown[];
|
|
118
|
+
metadata?: { kind?: unknown };
|
|
117
119
|
}
|
|
118
120
|
|
|
119
121
|
export interface QuestionAutoDelegateSessionInfo {
|
|
@@ -167,11 +169,13 @@ export interface QuestionAutoDelegateIO {
|
|
|
167
169
|
requestID: string,
|
|
168
170
|
directory: string | undefined,
|
|
169
171
|
answers: string[][],
|
|
172
|
+
sessionID: string,
|
|
170
173
|
): Promise<QuestionAutoDelegateUpstreamResult>;
|
|
171
174
|
postReject(
|
|
172
175
|
requestID: string,
|
|
173
176
|
directory: string | undefined,
|
|
174
|
-
body
|
|
177
|
+
body: unknown,
|
|
178
|
+
sessionID: string,
|
|
175
179
|
): Promise<QuestionAutoDelegateUpstreamResult>;
|
|
176
180
|
/** Project directories used for reconnect/start reconcile. */
|
|
177
181
|
listDirectories(): Promise<string[]>;
|