@openchambery/web 2.0.0-beta.1 → 2.0.0-beta.11
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-pLaqWWmB.js} +1 -1
- package/dist/assets/AssistantView-C4GkABnI.js +3 -0
- package/dist/assets/{DiagramView-xuSJTnv7.js → DiagramView-ByYLIfWo.js} +1 -1
- package/dist/assets/{FadeInOnReveal-Ov0D96d0.js → FadeInOnReveal-MuJAzJq_.js} +1 -1
- package/dist/assets/Icon-Dz2kO9RN.js +1 -0
- package/dist/assets/MarkdownRenderer-CPU6kSHd.js +2 -0
- package/dist/assets/{MarkdownRendererImpl-SKi4oG0c.js → MarkdownRendererImpl-2hw5cdqQ.js} +3 -3
- package/dist/assets/MarkstreamRendererImpl-DXZQkLTa.js +3 -0
- package/dist/assets/{MobileDetailNavigation-Bnx59U_J.js → MobileDetailNavigation-C9BzieQY.js} +1 -1
- package/dist/assets/{MobileShareBridge-BQlP4J-F.js → MobileShareBridge-2_iOfzqO.js} +2 -2
- package/dist/assets/MobileSurface-DNVA7K_1.js +2 -0
- package/dist/assets/{MultiRunWindow-ChZGob0v.js → MultiRunWindow-QA6pMrJ9.js} +1 -1
- package/dist/assets/{OnboardingScreen-DtQJxZG-.js → OnboardingScreen-XWkr7MjT.js} +1 -1
- package/dist/assets/OverlayScrollbar-DCWyGseO.js +2 -0
- package/dist/assets/SettingsView-dWuu0VIb.js +25 -0
- package/dist/assets/{SettingsWindow-DfnsFa8y.js → SettingsWindow-CetgiJMX.js} +1 -1
- package/dist/assets/TerminalView-BuXuo7gm.js +8 -0
- package/dist/assets/{ToolOutputDialog-BejmjrRk.js → ToolOutputDialog-DNSSOaCt.js} +2 -2
- package/dist/assets/appThemeRegistry-Cb7qFBsc.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-Doq9FI1I.js} +1 -1
- package/dist/assets/{collapsible-BXRd5_Hr.js → collapsible-CYUeu3_N.js} +1 -1
- package/dist/assets/{context-menu-jGtH6fR9.js → context-menu-C8kOMVtC.js} +1 -1
- package/dist/assets/{desktopHosts-B1ixEylr.js → desktopHosts-DbxnIQdS.js} +1 -1
- package/dist/assets/{desktopRelayRestore-4JmFUw8r.js → desktopRelayRestore-dvxF4V2p.js} +1 -1
- package/dist/assets/{diagram-DQf7Y_uR.js → diagram-B1p9Yn1h.js} +1 -1
- package/dist/assets/{dialog-bfbvR4VH.js → dialog-D08-Lg-e.js} +1 -1
- package/dist/assets/es-Dsmc0A_3.js +15 -0
- package/dist/assets/fileMentionTouchSelection-BCAKb-NS.js +3 -0
- package/dist/assets/{fileQueries-DQc8EJUf.js → fileQueries-X773xxvX.js} +1 -1
- package/dist/assets/fr-C_ZlZDI2.js +13 -0
- package/dist/assets/gitApi-3VBj3fbR.js +10 -0
- package/dist/assets/{imageSource-BlAJJUEI.js → imageSource-BMztRu0c.js} +1 -1
- package/dist/assets/{input-8r6F_Fza.js → input-DZZuQ1IL.js} +1 -1
- package/dist/assets/insertionBoundaries-BMuCcvPy.js +1 -0
- package/dist/assets/ja-BqqTaWTi.js +15 -0
- package/dist/assets/ko-DP2XPM37.js +15 -0
- package/dist/assets/main-COEILjRF.js +169 -0
- package/dist/assets/main-D2BzpnUt.css +1 -0
- package/dist/assets/{main-CCE_4y5F.js → main-DsUewWPt.js} +2 -2
- package/dist/assets/markdown-worker-bS5OgGO3.js +2 -0
- package/dist/assets/{markdownHeightCache-1Qo6y6IJ.js → markdownHeightCache-DBgXGze8.js} +1 -1
- package/dist/assets/markdownTheme-DM5h1i18.js +1 -0
- package/dist/assets/mcpQueries-B05QR67V.js +1 -0
- package/dist/assets/miniChat-D7IPMm-k.js +2 -0
- package/dist/assets/mobile-BnCoDxnv.js +2 -0
- package/dist/assets/multirun-BDmFjX9N.js +4 -0
- package/dist/assets/persistence-DznVIKwz.js +3 -0
- package/dist/assets/pl-BCf5uLlK.js +15 -0
- package/dist/assets/platform-DSTtx-DI.js +6 -0
- package/dist/assets/projectMeta-C5gcAd5f.js +3 -0
- package/dist/assets/pt-BR-W09Q2pvn.js +15 -0
- package/dist/assets/{relay-image-stream-DM7atqJs.js → relay-image-stream-DrdVj99c.js} +1 -1
- package/dist/assets/renderElectronMiniChatApp-DsAaWCss.js +2 -0
- package/dist/assets/renderMobileApp-DOaoohlp.js +28 -0
- package/dist/assets/runtime-fetch-Fs9ZV2V8.js +1 -0
- package/dist/assets/{runtimeConfig-BMvqa5we.js → runtimeConfig-CzPK1uMj.js} +1 -1
- package/dist/assets/runtimeEndpointReset-DKjVJCdt.js +149 -0
- package/dist/assets/sessionChangeSummary-CktSWasa.js +2 -0
- package/dist/assets/{sessionLookup-gh-hVRIc.js → sessionLookup-CDA1aAcI.js} +1 -1
- package/dist/assets/{sortable-tabs-strip-CudoSgV4.js → sortable-tabs-strip-Q3bb6MVj.js} +1 -1
- package/dist/assets/src-lS1keDVV.css +2 -0
- package/dist/assets/store-jaP79mqI.js +15 -0
- package/dist/assets/{terminalApi-DiF6JmBP.js → terminalApi-Da_DO_Uh.js} +1 -1
- package/dist/assets/textarea--CYehp07.js +1 -0
- package/dist/assets/{tooltip-CQvpK-UV.js → tooltip-CGLzBwEK.js} +1 -1
- package/dist/assets/uk-BgFzz8m2.js +15 -0
- package/dist/assets/{url-_z7evzdK.js → url-Byz3kd29.js} +1 -1
- package/dist/assets/{useAppFontEffects-Cpmpo7Wb.js → useAppFontEffects-2zzPMQF2.js} +64 -45
- package/dist/assets/{useAssistantUIStore-CMutVh9B.js → useAssistantUIStore-Bz4tRflF.js} +1 -1
- package/dist/assets/useDesktopWindowControlsLayout-B-2YooUd.js +1 -0
- package/dist/assets/useEffectiveDirectory-K3U4eL5F.js +1 -0
- package/dist/assets/{useFontPreferences-DcbXQOkc.js → useFontPreferences-D9OIZs1p.js} +1 -1
- package/dist/assets/useMobileNavigationStore-B0zSdCwP.js +414 -0
- package/dist/assets/useSessionAutoCleanup-BCXHAcbX.js +2 -0
- package/dist/assets/useUIStore-syVmbuDA.js +1 -0
- package/dist/assets/{useWorkerHighlightedLines-Bqn4AiBL.js → useWorkerHighlightedLines-CsoMQNik.js} +2 -2
- package/dist/assets/vendor-opencode-client-C12gfjag.js +12 -0
- package/dist/assets/{vendor-reactuses-core-BnW6GxJt.js → vendor-reactuses-core-Dq5ocTF8.js} +1 -1
- package/dist/assets/zh-CN-BhqSLgUY.js +15 -0
- package/dist/assets/zh-TW-BsaBgY-p.js +15 -0
- package/dist/index.html +12 -12
- package/dist/mini-chat.html +12 -12
- package/dist/mobile.html +12 -12
- package/package.json +3 -2
- package/server/index.js +359 -63
- 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/browser-provider/DOCUMENTATION.md +141 -0
- package/server/lib/browser-provider/contract.js +312 -0
- package/server/lib/browser-provider/host.js +421 -0
- package/server/lib/browser-provider/host.test.js +304 -0
- package/server/lib/browser-provider/routes.js +122 -0
- package/server/lib/browser-provider/routes.test.js +120 -0
- package/server/lib/browser-provider/selection-store.js +43 -0
- package/server/lib/browser-provider/selection-store.test.js +35 -0
- package/server/lib/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/DOCUMENTATION.md +3 -1
- package/server/lib/notifications/runtime.js +133 -31
- package/server/lib/notifications/runtime.test.js +103 -0
- package/server/lib/notifications/template-runtime.js +130 -38
- package/server/lib/notifications/template-runtime.test.js +96 -0
- package/server/lib/opencode/DOCUMENTATION.md +89 -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.d.ts +8 -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 +53 -4
- package/server/lib/opencode/feature-routes-runtime.test.js +25 -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 +254 -46
- package/server/lib/opencode/proxy.test.js +238 -5
- package/server/lib/opencode/routes.js +326 -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/upgrade-screen.d.ts +30 -0
- package/server/lib/opencode/upgrade-screen.js +299 -0
- package/server/lib/opencode/upgrade-screen.test.js +196 -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 +73 -18
- package/server/lib/session-metadata/opencode-session-record.d.ts +57 -0
- package/server/lib/session-metadata/opencode-session-record.js +316 -0
- package/server/lib/session-metadata/opencode-session-record.test.js +242 -0
- package/server/lib/session-metadata/routes.js +112 -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 +38 -0
- package/server/lib/session-metadata/session-metadata-store.js +270 -83
- 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 +279 -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 +69 -44
- 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/Icon-DD-BuMfZ.js +0 -1
- package/dist/assets/MarkdownRenderer-bc_p1H7l.js +0 -2
- 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/TerminalView-Dh9MWCpS.js +0 -8
- package/dist/assets/appThemeRegistry-COGfxTFV.js +0 -1
- package/dist/assets/es-c52LTHHe.js +0 -15
- package/dist/assets/fileMentionTouchSelection-BvrNCroG.js +0 -4
- 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/main-qSeeJRW1.css +0 -1
- package/dist/assets/markdown-worker-CptoICmq.js +0 -2
- package/dist/assets/markdownTheme-DOfGg-A7.js +0 -1
- 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/persistence-Ce3AnpEr.js +0 -3
- 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/vendor-opencode-client-BOe2qpQb.js +0 -12
- 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
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Startup upgrade screen for OpenCode below 2.0.15.
|
|
3
|
+
*
|
|
4
|
+
* This module does not own process lifecycle. It classifies the version the
|
|
5
|
+
* existing startup path already detected, and installs through the owned-cache
|
|
6
|
+
* installer (`installPinnedOpenCode2Cli`) plus the caller's restart hook.
|
|
7
|
+
* Success is a verified running version, never a download that has not started.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import {
|
|
11
|
+
PINNED_OPENCODE2_VERSION,
|
|
12
|
+
isAcceptableOpenCode2HealthVersion,
|
|
13
|
+
isOpenCode1xVersion,
|
|
14
|
+
isOpenCode2VersionAtLeast,
|
|
15
|
+
npmPackageForOpenCode2,
|
|
16
|
+
} from './opencode2-pin.js';
|
|
17
|
+
|
|
18
|
+
/** Oldest OpenCode this OpenChamber build will enter a session against. */
|
|
19
|
+
export const REQUIRED_OPENCODE_VERSION = '2.0.15';
|
|
20
|
+
|
|
21
|
+
const normalizeVersion = (value) => {
|
|
22
|
+
if (typeof value !== 'string') return null;
|
|
23
|
+
const trimmed = value.trim().replace(/^v/i, '');
|
|
24
|
+
return trimmed || null;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Install target is the pin when the pin already satisfies the screen, otherwise
|
|
29
|
+
* the screen minimum. A pin still at 2.0.12 must not be what this screen installs.
|
|
30
|
+
* @param {string} [pin]
|
|
31
|
+
*/
|
|
32
|
+
export function resolveUpgradeScreenTarget(pin = PINNED_OPENCODE2_VERSION) {
|
|
33
|
+
if (isOpenCode2VersionAtLeast(pin, REQUIRED_OPENCODE_VERSION)) return pin;
|
|
34
|
+
return REQUIRED_OPENCODE_VERSION;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** 2.x at or above the screen minimum. A future major is not this contract. */
|
|
38
|
+
function isUpgradeScreenSatisfied(version) {
|
|
39
|
+
const normalized = normalizeVersion(version);
|
|
40
|
+
if (!normalized || !isAcceptableOpenCode2HealthVersion(normalized)) return false;
|
|
41
|
+
return isOpenCode2VersionAtLeast(normalized, REQUIRED_OPENCODE_VERSION);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function isOlderThanUpgradeScreenMinimum(version) {
|
|
45
|
+
const normalized = normalizeVersion(version);
|
|
46
|
+
if (!normalized) return false;
|
|
47
|
+
if (isOpenCode1xVersion(normalized)) return true;
|
|
48
|
+
if (!isAcceptableOpenCode2HealthVersion(normalized)) return false;
|
|
49
|
+
return !isOpenCode2VersionAtLeast(normalized, REQUIRED_OPENCODE_VERSION);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* @param {string | null | undefined} ownership
|
|
54
|
+
* @returns {'managed' | 'external' | 'bundled'}
|
|
55
|
+
*/
|
|
56
|
+
function classifyUpgradeScreenInstallation(ownership) {
|
|
57
|
+
if (ownership === 'external-serve') return 'external';
|
|
58
|
+
if (ownership === 'bundled') return 'bundled';
|
|
59
|
+
return 'managed';
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function platformSupportsUpgradeScreenInstall(platform = process.platform, arch = process.arch) {
|
|
63
|
+
try {
|
|
64
|
+
npmPackageForOpenCode2(platform, arch);
|
|
65
|
+
return true;
|
|
66
|
+
} catch {
|
|
67
|
+
return false;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const pickDetectedVersion = (serveVersion, cliVersion) => {
|
|
72
|
+
const serve = normalizeVersion(serveVersion);
|
|
73
|
+
const cli = normalizeVersion(cliVersion);
|
|
74
|
+
if (serve && !isUpgradeScreenSatisfied(serve)) return serve;
|
|
75
|
+
if (serve) return serve;
|
|
76
|
+
return cli;
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* @param {{
|
|
81
|
+
* version?: string | null,
|
|
82
|
+
* installation?: 'managed' | 'external' | 'bundled',
|
|
83
|
+
* platformCanInstall?: boolean,
|
|
84
|
+
* guidance?: string | null,
|
|
85
|
+
* pin?: string,
|
|
86
|
+
* }} input
|
|
87
|
+
*/
|
|
88
|
+
export function describeUpgradeScreen(input = {}) {
|
|
89
|
+
const version = normalizeVersion(input.version);
|
|
90
|
+
const installation = input.installation === 'external' || input.installation === 'bundled'
|
|
91
|
+
? input.installation
|
|
92
|
+
: 'managed';
|
|
93
|
+
const state = version == null
|
|
94
|
+
? 'unavailable'
|
|
95
|
+
: isUpgradeScreenSatisfied(version)
|
|
96
|
+
? 'compatible'
|
|
97
|
+
: 'incompatible';
|
|
98
|
+
const older = version != null && isOlderThanUpgradeScreenMinimum(version);
|
|
99
|
+
// External processes are not replaced. Bundled and managed locals can receive
|
|
100
|
+
// an owned-cache install; that does not overwrite the user's other binaries.
|
|
101
|
+
const canInstall = state === 'incompatible'
|
|
102
|
+
&& older
|
|
103
|
+
&& installation !== 'external'
|
|
104
|
+
&& input.platformCanInstall === true;
|
|
105
|
+
const guidance = canInstall
|
|
106
|
+
? null
|
|
107
|
+
: (typeof input.guidance === 'string' && input.guidance.trim() ? input.guidance.trim() : null);
|
|
108
|
+
return {
|
|
109
|
+
state,
|
|
110
|
+
version,
|
|
111
|
+
installation,
|
|
112
|
+
minimumVersion: REQUIRED_OPENCODE_VERSION,
|
|
113
|
+
targetVersion: resolveUpgradeScreenTarget(input.pin),
|
|
114
|
+
canInstall,
|
|
115
|
+
guidance,
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* A running serve below the minimum, or a missing version, is not an upgrade.
|
|
121
|
+
* @param {{ targetVersion?: string | null, serveVersion?: string | null }} input
|
|
122
|
+
*/
|
|
123
|
+
export function evaluateUpgradeScreenInstallResult(input = {}) {
|
|
124
|
+
const targetVersion = resolveUpgradeScreenTarget(input.targetVersion || REQUIRED_OPENCODE_VERSION);
|
|
125
|
+
const version = normalizeVersion(input.serveVersion);
|
|
126
|
+
if (!version || !isUpgradeScreenSatisfied(version) || !isOpenCode2VersionAtLeast(version, targetVersion)) {
|
|
127
|
+
const found = version || 'unknown';
|
|
128
|
+
return {
|
|
129
|
+
ok: false,
|
|
130
|
+
upgraded: false,
|
|
131
|
+
version,
|
|
132
|
+
targetVersion,
|
|
133
|
+
errorCode: 'UPGRADE_SCREEN_NOT_VERIFIED',
|
|
134
|
+
error: `OpenCode is still ${found}. OpenChamber requires ${REQUIRED_OPENCODE_VERSION} or newer.`,
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
return {
|
|
138
|
+
ok: true,
|
|
139
|
+
upgraded: true,
|
|
140
|
+
version,
|
|
141
|
+
targetVersion,
|
|
142
|
+
error: null,
|
|
143
|
+
errorCode: null,
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
async function readUpgradeScreenStatus(deps) {
|
|
148
|
+
const ownership = typeof deps.resolveOwnership === 'function'
|
|
149
|
+
? await deps.resolveOwnership()
|
|
150
|
+
: { ownership: 'unknown', guidance: null };
|
|
151
|
+
const installation = classifyUpgradeScreenInstallation(ownership?.ownership);
|
|
152
|
+
const serveVersion = typeof deps.readServeVersion === 'function'
|
|
153
|
+
? await Promise.resolve(deps.readServeVersion()).catch(() => null)
|
|
154
|
+
: null;
|
|
155
|
+
const cliVersion = typeof deps.readCliVersion === 'function'
|
|
156
|
+
? await Promise.resolve(deps.readCliVersion()).catch(() => null)
|
|
157
|
+
: null;
|
|
158
|
+
return describeUpgradeScreen({
|
|
159
|
+
version: pickDetectedVersion(serveVersion, cliVersion),
|
|
160
|
+
installation,
|
|
161
|
+
platformCanInstall: typeof deps.platformCanInstall === 'boolean'
|
|
162
|
+
? deps.platformCanInstall
|
|
163
|
+
: platformSupportsUpgradeScreenInstall(deps.platform, deps.arch),
|
|
164
|
+
guidance: ownership?.guidance ?? null,
|
|
165
|
+
pin: deps.pin,
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const failure = (message, code, status = 500) => {
|
|
170
|
+
const error = new Error(message);
|
|
171
|
+
error.code = code;
|
|
172
|
+
error.status = status;
|
|
173
|
+
return error;
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Install the screen target into the owned cache and restart through the
|
|
178
|
+
* caller's existing lifecycle hooks. Restores the previous binary selection
|
|
179
|
+
* when verification fails. Never returns upgraded:true for a failed install.
|
|
180
|
+
* @param {object} deps
|
|
181
|
+
*/
|
|
182
|
+
export async function installRequiredOpenCode(deps) {
|
|
183
|
+
const status = await readUpgradeScreenStatus(deps);
|
|
184
|
+
if (status.state !== 'incompatible' || status.canInstall !== true) {
|
|
185
|
+
throw failure(
|
|
186
|
+
status.guidance || 'Automatic OpenCode installation is unavailable for this runtime.',
|
|
187
|
+
'UPGRADE_SCREEN_UNAVAILABLE',
|
|
188
|
+
409,
|
|
189
|
+
);
|
|
190
|
+
}
|
|
191
|
+
const target = status.targetVersion;
|
|
192
|
+
const previousBinary = typeof deps.getResolvedBinary === 'function' ? deps.getResolvedBinary() : null;
|
|
193
|
+
const previousSource = typeof deps.getResolvedBinarySource === 'function' ? deps.getResolvedBinarySource() : null;
|
|
194
|
+
const previousSetting = typeof deps.readConfiguredBinary === 'function'
|
|
195
|
+
? await deps.readConfiguredBinary()
|
|
196
|
+
: undefined;
|
|
197
|
+
let persisted = false;
|
|
198
|
+
try {
|
|
199
|
+
if (typeof deps.install !== 'function') {
|
|
200
|
+
throw failure('OpenCode install is not configured.', 'UPGRADE_SCREEN_INSTALL_UNAVAILABLE', 500);
|
|
201
|
+
}
|
|
202
|
+
const installedPath = await deps.install({ version: target });
|
|
203
|
+
const diskVersion = typeof deps.readBinaryVersion === 'function'
|
|
204
|
+
? await Promise.resolve(deps.readBinaryVersion(installedPath))
|
|
205
|
+
: null;
|
|
206
|
+
if (diskVersion !== target) {
|
|
207
|
+
throw failure(
|
|
208
|
+
`Installed OpenCode version mismatch: expected ${target}, got ${diskVersion || 'unknown'}`,
|
|
209
|
+
'UPGRADE_SCREEN_BINARY_MISMATCH',
|
|
210
|
+
500,
|
|
211
|
+
);
|
|
212
|
+
}
|
|
213
|
+
if (typeof deps.persistBinary === 'function') {
|
|
214
|
+
await deps.persistBinary(installedPath);
|
|
215
|
+
persisted = true;
|
|
216
|
+
}
|
|
217
|
+
if (typeof deps.forceBinary === 'function') {
|
|
218
|
+
deps.forceBinary(installedPath, 'installed');
|
|
219
|
+
}
|
|
220
|
+
if (typeof deps.restart === 'function') {
|
|
221
|
+
const shared = typeof deps.isSharedService === 'function' && deps.isSharedService() === true;
|
|
222
|
+
await deps.restart(shared ? { binaryPath: installedPath } : undefined);
|
|
223
|
+
}
|
|
224
|
+
if (typeof deps.waitReady === 'function') {
|
|
225
|
+
await deps.waitReady();
|
|
226
|
+
}
|
|
227
|
+
const serveVersion = typeof deps.readServeVersion === 'function'
|
|
228
|
+
? await deps.readServeVersion()
|
|
229
|
+
: null;
|
|
230
|
+
const verification = evaluateUpgradeScreenInstallResult({ targetVersion: target, serveVersion });
|
|
231
|
+
if (!verification.ok) {
|
|
232
|
+
throw failure(verification.error, verification.errorCode, 500);
|
|
233
|
+
}
|
|
234
|
+
return verification;
|
|
235
|
+
} catch (error) {
|
|
236
|
+
if (persisted && typeof deps.persistBinary === 'function') {
|
|
237
|
+
try {
|
|
238
|
+
await deps.persistBinary(typeof previousSetting === 'string' ? previousSetting : '');
|
|
239
|
+
} catch {
|
|
240
|
+
// Keep the install error. A failed restore must not become success.
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
if (previousBinary && typeof deps.forceBinary === 'function') {
|
|
244
|
+
try {
|
|
245
|
+
deps.forceBinary(previousBinary, previousSource || 'discovered');
|
|
246
|
+
} catch {
|
|
247
|
+
// Same: report the install failure, not a restored-success.
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
throw error;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
export function registerUpgradeScreenRoutes(app, deps) {
|
|
255
|
+
app.get('/api/opencode/compatibility', async (_req, res) => {
|
|
256
|
+
try {
|
|
257
|
+
res.json(await readUpgradeScreenStatus(deps));
|
|
258
|
+
} catch (error) {
|
|
259
|
+
res.status(503).json({
|
|
260
|
+
error: error instanceof Error ? error.message : 'Could not check OpenCode compatibility',
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
let inFlight = null;
|
|
266
|
+
app.post('/api/opencode/install-required', async (_req, res) => {
|
|
267
|
+
try {
|
|
268
|
+
if (!inFlight) {
|
|
269
|
+
inFlight = installRequiredOpenCode(deps).finally(() => {
|
|
270
|
+
inFlight = null;
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
const result = await inFlight;
|
|
274
|
+
if (!result || result.ok !== true || result.upgraded !== true || !result.version) {
|
|
275
|
+
return res.status(500).json({
|
|
276
|
+
success: false,
|
|
277
|
+
upgraded: false,
|
|
278
|
+
error: result?.error || 'OpenCode installation did not verify a new version',
|
|
279
|
+
errorCode: result?.errorCode || 'UPGRADE_SCREEN_NOT_VERIFIED',
|
|
280
|
+
version: result?.version ?? null,
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
return res.json({
|
|
284
|
+
success: true,
|
|
285
|
+
upgraded: true,
|
|
286
|
+
version: result.version,
|
|
287
|
+
targetVersion: result.targetVersion,
|
|
288
|
+
});
|
|
289
|
+
} catch (error) {
|
|
290
|
+
const status = Number.isInteger(error?.status) ? error.status : 500;
|
|
291
|
+
return res.status(status).json({
|
|
292
|
+
success: false,
|
|
293
|
+
upgraded: false,
|
|
294
|
+
error: error instanceof Error ? error.message : 'OpenCode installation failed',
|
|
295
|
+
errorCode: error?.code || 'UPGRADE_SCREEN_FAILED',
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
});
|
|
299
|
+
}
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import express from 'express';
|
|
2
|
+
import request from 'supertest';
|
|
3
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
4
|
+
import {
|
|
5
|
+
REQUIRED_OPENCODE_VERSION,
|
|
6
|
+
describeUpgradeScreen,
|
|
7
|
+
evaluateUpgradeScreenInstallResult,
|
|
8
|
+
installRequiredOpenCode,
|
|
9
|
+
registerUpgradeScreenRoutes,
|
|
10
|
+
resolveUpgradeScreenTarget,
|
|
11
|
+
} from './upgrade-screen.js';
|
|
12
|
+
|
|
13
|
+
describe('upgrade screen version gate', () => {
|
|
14
|
+
it('uses 2.0.15 even when the runtime pin is older', () => {
|
|
15
|
+
expect(REQUIRED_OPENCODE_VERSION).toBe('2.0.15');
|
|
16
|
+
expect(resolveUpgradeScreenTarget('2.0.12')).toBe('2.0.15');
|
|
17
|
+
expect(resolveUpgradeScreenTarget('2.0.16')).toBe('2.0.16');
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it('does not open the gate at or above 2.0.15', () => {
|
|
21
|
+
expect(describeUpgradeScreen({
|
|
22
|
+
version: '2.0.15',
|
|
23
|
+
installation: 'managed',
|
|
24
|
+
platformCanInstall: true,
|
|
25
|
+
})).toMatchObject({ state: 'compatible', canInstall: false, minimumVersion: '2.0.15' });
|
|
26
|
+
expect(describeUpgradeScreen({
|
|
27
|
+
version: 'v2.0.16',
|
|
28
|
+
installation: 'managed',
|
|
29
|
+
platformCanInstall: true,
|
|
30
|
+
}).state).toBe('compatible');
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('opens the gate below 2.0.15 and offers install only when this runtime can switch', () => {
|
|
34
|
+
expect(describeUpgradeScreen({
|
|
35
|
+
version: '2.0.14',
|
|
36
|
+
installation: 'managed',
|
|
37
|
+
platformCanInstall: true,
|
|
38
|
+
})).toMatchObject({ state: 'incompatible', canInstall: true, version: '2.0.14' });
|
|
39
|
+
expect(describeUpgradeScreen({
|
|
40
|
+
version: '1.18.30',
|
|
41
|
+
installation: 'managed',
|
|
42
|
+
platformCanInstall: true,
|
|
43
|
+
})).toMatchObject({ state: 'incompatible', canInstall: true });
|
|
44
|
+
expect(describeUpgradeScreen({
|
|
45
|
+
version: '2.0.14',
|
|
46
|
+
installation: 'external',
|
|
47
|
+
platformCanInstall: true,
|
|
48
|
+
}).canInstall).toBe(false);
|
|
49
|
+
expect(describeUpgradeScreen({
|
|
50
|
+
version: '2.0.14',
|
|
51
|
+
installation: 'bundled',
|
|
52
|
+
platformCanInstall: true,
|
|
53
|
+
})).toMatchObject({ state: 'incompatible', canInstall: true, installation: 'bundled' });
|
|
54
|
+
expect(describeUpgradeScreen({
|
|
55
|
+
version: '2.0.14',
|
|
56
|
+
installation: 'managed',
|
|
57
|
+
platformCanInstall: false,
|
|
58
|
+
}).canInstall).toBe(false);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('does not treat a missing version as an old install', () => {
|
|
62
|
+
expect(describeUpgradeScreen({
|
|
63
|
+
version: null,
|
|
64
|
+
installation: 'managed',
|
|
65
|
+
platformCanInstall: true,
|
|
66
|
+
}).state).toBe('unavailable');
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('does not report an unverified install as upgraded', () => {
|
|
70
|
+
expect(evaluateUpgradeScreenInstallResult({
|
|
71
|
+
targetVersion: '2.0.15',
|
|
72
|
+
serveVersion: '2.0.14',
|
|
73
|
+
})).toMatchObject({ ok: false, upgraded: false, version: '2.0.14' });
|
|
74
|
+
expect(evaluateUpgradeScreenInstallResult({
|
|
75
|
+
targetVersion: '2.0.15',
|
|
76
|
+
serveVersion: null,
|
|
77
|
+
}).upgraded).toBe(false);
|
|
78
|
+
expect(evaluateUpgradeScreenInstallResult({
|
|
79
|
+
targetVersion: '2.0.15',
|
|
80
|
+
serveVersion: '2.0.15',
|
|
81
|
+
})).toMatchObject({ ok: true, upgraded: true, version: '2.0.15' });
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
describe('installRequiredOpenCode', () => {
|
|
86
|
+
const createDeps = (overrides = {}) => {
|
|
87
|
+
const calls = [];
|
|
88
|
+
const deps = {
|
|
89
|
+
resolveOwnership: async () => ({ ownership: 'global-cli', guidance: null }),
|
|
90
|
+
readServeVersion: async () => '2.0.14',
|
|
91
|
+
readCliVersion: () => '2.0.14',
|
|
92
|
+
platformCanInstall: true,
|
|
93
|
+
install: vi.fn(async () => {
|
|
94
|
+
calls.push('install');
|
|
95
|
+
return '/cache/opencode';
|
|
96
|
+
}),
|
|
97
|
+
readBinaryVersion: vi.fn(() => '2.0.15'),
|
|
98
|
+
persistBinary: vi.fn(async () => {
|
|
99
|
+
calls.push('persist');
|
|
100
|
+
}),
|
|
101
|
+
forceBinary: vi.fn(() => {
|
|
102
|
+
calls.push('force');
|
|
103
|
+
}),
|
|
104
|
+
restart: vi.fn(async () => {
|
|
105
|
+
calls.push('restart');
|
|
106
|
+
}),
|
|
107
|
+
waitReady: vi.fn(async () => {
|
|
108
|
+
calls.push('ready');
|
|
109
|
+
}),
|
|
110
|
+
readConfiguredBinary: async () => null,
|
|
111
|
+
isSharedService: () => false,
|
|
112
|
+
...overrides,
|
|
113
|
+
};
|
|
114
|
+
return { deps, calls };
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
it('installs 2.0.15 and only succeeds after the running serve is verified', async () => {
|
|
118
|
+
const { deps, calls } = createDeps({
|
|
119
|
+
readServeVersion: vi.fn()
|
|
120
|
+
.mockResolvedValueOnce('2.0.14')
|
|
121
|
+
.mockResolvedValueOnce('2.0.15'),
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
await expect(installRequiredOpenCode(deps)).resolves.toMatchObject({
|
|
125
|
+
ok: true,
|
|
126
|
+
upgraded: true,
|
|
127
|
+
version: '2.0.15',
|
|
128
|
+
targetVersion: '2.0.15',
|
|
129
|
+
});
|
|
130
|
+
expect(deps.install).toHaveBeenCalledWith({ version: '2.0.15' });
|
|
131
|
+
expect(calls).toEqual(['install', 'persist', 'force', 'restart', 'ready']);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it('returns the failure reason and does not claim an upgrade when verification fails', async () => {
|
|
135
|
+
const { deps } = createDeps({
|
|
136
|
+
readServeVersion: vi.fn()
|
|
137
|
+
.mockResolvedValueOnce('1.18.30')
|
|
138
|
+
.mockResolvedValueOnce('1.18.30'),
|
|
139
|
+
readCliVersion: () => '1.18.30',
|
|
140
|
+
readBinaryVersion: () => '2.0.15',
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
await expect(installRequiredOpenCode(deps)).rejects.toThrow(/still 1\.18\.30/);
|
|
144
|
+
expect(deps.persistBinary).toHaveBeenCalledWith('');
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it('does not install when the runtime cannot switch', async () => {
|
|
148
|
+
const { deps } = createDeps({
|
|
149
|
+
resolveOwnership: async () => ({
|
|
150
|
+
ownership: 'external-serve',
|
|
151
|
+
guidance: 'Restart that server yourself.',
|
|
152
|
+
}),
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
await expect(installRequiredOpenCode(deps)).rejects.toMatchObject({
|
|
156
|
+
status: 409,
|
|
157
|
+
message: 'Restart that server yourself.',
|
|
158
|
+
});
|
|
159
|
+
expect(deps.install).not.toHaveBeenCalled();
|
|
160
|
+
});
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
describe('upgrade screen routes', () => {
|
|
164
|
+
it('hides the gate for 2.0.15 and reports install failure without upgraded:true', async () => {
|
|
165
|
+
const app = express();
|
|
166
|
+
app.use(express.json());
|
|
167
|
+
const readServeVersion = vi.fn()
|
|
168
|
+
.mockResolvedValueOnce('2.0.15')
|
|
169
|
+
.mockResolvedValueOnce('2.0.12')
|
|
170
|
+
.mockResolvedValueOnce('2.0.12');
|
|
171
|
+
registerUpgradeScreenRoutes(app, {
|
|
172
|
+
resolveOwnership: async () => ({ ownership: 'owned-cache', guidance: null }),
|
|
173
|
+
readServeVersion,
|
|
174
|
+
readCliVersion: () => '2.0.12',
|
|
175
|
+
platformCanInstall: true,
|
|
176
|
+
install: vi.fn(async () => {
|
|
177
|
+
throw new Error('registry unreachable');
|
|
178
|
+
}),
|
|
179
|
+
readBinaryVersion: () => '2.0.15',
|
|
180
|
+
persistBinary: vi.fn(),
|
|
181
|
+
forceBinary: vi.fn(),
|
|
182
|
+
restart: vi.fn(),
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
const compatible = await request(app).get('/api/opencode/compatibility').expect(200);
|
|
186
|
+
expect(compatible.body).toMatchObject({ state: 'compatible', version: '2.0.15', minimumVersion: '2.0.15' });
|
|
187
|
+
|
|
188
|
+
const failed = await request(app).post('/api/opencode/install-required').expect(500);
|
|
189
|
+
expect(failed.body).toMatchObject({
|
|
190
|
+
success: false,
|
|
191
|
+
upgraded: false,
|
|
192
|
+
error: 'registry unreachable',
|
|
193
|
+
});
|
|
194
|
+
expect(failed.body.upgraded).not.toBe(true);
|
|
195
|
+
});
|
|
196
|
+
});
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { OpenCode } from '@opencode/client';
|
|
2
|
+
import { wrapFetchWithRuntimeContractGate } from './server-opencode-fetch.js';
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* @typedef {import('@opencode/client').OpenCodeClient} OpenCodeV2ClientRaw
|
|
@@ -19,15 +20,25 @@ import { OpenCode } from '@opencode/client';
|
|
|
19
20
|
|
|
20
21
|
/**
|
|
21
22
|
* Create the single server-side client for official OpenCode v2 APIs.
|
|
23
|
+
* Fetch is always wrapped with runtime-contract write admission so Host
|
|
24
|
+
* background clients (queue / scheduled / assistants) cannot bypass the proxy gate.
|
|
22
25
|
*
|
|
23
|
-
* @param {{
|
|
26
|
+
* @param {{
|
|
27
|
+
* baseUrl: string,
|
|
28
|
+
* authHeaders?: Record<string, string>,
|
|
29
|
+
* fetchImpl?: typeof fetch,
|
|
30
|
+
* getRuntimeContract?: () => (object | null | undefined),
|
|
31
|
+
* }} input
|
|
24
32
|
* @returns {OpenCodeV2Client}
|
|
25
33
|
*/
|
|
26
|
-
export function makeOpenCodeV2Client({ baseUrl, authHeaders, fetchImpl }) {
|
|
34
|
+
export function makeOpenCodeV2Client({ baseUrl, authHeaders, fetchImpl, getRuntimeContract }) {
|
|
35
|
+
const gatedFetch = wrapFetchWithRuntimeContractGate(fetchImpl || fetch, {
|
|
36
|
+
...(typeof getRuntimeContract === 'function' ? { getRuntimeContract } : {}),
|
|
37
|
+
});
|
|
27
38
|
const client = OpenCode.make({
|
|
28
39
|
baseUrl: baseUrl.replace(/\/$/, ''),
|
|
29
40
|
...(authHeaders ? { headers: authHeaders } : {}),
|
|
30
|
-
|
|
41
|
+
fetch: gatedFetch,
|
|
31
42
|
});
|
|
32
43
|
|
|
33
44
|
const messageGet = client.session.message?.get?.bind(client.session.message);
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import { readdirSync, readFileSync, statSync } from "node:fs"
|
|
2
|
+
import { homedir } from "node:os"
|
|
3
|
+
import path from "node:path"
|
|
4
|
+
import { pathToFileURL } from "node:url"
|
|
5
|
+
|
|
6
|
+
const stripJsonc = (text) => text
|
|
7
|
+
.replace(/\/\*[\s\S]*?\*\//g, "")
|
|
8
|
+
.replace(/(^|[^:\\])\/\/.*$/gm, "$1")
|
|
9
|
+
|
|
10
|
+
const readConfigPlugins = () => {
|
|
11
|
+
const specs = []
|
|
12
|
+
const files = []
|
|
13
|
+
if (process.env.OPENCODE_CONFIG) files.push(process.env.OPENCODE_CONFIG)
|
|
14
|
+
const dirs = [process.env.OPENCODE_CONFIG_DIR, path.join(homedir(), ".config", "opencode")].filter(Boolean)
|
|
15
|
+
for (const dir of dirs) {
|
|
16
|
+
files.push(path.join(dir, "opencode.jsonc"), path.join(dir, "opencode.json"), path.join(dir, "config.json"))
|
|
17
|
+
}
|
|
18
|
+
for (const file of files) {
|
|
19
|
+
try {
|
|
20
|
+
const parsed = JSON.parse(stripJsonc(readFileSync(file, "utf8")))
|
|
21
|
+
const plugin = parsed?.plugin
|
|
22
|
+
const list = Array.isArray(plugin) ? plugin : plugin ? [plugin] : []
|
|
23
|
+
for (const entry of list) {
|
|
24
|
+
if (typeof entry === "string") specs.push(entry)
|
|
25
|
+
else if (Array.isArray(entry) && typeof entry[0] === "string") specs.push(entry[0])
|
|
26
|
+
}
|
|
27
|
+
} catch {
|
|
28
|
+
// Missing or invalid config files are skipped; OpenCode still loads the ones it can parse.
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return [...new Set(specs)]
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const packageName = (spec) => {
|
|
35
|
+
const slash = spec.startsWith("@") ? spec.indexOf("/", 1) : -1
|
|
36
|
+
const versionAt = spec.lastIndexOf("@")
|
|
37
|
+
if (versionAt > 0 && versionAt > slash) return spec.slice(0, versionAt)
|
|
38
|
+
return spec
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const resolvePackageEntry = (spec) => {
|
|
42
|
+
const name = packageName(spec)
|
|
43
|
+
const roots = [
|
|
44
|
+
path.join(homedir(), ".cache", "opencode", "npm"),
|
|
45
|
+
path.join(homedir(), ".cache", "opencode", "packages"),
|
|
46
|
+
]
|
|
47
|
+
const candidates = []
|
|
48
|
+
for (const root of roots) {
|
|
49
|
+
let entries = []
|
|
50
|
+
try {
|
|
51
|
+
entries = readdirSync(root)
|
|
52
|
+
} catch {
|
|
53
|
+
continue
|
|
54
|
+
}
|
|
55
|
+
for (const entry of entries) {
|
|
56
|
+
if (entry !== name && !entry.startsWith(`${name}@`)) continue
|
|
57
|
+
const base = path.join(root, entry)
|
|
58
|
+
const packageDirs = [path.join(base, "node_modules", name)]
|
|
59
|
+
try {
|
|
60
|
+
for (const child of readdirSync(base)) {
|
|
61
|
+
if (/^\d+$/.test(child)) packageDirs.push(path.join(base, child, "node_modules", name))
|
|
62
|
+
}
|
|
63
|
+
} catch {
|
|
64
|
+
// The cache entry may be the package directory itself.
|
|
65
|
+
}
|
|
66
|
+
for (const pkgDir of packageDirs) {
|
|
67
|
+
try {
|
|
68
|
+
const pkg = JSON.parse(readFileSync(path.join(pkgDir, "package.json"), "utf8"))
|
|
69
|
+
if (pkg.name !== name) continue
|
|
70
|
+
const exported = pkg.exports?.["."]
|
|
71
|
+
const rel = typeof exported === "string" ? exported : pkg.module || pkg.main || "index.mjs"
|
|
72
|
+
candidates.push({
|
|
73
|
+
dir: pkgDir,
|
|
74
|
+
entry: path.resolve(pkgDir, rel),
|
|
75
|
+
mtime: statSync(path.join(pkgDir, "package.json")).mtimeMs,
|
|
76
|
+
})
|
|
77
|
+
} catch {
|
|
78
|
+
// Not a readable package directory.
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
candidates.sort((left, right) => right.mtime - left.mtime)
|
|
84
|
+
return candidates[0] || null
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const usesCatalog = (dir) => {
|
|
88
|
+
const stack = [dir]
|
|
89
|
+
let seen = 0
|
|
90
|
+
while (stack.length > 0 && seen < 80) {
|
|
91
|
+
const current = stack.pop()
|
|
92
|
+
let names = []
|
|
93
|
+
try {
|
|
94
|
+
names = readdirSync(current)
|
|
95
|
+
} catch {
|
|
96
|
+
continue
|
|
97
|
+
}
|
|
98
|
+
for (const name of names) {
|
|
99
|
+
if (name === "node_modules" || name === ".git") continue
|
|
100
|
+
const full = path.join(current, name)
|
|
101
|
+
let info
|
|
102
|
+
try {
|
|
103
|
+
info = statSync(full)
|
|
104
|
+
} catch {
|
|
105
|
+
continue
|
|
106
|
+
}
|
|
107
|
+
if (info.isDirectory()) {
|
|
108
|
+
stack.push(full)
|
|
109
|
+
continue
|
|
110
|
+
}
|
|
111
|
+
if (!/\.(?:mjs|cjs|js)$/.test(name) || info.size > 2_000_000) continue
|
|
112
|
+
seen += 1
|
|
113
|
+
try {
|
|
114
|
+
if (readFileSync(full, "utf8").includes(".catalog")) return true
|
|
115
|
+
} catch {
|
|
116
|
+
// Unreadable files are not catalog callers.
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
return false
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const catalogFor = (ctx) => ({
|
|
124
|
+
// OpenCode 2.0.12 removed ctx.catalog. Model plugins still call
|
|
125
|
+
// draft.provider.update / draft.model.update inside catalog.transform.
|
|
126
|
+
transform: (fn) => ctx.provider?.transform?.((draft) => fn({
|
|
127
|
+
provider: {
|
|
128
|
+
update: (id, updater) => draft.update(id, updater),
|
|
129
|
+
add: (...args) => draft.add?.(...args),
|
|
130
|
+
},
|
|
131
|
+
model: {
|
|
132
|
+
update: (providerID, modelID, updater) => draft.models?.update?.(providerID, modelID, updater),
|
|
133
|
+
set: (providerID, modelID, value) => draft.models?.set?.(providerID, modelID, value),
|
|
134
|
+
},
|
|
135
|
+
})),
|
|
136
|
+
reload: () => ctx.provider?.reload?.() ?? ctx.model?.reload?.(),
|
|
137
|
+
})
|
|
138
|
+
|
|
139
|
+
export default {
|
|
140
|
+
id: "openchamber-v2-plugin-host-shim",
|
|
141
|
+
setup: async (ctx) => {
|
|
142
|
+
if (typeof ctx?.catalog?.transform === "function") return
|
|
143
|
+
const patched = { ...ctx, catalog: catalogFor(ctx) }
|
|
144
|
+
for (const spec of readConfigPlugins()) {
|
|
145
|
+
const resolved = resolvePackageEntry(spec)
|
|
146
|
+
if (!resolved || !usesCatalog(resolved.dir)) continue
|
|
147
|
+
try {
|
|
148
|
+
const mod = await import(pathToFileURL(resolved.entry).href)
|
|
149
|
+
if (typeof mod.default?.setup === "function") await mod.default.setup(patched)
|
|
150
|
+
} catch (error) {
|
|
151
|
+
console.error(`[openchamber] v2 plugin host shim skipped ${spec}: ${error instanceof Error ? error.message : String(error)}`)
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
},
|
|
155
|
+
}
|