@openchambery/web 2.0.0-beta.1 → 2.0.0-beta.4
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 +15 -11
- package/bin/cli-opencode.test.js +4 -4
- package/bin/cli.js +1 -1
- package/bin/lib/cli-startup.js +1 -1
- package/bin/lib/cli-startup.test.js +18 -4
- package/dist/assets/{AgentAvatar-CLWWNu_Q.js → AgentAvatar-Q1SmFRx2.js} +1 -1
- package/dist/assets/AssistantView-CwXkiCAd.js +3 -0
- package/dist/assets/{DiagramView-xuSJTnv7.js → DiagramView-BW92schO.js} +1 -1
- package/dist/assets/{FadeInOnReveal-Ov0D96d0.js → FadeInOnReveal-BZmwl-zE.js} +1 -1
- package/dist/assets/{Icon-DD-BuMfZ.js → Icon-AQPvxMiU.js} +1 -1
- package/dist/assets/{MarkdownRenderer-bc_p1H7l.js → MarkdownRenderer-Bv05iDZZ.js} +2 -2
- package/dist/assets/{MarkdownRendererImpl-SKi4oG0c.js → MarkdownRendererImpl-C81xqYib.js} +3 -3
- package/dist/assets/MarkstreamRendererImpl-CXUCQuKJ.js +3 -0
- package/dist/assets/{MobileDetailNavigation-Bnx59U_J.js → MobileDetailNavigation-BMgIu7zc.js} +1 -1
- package/dist/assets/{MobileShareBridge-BQlP4J-F.js → MobileShareBridge-BOXGNHmC.js} +2 -2
- package/dist/assets/MobileSurface-xuc_z0_G.js +2 -0
- package/dist/assets/{MultiRunWindow-ChZGob0v.js → MultiRunWindow-B5At2Z95.js} +1 -1
- package/dist/assets/{OnboardingScreen-DtQJxZG-.js → OnboardingScreen-bsPLgz4r.js} +1 -1
- package/dist/assets/OverlayScrollbar-4wp4migh.js +2 -0
- package/dist/assets/SettingsView-CJ6tcH5_.js +25 -0
- package/dist/assets/{SettingsWindow-DfnsFa8y.js → SettingsWindow-CMtX2bMl.js} +1 -1
- package/dist/assets/{TerminalView-Dh9MWCpS.js → TerminalView-Dvr-ysTS.js} +1 -1
- package/dist/assets/{ToolOutputDialog-BejmjrRk.js → ToolOutputDialog-uH1ZZDSS.js} +2 -2
- package/dist/assets/appThemeRegistry-DK7q2juD.js +1 -0
- package/dist/assets/{assistant-attachment-cache-C0aRnFkj.js → assistant-attachment-cache-DphJleHr.js} +1 -1
- package/dist/assets/{button-D-rATN7w.js → button-CcNJUxUd.js} +1 -1
- package/dist/assets/collapsible-ZmZ6rLWs.js +3 -0
- package/dist/assets/context-menu-B2LXJgXL.js +1 -0
- package/dist/assets/{desktopHosts-B1ixEylr.js → desktopHosts-C6HrP0jq.js} +1 -1
- package/dist/assets/{desktopRelayRestore-4JmFUw8r.js → desktopRelayRestore-DdVBp45U.js} +1 -1
- package/dist/assets/{diagram-DQf7Y_uR.js → diagram-CxTs9hCQ.js} +1 -1
- package/dist/assets/{dialog-bfbvR4VH.js → dialog-KMVL9QKF.js} +1 -1
- package/dist/assets/es-CN3E_zXj.js +15 -0
- package/dist/assets/{fileMentionTouchSelection-BvrNCroG.js → fileMentionTouchSelection-7RIwlpkB.js} +1 -1
- package/dist/assets/{fileQueries-DQc8EJUf.js → fileQueries-CjdBRy8y.js} +1 -1
- package/dist/assets/fr-9zZvQZD2.js +13 -0
- package/dist/assets/gitApi-yuWg9BN4.js +10 -0
- package/dist/assets/{imageSource-BlAJJUEI.js → imageSource-9vQBq3zp.js} +1 -1
- package/dist/assets/{input-8r6F_Fza.js → input-CrIvk3HZ.js} +1 -1
- package/dist/assets/insertionBoundaries-BiUCSkx-.js +1 -0
- package/dist/assets/ja-Dnjk_82N.js +15 -0
- package/dist/assets/ko-BxCEE99C.js +15 -0
- package/dist/assets/{main-CCE_4y5F.js → main-C9mIBo77.js} +2 -2
- package/dist/assets/main-DiL6w6KF.js +169 -0
- package/dist/assets/markdown-worker-bS5OgGO3.js +2 -0
- package/dist/assets/{markdownHeightCache-1Qo6y6IJ.js → markdownHeightCache-BmXVY_gc.js} +1 -1
- package/dist/assets/{markdownTheme-DOfGg-A7.js → markdownTheme-CKac3wKC.js} +1 -1
- package/dist/assets/mcpQueries-C7jknHV8.js +1 -0
- package/dist/assets/miniChat-CwtUaElU.js +2 -0
- package/dist/assets/mobile-DB-6--ws.js +2 -0
- package/dist/assets/multirun-TAsxFI8e.js +4 -0
- package/dist/assets/{persistence-Ce3AnpEr.js → persistence-56kq8Z4V.js} +2 -2
- package/dist/assets/pl-CNNsWnPM.js +15 -0
- package/dist/assets/platform-C13fMVEs.js +6 -0
- package/dist/assets/pt-BR-BZB2DJMp.js +15 -0
- package/dist/assets/{relay-image-stream-DM7atqJs.js → relay-image-stream-DrdVj99c.js} +1 -1
- package/dist/assets/renderElectronMiniChatApp-CGj0MNRU.js +2 -0
- package/dist/assets/renderMobileApp-CGMMqbsO.js +28 -0
- package/dist/assets/runtime-fetch-Fs9ZV2V8.js +1 -0
- package/dist/assets/{runtimeConfig-BMvqa5we.js → runtimeConfig-BEANkvOd.js} +1 -1
- package/dist/assets/runtimeEndpointReset-A_ZBUJFS.js +147 -0
- package/dist/assets/sessionChangeSummary-qOLrP9fw.js +2 -0
- package/dist/assets/{sessionLookup-gh-hVRIc.js → sessionLookup-34XFmP9C.js} +1 -1
- package/dist/assets/{sortable-tabs-strip-CudoSgV4.js → sortable-tabs-strip-DyEV0Heg.js} +1 -1
- package/dist/assets/src-ubCWXZhR.css +2 -0
- package/dist/assets/store-DLCiZaA6.js +15 -0
- package/dist/assets/{terminalApi-DiF6JmBP.js → terminalApi-CXaBEoa_.js} +1 -1
- package/dist/assets/textarea-BBOlRI9O.js +1 -0
- package/dist/assets/{tooltip-CQvpK-UV.js → tooltip-CPI4fr1a.js} +1 -1
- package/dist/assets/uk-rwQrxeYM.js +15 -0
- package/dist/assets/{url-_z7evzdK.js → url-DRDQXjyX.js} +1 -1
- package/dist/assets/{useAppFontEffects-Cpmpo7Wb.js → useAppFontEffects-CiDe42I5.js} +57 -43
- package/dist/assets/{useAssistantUIStore-CMutVh9B.js → useAssistantUIStore-asNG6K7n.js} +1 -1
- package/dist/assets/useDesktopWindowControlsLayout-BlizAlOt.js +1 -0
- package/dist/assets/useEffectiveDirectory-C_CouL5v.js +1 -0
- package/dist/assets/{useFontPreferences-DcbXQOkc.js → useFontPreferences-B0oUZAiB.js} +1 -1
- package/dist/assets/useMobileNavigationStore-DfPClaW-.js +414 -0
- package/dist/assets/{useRuntimeAPIs-DSLXJybW.js → useRuntimeAPIs-DwLtO0c_.js} +1 -1
- package/dist/assets/useSessionAutoCleanup-DtrfbxAp.js +2 -0
- package/dist/assets/{useThemeSystem-DMzWVJaW.js → useThemeSystem-BlNUWH47.js} +1 -1
- package/dist/assets/useUIStore-CQYKJnrg.js +1 -0
- package/dist/assets/{useWorkerHighlightedLines-Bqn4AiBL.js → useWorkerHighlightedLines-CPNYz6Wf.js} +2 -2
- package/dist/assets/{vendor-base-ui-CnHfUmDN.js → vendor-base-ui-CDjyofeD.js} +1 -1
- package/dist/assets/{vendor-cmdk-CeAGfrLo.js → vendor-cmdk-BFH0UpXX.js} +1 -1
- package/dist/assets/{vendor-dnd-kit-accessibility-BY5LhhUB.js → vendor-dnd-kit-accessibility-BrR3AKvM.js} +1 -1
- package/dist/assets/{vendor-dnd-kit-core-CvWxUPdb.js → vendor-dnd-kit-core-CNfdvk6H.js} +1 -1
- package/dist/assets/{vendor-dnd-kit-sortable-hHdP9lLb.js → vendor-dnd-kit-sortable-DQUaTeGj.js} +1 -1
- package/dist/assets/{vendor-framer-motion-DFy_BSVd.js → vendor-framer-motion-4dogsUk3.js} +1 -1
- package/dist/assets/{vendor-legendapp-list-WE32lwNk.js → vendor-legendapp-list-BU7MdPZR.js} +1 -1
- package/dist/assets/{vendor-markstream-react-DX55XLGO.js → vendor-markstream-react-qHC2FykU.js} +3 -3
- package/dist/assets/{vendor-pierre-diffs-DX7XTPG3.js → vendor-pierre-diffs-CUfqC003.js} +1 -1
- package/dist/assets/{vendor-react-DLPElpoQ.js → vendor-react-C94A0AHl.js} +1 -1
- package/dist/assets/{vendor-react-drawio-ECtEwvbc.js → vendor-react-drawio-FVlWqKVE.js} +1 -1
- package/dist/assets/{vendor-reactuses-core-BnW6GxJt.js → vendor-reactuses-core-C2A3EjUi.js} +1 -1
- package/dist/assets/{vendor-sonner-DVzioF3d.js → vendor-sonner-DNqHmUQs.js} +1 -1
- package/dist/assets/{vendor-stream-diffs-DWy3xSmp.js → vendor-stream-diffs-CwbKsmlv.js} +2 -2
- package/dist/assets/{vendor-tanstack-react-query-D7pys_c9.js → vendor-tanstack-react-query-D0iFK57C.js} +1 -1
- package/dist/assets/{vendor-tanstack-react-virtual-tlDgH6PG.js → vendor-tanstack-react-virtual-B2kV6cnT.js} +1 -1
- package/dist/assets/{vendor-zustand-B4dlGEM2.js → vendor-zustand-BqLKf6ZY.js} +1 -1
- package/dist/assets/zh-CN-BvmBDnkD.js +15 -0
- package/dist/assets/zh-TW-9wcHTX0y.js +15 -0
- package/dist/index.html +13 -13
- package/dist/mini-chat.html +13 -13
- package/dist/mobile.html +13 -13
- package/package.json +1 -1
- package/server/index.js +327 -62
- package/server/lib/assistants/DOCUMENTATION.md +14 -9
- package/server/lib/assistants/contracts.js +11 -1
- package/server/lib/assistants/projection-history.test.js +265 -18
- package/server/lib/assistants/routes.js +6 -0
- package/server/lib/assistants/service.js +417 -231
- package/server/lib/assistants/service.test.js +392 -148
- package/server/lib/conversations/DOCUMENTATION.md +1 -1
- package/server/lib/conversations/service.js +1 -0
- package/server/lib/conversations/service.test.js +16 -0
- package/server/lib/conversations/validation.js +11 -1
- package/server/lib/event-stream/DOCUMENTATION.md +3 -1
- package/server/lib/event-stream/directory-ws-bridge.js +13 -2
- package/server/lib/event-stream/global-hub.js +49 -4
- package/server/lib/event-stream/reasoning-projection.js +16 -1
- package/server/lib/event-stream/reasoning-projection.test.js +15 -0
- package/server/lib/event-stream/runtime.js +57 -20
- package/server/lib/event-stream/runtime.test.js +112 -0
- package/server/lib/llm/DOCUMENTATION.md +33 -9
- package/server/lib/llm/catalog.js +1 -1
- package/server/lib/llm/completions.js +5 -1
- package/server/lib/llm/completions.test.js +3 -3
- package/server/lib/llm/generate.js +237 -145
- package/server/lib/llm/generate.test.js +201 -46
- package/server/lib/llm/routes.js +2 -0
- package/server/lib/message-queue/runtime.js +15 -2
- package/server/lib/notifications/runtime.js +23 -5
- package/server/lib/notifications/runtime.test.js +11 -0
- package/server/lib/opencode/DOCUMENTATION.md +85 -17
- package/server/lib/opencode/agent-document.d.ts +28 -0
- package/server/lib/opencode/agent-document.js +280 -0
- package/server/lib/opencode/agent-document.test.js +137 -0
- package/server/lib/opencode/agents.js +96 -0
- package/server/lib/opencode/auth-state-runtime.js +5 -0
- package/server/lib/opencode/bootstrap-runtime.js +2 -0
- package/server/lib/opencode/config-entity-routes.js +42 -99
- package/server/lib/opencode/config-entity-routes.test.js +107 -0
- package/server/lib/opencode/config-hot-reload.integration.test.js +105 -0
- package/server/lib/opencode/ensure-cli.js +1 -2
- package/server/lib/opencode/env-runtime.js +40 -26
- package/server/lib/opencode/env-runtime.test.js +10 -17
- package/server/lib/opencode/feature-routes-runtime.js +43 -4
- package/server/lib/opencode/feature-routes-runtime.test.js +18 -0
- package/server/lib/opencode/http-server-connections.js +98 -0
- package/server/lib/opencode/http-server-connections.test.js +321 -0
- package/server/lib/opencode/lifecycle.js +349 -39
- package/server/lib/opencode/lifecycle.test.js +322 -0
- package/server/lib/opencode/managed-capabilities-runtime.js +15 -2
- package/server/lib/opencode/managed-capabilities-runtime.test.js +1 -0
- package/server/lib/opencode/openchamber-routes.js +7 -1
- package/server/lib/opencode/opencode2-pin.js +4 -4
- package/server/lib/opencode/opencode2-pin.test.js +1 -0
- package/server/lib/opencode/opencode2-sidecar.e2e.test.js +127 -4
- package/server/lib/opencode/owned-runtime-upgrade.js +431 -0
- package/server/lib/opencode/owned-runtime-upgrade.test.js +160 -0
- package/server/lib/opencode/plugin-routes.js +3 -18
- package/server/lib/opencode/plugin-routes.test.js +9 -9
- package/server/lib/opencode/provider-catalog.js +58 -12
- package/server/lib/opencode/provider-catalog.test.js +48 -0
- package/server/lib/opencode/proxy.js +252 -46
- package/server/lib/opencode/proxy.test.js +235 -2
- package/server/lib/opencode/routes.js +296 -62
- package/server/lib/opencode/routes.test.js +278 -12
- package/server/lib/opencode/runtime-contract.js +401 -0
- package/server/lib/opencode/runtime-contract.test.js +214 -0
- package/server/lib/opencode/server-opencode-fetch.js +242 -0
- package/server/lib/opencode/server-opencode-fetch.test.js +130 -0
- package/server/lib/opencode/server-utils-runtime.js +33 -0
- package/server/lib/opencode/shared-service.integration.test.js +66 -0
- package/server/lib/opencode/shared-service.js +147 -0
- package/server/lib/opencode/shared-service.test.js +93 -0
- package/server/lib/opencode/shared.js +3 -3
- package/server/lib/opencode/shutdown-runtime.js +15 -6
- package/server/lib/opencode/shutdown-runtime.test.js +49 -1
- package/server/lib/opencode/skill-routes.js +20 -33
- package/server/lib/opencode/skill-routes.test.js +35 -2
- package/server/lib/opencode/skills.test.js +15 -0
- package/server/lib/opencode/startup-pipeline-runtime.js +4 -0
- package/server/lib/opencode/static-routes-runtime.js +3 -1
- package/server/lib/opencode/static-routes-runtime.test.js +33 -0
- package/server/lib/opencode/v2-client.js +14 -3
- package/server/lib/opencode/v2-plugin-host-shim/index.mjs +155 -0
- package/server/lib/opencode/v2-plugin-host-shim/package.json +4 -0
- package/server/lib/permission-auto-accept/DOCUMENTATION.md +5 -5
- package/server/lib/permission-auto-accept/runtime.js +14 -7
- package/server/lib/permission-auto-accept/runtime.test.js +27 -12
- package/server/lib/relay/reasoning-projection.e2e.test.ts +22 -7
- package/server/lib/scheduled-tasks/DOCUMENTATION.md +8 -0
- package/server/lib/scheduled-tasks/runtime.js +52 -4
- package/server/lib/scheduled-tasks/runtime.test.js +80 -1
- package/server/lib/session-goal/DOCUMENTATION.md +113 -13
- package/server/lib/session-goal/routes.js +9 -0
- package/server/lib/session-goal/runtime.js +1579 -117
- package/server/lib/session-goal/runtime.test.js +1453 -84
- package/server/lib/session-index/DOCUMENTATION.md +18 -15
- package/server/lib/session-index/event-ingest.js +89 -16
- package/server/lib/session-index/event-ingest.test.js +73 -0
- package/server/lib/session-index/host-authority-snapshot.js +92 -0
- package/server/lib/session-index/routes.host-archive.test.js +201 -0
- package/server/lib/session-index/routes.js +63 -10
- package/server/lib/session-index/routes.test.js +8 -8
- package/server/lib/session-index/service.js +7 -1
- package/server/lib/session-index/sync-runtime.js +161 -34
- package/server/lib/session-index/sync-runtime.test.js +155 -1
- package/server/lib/session-metadata/DOCUMENTATION.md +61 -16
- package/server/lib/session-metadata/routes.js +108 -9
- package/server/lib/session-metadata/routes.test.js +47 -2
- package/server/lib/session-metadata/session-archive.d.ts +73 -0
- package/server/lib/session-metadata/session-archive.js +654 -0
- package/server/lib/session-metadata/session-archive.test.js +359 -0
- package/server/lib/session-metadata/session-metadata-store.d.ts +36 -0
- package/server/lib/session-metadata/session-metadata-store.js +212 -72
- package/server/lib/session-metadata/session-metadata-store.test.js +205 -10
- package/server/lib/session-metadata/session-projection.d.ts +41 -0
- package/server/lib/session-metadata/session-projection.js +255 -0
- package/server/lib/session-metadata/session-projection.test.js +64 -0
- package/server/lib/session-metadata/system-session.js +55 -0
- package/server/lib/session-metadata/system-session.test.js +55 -0
- package/server/lib/session-title/DOCUMENTATION.md +40 -7
- package/server/lib/session-title/native-runtime.test.js +167 -0
- package/server/lib/session-title/runtime.js +67 -42
- package/server/lib/session-title/session-access.js +46 -0
- package/server/lib/session-title/session-access.test.js +91 -0
- package/server/lib/session-turn-pages/DOCUMENTATION.md +9 -8
- package/server/lib/session-turn-pages/routes.js +17 -102
- package/server/lib/session-turn-pages/routes.test.js +281 -0
- package/server/lib/session-turn-pages/session-message-projection.d.ts +13 -0
- package/server/lib/session-turn-pages/session-message-projection.js +281 -0
- package/server/lib/small-model/DOCUMENTATION.md +101 -147
- package/server/lib/small-model/catalog.js +31 -51
- package/server/lib/small-model/catalog.test.js +65 -10
- package/server/lib/small-model/custom-api.js +68 -0
- package/server/lib/small-model/custom-api.test.js +64 -0
- package/server/lib/small-model/index.js +124 -240
- package/server/lib/small-model/resolve.js +35 -120
- package/server/lib/small-model/resolve.test.js +20 -110
- package/server/lib/small-model/routes.js +1 -3
- package/server/lib/small-model/summary-settings.test.js +157 -253
- package/server/opencode-proxy.test.js +2 -0
- package/dist/assets/AssistantView-BTCkm8yk.js +0 -3
- package/dist/assets/MarkstreamRendererImpl-CnkodGJW.js +0 -3
- package/dist/assets/MobileSurface-BXLeiZ4d.js +0 -2
- package/dist/assets/OverlayScrollbar-pMwxaCca.js +0 -2
- package/dist/assets/SettingsView-B5I0KDTZ.js +0 -25
- package/dist/assets/appThemeRegistry-COGfxTFV.js +0 -1
- package/dist/assets/collapsible-BXRd5_Hr.js +0 -1
- package/dist/assets/context-menu-jGtH6fR9.js +0 -1
- package/dist/assets/es-c52LTHHe.js +0 -15
- package/dist/assets/fr-PisFbUE8.js +0 -13
- package/dist/assets/gitApi-ButwzQ0N.js +0 -9
- package/dist/assets/insertionBoundaries-DGs7S5zv.js +0 -1
- package/dist/assets/ja-C8Ikun3Z.js +0 -15
- package/dist/assets/ko-ClFys5o2.js +0 -15
- package/dist/assets/main-Dn9rpsgl.js +0 -169
- package/dist/assets/markdown-worker-CptoICmq.js +0 -2
- package/dist/assets/miniChat-BpjxVqgg.js +0 -2
- package/dist/assets/mobile-nMJCaNq1.js +0 -2
- package/dist/assets/multirun-BvSG94mW.js +0 -4
- package/dist/assets/pl-CnaKWN5F.js +0 -15
- package/dist/assets/platform-CWTIp02q.js +0 -4
- package/dist/assets/projectMeta-DOe49dM_.js +0 -3
- package/dist/assets/pt-BR-s8OpfRI6.js +0 -15
- package/dist/assets/renderElectronMiniChatApp-R6dKh79f.js +0 -2
- package/dist/assets/renderMobileApp-y-S8y8v_.js +0 -28
- package/dist/assets/runtime-fetch-zIalUnbb.js +0 -1
- package/dist/assets/runtimeEndpointReset-B6BxhvR0.js +0 -147
- package/dist/assets/sessionChangeSummary-VVBlEXuh.js +0 -2
- package/dist/assets/src-BzX5UI-c.css +0 -2
- package/dist/assets/store-DJczmczv.js +0 -15
- package/dist/assets/textarea-3XR8sUfy.js +0 -1
- package/dist/assets/uk-CaX0iqkB.js +0 -15
- package/dist/assets/useDesktopWindowControlsLayout-B1GX59-L.js +0 -1
- package/dist/assets/useEffectiveDirectory-CN6JoCFw.js +0 -1
- package/dist/assets/useMobileNavigationStore-BVjFtXSy.js +0 -414
- package/dist/assets/useSessionAutoCleanup-BLjymaFt.js +0 -2
- package/dist/assets/useUIStore-Bxv3f23A.js +0 -1
- package/dist/assets/zh-CN-20MYEjha.js +0 -15
- package/dist/assets/zh-TW-DiN04qTw.js +0 -15
- package/server/lib/small-model/call.js +0 -427
- package/server/lib/small-model/call.test.js +0 -382
- package/server/lib/small-model/opencode-session.js +0 -183
- package/server/lib/small-model/opencode-session.test.js +0 -147
|
@@ -50,7 +50,50 @@ const createDependencies = (getCommandSources, configDirectory, getAgentSources
|
|
|
50
50
|
getOpenCodePort: () => 4096,
|
|
51
51
|
});
|
|
52
52
|
|
|
53
|
+
describe('V2 configuration persistence', () => {
|
|
54
|
+
it.each(['agents', 'commands', 'mcp'])('%s CRUD never restarts or reloads OpenCode', async (entity) => {
|
|
55
|
+
const dependencies = createDependencies(vi.fn());
|
|
56
|
+
dependencies.refreshOpenCodeAfterConfigChange.mockRejectedValue(new Error('must not manage the service'));
|
|
57
|
+
const app = express();
|
|
58
|
+
app.use(express.json());
|
|
59
|
+
registerConfigEntityRoutes(app, dependencies);
|
|
60
|
+
for (const method of ['post', 'patch', 'delete']) {
|
|
61
|
+
const response = await request(app)[method](`/api/config/${entity}/example?directory=%2Frepo`)
|
|
62
|
+
.send({ name: 'example', scope: 'project', type: 'local', command: ['example'] });
|
|
63
|
+
expect(response.status).toBe(200);
|
|
64
|
+
expect(response.body).toMatchObject({ success: true, requiresReload: false, application: 'watch' });
|
|
65
|
+
expect(response.body.requiresManualRestart).not.toBe(true);
|
|
66
|
+
}
|
|
67
|
+
expect(dependencies.refreshOpenCodeAfterConfigChange).not.toHaveBeenCalled();
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
|
|
53
71
|
describe('config entity command metadata route', () => {
|
|
72
|
+
it('preserves real SDK authentication and request options when fetching the catalog', async () => {
|
|
73
|
+
const { OpenCode: realOpenCode } = await vi.importActual('@opencode/client');
|
|
74
|
+
createOpencodeClient.mockImplementation(realOpenCode.make);
|
|
75
|
+
const upstream = vi.spyOn(globalThis, 'fetch').mockImplementation(async (_input, init) => {
|
|
76
|
+
if (new Headers(init?.headers).get('Authorization') !== 'Basic example') {
|
|
77
|
+
return new Response('Unauthorized', { status: 401 });
|
|
78
|
+
}
|
|
79
|
+
return Response.json({ data: [{ name: 'review', description: 'Review changes' }] });
|
|
80
|
+
});
|
|
81
|
+
try {
|
|
82
|
+
const app = express();
|
|
83
|
+
app.use(express.json());
|
|
84
|
+
registerConfigEntityRoutes(app, createDependencies(() => ({ md: { exists: false }, json: { exists: false } })));
|
|
85
|
+
const response = await request(app).post('/api/config/commands/metadata').send({ catalog: true });
|
|
86
|
+
expect(response.status).toBe(200);
|
|
87
|
+
expect(response.body.commands).toEqual([expect.objectContaining({ name: 'review' })]);
|
|
88
|
+
const [url, init] = upstream.mock.calls[0];
|
|
89
|
+
expect(new URL(url).pathname).toBe('/api/command');
|
|
90
|
+
expect(init.method).toBe('GET');
|
|
91
|
+
expect(init.signal).toBeInstanceOf(AbortSignal);
|
|
92
|
+
} finally {
|
|
93
|
+
upstream.mockRestore();
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
|
|
54
97
|
it('returns metadata for many commands through one request', async () => {
|
|
55
98
|
const app = express();
|
|
56
99
|
app.use(express.json());
|
|
@@ -203,6 +246,70 @@ describe('provider catalog route', () => {
|
|
|
203
246
|
expect(response.body).toEqual({ schemaVersion: 1, providers: [], default: {}, partial: false });
|
|
204
247
|
});
|
|
205
248
|
|
|
249
|
+
it('projects ModelInfo.id as the catalog model id and default', async () => {
|
|
250
|
+
const app = express();
|
|
251
|
+
createOpencodeClient.mockReturnValue({
|
|
252
|
+
provider: { list: vi.fn(async () => ({ data: [{ id: 'openai', name: 'OpenAI' }] })) },
|
|
253
|
+
model: {
|
|
254
|
+
list: vi.fn(async () => ({
|
|
255
|
+
data: [{
|
|
256
|
+
id: 'gpt-5.4-mini',
|
|
257
|
+
modelID: 'internal-pack',
|
|
258
|
+
providerID: 'openai',
|
|
259
|
+
name: 'GPT-5.4 Mini',
|
|
260
|
+
}],
|
|
261
|
+
})),
|
|
262
|
+
default: vi.fn(async () => ({
|
|
263
|
+
data: { id: 'gpt-5.4-mini', modelID: 'internal-pack', providerID: 'openai', name: 'GPT-5.4 Mini' },
|
|
264
|
+
})),
|
|
265
|
+
},
|
|
266
|
+
});
|
|
267
|
+
registerConfigEntityRoutes(app, createDependencies(vi.fn()));
|
|
268
|
+
|
|
269
|
+
const response = await request(app).get('/api/config/catalog/providers').expect(200);
|
|
270
|
+
expect(response.body.providers).toEqual([{
|
|
271
|
+
id: 'openai',
|
|
272
|
+
name: 'OpenAI',
|
|
273
|
+
models: { 'gpt-5.4-mini': { id: 'gpt-5.4-mini', name: 'GPT-5.4 Mini' } },
|
|
274
|
+
}]);
|
|
275
|
+
expect(response.body.default).toEqual({ openai: 'gpt-5.4-mini' });
|
|
276
|
+
expect(JSON.stringify(response.body)).not.toContain('internal-pack');
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
it('projects v2 thinking variants into the provider catalog and strips request payloads', async () => {
|
|
280
|
+
const app = express();
|
|
281
|
+
createOpencodeClient.mockReturnValue({
|
|
282
|
+
provider: { list: vi.fn(async () => ({ data: [{ id: 'openai', name: 'OpenAI' }] })) },
|
|
283
|
+
model: {
|
|
284
|
+
list: vi.fn(async () => ({
|
|
285
|
+
data: [{
|
|
286
|
+
id: 'gpt-5.4',
|
|
287
|
+
modelID: 'internal-pack',
|
|
288
|
+
providerID: 'openai',
|
|
289
|
+
name: 'GPT 5.4',
|
|
290
|
+
capabilities: { tools: true, input: ['text', 'image'], output: ['text'] },
|
|
291
|
+
variants: [
|
|
292
|
+
{ id: 'low', settings: { reasoningEffort: 'low', apiKey: 'variant-settings-sentinel' } },
|
|
293
|
+
{ id: 'high', headers: { Authorization: 'variant-headers-sentinel' }, body: { store: false } },
|
|
294
|
+
],
|
|
295
|
+
}],
|
|
296
|
+
})),
|
|
297
|
+
default: vi.fn(async () => ({ data: null })),
|
|
298
|
+
},
|
|
299
|
+
});
|
|
300
|
+
registerConfigEntityRoutes(app, createDependencies(vi.fn()));
|
|
301
|
+
|
|
302
|
+
const response = await request(app).get('/api/config/catalog/providers').expect(200);
|
|
303
|
+
expect(response.body.providers[0].models['gpt-5.4'].variants).toEqual({ low: {}, high: {} });
|
|
304
|
+
expect(response.body.providers[0].models['gpt-5.4'].capabilities).toEqual({
|
|
305
|
+
toolcall: true,
|
|
306
|
+
input: { text: true, image: true },
|
|
307
|
+
output: { text: true },
|
|
308
|
+
});
|
|
309
|
+
expect(JSON.stringify(response.body)).not.toContain('sentinel');
|
|
310
|
+
expect(JSON.stringify(response.body)).not.toContain('internal-pack');
|
|
311
|
+
});
|
|
312
|
+
|
|
206
313
|
it('returns 502 for SDK failure and malformed catalog responses', async () => {
|
|
207
314
|
const app = express();
|
|
208
315
|
createOpencodeClient.mockReturnValue({
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { test, expect } from 'vitest';
|
|
2
|
+
import { OpenCode } from '@opencode/client';
|
|
3
|
+
import { spawn } from 'node:child_process';
|
|
4
|
+
import { once } from 'node:events';
|
|
5
|
+
import { randomBytes } from 'node:crypto';
|
|
6
|
+
import fs from 'node:fs/promises';
|
|
7
|
+
import os from 'node:os';
|
|
8
|
+
import path from 'node:path';
|
|
9
|
+
|
|
10
|
+
// Opt-in, isolated real V2 server. No user config, credentials, model calls,
|
|
11
|
+
// shared service registration, or npm plugin installs are used.
|
|
12
|
+
test.skipIf(!process.env.OPENCHAMBER_TEST_OPENCODE_BINARY)('V2 applies credentials, global config and plugin changes in the same process', async () => {
|
|
13
|
+
const temporary = path.join(os.tmpdir(), 'opencode');
|
|
14
|
+
await fs.mkdir(temporary, { recursive: true });
|
|
15
|
+
const root = await fs.mkdtemp(path.join(temporary, 'config-hot-reload-'));
|
|
16
|
+
const config = path.join(root, 'config', 'opencode');
|
|
17
|
+
const directory = path.join(root, 'project');
|
|
18
|
+
await fs.mkdir(config, { recursive: true });
|
|
19
|
+
await fs.mkdir(directory, { recursive: true });
|
|
20
|
+
const password = randomBytes(24).toString('hex');
|
|
21
|
+
const child = spawn(process.env.OPENCHAMBER_TEST_OPENCODE_BINARY, ['serve', '--hostname', '127.0.0.1', '--port', '0'], {
|
|
22
|
+
cwd: directory,
|
|
23
|
+
env: {
|
|
24
|
+
PATH: process.env.PATH,
|
|
25
|
+
HOME: root,
|
|
26
|
+
TMPDIR: temporary,
|
|
27
|
+
XDG_CONFIG_HOME: path.join(root, 'config'),
|
|
28
|
+
XDG_DATA_HOME: path.join(root, 'data'),
|
|
29
|
+
XDG_STATE_HOME: path.join(root, 'state'),
|
|
30
|
+
XDG_CACHE_HOME: path.join(root, 'cache'),
|
|
31
|
+
OPENCODE_CONFIG_DIR: config,
|
|
32
|
+
OPENCODE_SERVER_PASSWORD: password,
|
|
33
|
+
OPENCODE_DISABLE_MODELS_FETCH: '1',
|
|
34
|
+
},
|
|
35
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
36
|
+
});
|
|
37
|
+
let output = '';
|
|
38
|
+
let spawnError;
|
|
39
|
+
child.on('error', (error) => { spawnError = error; });
|
|
40
|
+
child.stdout.on('data', (data) => { output = (output + data).slice(-8192); });
|
|
41
|
+
child.stderr.on('data', () => undefined);
|
|
42
|
+
const closed = once(child, 'close').catch(() => undefined);
|
|
43
|
+
const until = async (read, accepts, label) => {
|
|
44
|
+
const deadline = Date.now() + 20_000;
|
|
45
|
+
while (Date.now() < deadline) {
|
|
46
|
+
if (spawnError) throw spawnError;
|
|
47
|
+
if (child.exitCode !== null) throw new Error(`Isolated OpenCode exited during ${label}`);
|
|
48
|
+
const value = await read();
|
|
49
|
+
if (accepts(value)) return value;
|
|
50
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
51
|
+
}
|
|
52
|
+
throw new Error(`Timed out waiting for ${label}`);
|
|
53
|
+
};
|
|
54
|
+
const write = async (info) => {
|
|
55
|
+
const file = path.join(config, 'opencode.json');
|
|
56
|
+
await fs.writeFile(`${file}.tmp`, JSON.stringify(info));
|
|
57
|
+
await fs.rename(`${file}.tmp`, file);
|
|
58
|
+
};
|
|
59
|
+
try {
|
|
60
|
+
const origin = await until(
|
|
61
|
+
async () => output.match(/(?:server listening on|opencode server listening on) (http:\/\/[^\s]+)/)?.[1],
|
|
62
|
+
Boolean, 'listener',
|
|
63
|
+
);
|
|
64
|
+
const client = OpenCode.make({
|
|
65
|
+
baseUrl: origin,
|
|
66
|
+
headers: { Authorization: `Basic ${Buffer.from(`opencode:${password}`).toString('base64')}` },
|
|
67
|
+
fetch: (input, init) => fetch(input, { ...init, signal: AbortSignal.timeout(15_000) }),
|
|
68
|
+
});
|
|
69
|
+
const location = { directory };
|
|
70
|
+
await client.command.list({ location });
|
|
71
|
+
const pid = child.pid;
|
|
72
|
+
const commands = { 'hot-reload-probe': { template: 'Do not execute anything.', description: 'one' } };
|
|
73
|
+
await write({ commands });
|
|
74
|
+
await until(() => client.command.list({ location }), (result) => result.data.some((entry) => entry.name === 'hot-reload-probe'), 'global command create');
|
|
75
|
+
await write({ commands: { 'hot-reload-probe': { ...commands['hot-reload-probe'], description: 'two' } } });
|
|
76
|
+
await until(() => client.command.list({ location }), (result) => result.data.some((entry) => entry.name === 'hot-reload-probe' && entry.description === 'two'), 'command update');
|
|
77
|
+
|
|
78
|
+
await write({ commands, plugins: ['*', '-opencode.config.command'] });
|
|
79
|
+
await until(() => client.command.list({ location }), (result) => !result.data.some((entry) => entry.name === 'hot-reload-probe'), 'plugin disable');
|
|
80
|
+
await write({ commands });
|
|
81
|
+
await until(() => client.command.list({ location }), (result) => result.data.some((entry) => entry.name === 'hot-reload-probe'), 'plugin re-enable');
|
|
82
|
+
|
|
83
|
+
await client.integration.connect.key({ integrationID: 'anthropic', key: 'isolated-test-placeholder', location });
|
|
84
|
+
const integration = await until(() => client.integration.get({ integrationID: 'anthropic', location }),
|
|
85
|
+
(result) => result.data.connections.some((connection) => connection.type === 'credential'), 'credential connection');
|
|
86
|
+
for (const connection of integration.data.connections) {
|
|
87
|
+
if (connection.type === 'credential') await client.credential.remove({ credentialID: connection.id });
|
|
88
|
+
}
|
|
89
|
+
await until(() => client.integration.get({ integrationID: 'anthropic', location }),
|
|
90
|
+
(result) => result.data.connections.length === 0, 'credential removal');
|
|
91
|
+
await write({ commands, mcp: { servers: { 'hot-reload-mcp': { type: 'local', command: ['unused-test-command'], disabled: true } } } });
|
|
92
|
+
await until(() => client.mcp.list({ location }), (result) => result.data.some((server) => server.name === 'hot-reload-mcp'), 'MCP config add');
|
|
93
|
+
await write({});
|
|
94
|
+
await until(() => client.mcp.list({ location }), (result) => !result.data.some((server) => server.name === 'hot-reload-mcp'), 'MCP config removal');
|
|
95
|
+
await until(() => client.command.list({ location }), (result) => !result.data.some((entry) => entry.name === 'hot-reload-probe'), 'command removal');
|
|
96
|
+
expect(child.pid).toBe(pid);
|
|
97
|
+
expect(child.exitCode).toBeNull();
|
|
98
|
+
} finally {
|
|
99
|
+
child.kill('SIGTERM');
|
|
100
|
+
const kill = setTimeout(() => child.kill('SIGKILL'), 5000);
|
|
101
|
+
await closed;
|
|
102
|
+
clearTimeout(kill);
|
|
103
|
+
await fs.rm(root, { recursive: true, force: true });
|
|
104
|
+
}
|
|
105
|
+
}, 120_000);
|
|
@@ -180,8 +180,7 @@ export async function installPinnedOpenCode2Cli(options = {}) {
|
|
|
180
180
|
});
|
|
181
181
|
}
|
|
182
182
|
|
|
183
|
-
const extractedBinary = findBinary(extractDir, binaryName)
|
|
184
|
-
|| findBinary(extractDir, platform === 'win32' ? 'opencode2.exe' : 'opencode2');
|
|
183
|
+
const extractedBinary = findBinary(extractDir, binaryName);
|
|
185
184
|
if (!extractedBinary) {
|
|
186
185
|
throw new Error(`Archive ${archivePath} did not contain ${binaryName} or opencode`);
|
|
187
186
|
}
|
|
@@ -314,14 +314,6 @@ export const createOpenCodeEnvRuntime = (deps) => {
|
|
|
314
314
|
return trimmed;
|
|
315
315
|
};
|
|
316
316
|
|
|
317
|
-
const createOpenCode1xVersionError = (candidate, version) => {
|
|
318
|
-
const error = new Error(
|
|
319
|
-
`OpenCode 1.x is not supported (${candidate} reports ${version}). Install OpenCode v2 (opencode).`
|
|
320
|
-
);
|
|
321
|
-
error.code = 'OPENCODE_BINARY_INVALID';
|
|
322
|
-
return error;
|
|
323
|
-
};
|
|
324
|
-
|
|
325
317
|
const acceptOpenCodeV2Candidate = (candidate, source) => {
|
|
326
318
|
if (typeof candidate !== 'string' || !candidate.trim()) return null;
|
|
327
319
|
if (!isExecutable(candidate) || isWindowsOpenCodeDesktopAppPath(candidate) || isMacOpenCodeAppBundlePath(candidate)) {
|
|
@@ -353,22 +345,25 @@ export const createOpenCodeEnvRuntime = (deps) => {
|
|
|
353
345
|
if (!isExecutable(candidate)) continue;
|
|
354
346
|
const version = readOpenCode2BinaryVersion(candidate);
|
|
355
347
|
if (isOpenCode1xVersion(version)) {
|
|
356
|
-
|
|
348
|
+
// Inherited OPENCODE_BINARY often points at a still-installed 1.x.
|
|
349
|
+
// Skip it and keep looking; never overwrite that binary.
|
|
350
|
+
console.warn(`Skipping OpenCode 1.x at ${candidate} (${version}); looking for v2.`);
|
|
351
|
+
continue;
|
|
357
352
|
}
|
|
358
353
|
const accepted = acceptOpenCodeV2Candidate(candidate, 'env');
|
|
359
354
|
if (accepted) return accepted;
|
|
360
355
|
}
|
|
361
356
|
|
|
362
357
|
// Match master: reuse a globally installed CLI. Official v2 is `opencode`;
|
|
363
|
-
//
|
|
364
|
-
for (const name of
|
|
358
|
+
// Skip 1.x binaries by version; auto-discovery uses only the official name.
|
|
359
|
+
for (const name of ['opencode']) {
|
|
365
360
|
const resolvedFromPath = searchPathFor(name);
|
|
366
361
|
const accepted = acceptOpenCodeV2Candidate(resolvedFromPath, 'path');
|
|
367
362
|
if (accepted) return accepted;
|
|
368
363
|
}
|
|
369
364
|
|
|
370
365
|
const home = resolveHomeDir();
|
|
371
|
-
const unixFallbacks = ['opencode'
|
|
366
|
+
const unixFallbacks = ['opencode'].flatMap((name) => [
|
|
372
367
|
path.join(home, '.opencode', 'bin', name),
|
|
373
368
|
path.join(home, '.bun', 'bin', name),
|
|
374
369
|
path.join(home, '.local', 'bin', name),
|
|
@@ -386,7 +381,7 @@ export const createOpenCodeEnvRuntime = (deps) => {
|
|
|
386
381
|
const localAppData = process.env.LOCALAPPDATA || '';
|
|
387
382
|
const programData = process.env.ProgramData || 'C:\\ProgramData';
|
|
388
383
|
const programFiles = process.env.ProgramFiles || 'C:\\Program Files';
|
|
389
|
-
const names = ['opencode.exe', 'opencode.cmd'
|
|
384
|
+
const names = ['opencode.exe', 'opencode.cmd'];
|
|
390
385
|
const dirs = [
|
|
391
386
|
path.join(userProfile, '.opencode', 'bin'),
|
|
392
387
|
path.join(appData, 'npm'),
|
|
@@ -405,7 +400,7 @@ export const createOpenCodeEnvRuntime = (deps) => {
|
|
|
405
400
|
}
|
|
406
401
|
|
|
407
402
|
if (process.platform === 'win32') {
|
|
408
|
-
for (const name of ['opencode'
|
|
403
|
+
for (const name of ['opencode']) {
|
|
409
404
|
try {
|
|
410
405
|
const result = runSpawnSync('where', [name], {
|
|
411
406
|
encoding: 'utf8',
|
|
@@ -432,7 +427,7 @@ export const createOpenCodeEnvRuntime = (deps) => {
|
|
|
432
427
|
}
|
|
433
428
|
|
|
434
429
|
const shells = [process.env.SHELL, '/bin/zsh', '/bin/bash', '/bin/sh'].filter(Boolean);
|
|
435
|
-
for (const name of ['opencode'
|
|
430
|
+
for (const name of ['opencode']) {
|
|
436
431
|
for (const shell of shells) {
|
|
437
432
|
if (!isExecutable(shell)) continue;
|
|
438
433
|
try {
|
|
@@ -1023,10 +1018,7 @@ export const createOpenCodeEnvRuntime = (deps) => {
|
|
|
1023
1018
|
if (normalized && isExecutable(normalized)) {
|
|
1024
1019
|
const version = readOpenCode2BinaryVersion(normalized);
|
|
1025
1020
|
if (isOpenCode1xVersion(version)) {
|
|
1026
|
-
|
|
1027
|
-
throw createOpenCode1xVersionError(normalized, version);
|
|
1028
|
-
}
|
|
1029
|
-
console.warn(`Configured settings.opencodeBinary points at OpenCode 1.x, which OpenChamber refuses: ${normalized}`);
|
|
1021
|
+
console.warn(`Skipping configured OpenCode 1.x at ${normalized} (${version}); looking for v2.`);
|
|
1030
1022
|
return null;
|
|
1031
1023
|
}
|
|
1032
1024
|
}
|
|
@@ -1061,15 +1053,16 @@ export const createOpenCodeEnvRuntime = (deps) => {
|
|
|
1061
1053
|
if (state.useWslForOpencode) {
|
|
1062
1054
|
return state.resolvedOpencodeBinary;
|
|
1063
1055
|
}
|
|
1056
|
+
process.env.OPENCODE_BINARY = state.resolvedOpencodeBinary;
|
|
1064
1057
|
ensureOpencodeShimRuntime(state.resolvedOpencodeBinary);
|
|
1065
1058
|
return state.resolvedOpencodeBinary;
|
|
1066
1059
|
}
|
|
1067
1060
|
|
|
1068
1061
|
const existing = typeof process.env.OPENCODE_BINARY === 'string' ? process.env.OPENCODE_BINARY.trim() : '';
|
|
1069
1062
|
if (existing && isExecutable(existing) && isOpenCode1xVersion(readOpenCode2BinaryVersion(existing))) {
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
if (existing && isExecutable(existing)) {
|
|
1063
|
+
console.warn(`Ignoring OpenCode 1.x OPENCODE_BINARY at ${existing}; looking for v2.`);
|
|
1064
|
+
delete process.env.OPENCODE_BINARY;
|
|
1065
|
+
} else if (existing && isExecutable(existing)) {
|
|
1073
1066
|
clearWslOpencodeResolution();
|
|
1074
1067
|
state.resolvedOpencodeBinary = existing;
|
|
1075
1068
|
state.resolvedOpencodeBinarySource = state.resolvedOpencodeBinarySource || 'env';
|
|
@@ -1113,8 +1106,8 @@ export const createOpenCodeEnvRuntime = (deps) => {
|
|
|
1113
1106
|
dirs.push(path.join(process.resourcesPath, 'opencode-cli'));
|
|
1114
1107
|
}
|
|
1115
1108
|
|
|
1116
|
-
//
|
|
1117
|
-
const names = ['opencode', 'opencode.exe'
|
|
1109
|
+
// Runtime and packaging use the official v2 executable name.
|
|
1110
|
+
const names = ['opencode', 'opencode.exe'];
|
|
1118
1111
|
for (const dir of dirs) {
|
|
1119
1112
|
for (const name of names) {
|
|
1120
1113
|
const accepted = acceptOpenCodeV2Candidate(path.join(dir, name), 'bundled');
|
|
@@ -1141,11 +1134,11 @@ export const createOpenCodeEnvRuntime = (deps) => {
|
|
|
1141
1134
|
if (!result?.path) {
|
|
1142
1135
|
return ensureOpencodeCliEnv();
|
|
1143
1136
|
}
|
|
1144
|
-
if (result.
|
|
1137
|
+
if (result.path) {
|
|
1145
1138
|
clearWslOpencodeResolution();
|
|
1146
1139
|
process.env.OPENCODE_BINARY = result.path;
|
|
1147
1140
|
state.resolvedOpencodeBinary = result.path;
|
|
1148
|
-
state.resolvedOpencodeBinarySource = 'installed';
|
|
1141
|
+
state.resolvedOpencodeBinarySource = result.source === 'installed' ? 'installed' : (state.resolvedOpencodeBinarySource || 'discovered');
|
|
1149
1142
|
}
|
|
1150
1143
|
return ensureOpencodeCliEnv();
|
|
1151
1144
|
} catch (error) {
|
|
@@ -1241,6 +1234,26 @@ export const createOpenCodeEnvRuntime = (deps) => {
|
|
|
1241
1234
|
state.resolvedOpencodeBinary = null;
|
|
1242
1235
|
};
|
|
1243
1236
|
|
|
1237
|
+
/**
|
|
1238
|
+
* Pin managed launch to an explicit binary for the duration of an owned-cache
|
|
1239
|
+
* upgrade (ticket 12). Avoids rediscovery that would re-attach a global CLI.
|
|
1240
|
+
*/
|
|
1241
|
+
const forceResolvedOpenCodeBinary = (binaryPath, source = 'installed') => {
|
|
1242
|
+
const trimmed = typeof binaryPath === 'string' ? binaryPath.trim() : '';
|
|
1243
|
+
if (!trimmed) {
|
|
1244
|
+
const error = new Error('forceResolvedOpenCodeBinary requires a binary path');
|
|
1245
|
+
error.code = 'OPENCODE_BINARY_INVALID';
|
|
1246
|
+
throw error;
|
|
1247
|
+
}
|
|
1248
|
+
clearWslOpencodeResolution();
|
|
1249
|
+
process.env.OPENCODE_BINARY = trimmed;
|
|
1250
|
+
state.resolvedOpencodeBinary = trimmed;
|
|
1251
|
+
state.resolvedOpencodeBinarySource = source || 'installed';
|
|
1252
|
+
prependToPath(path.dirname(trimmed));
|
|
1253
|
+
ensureOpencodeShimRuntime(trimmed);
|
|
1254
|
+
return trimmed;
|
|
1255
|
+
};
|
|
1256
|
+
|
|
1244
1257
|
return {
|
|
1245
1258
|
applyLoginShellEnvSnapshot,
|
|
1246
1259
|
ensureOpencodeCliEnv,
|
|
@@ -1253,5 +1266,6 @@ export const createOpenCodeEnvRuntime = (deps) => {
|
|
|
1253
1266
|
searchPathFor,
|
|
1254
1267
|
resolveGitBinaryForSpawn,
|
|
1255
1268
|
clearResolvedOpenCodeBinary,
|
|
1269
|
+
forceResolvedOpenCodeBinary,
|
|
1256
1270
|
};
|
|
1257
1271
|
};
|
|
@@ -168,31 +168,24 @@ describe('OpenCode env runtime', () => {
|
|
|
168
168
|
expect(state.resolvedOpencodeBinarySource).toBe('settings');
|
|
169
169
|
});
|
|
170
170
|
|
|
171
|
-
it('
|
|
171
|
+
it('skips a configured 1.x opencode version instead of aborting startup', async () => {
|
|
172
172
|
const dir = createTempDir('openchamber-opencode-1x-');
|
|
173
173
|
const binary = path.join(dir, 'opencode');
|
|
174
174
|
writeVersionBinary(binary, '1.18.4');
|
|
175
175
|
const { runtime } = createRuntime({ opencodeBinary: binary });
|
|
176
176
|
|
|
177
|
-
await expect(runtime.applyOpencodeBinaryFromSettings({ strict: true })).
|
|
178
|
-
|
|
179
|
-
message: expect.stringMatching(/1\.x.*OpenCode v2/s),
|
|
180
|
-
});
|
|
177
|
+
await expect(runtime.applyOpencodeBinaryFromSettings({ strict: true })).resolves.toBeNull();
|
|
178
|
+
expect(process.env.OPENCODE_BINARY).not.toBe(binary);
|
|
181
179
|
});
|
|
182
180
|
|
|
183
|
-
it('
|
|
181
|
+
it('skips an explicit OPENCODE_BINARY that reports a 1.x version', () => {
|
|
184
182
|
const dir = createTempDir('openchamber-env-opencode-1x-');
|
|
185
183
|
const binary = path.join(dir, process.platform === 'win32' ? 'opencode.exe' : 'opencode');
|
|
186
184
|
writeVersionBinary(binary, '1.18.4');
|
|
187
185
|
process.env.OPENCODE_BINARY = binary;
|
|
188
186
|
const { runtime } = createRuntime({});
|
|
189
187
|
|
|
190
|
-
expect(
|
|
191
|
-
expect.objectContaining({
|
|
192
|
-
code: 'OPENCODE_BINARY_INVALID',
|
|
193
|
-
message: expect.stringMatching(/1\.x.*OpenCode v2/s),
|
|
194
|
-
})
|
|
195
|
-
);
|
|
188
|
+
expect(runtime.resolveOpencodeCliPath()).not.toBe(binary);
|
|
196
189
|
});
|
|
197
190
|
|
|
198
191
|
it('discovers opencode from a home-directory install location', () => {
|
|
@@ -233,7 +226,7 @@ describe('OpenCode env runtime', () => {
|
|
|
233
226
|
|
|
234
227
|
it('prefers a user-installed OpenCode from PATH over the bundled CLI', () => {
|
|
235
228
|
const bundledDir = createTempDir('openchamber-bundled-opencode-');
|
|
236
|
-
const bundledBinary = path.join(bundledDir, process.platform === 'win32' ? '
|
|
229
|
+
const bundledBinary = path.join(bundledDir, process.platform === 'win32' ? 'opencode.exe' : 'opencode');
|
|
237
230
|
const pathDir = createTempDir('openchamber-path-opencode-');
|
|
238
231
|
const pathBinary = path.join(pathDir, process.platform === 'win32' ? 'opencode.exe' : 'opencode');
|
|
239
232
|
fs.writeFileSync(bundledBinary, '#!/bin/sh\nexit 0\n');
|
|
@@ -252,7 +245,7 @@ describe('OpenCode env runtime', () => {
|
|
|
252
245
|
|
|
253
246
|
it('keeps explicit OpenCode binary ahead of bundled CLI', () => {
|
|
254
247
|
const bundledDir = createTempDir('openchamber-bundled-opencode-');
|
|
255
|
-
const bundledBinary = path.join(bundledDir, process.platform === 'win32' ? '
|
|
248
|
+
const bundledBinary = path.join(bundledDir, process.platform === 'win32' ? 'opencode.exe' : 'opencode');
|
|
256
249
|
const explicitDir = createTempDir('openchamber-explicit-opencode-');
|
|
257
250
|
const explicitBinary = path.join(explicitDir, process.platform === 'win32' ? 'opencode2.exe' : 'opencode2');
|
|
258
251
|
fs.writeFileSync(bundledBinary, '#!/bin/sh\nexit 0\n');
|
|
@@ -271,7 +264,7 @@ describe('OpenCode env runtime', () => {
|
|
|
271
264
|
it('does not use a bundled OpenCode CLI from Electron resourcesPath', () => {
|
|
272
265
|
const resourcesPath = createTempDir('openchamber-resources-');
|
|
273
266
|
const bundledDir = path.join(resourcesPath, 'opencode-cli');
|
|
274
|
-
const bundledBinary = path.join(bundledDir, process.platform === 'win32' ? '
|
|
267
|
+
const bundledBinary = path.join(bundledDir, process.platform === 'win32' ? 'opencode.exe' : 'opencode');
|
|
275
268
|
fs.mkdirSync(bundledDir, { recursive: true });
|
|
276
269
|
fs.writeFileSync(bundledBinary, '#!/bin/sh\nexit 0\n');
|
|
277
270
|
if (process.platform !== 'win32') {
|
|
@@ -346,7 +339,7 @@ describe('OpenCode env runtime', () => {
|
|
|
346
339
|
|
|
347
340
|
it('falls back to a bundled 2.x CLI only after pin install fails', async () => {
|
|
348
341
|
const bundledDir = createTempDir('openchamber-bundled-fallback-');
|
|
349
|
-
const bundledName = process.platform === 'win32' ? '
|
|
342
|
+
const bundledName = process.platform === 'win32' ? 'opencode.exe' : 'opencode';
|
|
350
343
|
const bundledBinary = path.join(bundledDir, bundledName);
|
|
351
344
|
writeVersionBinary(bundledBinary, '2.0.12');
|
|
352
345
|
process.env.OPENCHAMBER_BUNDLED_OPENCODE_CLI_DIR = bundledDir;
|
|
@@ -395,7 +388,7 @@ describe('OpenCode env runtime', () => {
|
|
|
395
388
|
|
|
396
389
|
it('does not adopt a bundled 1.x CLI after pin install fails', async () => {
|
|
397
390
|
const bundledDir = createTempDir('openchamber-bundled-1x-');
|
|
398
|
-
const bundledName = process.platform === 'win32' ? '
|
|
391
|
+
const bundledName = process.platform === 'win32' ? 'opencode.exe' : 'opencode';
|
|
399
392
|
const bundledBinary = path.join(bundledDir, bundledName);
|
|
400
393
|
writeVersionBinary(bundledBinary, '1.18.4');
|
|
401
394
|
process.env.OPENCHAMBER_BUNDLED_OPENCODE_CLI_DIR = bundledDir;
|
|
@@ -25,7 +25,7 @@ import { getNpmInfo, clearCache as clearNpmCache } from './npm-registry.js';
|
|
|
25
25
|
import { parseNpmSpec, parsePathSpec, isExactSemver } from './plugin-spec.js';
|
|
26
26
|
import { registerOpenCodeRoutes } from './routes.js';
|
|
27
27
|
import { getProviderSources, removeProviderConfig } from './providers.js';
|
|
28
|
-
import { getAgentSources, getAgentConfig, createAgent, updateAgent, deleteAgent } from './agents.js';
|
|
28
|
+
import { getAgentSources, getAgentConfig, listDisabledAgentOverrides, createAgent, updateAgent, deleteAgent } from './agents.js';
|
|
29
29
|
import { getCommandSources, createCommand, updateCommand, deleteCommand } from './commands.js';
|
|
30
30
|
import { listMcpConfigs, getMcpConfig, createMcpConfig, updateMcpConfig, deleteMcpConfig } from './mcp.js';
|
|
31
31
|
import { listSnippets, getSnippet, createSnippet, updateSnippet, deleteSnippet, expandSnippets } from './snippets.js';
|
|
@@ -115,6 +115,15 @@ export const createFeatureRoutesRuntime = (dependencies) => {
|
|
|
115
115
|
buildOpenCodeUrl,
|
|
116
116
|
getOpenCodeAuthHeaders,
|
|
117
117
|
getIsExternalOpenCode = () => false,
|
|
118
|
+
getIsSharedOpenCodeService = () => false,
|
|
119
|
+
forceResolvedOpenCodeBinary = null,
|
|
120
|
+
restartOpenCode = null,
|
|
121
|
+
getRuntimeContract = () => null,
|
|
122
|
+
getOpenCodeServeVersion = () => null,
|
|
123
|
+
getOpenCodeCliVersion = () => null,
|
|
124
|
+
getResolvedOpenCodeBinary = () => null,
|
|
125
|
+
getResolvedOpenCodeBinarySource = () => null,
|
|
126
|
+
getActiveSessionCount = () => 0,
|
|
118
127
|
getSmallModelService: routeGetSmallModelService,
|
|
119
128
|
getOpenCodePort,
|
|
120
129
|
buildAugmentedPath,
|
|
@@ -135,8 +144,11 @@ export const createFeatureRoutesRuntime = (dependencies) => {
|
|
|
135
144
|
sessionIndexService,
|
|
136
145
|
notifyContactTurnComplete,
|
|
137
146
|
sessionMetadataStore = null,
|
|
147
|
+
sessionArchiveService = null,
|
|
138
148
|
onSessionMetadataWritten = null,
|
|
139
149
|
persistSessionGoal = null,
|
|
150
|
+
persistSessionMetadata = null,
|
|
151
|
+
onSystemSessionPersisted = null,
|
|
140
152
|
readSessionMetadata = null,
|
|
141
153
|
} = routeDependencies;
|
|
142
154
|
|
|
@@ -171,6 +183,17 @@ export const createFeatureRoutesRuntime = (dependencies) => {
|
|
|
171
183
|
buildOpenCodeUrl,
|
|
172
184
|
getOpenCodeAuthHeaders,
|
|
173
185
|
getIsExternalOpenCode,
|
|
186
|
+
getIsSharedOpenCodeService,
|
|
187
|
+
forceResolvedOpenCodeBinary,
|
|
188
|
+
restartOpenCode,
|
|
189
|
+
waitForOpenCodeReady,
|
|
190
|
+
getRuntimeContract,
|
|
191
|
+
getOpenCodeServeVersion,
|
|
192
|
+
getOpenCodeCliVersion,
|
|
193
|
+
getResolvedOpenCodeBinary,
|
|
194
|
+
getResolvedOpenCodeBinarySource,
|
|
195
|
+
getActiveSessionCount,
|
|
196
|
+
openchamberDataDir,
|
|
174
197
|
onSettingsPersisted: (updated, changes) => {
|
|
175
198
|
if (!questionAutoDelegateRuntime) return;
|
|
176
199
|
if (!Object.prototype.hasOwnProperty.call(changes ?? {}, 'questionAutoDelegateEnabled')) return;
|
|
@@ -254,6 +277,8 @@ export const createFeatureRoutesRuntime = (dependencies) => {
|
|
|
254
277
|
registerLlmRoutes(app, {
|
|
255
278
|
buildOpenCodeUrl,
|
|
256
279
|
getOpenCodeAuthHeaders,
|
|
280
|
+
persistSessionMetadata,
|
|
281
|
+
onSystemSessionPersisted,
|
|
257
282
|
});
|
|
258
283
|
assistantRoutesRuntime = registerAssistantRoutes(app, {
|
|
259
284
|
openchamberDataDir,
|
|
@@ -280,6 +305,18 @@ export const createFeatureRoutesRuntime = (dependencies) => {
|
|
|
280
305
|
},
|
|
281
306
|
listScheduledTasks: (projectID) => projectConfigRuntime.listScheduledTasks(projectID),
|
|
282
307
|
sessionIndexService,
|
|
308
|
+
archiveSessionHost: sessionArchiveService
|
|
309
|
+
? async ({ sessionID, directory, archivedAt }) => sessionArchiveService.archiveSession(
|
|
310
|
+
sessionID,
|
|
311
|
+
directory,
|
|
312
|
+
archivedAt,
|
|
313
|
+
)
|
|
314
|
+
: null,
|
|
315
|
+
forgetSessionHost: sessionArchiveService
|
|
316
|
+
? (sessionID) => sessionArchiveService.forgetSession(sessionID)
|
|
317
|
+
: null,
|
|
318
|
+
persistSessionMetadata,
|
|
319
|
+
onSystemSessionPersisted,
|
|
283
320
|
upsertScheduledTask: (projectID, task) => projectConfigRuntime.upsertScheduledTask(projectID, task),
|
|
284
321
|
syncScheduledTaskProject: (projectID) => scheduledTasksRuntime.syncProject(projectID),
|
|
285
322
|
refreshAllowedRoots: refreshAssistantAllowedRoots,
|
|
@@ -301,9 +338,10 @@ export const createFeatureRoutesRuntime = (dependencies) => {
|
|
|
301
338
|
refreshOpenCodeAfterConfigChange,
|
|
302
339
|
clientReloadDelayMs,
|
|
303
340
|
waitForOpenCodeReady,
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
341
|
+
getAgentSources,
|
|
342
|
+
getAgentConfig,
|
|
343
|
+
listDisabledAgentOverrides,
|
|
344
|
+
createAgent,
|
|
307
345
|
updateAgent,
|
|
308
346
|
deleteAgent,
|
|
309
347
|
getCommandSources,
|
|
@@ -420,6 +458,7 @@ export const createFeatureRoutesRuntime = (dependencies) => {
|
|
|
420
458
|
if (sessionMetadataStore) {
|
|
421
459
|
registerSessionMetadataRoutes(app, {
|
|
422
460
|
sessionMetadataStore,
|
|
461
|
+
sessionArchiveService,
|
|
423
462
|
broadcastGlobalUiEvent,
|
|
424
463
|
onMetadataWritten: onSessionMetadataWritten,
|
|
425
464
|
});
|
|
@@ -3,6 +3,20 @@ import { describe, expect, it, vi } from 'vitest';
|
|
|
3
3
|
import { createWorktreeTopologyBroadcaster } from './feature-routes-runtime.js';
|
|
4
4
|
|
|
5
5
|
describe('feature routes runtime composition', () => {
|
|
6
|
+
it('passes live shared-service ownership from the host to upgrade routes', async () => {
|
|
7
|
+
const host = await fs.readFile(new URL('../../index.js', import.meta.url), 'utf8');
|
|
8
|
+
const routes = await fs.readFile(new URL('./feature-routes-runtime.js', import.meta.url), 'utf8');
|
|
9
|
+
expect(host).toContain('getIsSharedOpenCodeService: () => openCodeLifecycleRuntime.isSharedOpenCodeService()');
|
|
10
|
+
expect(routes).toMatch(/getIsSharedOpenCodeService = \(\) => false,[\s\S]*\} = routeDependencies;/);
|
|
11
|
+
expect(routes).toMatch(/registerOpenCodeRoutes\(app, \{[^}]*getIsSharedOpenCodeService,/);
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it('wires Host isolation persist into scheduled-task runtime from the composition root', async () => {
|
|
15
|
+
const source = await fs.readFile(new URL('../../index.js', import.meta.url), 'utf8');
|
|
16
|
+
expect(source).toMatch(/createScheduledTasksRuntime\(\{[\s\S]*persistSessionMetadata: persistSessionMetadataToStore/);
|
|
17
|
+
expect(source).toMatch(/createScheduledTasksRuntime\(\{[\s\S]*onSystemSessionPersisted: hideSystemSessionFromIndex/);
|
|
18
|
+
});
|
|
19
|
+
|
|
6
20
|
it('wires scheduled-task upsert into assistant contact tools', async () => {
|
|
7
21
|
const source = await fs.readFile(new URL('./feature-routes-runtime.js', import.meta.url), 'utf8');
|
|
8
22
|
expect(source).toMatch(/registerAssistantRoutes\(app, \{[\s\S]*upsertScheduledTask:[\s\S]*projectConfigRuntime\.upsertScheduledTask/);
|
|
@@ -10,6 +24,10 @@ describe('feature routes runtime composition', () => {
|
|
|
10
24
|
expect(source).toMatch(/registerAssistantRoutes\(app, \{[\s\S]*listProjects:/);
|
|
11
25
|
expect(source).toMatch(/registerAssistantRoutes\(app, \{[\s\S]*listScheduledTasks:[\s\S]*projectConfigRuntime\.listScheduledTasks/);
|
|
12
26
|
expect(source).toMatch(/registerAssistantRoutes\(app, \{[\s\S]*sessionIndexService/);
|
|
27
|
+
expect(source).toMatch(/registerAssistantRoutes\(app, \{[\s\S]*persistSessionMetadata/);
|
|
28
|
+
expect(source).toMatch(/registerAssistantRoutes\(app, \{[\s\S]*onSystemSessionPersisted/);
|
|
29
|
+
expect(source).toMatch(/registerLlmRoutes\(app, \{[\s\S]*persistSessionMetadata/);
|
|
30
|
+
expect(source).toMatch(/registerLlmRoutes\(app, \{[\s\S]*onSystemSessionPersisted/);
|
|
13
31
|
expect(source).toMatch(/label: project\.label\.trim\(\)/);
|
|
14
32
|
});
|
|
15
33
|
|