@openchambery/web 2.0.0-beta.3 → 2.0.0-beta.30
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-pBywAsqy.js +3 -0
- package/dist/assets/{DiagramView-BNnda8qO.js → DiagramView-B984aE-R.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-DHiNU8n3.js +2 -0
- package/dist/assets/{MarkdownRendererImpl-DwrHFGUp.js → MarkdownRendererImpl-xJHMGHju.js} +3 -3
- package/dist/assets/MarkstreamRendererImpl-BZflns6x.js +3 -0
- package/dist/assets/{MobileDetailNavigation-BXr6AGLT.js → MobileDetailNavigation-DUvxsvrB.js} +1 -1
- package/dist/assets/{MobileShareBridge-D7OE9OP_.js → MobileShareBridge-DWo-J3Vc.js} +3 -3
- package/dist/assets/MobileSurface-B-Y6U5xm.js +2 -0
- package/dist/assets/{MultiRunWindow-DF3iIFCd.js → MultiRunWindow-rYiVSNKK.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-pjD0LrHE.js +25 -0
- package/dist/assets/{SettingsWindow-BI5swspb.js → SettingsWindow-CFOQRwjA.js} +1 -1
- package/dist/assets/TerminalView-Dw5jaYkc.js +8 -0
- package/dist/assets/{ToolOutputDialog-BEWhBcRd.js → ToolOutputDialog-B4v-ona2.js} +1 -1
- package/dist/assets/appThemeRegistry-BqDCGocy.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-Cgkxwzqb.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-Dwqkc2nZ.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-BhWfAOPY.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-CGMinhgQ.js +169 -0
- package/dist/assets/main-pMpcLJha.js +2 -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-DfrW3VHF.js} +1 -1
- package/dist/assets/miniChat-YOt30ECc.js +2 -0
- package/dist/assets/mobile-BK1bBCYD.js +2 -0
- package/dist/assets/multirun-Dua24A69.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-DXdoPmbz.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-BOuEz_oT.js +2 -0
- package/dist/assets/renderMobileApp-C8Alpv-B.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-DX_NP2Zm.js +2 -0
- package/dist/assets/runtimeEndpointReset-BSQU3e5U.js +148 -0
- package/dist/assets/runtimeEndpointReset-DsuP5FPT.css +1 -0
- package/dist/assets/sessionChangeSummary-8AIBkExf.js +2 -0
- package/dist/assets/{sessionLookup-myHe5ltu.js → sessionLookup-Bdwd1Fit.js} +1 -1
- package/dist/assets/{sortable-tabs-strip-BQtmHx9r.js → sortable-tabs-strip-T-dk_L69.js} +1 -1
- package/dist/assets/src-OzbnJsph.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-bafOEpgK.js} +57 -52
- package/dist/assets/{useAssistantUIStore-asNG6K7n.js → useAssistantUIStore-DBwfg9nX.js} +1 -1
- package/dist/assets/useDesktopWindowControlsLayout-CkCVOkxa.js +1 -0
- package/dist/assets/{useEffectiveDirectory-B4LvR8Gt.js → useEffectiveDirectory-B7htpJfU.js} +1 -1
- package/dist/assets/{useFontPreferences-CUnQ4TVm.js → useFontPreferences-CMrbTA0r.js} +1 -1
- package/dist/assets/useMobileNavigationStore-D2tnhKYV.js +415 -0
- package/dist/assets/{useRuntimeAPIs-DwLtO0c_.js → useRuntimeAPIs-DSLXJybW.js} +1 -1
- package/dist/assets/useSessionAutoCleanup-BC5OOH3M.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-BNQUAMEB.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,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Explicit worktree deletion only. An eviction acknowledgement detaches cached
|
|
3
|
+
* services; it does not prove that borrowers have released every OS handle.
|
|
4
|
+
* Never use this operation for automatic navigation cleanup on a shared serve.
|
|
5
|
+
*
|
|
6
|
+
* @param {{ getClient: () => import('@opencode/client').OpenCodeClient | null, directory: string, timeoutMs?: number }} input
|
|
7
|
+
* @returns {Promise<{ state: 'released' | 'timeout' | 'failed' | 'unavailable' }>}
|
|
8
|
+
*/
|
|
9
|
+
export async function releaseDeletedWorktreeLocation({ getClient, directory, timeoutMs = 2000 }) {
|
|
10
|
+
const controller = new AbortController();
|
|
11
|
+
let timer;
|
|
12
|
+
try {
|
|
13
|
+
if (!directory || !directory.trim()) return { state: 'unavailable' };
|
|
14
|
+
const client = getClient();
|
|
15
|
+
if (!client) return { state: 'unavailable' };
|
|
16
|
+
const deadline = new Promise((resolve) => {
|
|
17
|
+
timer = setTimeout(() => {
|
|
18
|
+
controller.abort();
|
|
19
|
+
resolve({ state: 'timeout' });
|
|
20
|
+
}, timeoutMs);
|
|
21
|
+
});
|
|
22
|
+
const request = Promise.resolve().then(() => client.debug.location.evict(
|
|
23
|
+
{ location: { directory } },
|
|
24
|
+
{ signal: controller.signal },
|
|
25
|
+
)).then(() => ({ state: 'released' }), () => ({ state: controller.signal.aborted ? 'timeout' : 'failed' }));
|
|
26
|
+
return await Promise.race([request, deadline]);
|
|
27
|
+
} catch {
|
|
28
|
+
return { state: 'failed' };
|
|
29
|
+
} finally {
|
|
30
|
+
clearTimeout(timer);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import { OpenCode } from '@opencode/client';
|
|
3
|
+
import { releaseDeletedWorktreeLocation } from './location-release.js';
|
|
4
|
+
|
|
5
|
+
afterEach(() => vi.useRealTimers());
|
|
6
|
+
|
|
7
|
+
describe('explicit worktree location release', () => {
|
|
8
|
+
it('uses the official SDK exact target, current origin and auth', async () => {
|
|
9
|
+
const requests = [];
|
|
10
|
+
let origin = 'https://old.invalid';
|
|
11
|
+
const getClient = () => OpenCode.make({ baseUrl: origin, headers: { Authorization: 'test-auth' }, fetch: async (input, init) => {
|
|
12
|
+
requests.push(new Request(input, init));
|
|
13
|
+
return new Response(null, { status: 204 });
|
|
14
|
+
} });
|
|
15
|
+
origin = 'https://current.invalid';
|
|
16
|
+
expect(await releaseDeletedWorktreeLocation({ getClient, directory: '/repo/work tree' })).toEqual({ state: 'released' });
|
|
17
|
+
expect(requests).toHaveLength(1);
|
|
18
|
+
const url = new URL(requests[0].url);
|
|
19
|
+
expect(url.origin + url.pathname).toBe('https://current.invalid/api/debug/location');
|
|
20
|
+
expect(url.searchParams.get('location[directory]')).toBe('/repo/work tree');
|
|
21
|
+
expect(requests[0].method).toBe('DELETE');
|
|
22
|
+
expect(requests[0].headers.get('Authorization')).toBe('test-auth');
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('reports failure without claiming successful disposal', async () => {
|
|
26
|
+
const getClient = () => ({ debug: { location: { evict: async () => { throw new Error('unavailable'); } } } });
|
|
27
|
+
expect(await releaseDeletedWorktreeLocation({ getClient, directory: '/repo' })).toEqual({ state: 'failed' });
|
|
28
|
+
expect(await releaseDeletedWorktreeLocation({ getClient: () => null, directory: '/repo' })).toEqual({ state: 'unavailable' });
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it('bounds even a transport that ignores abort and never settles', async () => {
|
|
32
|
+
vi.useFakeTimers();
|
|
33
|
+
let signal;
|
|
34
|
+
const getClient = () => ({ debug: { location: { evict: (_input, options) => {
|
|
35
|
+
signal = options.signal;
|
|
36
|
+
return new Promise(() => {});
|
|
37
|
+
} } } });
|
|
38
|
+
const result = releaseDeletedWorktreeLocation({ getClient, directory: '/repo', timeoutMs: 50 });
|
|
39
|
+
await vi.advanceTimersByTimeAsync(50);
|
|
40
|
+
expect(await result).toEqual({ state: 'timeout' });
|
|
41
|
+
expect(signal.aborted).toBe(true);
|
|
42
|
+
});
|
|
43
|
+
});
|
|
@@ -29,7 +29,7 @@ function validateMcpName(name) {
|
|
|
29
29
|
*/
|
|
30
30
|
function resolveMcpScopeFromPath(layers, sourcePath) {
|
|
31
31
|
if (!sourcePath) return null;
|
|
32
|
-
return
|
|
32
|
+
return layers.projectLayers.some((layer) => layer.path === sourcePath) ? AGENT_SCOPE.PROJECT : AGENT_SCOPE.USER;
|
|
33
33
|
}
|
|
34
34
|
|
|
35
35
|
function ensureProjectMcpConfigPath(workingDirectory) {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// Pinned desktop/runtime opencode2. Upgrade and prepare must not fall back to 1.18.x.
|
|
2
|
-
export const PINNED_OPENCODE2_VERSION = '2.0.
|
|
2
|
+
export const PINNED_OPENCODE2_VERSION = '2.0.23';
|
|
3
|
+
export const MINIMUM_OPENCODE2_VERSION = '2.0.20';
|
|
3
4
|
|
|
4
5
|
// Global npm/bun package that installs the opencode v2 binary (not 1.x opencode-ai).
|
|
5
6
|
export const OPENCODE2_NPM_PACKAGE = '@opencode/cli';
|
|
@@ -86,15 +86,15 @@ describe('owned runtime upgrade (ticket 12)', () => {
|
|
|
86
86
|
|
|
87
87
|
it('verifies running serve identity before upgrade success', () => {
|
|
88
88
|
const contractOk = evaluateRuntimeContract({
|
|
89
|
-
serveVersion: '2.0.
|
|
89
|
+
serveVersion: '2.0.23',
|
|
90
90
|
reachable: true,
|
|
91
91
|
authenticated: true,
|
|
92
92
|
healthOk: true,
|
|
93
93
|
migrationAdmitTranscript: true,
|
|
94
94
|
});
|
|
95
95
|
expect(evaluateOwnedUpgradeResult({
|
|
96
|
-
targetVersion: '2.0.
|
|
97
|
-
serveVersion: '2.0.
|
|
96
|
+
targetVersion: '2.0.23',
|
|
97
|
+
serveVersion: '2.0.23',
|
|
98
98
|
binaryPath: ownedBinary,
|
|
99
99
|
expectedBinaryPath: ownedBinary,
|
|
100
100
|
contract: contractOk,
|
|
@@ -116,8 +116,8 @@ describe('owned runtime upgrade (ticket 12)', () => {
|
|
|
116
116
|
});
|
|
117
117
|
|
|
118
118
|
expect(evaluateOwnedUpgradeResult({
|
|
119
|
-
targetVersion: '2.0.
|
|
120
|
-
serveVersion: '2.0.
|
|
119
|
+
targetVersion: '2.0.23',
|
|
120
|
+
serveVersion: '2.0.23',
|
|
121
121
|
binaryPath: '/usr/bin/opencode',
|
|
122
122
|
expectedBinaryPath: ownedBinary,
|
|
123
123
|
contract: contractOk,
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, mock, test } from 'bun:test';
|
|
2
|
+
import { vi } from 'vitest';
|
|
2
3
|
import express from 'express';
|
|
3
4
|
import fs from 'fs';
|
|
4
5
|
import os from 'os';
|
|
@@ -67,7 +68,8 @@ describe('opencode plugin routes', () => {
|
|
|
67
68
|
beforeAll(async () => {
|
|
68
69
|
rootDir = fs.mkdtempSync(path.join(os.tmpdir(), 'openchamber-plugin-routes-'));
|
|
69
70
|
userConfigPath = path.join(rootDir, 'user-opencode.json');
|
|
70
|
-
|
|
71
|
+
vi.stubEnv('OPENCODE_CONFIG', userConfigPath);
|
|
72
|
+
vi.stubEnv('OPENCODE_CONFIG_DIR', path.join(rootDir, 'global-config'));
|
|
71
73
|
plugins = await import('./plugins.js');
|
|
72
74
|
});
|
|
73
75
|
|
|
@@ -91,7 +93,7 @@ describe('opencode plugin routes', () => {
|
|
|
91
93
|
|
|
92
94
|
afterAll(() => {
|
|
93
95
|
fs.rmSync(rootDir, { recursive: true, force: true });
|
|
94
|
-
|
|
96
|
+
vi.unstubAllEnvs();
|
|
95
97
|
});
|
|
96
98
|
|
|
97
99
|
test('GET /api/config/plugins empty returns entries and files arrays', async () => {
|
|
@@ -4,6 +4,8 @@ import path from 'path';
|
|
|
4
4
|
import {
|
|
5
5
|
AGENT_SCOPE,
|
|
6
6
|
readConfigFile,
|
|
7
|
+
readConfigLayers,
|
|
8
|
+
getConfigForPath,
|
|
7
9
|
writeConfig,
|
|
8
10
|
} from './shared.js';
|
|
9
11
|
import { isPathSpec } from './plugin-spec.js';
|
|
@@ -74,54 +76,8 @@ function getActiveOpencodeConfigDir() {
|
|
|
74
76
|
return path.join(os.homedir(), '.config', 'opencode');
|
|
75
77
|
}
|
|
76
78
|
|
|
77
|
-
function getActiveUserConfigPaths() {
|
|
78
|
-
const configDir = getActiveOpencodeConfigDir();
|
|
79
|
-
return [
|
|
80
|
-
path.join(configDir, 'config.json'),
|
|
81
|
-
path.join(configDir, 'opencode.json'),
|
|
82
|
-
path.join(configDir, 'opencode.jsonc'),
|
|
83
|
-
];
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
function getActiveCustomConfigPath() {
|
|
87
|
-
return process.env.OPENCODE_CONFIG ? path.resolve(process.env.OPENCODE_CONFIG) : null;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
function getPrimaryUserConfigPath() {
|
|
91
|
-
const [defaultPath, ...fallbackPaths] = getActiveUserConfigPaths();
|
|
92
|
-
for (const userPath of [defaultPath, ...fallbackPaths]) {
|
|
93
|
-
if (fs.existsSync(userPath)) {
|
|
94
|
-
return userPath;
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
return defaultPath;
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
function getProjectConfigPath(workingDirectory) {
|
|
101
|
-
if (!workingDirectory) return null;
|
|
102
|
-
const candidates = [
|
|
103
|
-
path.join(workingDirectory, 'opencode.json'),
|
|
104
|
-
path.join(workingDirectory, 'opencode.jsonc'),
|
|
105
|
-
path.join(workingDirectory, '.opencode', 'opencode.json'),
|
|
106
|
-
path.join(workingDirectory, '.opencode', 'opencode.jsonc'),
|
|
107
|
-
];
|
|
108
|
-
return candidates.find((candidate) => fs.existsSync(candidate)) || candidates[0];
|
|
109
|
-
}
|
|
110
|
-
|
|
111
79
|
function readPluginConfigLayers(workingDirectory) {
|
|
112
|
-
|
|
113
|
-
const userPath = getPrimaryUserConfigPath();
|
|
114
|
-
const projectPath = getProjectConfigPath(workingDirectory);
|
|
115
|
-
return {
|
|
116
|
-
userConfig: readConfigFile(userPath),
|
|
117
|
-
projectConfig: readConfigFile(projectPath),
|
|
118
|
-
customConfig: readConfigFile(customPath),
|
|
119
|
-
paths: {
|
|
120
|
-
userPath,
|
|
121
|
-
projectPath,
|
|
122
|
-
customPath,
|
|
123
|
-
},
|
|
124
|
-
};
|
|
80
|
+
return readConfigLayers(workingDirectory);
|
|
125
81
|
}
|
|
126
82
|
|
|
127
83
|
function validateFileName(fileName) {
|
|
@@ -144,16 +100,10 @@ function ensureProjectConfigPath(workingDirectory) {
|
|
|
144
100
|
}
|
|
145
101
|
|
|
146
102
|
function configSources(layers) {
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
}
|
|
151
|
-
sources.push({ config: layers.userConfig, filePath: layers.paths.userPath, scope: AGENT_SCOPE.USER });
|
|
152
|
-
}
|
|
153
|
-
if (layers.paths.projectPath) {
|
|
154
|
-
sources.push({ config: layers.projectConfig, filePath: layers.paths.projectPath, scope: AGENT_SCOPE.PROJECT });
|
|
155
|
-
}
|
|
156
|
-
return sources;
|
|
103
|
+
return [...layers.documents.filter((layer) => layer.scope !== 'project').reverse(), ...layers.projectLayers.toReversed()].map((layer) => ({
|
|
104
|
+
config: layer.config, filePath: layer.path,
|
|
105
|
+
scope: layer.scope === 'project' ? AGENT_SCOPE.PROJECT : AGENT_SCOPE.USER,
|
|
106
|
+
}));
|
|
157
107
|
}
|
|
158
108
|
|
|
159
109
|
function splitScopedValue(value) {
|
|
@@ -175,7 +125,9 @@ function getPluginTarget(id, workingDirectory) {
|
|
|
175
125
|
const { scope, value: spec } = splitScopedValue(decoded.value);
|
|
176
126
|
validateScope(scope);
|
|
177
127
|
const layers = readPluginConfigLayers(workingDirectory);
|
|
178
|
-
const source = configSources(layers).find((candidate) => candidate.scope === scope
|
|
128
|
+
const source = configSources(layers).find((candidate) => candidate.scope === scope
|
|
129
|
+
&& Array.isArray(candidate.config?.plugin)
|
|
130
|
+
&& candidate.config.plugin.some((raw) => parsePluginRaw(raw).spec === spec));
|
|
179
131
|
const plugin = Array.isArray(source?.config?.plugin) ? source.config.plugin : [];
|
|
180
132
|
const index = plugin.findIndex((raw) => parsePluginRaw(raw).spec === spec);
|
|
181
133
|
if (!source || index === -1) {
|
|
@@ -281,14 +233,14 @@ function createPluginEntry(entry, workingDirectory) {
|
|
|
281
233
|
throw codedError(`Plugin "${spec}" already exists`, 'ENTRY_EXISTS');
|
|
282
234
|
}
|
|
283
235
|
|
|
284
|
-
let targetPath =
|
|
236
|
+
let targetPath = layers.paths.userPath;
|
|
285
237
|
let config = {};
|
|
286
238
|
if (scope === AGENT_SCOPE.PROJECT) {
|
|
287
239
|
targetPath = ensureProjectConfigPath(workingDirectory);
|
|
288
240
|
config = fs.existsSync(targetPath) ? readConfigFile(targetPath) : {};
|
|
289
241
|
} else {
|
|
290
242
|
targetPath = layers.paths.customPath || layers.paths.userPath;
|
|
291
|
-
config = layers
|
|
243
|
+
config = getConfigForPath(layers, targetPath);
|
|
292
244
|
}
|
|
293
245
|
|
|
294
246
|
if (!Array.isArray(config.plugin)) {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { afterAll, beforeAll, beforeEach, describe, expect, test } from 'bun:test';
|
|
2
|
+
import { vi } from 'vitest';
|
|
2
3
|
import fs from 'fs';
|
|
3
4
|
import os from 'os';
|
|
4
5
|
import path from 'path';
|
|
@@ -30,19 +31,20 @@ describe('opencode plugins data layer', () => {
|
|
|
30
31
|
beforeAll(async () => {
|
|
31
32
|
rootDir = fs.mkdtempSync(path.join(os.tmpdir(), 'openchamber-plugins-'));
|
|
32
33
|
userConfigPath = path.join(rootDir, 'user-opencode.json');
|
|
33
|
-
|
|
34
|
+
vi.stubEnv('OPENCODE_CONFIG', userConfigPath);
|
|
35
|
+
vi.stubEnv('OPENCODE_CONFIG_DIR', path.join(rootDir, 'global-config'));
|
|
34
36
|
plugins = await import('./plugins.js');
|
|
35
37
|
});
|
|
36
38
|
|
|
37
39
|
beforeEach(() => {
|
|
38
|
-
|
|
40
|
+
vi.stubEnv('OPENCODE_CONFIG', userConfigPath);
|
|
39
41
|
projectDir = fs.mkdtempSync(path.join(rootDir, 'project-'));
|
|
40
42
|
fs.rmSync(userConfigPath, { force: true });
|
|
41
43
|
});
|
|
42
44
|
|
|
43
45
|
afterAll(() => {
|
|
44
46
|
fs.rmSync(rootDir, { recursive: true, force: true });
|
|
45
|
-
|
|
47
|
+
vi.unstubAllEnvs();
|
|
46
48
|
});
|
|
47
49
|
|
|
48
50
|
test('parses raw plugin entries', () => {
|
|
@@ -30,8 +30,8 @@ function getProviderSources(providerId, workingDirectory) {
|
|
|
30
30
|
return {
|
|
31
31
|
sources: {
|
|
32
32
|
auth: { exists: false },
|
|
33
|
-
user: { exists: userExists, path: paths.userPath },
|
|
34
|
-
project: { exists: projectExists, path: paths.projectPath || null },
|
|
33
|
+
user: { exists: userExists, path: layers.userLayers.findLast((layer) => layer.config?.provider?.[providerId] !== undefined || layer.config?.providers?.[providerId] !== undefined)?.path || paths.userPath },
|
|
34
|
+
project: { exists: projectExists, path: layers.projectLayers.findLast((layer) => layer.config?.provider?.[providerId] !== undefined || layer.config?.providers?.[providerId] !== undefined)?.path || paths.projectPath || null },
|
|
35
35
|
custom: { exists: customExists, path: paths.customPath }
|
|
36
36
|
}
|
|
37
37
|
};
|
|
@@ -44,17 +44,20 @@ function removeProviderConfig(providerId, workingDirectory, scope = 'user') {
|
|
|
44
44
|
|
|
45
45
|
const layers = readConfigLayers(workingDirectory);
|
|
46
46
|
let targetPath = layers.paths.userPath;
|
|
47
|
+
const sources = getProviderSources(providerId, workingDirectory).sources;
|
|
47
48
|
|
|
48
49
|
if (scope === 'project') {
|
|
49
50
|
if (!workingDirectory) {
|
|
50
51
|
throw new Error('Working directory is required for project scope');
|
|
51
52
|
}
|
|
52
|
-
targetPath =
|
|
53
|
+
targetPath = sources.project.path || targetPath;
|
|
53
54
|
} else if (scope === 'custom') {
|
|
54
55
|
if (!layers.paths.customPath) {
|
|
55
56
|
return false;
|
|
56
57
|
}
|
|
57
58
|
targetPath = layers.paths.customPath;
|
|
59
|
+
} else {
|
|
60
|
+
targetPath = sources.user.path;
|
|
58
61
|
}
|
|
59
62
|
|
|
60
63
|
const targetConfig = getConfigForPath(layers, targetPath);
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { createProxyMiddleware } from 'http-proxy-middleware';
|
|
2
|
+
import { isSensitiveCredentialResponse } from './credential-boundary.js';
|
|
2
3
|
|
|
3
4
|
import {
|
|
4
5
|
applyForwardProxyResponseHeaders,
|
|
@@ -244,6 +245,12 @@ export const resolveSessionTurnAdmissionRequest = (req) => {
|
|
|
244
245
|
};
|
|
245
246
|
|
|
246
247
|
export const registerOpenCodeProxy = (app, deps) => {
|
|
248
|
+
app.use((req, res, next) => {
|
|
249
|
+
if (isSensitiveCredentialResponse(req.method, req.originalUrl || req.url)) {
|
|
250
|
+
return res.status(403).json({ code: 'credential_response_refused', error: 'Provider credential values are host-only.' });
|
|
251
|
+
}
|
|
252
|
+
return next();
|
|
253
|
+
});
|
|
247
254
|
const {
|
|
248
255
|
fs,
|
|
249
256
|
os,
|
|
@@ -957,7 +964,9 @@ export const registerOpenCodeProxy = (app, deps) => {
|
|
|
957
964
|
req.path === '/opencode/version' ||
|
|
958
965
|
req.path === '/opencode/contract' ||
|
|
959
966
|
req.path === '/opencode/upgrade-status' ||
|
|
960
|
-
req.path === '/opencode/upgrade'
|
|
967
|
+
req.path === '/opencode/upgrade' ||
|
|
968
|
+
req.path === '/opencode/compatibility' ||
|
|
969
|
+
req.path === '/opencode/install-required'
|
|
961
970
|
) {
|
|
962
971
|
return next();
|
|
963
972
|
}
|
|
@@ -1207,6 +1216,9 @@ export const registerOpenCodeProxy = (app, deps) => {
|
|
|
1207
1216
|
app.get('/api/session/:sessionID/message', (req, res, next) => {
|
|
1208
1217
|
return forwardSessionMessagesListRequest(req, res, next);
|
|
1209
1218
|
});
|
|
1219
|
+
app.get('/api/session/:sessionID/context', (req, res, next) => {
|
|
1220
|
+
return forwardSessionMessagesListRequest(req, res, next);
|
|
1221
|
+
});
|
|
1210
1222
|
|
|
1211
1223
|
app.get('/api/global/event', forwardSseRequest);
|
|
1212
1224
|
app.get('/api/event', forwardSseRequest);
|
|
@@ -57,6 +57,19 @@ const mountProxy = () => {
|
|
|
57
57
|
return app;
|
|
58
58
|
};
|
|
59
59
|
|
|
60
|
+
describe('credential response boundary', () => {
|
|
61
|
+
it.each(['/api/credential', '/api/credential/', '/api/%63redential?directory=/repo'])('refuses raw credential list %s before upstream', async (path) => {
|
|
62
|
+
const fetchSpy = vi.fn();
|
|
63
|
+
globalThis.fetch = fetchSpy;
|
|
64
|
+
const response = await request(mountProxy()).get(path).expect(403);
|
|
65
|
+
expect(response.body.code).toBe('credential_response_refused');
|
|
66
|
+
expect(fetchSpy).not.toHaveBeenCalled();
|
|
67
|
+
});
|
|
68
|
+
it('refuses credential create replies which echo values', async () => {
|
|
69
|
+
await request(mountProxy()).post('/api/credential').send({}).expect(403);
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
|
|
60
73
|
const sseBody = (blocks) => {
|
|
61
74
|
const encoder = new TextEncoder();
|
|
62
75
|
const text = blocks.join('');
|
|
@@ -460,6 +473,21 @@ describe('registerOpenCodeProxy reasoning projection routes', () => {
|
|
|
460
473
|
expect(JSON.stringify(res.body)).not.toContain('hidden');
|
|
461
474
|
});
|
|
462
475
|
|
|
476
|
+
it.each(['message', 'context'])('filters native v2 %s before outbound transport', async (surface) => {
|
|
477
|
+
const upstreamUrls = [];
|
|
478
|
+
globalThis.fetch = vi.fn(async (url) => {
|
|
479
|
+
upstreamUrls.push(String(url));
|
|
480
|
+
return new Response(JSON.stringify({ data: [{ id: 'm', type: 'assistant', content: [
|
|
481
|
+
{ type: 'reasoning', text: 'hidden' }, { type: 'text', text: 'visible' },
|
|
482
|
+
] }], cursor: { next: 'older' } }), { headers: { 'content-type': 'application/json' } });
|
|
483
|
+
});
|
|
484
|
+
const res = await request(mountProxy()).get(`/api/session/ses_1/${surface}?includeReasoning=false`);
|
|
485
|
+
expect(res.status).toBe(200);
|
|
486
|
+
expect(res.body.data[0].content).toEqual([{ type: 'text', text: 'visible' }]);
|
|
487
|
+
expect(res.body.cursor.next).toBe('older');
|
|
488
|
+
expect(upstreamUrls[0]).not.toContain('includeReasoning');
|
|
489
|
+
});
|
|
490
|
+
|
|
463
491
|
it('keeps reasoning parts on session.messages list when includeReasoning is omitted', async () => {
|
|
464
492
|
globalThis.fetch = vi.fn(async () => ({
|
|
465
493
|
ok: true,
|
|
@@ -503,7 +531,7 @@ describe('registerOpenCodeProxy session metadata overlay', () => {
|
|
|
503
531
|
return app;
|
|
504
532
|
};
|
|
505
533
|
|
|
506
|
-
it('merges
|
|
534
|
+
it('merges side-store gaps onto the OpenCode record without letting the side row win conflicts', async () => {
|
|
507
535
|
globalThis.fetch = vi.fn(async (url) => {
|
|
508
536
|
const path = String(url).split('?')[0];
|
|
509
537
|
if (path.endsWith('/api/session') || path.endsWith('/session')) {
|
|
@@ -551,7 +579,7 @@ describe('registerOpenCodeProxy session metadata overlay', () => {
|
|
|
551
579
|
id: 'ses_1',
|
|
552
580
|
metadata: {
|
|
553
581
|
fromOpenCode: true,
|
|
554
|
-
shared: '
|
|
582
|
+
shared: 'theirs',
|
|
555
583
|
openchamber: { goal: { status: 'active' } },
|
|
556
584
|
},
|
|
557
585
|
});
|
|
@@ -563,7 +591,7 @@ describe('registerOpenCodeProxy session metadata overlay', () => {
|
|
|
563
591
|
id: 'ses_1',
|
|
564
592
|
metadata: {
|
|
565
593
|
fromOpenCode: true,
|
|
566
|
-
shared: '
|
|
594
|
+
shared: 'theirs',
|
|
567
595
|
openchamber: { goal: { status: 'active' } },
|
|
568
596
|
},
|
|
569
597
|
});
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { expect, test } from 'vitest';
|
|
2
|
+
import express from 'express';
|
|
3
|
+
import request from 'supertest';
|
|
4
|
+
import { execFile, spawn } from 'node:child_process';
|
|
5
|
+
import { promisify } from 'node:util';
|
|
6
|
+
import fs from 'node:fs/promises';
|
|
7
|
+
import os from 'node:os';
|
|
8
|
+
import path from 'node:path';
|
|
9
|
+
import net from 'node:net';
|
|
10
|
+
import { createOpenCodeUpdateDiscovery } from './update-discovery.js';
|
|
11
|
+
import { installPinnedOpenCode2Cli } from './ensure-cli.js';
|
|
12
|
+
import { createSharedOpenCodeService } from './shared-service.js';
|
|
13
|
+
import { registerOpenCodeRoutes } from './routes.js';
|
|
14
|
+
|
|
15
|
+
// Downloads real official binaries; never touches the user's shared service.
|
|
16
|
+
test.skipIf(process.env.OPENCHAMBER_TEST_REMOTE_UPGRADE !== '1')('remote discovery upgrades an isolated shared service and verifies its running version', async () => {
|
|
17
|
+
const root = await fs.mkdtemp(path.join(os.tmpdir(), 'opencode-upgrade-'));
|
|
18
|
+
const env = {
|
|
19
|
+
PATH: process.env.PATH, HOME: root, TMPDIR: os.tmpdir(),
|
|
20
|
+
XDG_CONFIG_HOME: path.join(root, 'config'), XDG_DATA_HOME: path.join(root, 'data'),
|
|
21
|
+
XDG_STATE_HOME: path.join(root, 'state'), XDG_CACHE_HOME: path.join(root, 'cache'),
|
|
22
|
+
OPENCODE_CONFIG_DIR: path.join(root, 'config', 'opencode'), OPENCODE_DISABLE_MODELS_FETCH: '1',
|
|
23
|
+
};
|
|
24
|
+
await fs.mkdir(env.OPENCODE_CONFIG_DIR, { recursive: true });
|
|
25
|
+
const listener = net.createServer();
|
|
26
|
+
await new Promise((resolve) => listener.listen(0, '127.0.0.1', resolve));
|
|
27
|
+
const port = listener.address().port;
|
|
28
|
+
await new Promise((resolve) => listener.close(resolve));
|
|
29
|
+
await fs.writeFile(path.join(env.OPENCODE_CONFIG_DIR, 'service.json'), JSON.stringify({ hostname: '127.0.0.1', port }));
|
|
30
|
+
const service = createSharedOpenCodeService({ envLike: env, spawnImpl: (command, args, options) => spawn(command, args, { ...options, cwd: root }) });
|
|
31
|
+
let binary;
|
|
32
|
+
let registration;
|
|
33
|
+
try {
|
|
34
|
+
const target = await createOpenCodeUpdateDiscovery()();
|
|
35
|
+
expect(target).not.toBe('2.0.16');
|
|
36
|
+
binary = await installPinnedOpenCode2Cli({ version: '2.0.16', dataDir: root });
|
|
37
|
+
await service.start({ binary, env });
|
|
38
|
+
registration = await service.readRegistration();
|
|
39
|
+
expect(registration.version).toBe('2.0.16');
|
|
40
|
+
const app = express();
|
|
41
|
+
app.use(express.json());
|
|
42
|
+
registerOpenCodeRoutes(app, {
|
|
43
|
+
getIsExternalOpenCode: () => true, getIsSharedOpenCodeService: () => true,
|
|
44
|
+
getResolvedOpenCodeBinary: () => binary, getResolvedOpenCodeBinarySource: () => 'installed',
|
|
45
|
+
readSettingsFromDiskMigrated: async () => ({}),
|
|
46
|
+
getOpenCodeResolutionSnapshot: async () => ({ source: 'installed', resolved: binary }),
|
|
47
|
+
buildOpenCodeUrl: (pathname) => `${registration.origin}${pathname}`,
|
|
48
|
+
getOpenCodeAuthHeaders: () => ({ Authorization: `Basic ${Buffer.from(`opencode:${registration.password}`).toString('base64')}` }),
|
|
49
|
+
forceResolvedOpenCodeBinary: (value) => { binary = value; },
|
|
50
|
+
restartOpenCode: async ({ binaryPath }) => {
|
|
51
|
+
await service.start({ binary: binaryPath, env, replace: true });
|
|
52
|
+
registration = await service.readRegistration();
|
|
53
|
+
},
|
|
54
|
+
openchamberDataDir: root,
|
|
55
|
+
});
|
|
56
|
+
const before = await request(app).get('/api/opencode/upgrade-status').expect(200);
|
|
57
|
+
expect(before.body).toMatchObject({ available: true, targetVersion: target });
|
|
58
|
+
const upgrade = await request(app).post('/api/opencode/upgrade').send({ target }).expect(200);
|
|
59
|
+
expect(upgrade.body).toMatchObject({ success: true, restarted: true, version: target });
|
|
60
|
+
const after = await request(app).get('/api/opencode/upgrade-status').expect(200);
|
|
61
|
+
expect(after.body).toMatchObject({ available: false, serveVersion: target });
|
|
62
|
+
} finally {
|
|
63
|
+
if (binary) await promisify(execFile)(binary, ['service', 'stop'], { env, cwd: root, timeout: 20_000, windowsHide: true });
|
|
64
|
+
await fs.rm(root, { recursive: true, force: true });
|
|
65
|
+
}
|
|
66
|
+
}, 180_000);
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { createProjectIdFromPath } from '../projects/project-id.js';
|
|
2
2
|
import { projectBootstrapSettingsResponse } from './settings-helpers.js';
|
|
3
3
|
import {
|
|
4
|
-
PINNED_OPENCODE2_VERSION,
|
|
5
4
|
evaluateOpenCodeHealthBody,
|
|
6
5
|
isOpenCode1xVersion,
|
|
7
6
|
resolveOpenCode2UpgradeTarget,
|
|
@@ -11,7 +10,9 @@ import {
|
|
|
11
10
|
readOpenCode2BinaryVersion,
|
|
12
11
|
resolveOpenChamberDataDir,
|
|
13
12
|
} from './ensure-cli.js';
|
|
13
|
+
import { registerUpgradeScreenRoutes } from './upgrade-screen.js';
|
|
14
14
|
import { evaluateRuntimeContract } from './runtime-contract.js';
|
|
15
|
+
import { createOpenCodeUpdateDiscovery } from './update-discovery.js';
|
|
15
16
|
import {
|
|
16
17
|
buildUpgradeStatusSnapshot,
|
|
17
18
|
classifyRuntimeOwnership,
|
|
@@ -53,6 +54,7 @@ export const registerOpenCodeRoutes = (app, dependencies) => {
|
|
|
53
54
|
getResolvedOpenCodeBinarySource = () => null,
|
|
54
55
|
getActiveSessionCount = () => 0,
|
|
55
56
|
openchamberDataDir = null,
|
|
57
|
+
discoverOpenCodeUpdate = createOpenCodeUpdateDiscovery(),
|
|
56
58
|
} = dependencies;
|
|
57
59
|
|
|
58
60
|
const upgradeOperation = createUpgradeOperationState();
|
|
@@ -155,12 +157,13 @@ export const registerOpenCodeRoutes = (app, dependencies) => {
|
|
|
155
157
|
const buildUpgradeStatus = async () => {
|
|
156
158
|
const ownership = await resolveOwnershipContext();
|
|
157
159
|
const contract = await buildLiveContract();
|
|
160
|
+
const targetVersion = await discoverOpenCodeUpdate();
|
|
158
161
|
const activeCount = typeof getActiveSessionCount === 'function' ? Number(getActiveSessionCount()) || 0 : 0;
|
|
159
162
|
return buildUpgradeStatusSnapshot({
|
|
160
163
|
ownership,
|
|
161
164
|
serveVersion: contract.serveVersion,
|
|
162
165
|
cliVersion: contract.cliVersion,
|
|
163
|
-
targetVersion
|
|
166
|
+
targetVersion,
|
|
164
167
|
contract,
|
|
165
168
|
operation: upgradeOperation.getState(),
|
|
166
169
|
hasActiveTasks: activeCount > 0,
|
|
@@ -253,6 +256,35 @@ export const registerOpenCodeRoutes = (app, dependencies) => {
|
|
|
253
256
|
return fallback;
|
|
254
257
|
};
|
|
255
258
|
|
|
259
|
+
registerUpgradeScreenRoutes(app, {
|
|
260
|
+
resolveOwnership: resolveOwnershipContext,
|
|
261
|
+
readServeVersion: async () => {
|
|
262
|
+
const probe = await readOpenCodeCurrentVersion().catch(() => ({ currentVersion: null }));
|
|
263
|
+
return probe.currentVersion || null;
|
|
264
|
+
},
|
|
265
|
+
readCliVersion: () => {
|
|
266
|
+
const fromRuntime = typeof getOpenCodeCliVersion === 'function' ? getOpenCodeCliVersion() : null;
|
|
267
|
+
if (fromRuntime) return fromRuntime;
|
|
268
|
+
const binaryPath = typeof getResolvedOpenCodeBinary === 'function' ? getResolvedOpenCodeBinary() : null;
|
|
269
|
+
return binaryPath ? readOpenCode2BinaryVersion(binaryPath) : null;
|
|
270
|
+
},
|
|
271
|
+
readConfiguredBinary: async () => {
|
|
272
|
+
const settings = await readSettingsFromDiskMigrated();
|
|
273
|
+
return typeof settings?.opencodeBinary === 'string' ? settings.opencodeBinary : null;
|
|
274
|
+
},
|
|
275
|
+
persistBinary: async (binaryPath) => {
|
|
276
|
+
await persistSettings({ opencodeBinary: binaryPath });
|
|
277
|
+
},
|
|
278
|
+
getResolvedBinary: getResolvedOpenCodeBinary,
|
|
279
|
+
getResolvedBinarySource: getResolvedOpenCodeBinarySource,
|
|
280
|
+
forceBinary: typeof forceResolvedOpenCodeBinary === 'function' ? forceResolvedOpenCodeBinary : null,
|
|
281
|
+
restart: typeof restartOpenCode === 'function' ? restartOpenCode : null,
|
|
282
|
+
waitReady: typeof waitForOpenCodeReady === 'function' ? waitForOpenCodeReady : null,
|
|
283
|
+
isSharedService: () => getIsSharedOpenCodeService() === true,
|
|
284
|
+
install: (options) => installPinnedOpenCode2Cli({ ...options, dataDir: resolveDataDir() }),
|
|
285
|
+
readBinaryVersion: readOpenCode2BinaryVersion,
|
|
286
|
+
});
|
|
287
|
+
|
|
256
288
|
app.get('/api/config/settings', async (req, res) => {
|
|
257
289
|
try {
|
|
258
290
|
const settings = await readSettingsFromDiskMigrated();
|
|
@@ -312,7 +344,7 @@ export const registerOpenCodeRoutes = (app, dependencies) => {
|
|
|
312
344
|
errorCode: 'OPENCODE_UPGRADE_1X_REFUSED',
|
|
313
345
|
});
|
|
314
346
|
}
|
|
315
|
-
const target = resolveOpenCode2UpgradeTarget(rawTarget);
|
|
347
|
+
const target = resolveOpenCode2UpgradeTarget(rawTarget ?? (await buildUpgradeStatus()).targetVersion);
|
|
316
348
|
const confirmActive = req.body?.confirmActiveTasks === true || req.body?.force === true;
|
|
317
349
|
const activeCount = typeof getActiveSessionCount === 'function' ? Number(getActiveSessionCount()) || 0 : 0;
|
|
318
350
|
if (activeCount > 0 && !confirmActive) {
|
|
@@ -677,7 +709,7 @@ export const registerOpenCodeRoutes = (app, dependencies) => {
|
|
|
677
709
|
|
|
678
710
|
const sources = getProviderSources(providerId, directory);
|
|
679
711
|
const { getProviderAuth } = await getAuthLibrary();
|
|
680
|
-
const auth = getProviderAuth(providerId);
|
|
712
|
+
const auth = await getProviderAuth(providerId);
|
|
681
713
|
sources.sources.auth.exists = Boolean(auth);
|
|
682
714
|
|
|
683
715
|
return res.json({
|
|
@@ -721,12 +753,12 @@ export const registerOpenCodeRoutes = (app, dependencies) => {
|
|
|
721
753
|
let removed = false;
|
|
722
754
|
if (scope === 'auth') {
|
|
723
755
|
const { removeProviderAuth } = await getAuthLibrary();
|
|
724
|
-
removed = removeProviderAuth(providerId);
|
|
756
|
+
removed = await removeProviderAuth(providerId);
|
|
725
757
|
} else if (scope === 'user' || scope === 'project' || scope === 'custom') {
|
|
726
758
|
removed = removeProviderConfig(providerId, directory, scope);
|
|
727
759
|
} else if (scope === 'all') {
|
|
728
760
|
const { removeProviderAuth } = await getAuthLibrary();
|
|
729
|
-
const authRemoved = removeProviderAuth(providerId);
|
|
761
|
+
const authRemoved = await removeProviderAuth(providerId);
|
|
730
762
|
const userRemoved = removeProviderConfig(providerId, directory, 'user');
|
|
731
763
|
const projectRemoved = directory ? removeProviderConfig(providerId, directory, 'project') : false;
|
|
732
764
|
const customRemoved = removeProviderConfig(providerId, directory, 'custom');
|
|
@@ -735,14 +767,14 @@ export const registerOpenCodeRoutes = (app, dependencies) => {
|
|
|
735
767
|
return res.status(400).json({ error: 'Invalid scope' });
|
|
736
768
|
}
|
|
737
769
|
|
|
738
|
-
if (removed) {
|
|
770
|
+
if (removed && scope !== 'auth') {
|
|
739
771
|
await refreshOpenCodeAfterConfigChange(`provider ${providerId} disconnected (${scope})`);
|
|
740
772
|
}
|
|
741
773
|
|
|
742
774
|
return res.json({
|
|
743
775
|
success: true,
|
|
744
776
|
removed,
|
|
745
|
-
requiresReload: removed,
|
|
777
|
+
requiresReload: removed && scope !== 'auth',
|
|
746
778
|
message: removed ? 'Provider disconnected successfully' : 'Provider was not connected',
|
|
747
779
|
reloadDelayMs: removed ? clientReloadDelayMs : undefined,
|
|
748
780
|
});
|