@openchambery/web 1.19.22-beta.1 → 2.0.0-beta.3
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 +13 -7
- package/bin/cli-opencode.test.js +208 -0
- package/bin/cli.js +62 -9
- package/bin/lib/cli-startup.js +105 -2
- package/bin/lib/cli-startup.test.js +179 -0
- package/dist/assets/{AgentAvatar-CRaBJXKs.js → AgentAvatar-B_JQoreg.js} +1 -1
- package/dist/assets/AssistantView-ClQAfGOT.js +3 -0
- package/dist/assets/{DiagramView-D1LGK_s2.js → DiagramView-BNnda8qO.js} +1 -1
- package/dist/assets/{FadeInOnReveal-B0kJo9Ii.js → FadeInOnReveal-VYQTSAkr.js} +1 -1
- package/dist/assets/{Icon-BgdfPhNs.js → Icon-BiEQEcih.js} +1 -1
- package/dist/assets/MarkdownRenderer-CJHSsWin.js +2 -0
- package/dist/assets/{MarkdownRendererImpl-DfoCJ1Mq.js → MarkdownRendererImpl-DwrHFGUp.js} +3 -3
- package/dist/assets/{MarkstreamRendererImpl-D2dsW5Lg.js → MarkstreamRendererImpl-CSnbQMu3.js} +1 -1
- package/dist/assets/MobileDetailNavigation-BXr6AGLT.js +1 -0
- package/dist/assets/{MobileShareBridge-2Yjn6wXg.js → MobileShareBridge-D7OE9OP_.js} +2 -2
- package/dist/assets/MobileSurface-D2jUkX9l.js +2 -0
- package/dist/assets/{MultiRunWindow-tTbMFowS.js → MultiRunWindow-DF3iIFCd.js} +1 -1
- package/dist/assets/{OnboardingScreen-CD2KtlF3.js → OnboardingScreen-BjHmlwlw.js} +1 -1
- package/dist/assets/OverlayScrollbar-DLtWO_Gp.js +2 -0
- package/dist/assets/SettingsView-BRxELjow.js +25 -0
- package/dist/assets/{SettingsWindow-zIa76s4t.js → SettingsWindow-BI5swspb.js} +1 -1
- package/dist/assets/{TerminalView-CDdzF4yi.js → TerminalView-BU3x8-fr.js} +1 -1
- package/dist/assets/{ToolOutputDialog-DNqA4uBl.js → ToolOutputDialog-BEWhBcRd.js} +2 -2
- package/dist/assets/appThemeRegistry-4MR0F7Jz.js +1 -0
- package/dist/assets/{button-CI7Q-Zw_.js → button-DKuz3L8l.js} +1 -1
- package/dist/assets/collapsible-DXbDynN4.js +3 -0
- package/dist/assets/context-menu-CqolsLCm.js +1 -0
- package/dist/assets/{desktopHosts-B1ixEylr.js → desktopHosts-CoDkOnyS.js} +1 -1
- package/dist/assets/{desktopRelayRestore-4JmFUw8r.js → desktopRelayRestore-DDlVKyOE.js} +1 -1
- package/dist/assets/{diagram-DdLO8HZ9.js → diagram-D7T8HQgj.js} +1 -1
- package/dist/assets/{dialog-B6pnIYmp.js → dialog-DQYl3W_o.js} +1 -1
- package/dist/assets/es-CN3E_zXj.js +15 -0
- package/dist/assets/{fileMentionTouchSelection-DNeiqns-.js → fileMentionTouchSelection-BZ6ovBYH.js} +1 -1
- package/dist/assets/{fileQueries-DQc8EJUf.js → fileQueries-JrpcsOCs.js} +1 -1
- package/dist/assets/fr-9zZvQZD2.js +13 -0
- package/dist/assets/gitApi-CM42qUed.js +10 -0
- package/dist/assets/{imageSource-BlAJJUEI.js → imageSource-CIl0FL10.js} +1 -1
- package/dist/assets/{input-D-__fwRi.js → input-CzQy8UhV.js} +1 -1
- package/dist/assets/insertionBoundaries-CtkZ9720.js +1 -0
- package/dist/assets/ja-Dnjk_82N.js +15 -0
- package/dist/assets/ko-BxCEE99C.js +15 -0
- package/dist/assets/main-BILgBI3B.js +169 -0
- package/dist/assets/main-BeveiW6F.js +2 -0
- package/dist/assets/markdown-worker-bS5OgGO3.js +2 -0
- package/dist/assets/{markdownHeightCache-BhxqJOvf.js → markdownHeightCache-Cj1I5NDw.js} +1 -1
- package/dist/assets/{markdownTheme-DOfGg-A7.js → markdownTheme-CKac3wKC.js} +1 -1
- package/dist/assets/mcpQueries-D5mQymgM.js +1 -0
- package/dist/assets/miniChat-BgrzM3u7.js +2 -0
- package/dist/assets/mobile-DtYoQE8C.js +2 -0
- package/dist/assets/multirun-jy8dEI96.js +4 -0
- package/dist/assets/{persistence-Ce3AnpEr.js → persistence-C9LQpiOS.js} +2 -2
- package/dist/assets/pl-CNNsWnPM.js +15 -0
- package/dist/assets/platform-BQghHRfp.js +6 -0
- package/dist/assets/pt-BR-BZB2DJMp.js +15 -0
- package/dist/assets/renderElectronMiniChatApp-BGrrmeME.js +2 -0
- package/dist/assets/renderMobileApp-u3c4GtFC.js +28 -0
- package/dist/assets/{runtimeConfig-CMkwoQ_X.js → runtimeConfig-BIGBRGQI.js} +1 -1
- package/dist/assets/{runtimeEndpointReset-ChOxt-J7.js → runtimeEndpointReset-D2savkuw.js} +12 -12
- package/dist/assets/{sessionChangeSummary-NnxcqCTI.js → sessionChangeSummary-eIbpSfrO.js} +1 -1
- package/dist/assets/{sessionLookup-BrU3rFHJ.js → sessionLookup-myHe5ltu.js} +1 -1
- package/dist/assets/{sortable-tabs-strip-Fnfhf3K9.js → sortable-tabs-strip-BQtmHx9r.js} +1 -1
- package/dist/assets/src-CLN7Z-zx.css +2 -0
- package/dist/assets/store-DLCiZaA6.js +15 -0
- package/dist/assets/{terminalApi-xjqssKke.js → terminalApi-BxcdiduX.js} +1 -1
- package/dist/assets/{textarea-bHYxZtfH.js → textarea-CakKvqFw.js} +1 -1
- package/dist/assets/{tooltip-0T5jv0cu.js → tooltip-Cx5gTbdY.js} +1 -1
- package/dist/assets/uk-rwQrxeYM.js +15 -0
- package/dist/assets/{url-_z7evzdK.js → url-CDHnjlsm.js} +1 -1
- package/dist/assets/{useAppFontEffects-C02YFyAv.js → useAppFontEffects-CNeo0aZs.js} +59 -45
- package/dist/assets/{useAssistantUIStore-CMutVh9B.js → useAssistantUIStore-asNG6K7n.js} +1 -1
- package/dist/assets/useDesktopWindowControlsLayout-DBJ56qIo.js +1 -0
- package/dist/assets/useEffectiveDirectory-B4LvR8Gt.js +1 -0
- package/dist/assets/{useFontPreferences-B1JYyzpK.js → useFontPreferences-CUnQ4TVm.js} +1 -1
- package/dist/assets/useMobileNavigationStore-ZHJPxlHt.js +414 -0
- package/dist/assets/{useRuntimeAPIs-DSLXJybW.js → useRuntimeAPIs-DwLtO0c_.js} +1 -1
- package/dist/assets/useSessionAutoCleanup-BIVmrnYU.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-C9z0ML1z.js → useWorkerHighlightedLines-CJZbJDW3.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-opencode-client-BOe2qpQb.js +12 -0
- 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-yaml-DIcAK3EC.js → vendor-yaml-C9KlJAlj.js} +37 -37
- 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 +2 -2
- package/server/index.js +392 -20
- package/server/lib/assistants/DOCUMENTATION.md +26 -21
- package/server/lib/assistants/assign.js +2 -1
- package/server/lib/assistants/contact-store.js +37 -19
- package/server/lib/assistants/contracts.js +11 -1
- package/server/lib/assistants/projection-history.test.js +454 -0
- package/server/lib/assistants/routes.js +11 -5
- package/server/lib/assistants/service.js +850 -294
- package/server/lib/assistants/service.test.js +588 -274
- package/server/lib/conversations/DOCUMENTATION.md +5 -5
- package/server/lib/conversations/service.js +87 -57
- package/server/lib/conversations/service.test.js +132 -100
- package/server/lib/conversations/validation.js +11 -1
- package/server/lib/event-stream/directory-ws-bridge.js +13 -2
- package/server/lib/event-stream/global-hub.js +51 -5
- 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 +95 -84
- package/server/lib/llm/catalog.js +111 -45
- package/server/lib/llm/catalog.test.js +121 -59
- package/server/lib/llm/completions.js +41 -12
- package/server/lib/llm/completions.test.js +45 -4
- package/server/lib/llm/generate.js +516 -453
- package/server/lib/llm/generate.test.js +705 -484
- package/server/lib/llm/routes.js +8 -4
- package/server/lib/message-queue/DOCUMENTATION.md +5 -5
- package/server/lib/message-queue/opencode-adapter.js +191 -104
- package/server/lib/message-queue/opencode-adapter.test.js +328 -58
- package/server/lib/message-queue/runtime.js +15 -2
- package/server/lib/message-queue/runtime.test.js +6 -6
- package/server/lib/message-queue/worker.js +7 -1
- package/server/lib/notifications/runtime.js +5 -0
- package/server/lib/opencode/DOCUMENTATION.md +103 -16
- 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 +111 -102
- package/server/lib/opencode/config-entity-routes.test.js +123 -10
- package/server/lib/opencode/config-hot-reload.integration.test.js +105 -0
- package/server/lib/opencode/ensure-cli.js +238 -0
- package/server/lib/opencode/ensure-cli.test.js +119 -0
- package/server/lib/opencode/env-runtime.js +208 -111
- package/server/lib/opencode/env-runtime.test.js +211 -14
- package/server/lib/opencode/feature-routes-runtime.js +80 -5
- 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 +539 -95
- package/server/lib/opencode/lifecycle.test.js +698 -6
- 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/network-runtime.js +28 -16
- package/server/lib/opencode/network-runtime.test.js +68 -4
- package/server/lib/opencode/openchamber-routes.js +7 -1
- package/server/lib/opencode/opencode2-pin.js +139 -0
- package/server/lib/opencode/opencode2-pin.test.js +88 -0
- package/server/lib/opencode/opencode2-sidecar.e2e.test.js +326 -0
- 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-compatibility.js +15 -0
- package/server/lib/opencode/plugin-compatibility.test.js +21 -0
- package/server/lib/opencode/plugin-routes.js +3 -18
- package/server/lib/opencode/plugin-routes.test.js +9 -9
- package/server/lib/opencode/plugins.js +4 -3
- package/server/lib/opencode/plugins.test.js +12 -0
- 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 +368 -28
- package/server/lib/opencode/proxy.test.js +338 -0
- package/server/lib/opencode/routes.js +303 -76
- package/server/lib/opencode/routes.test.js +315 -50
- 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 +35 -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 +26 -40
- package/server/lib/opencode/skill-routes.test.js +71 -7
- 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/v1-migration-gate.js +157 -0
- package/server/lib/opencode/v1-migration-gate.test.js +111 -0
- package/server/lib/opencode/v2-client.js +60 -0
- 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/opencode/watcher.js +2 -1
- package/server/lib/opencode/watcher.test.js +1 -1
- 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 +79 -39
- package/server/lib/scheduled-tasks/managed-tool-route.js +31 -1
- package/server/lib/scheduled-tasks/managed-tool-route.test.js +23 -0
- package/server/lib/scheduled-tasks/routes.js +27 -0
- package/server/lib/scheduled-tasks/routes.test.js +94 -0
- package/server/lib/scheduled-tasks/runtime.js +381 -310
- package/server/lib/scheduled-tasks/runtime.test.js +600 -309
- package/server/lib/session-goal/DOCUMENTATION.md +173 -22
- package/server/lib/session-goal/capability.js +57 -0
- package/server/lib/session-goal/capability.test.js +33 -0
- package/server/lib/session-goal/routes.js +240 -2
- package/server/lib/session-goal/routes.test.js +211 -0
- package/server/lib/session-goal/runtime.js +1617 -113
- package/server/lib/session-goal/runtime.test.js +1500 -70
- 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 +171 -41
- package/server/lib/session-index/sync-runtime.test.js +164 -9
- package/server/lib/session-metadata/DOCUMENTATION.md +86 -0
- package/server/lib/session-metadata/routes.js +193 -0
- package/server/lib/session-metadata/routes.test.js +156 -0
- 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 +375 -0
- package/server/lib/session-metadata/session-metadata-store.test.js +379 -0
- 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 +12 -11
- package/server/lib/session-turn-pages/reconcile.service.js +1 -1
- package/server/lib/session-turn-pages/reconcile.service.test.js +16 -0
- package/server/lib/session-turn-pages/routes.js +123 -75
- package/server/lib/session-turn-pages/routes.test.js +566 -0
- package/server/lib/session-turn-pages/service.js +6 -2
- package/server/lib/session-turn-pages/service.test.js +34 -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 -146
- package/server/lib/small-model/catalog.js +84 -54
- package/server/lib/small-model/catalog.test.js +111 -9
- 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 +125 -241
- 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 +63 -12
- package/dist/assets/AssistantView-BUveAgc4.js +0 -3
- package/dist/assets/MarkdownRenderer-BbrbDH6-.js +0 -2
- package/dist/assets/MobileDetailNavigation-B0aqKXd7.js +0 -1
- package/dist/assets/MobileSurface-DW7WfPsy.js +0 -2
- package/dist/assets/OverlayScrollbar-DrA0Vsg5.js +0 -2
- package/dist/assets/SettingsView-PkXIavr7.js +0 -25
- package/dist/assets/appThemeRegistry-D5Yxg4YF.js +0 -1
- package/dist/assets/collapsible-D5Phaykh.js +0 -1
- package/dist/assets/context-menu-CubSDW2g.js +0 -1
- package/dist/assets/es-DA3MqOo0.js +0 -15
- package/dist/assets/fr-Dbz0CdIA.js +0 -13
- package/dist/assets/gitApi-DXyC4MiV.js +0 -7
- package/dist/assets/insertionBoundaries-KlOVrHHt.js +0 -1
- package/dist/assets/ja-DumanVQA.js +0 -15
- package/dist/assets/ko-C0IkiUjQ.js +0 -15
- package/dist/assets/main-BPS5ylib.js +0 -2
- package/dist/assets/main-Cpd9VWYs.js +0 -169
- package/dist/assets/markdown-worker-CptoICmq.js +0 -2
- package/dist/assets/miniChat-B8_KLWiY.js +0 -2
- package/dist/assets/mobile-CtktKrXn.js +0 -2
- package/dist/assets/multirun-CYfP9Cfm.js +0 -4
- package/dist/assets/pl-B9No6MQp.js +0 -15
- package/dist/assets/platform-0w_Q9_ji.js +0 -2
- package/dist/assets/projectMeta-S1hJ7oBp.js +0 -3
- package/dist/assets/pt-BR-DyQ0WTDh.js +0 -15
- package/dist/assets/renderElectronMiniChatApp-iK14LWm8.js +0 -2
- package/dist/assets/renderMobileApp-DORLPWnb.js +0 -28
- package/dist/assets/src-rS3zCcsC.css +0 -2
- package/dist/assets/store-5B_LDnLM.js +0 -15
- package/dist/assets/uk-C_1KkqyP.js +0 -15
- package/dist/assets/useDesktopWindowControlsLayout-Dnr-jX0C.js +0 -1
- package/dist/assets/useEffectiveDirectory-CZLLyarc.js +0 -1
- package/dist/assets/useMobileNavigationStore-CoCiqNgm.js +0 -413
- package/dist/assets/useSessionAutoCleanup-xQyETQkG.js +0 -2
- package/dist/assets/useUIStore-Bxv3f23A.js +0 -1
- package/dist/assets/vendor-opencode-sdk-C07HQypa.js +0 -7
- package/dist/assets/zh-CN-uWILgBdt.js +0 -15
- package/dist/assets/zh-TW-MYfdvhbD.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 -339
- package/server/lib/small-model/opencode-session.test.js +0 -177
|
@@ -5,10 +5,11 @@ import fs from 'node:fs/promises';
|
|
|
5
5
|
import os from 'node:os';
|
|
6
6
|
import path from 'node:path';
|
|
7
7
|
|
|
8
|
-
vi.mock('@opencode
|
|
8
|
+
vi.mock('@opencode/client', () => ({ OpenCode: { make: vi.fn() } }));
|
|
9
9
|
|
|
10
|
-
const {
|
|
10
|
+
const { OpenCode } = await import('@opencode/client');
|
|
11
11
|
const { registerConfigEntityRoutes } = await import('./config-entity-routes.js');
|
|
12
|
+
const createOpencodeClient = OpenCode.make;
|
|
12
13
|
|
|
13
14
|
beforeEach(() => {
|
|
14
15
|
createOpencodeClient.mockReset();
|
|
@@ -49,6 +50,24 @@ const createDependencies = (getCommandSources, configDirectory, getAgentSources
|
|
|
49
50
|
getOpenCodePort: () => 4096,
|
|
50
51
|
});
|
|
51
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
|
+
|
|
52
71
|
describe('config entity command metadata route', () => {
|
|
53
72
|
it('returns metadata for many commands through one request', async () => {
|
|
54
73
|
const app = express();
|
|
@@ -100,9 +119,9 @@ describe('config entity command metadata route', () => {
|
|
|
100
119
|
|
|
101
120
|
expect(createOpencodeClient).toHaveBeenCalledWith(expect.objectContaining({
|
|
102
121
|
baseUrl: 'http://opencode-upstream:4096',
|
|
103
|
-
directory: '/repo',
|
|
104
122
|
headers: { Authorization: 'Basic example' },
|
|
105
123
|
}));
|
|
124
|
+
expect(createOpencodeClient.mock.calls[0][0].directory).toBeUndefined();
|
|
106
125
|
expect(response.body.commands).toEqual([
|
|
107
126
|
expect.objectContaining({ name: 'markdown', reference: markdownPath, description: `${'😀'.repeat(160)}…`, scope: 'project', isBuiltIn: false }),
|
|
108
127
|
expect.objectContaining({ name: 'json', reference: 'json', description: 'JSON command', scope: 'project', isBuiltIn: false }),
|
|
@@ -149,14 +168,35 @@ describe('config entity command metadata route', () => {
|
|
|
149
168
|
{ name: 'long-path', reference: 'long-path' },
|
|
150
169
|
]);
|
|
151
170
|
});
|
|
171
|
+
|
|
172
|
+
it('returns 502 when the command catalog is not an array instead of an empty success', async () => {
|
|
173
|
+
const app = express();
|
|
174
|
+
app.use(express.json());
|
|
175
|
+
createOpencodeClient.mockReturnValue({
|
|
176
|
+
command: { list: vi.fn(async () => ({ data: { commands: [] } })) },
|
|
177
|
+
});
|
|
178
|
+
registerConfigEntityRoutes(app, createDependencies(vi.fn()));
|
|
179
|
+
|
|
180
|
+
const response = await request(app)
|
|
181
|
+
.post('/api/config/commands/metadata?directory=%2Frepo')
|
|
182
|
+
.send({ catalog: true })
|
|
183
|
+
.expect(502);
|
|
184
|
+
expect(response.body).toEqual({ error: 'Command catalog is unavailable' });
|
|
185
|
+
});
|
|
152
186
|
});
|
|
153
187
|
|
|
154
188
|
describe('provider catalog route', () => {
|
|
155
189
|
it('uses the resolved directory, OpenCode auth, SDK providers call, and explicit catalog route', async () => {
|
|
156
190
|
const app = express();
|
|
157
191
|
const resolveProjectDirectory = vi.fn(async () => ({ directory: '/project/from-header' }));
|
|
158
|
-
const
|
|
159
|
-
|
|
192
|
+
const location = { directory: '/project/from-header' };
|
|
193
|
+
const providers = vi.fn(async () => ({ data: [] }));
|
|
194
|
+
const models = vi.fn(async () => ({ data: [] }));
|
|
195
|
+
const modelDefault = vi.fn(async () => ({ data: null }));
|
|
196
|
+
createOpencodeClient.mockReturnValue({
|
|
197
|
+
provider: { list: providers },
|
|
198
|
+
model: { list: models, default: modelDefault },
|
|
199
|
+
});
|
|
160
200
|
registerConfigEntityRoutes(app, {
|
|
161
201
|
...createDependencies(vi.fn()),
|
|
162
202
|
resolveProjectDirectory,
|
|
@@ -171,22 +211,94 @@ describe('provider catalog route', () => {
|
|
|
171
211
|
expect(resolveProjectDirectory).toHaveBeenCalledWith(expect.objectContaining({ query: { directory: '/repo' } }));
|
|
172
212
|
expect(createOpencodeClient).toHaveBeenCalledWith(expect.objectContaining({
|
|
173
213
|
baseUrl: 'http://opencode-upstream:4096',
|
|
174
|
-
directory: '/project/from-header',
|
|
175
214
|
headers: { Authorization: 'Basic example' },
|
|
176
215
|
fetch: expect.any(Function),
|
|
177
216
|
}));
|
|
178
|
-
expect(
|
|
217
|
+
expect(createOpencodeClient.mock.calls[0][0].directory).toBeUndefined();
|
|
218
|
+
expect(providers).toHaveBeenCalledWith({ location });
|
|
219
|
+
expect(models).toHaveBeenCalledWith({ location });
|
|
220
|
+
expect(modelDefault).toHaveBeenCalledWith({ location });
|
|
179
221
|
expect(response.body).toEqual({ schemaVersion: 1, providers: [], default: {}, partial: false });
|
|
180
222
|
});
|
|
181
223
|
|
|
224
|
+
it('projects ModelInfo.id as the catalog model id and default', async () => {
|
|
225
|
+
const app = express();
|
|
226
|
+
createOpencodeClient.mockReturnValue({
|
|
227
|
+
provider: { list: vi.fn(async () => ({ data: [{ id: 'openai', name: 'OpenAI' }] })) },
|
|
228
|
+
model: {
|
|
229
|
+
list: vi.fn(async () => ({
|
|
230
|
+
data: [{
|
|
231
|
+
id: 'gpt-5.4-mini',
|
|
232
|
+
modelID: 'internal-pack',
|
|
233
|
+
providerID: 'openai',
|
|
234
|
+
name: 'GPT-5.4 Mini',
|
|
235
|
+
}],
|
|
236
|
+
})),
|
|
237
|
+
default: vi.fn(async () => ({
|
|
238
|
+
data: { id: 'gpt-5.4-mini', modelID: 'internal-pack', providerID: 'openai', name: 'GPT-5.4 Mini' },
|
|
239
|
+
})),
|
|
240
|
+
},
|
|
241
|
+
});
|
|
242
|
+
registerConfigEntityRoutes(app, createDependencies(vi.fn()));
|
|
243
|
+
|
|
244
|
+
const response = await request(app).get('/api/config/catalog/providers').expect(200);
|
|
245
|
+
expect(response.body.providers).toEqual([{
|
|
246
|
+
id: 'openai',
|
|
247
|
+
name: 'OpenAI',
|
|
248
|
+
models: { 'gpt-5.4-mini': { id: 'gpt-5.4-mini', name: 'GPT-5.4 Mini' } },
|
|
249
|
+
}]);
|
|
250
|
+
expect(response.body.default).toEqual({ openai: 'gpt-5.4-mini' });
|
|
251
|
+
expect(JSON.stringify(response.body)).not.toContain('internal-pack');
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
it('projects v2 thinking variants into the provider catalog and strips request payloads', async () => {
|
|
255
|
+
const app = express();
|
|
256
|
+
createOpencodeClient.mockReturnValue({
|
|
257
|
+
provider: { list: vi.fn(async () => ({ data: [{ id: 'openai', name: 'OpenAI' }] })) },
|
|
258
|
+
model: {
|
|
259
|
+
list: vi.fn(async () => ({
|
|
260
|
+
data: [{
|
|
261
|
+
id: 'gpt-5.4',
|
|
262
|
+
modelID: 'internal-pack',
|
|
263
|
+
providerID: 'openai',
|
|
264
|
+
name: 'GPT 5.4',
|
|
265
|
+
capabilities: { tools: true, input: ['text', 'image'], output: ['text'] },
|
|
266
|
+
variants: [
|
|
267
|
+
{ id: 'low', settings: { reasoningEffort: 'low', apiKey: 'variant-settings-sentinel' } },
|
|
268
|
+
{ id: 'high', headers: { Authorization: 'variant-headers-sentinel' }, body: { store: false } },
|
|
269
|
+
],
|
|
270
|
+
}],
|
|
271
|
+
})),
|
|
272
|
+
default: vi.fn(async () => ({ data: null })),
|
|
273
|
+
},
|
|
274
|
+
});
|
|
275
|
+
registerConfigEntityRoutes(app, createDependencies(vi.fn()));
|
|
276
|
+
|
|
277
|
+
const response = await request(app).get('/api/config/catalog/providers').expect(200);
|
|
278
|
+
expect(response.body.providers[0].models['gpt-5.4'].variants).toEqual({ low: {}, high: {} });
|
|
279
|
+
expect(response.body.providers[0].models['gpt-5.4'].capabilities).toEqual({
|
|
280
|
+
toolcall: true,
|
|
281
|
+
input: { text: true, image: true },
|
|
282
|
+
output: { text: true },
|
|
283
|
+
});
|
|
284
|
+
expect(JSON.stringify(response.body)).not.toContain('sentinel');
|
|
285
|
+
expect(JSON.stringify(response.body)).not.toContain('internal-pack');
|
|
286
|
+
});
|
|
287
|
+
|
|
182
288
|
it('returns 502 for SDK failure and malformed catalog responses', async () => {
|
|
183
289
|
const app = express();
|
|
184
|
-
createOpencodeClient.mockReturnValue({
|
|
290
|
+
createOpencodeClient.mockReturnValue({
|
|
291
|
+
provider: { list: vi.fn(async () => ({ data: {} })) },
|
|
292
|
+
model: { list: vi.fn(async () => ({ data: [] })), default: vi.fn(async () => ({ data: null })) },
|
|
293
|
+
});
|
|
185
294
|
registerConfigEntityRoutes(app, createDependencies(vi.fn()));
|
|
186
295
|
await request(app).get('/api/config/catalog/providers').expect(502);
|
|
187
296
|
|
|
188
297
|
const failingApp = express();
|
|
189
|
-
createOpencodeClient.mockReturnValue({
|
|
298
|
+
createOpencodeClient.mockReturnValue({
|
|
299
|
+
provider: { list: vi.fn(async () => { throw new Error('upstream unavailable'); }) },
|
|
300
|
+
model: { list: vi.fn(async () => ({ data: [] })), default: vi.fn(async () => ({ data: null })) },
|
|
301
|
+
});
|
|
190
302
|
registerConfigEntityRoutes(failingApp, createDependencies(vi.fn()));
|
|
191
303
|
await request(failingApp).get('/api/config/catalog/providers').expect(502);
|
|
192
304
|
});
|
|
@@ -194,7 +306,8 @@ describe('provider catalog route', () => {
|
|
|
194
306
|
it('returns 502 for SDK error envelopes that include data', async () => {
|
|
195
307
|
const app = express();
|
|
196
308
|
createOpencodeClient.mockReturnValue({
|
|
197
|
-
|
|
309
|
+
provider: { list: vi.fn(async () => { throw new Error('upstream sentinel'); }) },
|
|
310
|
+
model: { list: vi.fn(async () => ({ data: [] })), default: vi.fn(async () => ({ data: null })) },
|
|
198
311
|
});
|
|
199
312
|
registerConfigEntityRoutes(app, createDependencies(vi.fn()));
|
|
200
313
|
|
|
@@ -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);
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
import { spawnSync } from 'node:child_process';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import os from 'node:os';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import {
|
|
6
|
+
PINNED_OPENCODE2_VERSION,
|
|
7
|
+
isAcceptableOpenCode2HealthVersion,
|
|
8
|
+
isOpenCode2VersionAtLeast,
|
|
9
|
+
npmPackageForOpenCode2,
|
|
10
|
+
openCodeBinaryName,
|
|
11
|
+
parseOpenCode2VersionOutput,
|
|
12
|
+
} from './opencode2-pin.js';
|
|
13
|
+
|
|
14
|
+
const INSTALL_FETCH_TIMEOUT_MS = 30_000;
|
|
15
|
+
|
|
16
|
+
export function isOpenCode2AutoInstallEnabled(env = process.env) {
|
|
17
|
+
const value = typeof env.OPENCHAMBER_OPENCODE2_AUTO_INSTALL === 'string'
|
|
18
|
+
? env.OPENCHAMBER_OPENCODE2_AUTO_INSTALL.trim().toLowerCase()
|
|
19
|
+
: '';
|
|
20
|
+
return value !== '0' && value !== 'false' && value !== 'no';
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function resolveOpenChamberDataDir(env = process.env, homedir = os.homedir) {
|
|
24
|
+
if (typeof env.OPENCHAMBER_DATA_DIR === 'string' && env.OPENCHAMBER_DATA_DIR.trim()) {
|
|
25
|
+
return path.resolve(env.OPENCHAMBER_DATA_DIR.trim());
|
|
26
|
+
}
|
|
27
|
+
const home = typeof homedir === 'function' ? homedir() : homedir;
|
|
28
|
+
return path.join(home, '.config', 'openchamber');
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function installedOpenCode2BinaryPath(version = PINNED_OPENCODE2_VERSION, options = {}) {
|
|
32
|
+
const dataDir = options.dataDir || resolveOpenChamberDataDir(options.env, options.homedir);
|
|
33
|
+
return path.join(dataDir, 'opencode-cli', version, openCodeBinaryName(options.platform));
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function readOpenCode2BinaryVersion(binaryPath, options = {}) {
|
|
37
|
+
if (typeof binaryPath !== 'string' || !binaryPath.trim()) return '';
|
|
38
|
+
const run = typeof options.spawnSync === 'function' ? options.spawnSync : spawnSync;
|
|
39
|
+
try {
|
|
40
|
+
const result = run(binaryPath, ['--version'], {
|
|
41
|
+
encoding: 'utf8',
|
|
42
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
43
|
+
timeout: 8_000,
|
|
44
|
+
windowsHide: true,
|
|
45
|
+
});
|
|
46
|
+
if (result.status !== 0) return '';
|
|
47
|
+
return parseOpenCode2VersionOutput(`${result.stdout || ''}\n${result.stderr || ''}`);
|
|
48
|
+
} catch {
|
|
49
|
+
return '';
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const resolveNpmRegistry = (options = {}) => {
|
|
54
|
+
const env = options.env || process.env;
|
|
55
|
+
const electronRoot = options.electronRoot;
|
|
56
|
+
const candidates = [
|
|
57
|
+
env.NPM_CONFIG_REGISTRY,
|
|
58
|
+
options.cwd ? path.join(options.cwd, '.npmrc') : null,
|
|
59
|
+
electronRoot ? path.join(electronRoot, '.npmrc') : null,
|
|
60
|
+
path.join(os.homedir(), '.npmrc'),
|
|
61
|
+
].filter(Boolean);
|
|
62
|
+
for (const candidate of candidates) {
|
|
63
|
+
try {
|
|
64
|
+
if (!String(candidate).endsWith('.npmrc')) {
|
|
65
|
+
if (/^https?:\/\//.test(candidate)) return String(candidate).replace(/\/+$/, '');
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
if (!fs.existsSync(candidate)) continue;
|
|
69
|
+
const match = fs.readFileSync(candidate, 'utf8').match(/^\s*registry\s*=\s*(\S+)\s*$/m);
|
|
70
|
+
if (match) return match[1].replace(/\/+$/, '');
|
|
71
|
+
} catch {
|
|
72
|
+
// Ignore unreadable npmrc candidates and fall back to the default registry.
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return 'https://registry.npmjs.org';
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
const downloadBuffer = async (url, fetchImpl, timeoutMs) => {
|
|
79
|
+
const response = await fetchImpl(url, { signal: AbortSignal.timeout(timeoutMs) });
|
|
80
|
+
if (!response.ok) {
|
|
81
|
+
throw new Error(`Failed to download ${url}: ${response.status} ${response.statusText}`);
|
|
82
|
+
}
|
|
83
|
+
return Buffer.from(await response.arrayBuffer());
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
const extractArchive = (archivePath, destination, run) => {
|
|
87
|
+
fs.rmSync(destination, { recursive: true, force: true });
|
|
88
|
+
fs.mkdirSync(destination, { recursive: true });
|
|
89
|
+
if (archivePath.endsWith('.zip')) {
|
|
90
|
+
if (process.platform === 'win32') {
|
|
91
|
+
run('powershell.exe', [
|
|
92
|
+
'-NoLogo',
|
|
93
|
+
'-NoProfile',
|
|
94
|
+
'-NonInteractive',
|
|
95
|
+
'-ExecutionPolicy',
|
|
96
|
+
'Bypass',
|
|
97
|
+
'-Command',
|
|
98
|
+
`Expand-Archive -LiteralPath ${JSON.stringify(archivePath)} -DestinationPath ${JSON.stringify(destination)} -Force`,
|
|
99
|
+
]);
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
run('unzip', ['-q', archivePath, '-d', destination]);
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
if (archivePath.endsWith('.tar.gz') || archivePath.endsWith('.tgz')) {
|
|
106
|
+
run('tar', ['-xzf', archivePath, '-C', destination]);
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
throw new Error(`Unsupported opencode2 CLI archive: ${archivePath}`);
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
const findBinary = (root, binaryName) => {
|
|
113
|
+
const entries = fs.readdirSync(root, { withFileTypes: true });
|
|
114
|
+
for (const entry of entries) {
|
|
115
|
+
const fullPath = path.join(root, entry.name);
|
|
116
|
+
if (entry.isFile() && entry.name.toLowerCase() === binaryName.toLowerCase()) {
|
|
117
|
+
return fullPath;
|
|
118
|
+
}
|
|
119
|
+
if (entry.isDirectory()) {
|
|
120
|
+
const found = findBinary(fullPath, binaryName);
|
|
121
|
+
if (found) return found;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
return null;
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
export async function installPinnedOpenCode2Cli(options = {}) {
|
|
128
|
+
const version = options.version || PINNED_OPENCODE2_VERSION;
|
|
129
|
+
const platform = options.platform || process.platform;
|
|
130
|
+
const arch = options.arch || process.arch;
|
|
131
|
+
const dataDir = options.dataDir || resolveOpenChamberDataDir(options.env, options.homedir);
|
|
132
|
+
const binaryName = openCodeBinaryName(platform);
|
|
133
|
+
const outputBinary = installedOpenCode2BinaryPath(version, { dataDir, platform });
|
|
134
|
+
const run = typeof options.spawnSync === 'function' ? options.spawnSync : spawnSync;
|
|
135
|
+
const fetchImpl = typeof options.fetch === 'function' ? options.fetch : fetch;
|
|
136
|
+
const timeoutMs = Number.isFinite(options.timeoutMs) ? options.timeoutMs : INSTALL_FETCH_TIMEOUT_MS;
|
|
137
|
+
|
|
138
|
+
const cacheDir = path.join(dataDir, 'opencode-cli', '.cache', version, `${platform}-${arch}`);
|
|
139
|
+
const archivePath = path.join(cacheDir, 'opencode2-cli.tgz');
|
|
140
|
+
const packageName = npmPackageForOpenCode2(platform, arch);
|
|
141
|
+
|
|
142
|
+
if (!fs.existsSync(archivePath)) {
|
|
143
|
+
const registry = resolveNpmRegistry(options);
|
|
144
|
+
const metadataResponse = await fetchImpl(`${registry}/${encodeURIComponent(packageName)}/${version}`, {
|
|
145
|
+
signal: AbortSignal.timeout(timeoutMs),
|
|
146
|
+
});
|
|
147
|
+
if (!metadataResponse.ok) {
|
|
148
|
+
throw new Error(
|
|
149
|
+
`Failed to resolve ${packageName}@${version} on ${registry}: ${metadataResponse.status} ${metadataResponse.statusText}`,
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
const metadata = await metadataResponse.json();
|
|
153
|
+
const tarball = metadata?.dist?.tarball;
|
|
154
|
+
if (typeof tarball !== 'string' || !/^https?:\/\//.test(tarball)) {
|
|
155
|
+
throw new Error(`No tarball found for ${packageName}@${version} on ${registry}`);
|
|
156
|
+
}
|
|
157
|
+
const bytes = await downloadBuffer(tarball, fetchImpl, timeoutMs);
|
|
158
|
+
fs.mkdirSync(path.dirname(archivePath), { recursive: true });
|
|
159
|
+
const temp = `${archivePath}.tmp`;
|
|
160
|
+
fs.writeFileSync(temp, bytes);
|
|
161
|
+
fs.renameSync(temp, archivePath);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const extractDir = options.extractDir || path.join(cacheDir, 'extract');
|
|
165
|
+
if (typeof options.extract === 'function') {
|
|
166
|
+
await options.extract(archivePath, extractDir);
|
|
167
|
+
} else {
|
|
168
|
+
extractArchive(archivePath, extractDir, (command, args) => {
|
|
169
|
+
const result = run(command, args, {
|
|
170
|
+
encoding: 'utf8',
|
|
171
|
+
stdio: 'pipe',
|
|
172
|
+
windowsHide: true,
|
|
173
|
+
});
|
|
174
|
+
if (result.status !== 0) {
|
|
175
|
+
const stderr = result.stderr ? `\n${result.stderr.trim()}` : '';
|
|
176
|
+
const stdout = result.stdout ? `\n${result.stdout.trim()}` : '';
|
|
177
|
+
throw new Error(`Command failed: ${command} ${args.join(' ')}${stderr}${stdout}`);
|
|
178
|
+
}
|
|
179
|
+
return result;
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const extractedBinary = findBinary(extractDir, binaryName);
|
|
184
|
+
if (!extractedBinary) {
|
|
185
|
+
throw new Error(`Archive ${archivePath} did not contain ${binaryName} or opencode`);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
fs.mkdirSync(path.dirname(outputBinary), { recursive: true });
|
|
189
|
+
fs.copyFileSync(extractedBinary, outputBinary);
|
|
190
|
+
if (platform !== 'win32') {
|
|
191
|
+
fs.chmodSync(outputBinary, 0o755);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const preparedVersion = readOpenCode2BinaryVersion(outputBinary, options);
|
|
195
|
+
if (preparedVersion !== version) {
|
|
196
|
+
throw new Error(`Installed opencode2 version mismatch: expected ${version}, got ${preparedVersion || 'unknown'}`);
|
|
197
|
+
}
|
|
198
|
+
return outputBinary;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Reuse any discovered acceptable 2.x CLI. Install the pin only when nothing
|
|
203
|
+
* usable is already installed (global PATH, explicit override, or cache).
|
|
204
|
+
*/
|
|
205
|
+
export async function ensurePinnedOpenCode2Cli(input = {}) {
|
|
206
|
+
const pin = input.pin || PINNED_OPENCODE2_VERSION;
|
|
207
|
+
const discoveredPath = typeof input.discoveredPath === 'string' ? input.discoveredPath.trim() : '';
|
|
208
|
+
const autoInstall = input.autoInstall ?? isOpenCode2AutoInstallEnabled(input.env);
|
|
209
|
+
const readVersion = typeof input.readVersion === 'function'
|
|
210
|
+
? input.readVersion
|
|
211
|
+
: (binaryPath) => readOpenCode2BinaryVersion(binaryPath, input);
|
|
212
|
+
const install = typeof input.install === 'function' ? input.install : installPinnedOpenCode2Cli;
|
|
213
|
+
|
|
214
|
+
if (discoveredPath) {
|
|
215
|
+
const version = readVersion(discoveredPath);
|
|
216
|
+
if (isAcceptableOpenCode2HealthVersion(version)) {
|
|
217
|
+
return { path: discoveredPath, version, source: 'discovered', installed: false };
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
const cached = installedOpenCode2BinaryPath(pin, input);
|
|
222
|
+
const cachedVersion = readVersion(cached);
|
|
223
|
+
if (cachedVersion === pin || isOpenCode2VersionAtLeast(cachedVersion, pin)) {
|
|
224
|
+
return { path: cached, version: cachedVersion, source: 'installed', installed: false };
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
if (!autoInstall) {
|
|
228
|
+
const error = new Error(
|
|
229
|
+
`Unable to locate opencode ${pin}. Set OPENCODE_BINARY or install @opencode/cli@${pin}.`,
|
|
230
|
+
);
|
|
231
|
+
error.code = 'OPENCODE_CLI_MISSING';
|
|
232
|
+
throw error;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
const installedPath = await install({ ...input, version: pin });
|
|
236
|
+
const version = readVersion(installedPath) || pin;
|
|
237
|
+
return { path: installedPath, version, source: 'installed', installed: true };
|
|
238
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { afterEach, describe, expect, it } from 'vitest';
|
|
5
|
+
import { PINNED_OPENCODE2_VERSION } from './opencode2-pin.js';
|
|
6
|
+
import {
|
|
7
|
+
ensurePinnedOpenCode2Cli,
|
|
8
|
+
installedOpenCode2BinaryPath,
|
|
9
|
+
isOpenCode2AutoInstallEnabled,
|
|
10
|
+
resolveOpenChamberDataDir,
|
|
11
|
+
} from './ensure-cli.js';
|
|
12
|
+
|
|
13
|
+
const tempDirs = [];
|
|
14
|
+
|
|
15
|
+
const createTempDir = (prefix) => {
|
|
16
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), prefix));
|
|
17
|
+
tempDirs.push(dir);
|
|
18
|
+
return dir;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
afterEach(() => {
|
|
22
|
+
for (const dir of tempDirs.splice(0)) {
|
|
23
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
24
|
+
}
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
describe('ensurePinnedOpenCode2Cli', () => {
|
|
28
|
+
it('keeps a discovered binary that already meets the pin', async () => {
|
|
29
|
+
const result = await ensurePinnedOpenCode2Cli({
|
|
30
|
+
discoveredPath: '/opt/opencode2',
|
|
31
|
+
pin: '2.0.12',
|
|
32
|
+
readVersion: () => '2.0.12',
|
|
33
|
+
install: async () => {
|
|
34
|
+
throw new Error('should not install');
|
|
35
|
+
},
|
|
36
|
+
});
|
|
37
|
+
expect(result).toEqual({
|
|
38
|
+
path: '/opt/opencode2',
|
|
39
|
+
version: '2.0.12',
|
|
40
|
+
source: 'discovered',
|
|
41
|
+
installed: false,
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('reuses an older global 2.x binary instead of reinstalling the pin', async () => {
|
|
46
|
+
const result = await ensurePinnedOpenCode2Cli({
|
|
47
|
+
discoveredPath: '/opt/old-opencode',
|
|
48
|
+
pin: '2.0.12',
|
|
49
|
+
readVersion: () => '2.0.5',
|
|
50
|
+
install: async () => {
|
|
51
|
+
throw new Error('should not install');
|
|
52
|
+
},
|
|
53
|
+
});
|
|
54
|
+
expect(result.source).toBe('discovered');
|
|
55
|
+
expect(result.path).toBe('/opt/old-opencode');
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('installs the pin only when no usable 2.x binary is present', async () => {
|
|
59
|
+
const dataDir = createTempDir('openchamber-ensure-cli-');
|
|
60
|
+
const installed = path.join(dataDir, 'opencode-cli', PINNED_OPENCODE2_VERSION, 'opencode');
|
|
61
|
+
let installedOnce = false;
|
|
62
|
+
const result = await ensurePinnedOpenCode2Cli({
|
|
63
|
+
discoveredPath: '',
|
|
64
|
+
pin: PINNED_OPENCODE2_VERSION,
|
|
65
|
+
dataDir,
|
|
66
|
+
platform: 'linux',
|
|
67
|
+
readVersion: (binaryPath) => {
|
|
68
|
+
if (binaryPath === installed && installedOnce) return PINNED_OPENCODE2_VERSION;
|
|
69
|
+
return '';
|
|
70
|
+
},
|
|
71
|
+
install: async () => {
|
|
72
|
+
installedOnce = true;
|
|
73
|
+
return installed;
|
|
74
|
+
},
|
|
75
|
+
});
|
|
76
|
+
expect(result).toEqual({
|
|
77
|
+
path: installed,
|
|
78
|
+
version: PINNED_OPENCODE2_VERSION,
|
|
79
|
+
source: 'installed',
|
|
80
|
+
installed: true,
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it('reuses a cached pin without downloading', async () => {
|
|
85
|
+
const dataDir = createTempDir('openchamber-ensure-cached-');
|
|
86
|
+
const cached = installedOpenCode2BinaryPath(PINNED_OPENCODE2_VERSION, { dataDir, platform: 'linux' });
|
|
87
|
+
const result = await ensurePinnedOpenCode2Cli({
|
|
88
|
+
pin: PINNED_OPENCODE2_VERSION,
|
|
89
|
+
dataDir,
|
|
90
|
+
platform: 'linux',
|
|
91
|
+
readVersion: (binaryPath) => (binaryPath === cached ? PINNED_OPENCODE2_VERSION : ''),
|
|
92
|
+
install: async () => {
|
|
93
|
+
throw new Error('should not install');
|
|
94
|
+
},
|
|
95
|
+
});
|
|
96
|
+
expect(result.source).toBe('installed');
|
|
97
|
+
expect(result.installed).toBe(false);
|
|
98
|
+
expect(result.path).toBe(cached);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it('fails closed when auto-install is disabled and nothing usable is present', async () => {
|
|
102
|
+
await expect(ensurePinnedOpenCode2Cli({
|
|
103
|
+
pin: PINNED_OPENCODE2_VERSION,
|
|
104
|
+
autoInstall: false,
|
|
105
|
+
dataDir: createTempDir('openchamber-ensure-disabled-'),
|
|
106
|
+
readVersion: () => '',
|
|
107
|
+
})).rejects.toMatchObject({ code: 'OPENCODE_CLI_MISSING' });
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it('treats OPENCHAMBER_OPENCODE2_AUTO_INSTALL=0 as disabled', () => {
|
|
111
|
+
expect(isOpenCode2AutoInstallEnabled({ OPENCHAMBER_OPENCODE2_AUTO_INSTALL: '0' })).toBe(false);
|
|
112
|
+
expect(isOpenCode2AutoInstallEnabled({})).toBe(true);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it('resolves the OpenChamber data dir from OPENCHAMBER_DATA_DIR', () => {
|
|
116
|
+
const dataDir = createTempDir('openchamber-data-dir-');
|
|
117
|
+
expect(resolveOpenChamberDataDir({ OPENCHAMBER_DATA_DIR: dataDir })).toBe(path.resolve(dataDir));
|
|
118
|
+
});
|
|
119
|
+
});
|