@openchambery/web 2.0.0-beta.4 → 2.0.0-beta.5
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/dist/assets/{AgentAvatar-Q1SmFRx2.js → AgentAvatar-BBmQXYDq.js} +1 -1
- package/dist/assets/{AssistantView-CwXkiCAd.js → AssistantView-BG8hK4y9.js} +3 -3
- package/dist/assets/{DiagramView-BW92schO.js → DiagramView-CN0qWNU5.js} +1 -1
- package/dist/assets/{FadeInOnReveal-BZmwl-zE.js → FadeInOnReveal-w_BQ7K7K.js} +1 -1
- package/dist/assets/Icon-B9kZgYZn.js +1 -0
- package/dist/assets/{MarkdownRenderer-Bv05iDZZ.js → MarkdownRenderer-Ds61--LT.js} +2 -2
- package/dist/assets/{MarkdownRendererImpl-C81xqYib.js → MarkdownRendererImpl-DOMmiOH0.js} +1 -1
- package/dist/assets/{MarkstreamRendererImpl-CXUCQuKJ.js → MarkstreamRendererImpl-F7MGg5ER.js} +1 -1
- package/dist/assets/{MobileDetailNavigation-BMgIu7zc.js → MobileDetailNavigation-DcmVOjkw.js} +1 -1
- package/dist/assets/{MobileShareBridge-BOXGNHmC.js → MobileShareBridge-C1Fc4ErB.js} +2 -2
- package/dist/assets/MobileSurface-BA7eS5Yd.js +2 -0
- package/dist/assets/{MultiRunWindow-B5At2Z95.js → MultiRunWindow-BoTnH6Yp.js} +1 -1
- package/dist/assets/{OnboardingScreen-bsPLgz4r.js → OnboardingScreen-BxItVnnO.js} +1 -1
- package/dist/assets/{OverlayScrollbar-4wp4migh.js → OverlayScrollbar-B4e0QD05.js} +1 -1
- package/dist/assets/SettingsView-CjesWNLw.js +25 -0
- package/dist/assets/{SettingsWindow-CMtX2bMl.js → SettingsWindow-CReMbg-w.js} +1 -1
- package/dist/assets/TerminalView-C64-QYOG.js +8 -0
- package/dist/assets/{ToolOutputDialog-uH1ZZDSS.js → ToolOutputDialog-CJu8DNrt.js} +1 -1
- package/dist/assets/{appThemeRegistry-DK7q2juD.js → appThemeRegistry-CF4lIxNt.js} +1 -1
- package/dist/assets/{button-CcNJUxUd.js → button-CwXyux0a.js} +1 -1
- package/dist/assets/collapsible-BZ6_cYJd.js +1 -0
- package/dist/assets/context-menu-DHrRifPE.js +1 -0
- package/dist/assets/{desktopHosts-C6HrP0jq.js → desktopHosts-JGFqhlaM.js} +1 -1
- package/dist/assets/{desktopRelayRestore-DdVBp45U.js → desktopRelayRestore-CHEwrI93.js} +1 -1
- package/dist/assets/{diagram-CxTs9hCQ.js → diagram-BwnPNKWt.js} +1 -1
- package/dist/assets/{dialog-KMVL9QKF.js → dialog-Dp0lQv5K.js} +1 -1
- package/dist/assets/es-DicJ8lKD.js +15 -0
- package/dist/assets/{fileMentionTouchSelection-7RIwlpkB.js → fileMentionTouchSelection-DPZRxNa2.js} +3 -4
- package/dist/assets/{fileQueries-CjdBRy8y.js → fileQueries-DN6gbLhM.js} +1 -1
- package/dist/assets/fr-lhgVcBW_.js +13 -0
- package/dist/assets/{gitApi-yuWg9BN4.js → gitApi-D8R7ic-T.js} +1 -1
- package/dist/assets/{imageSource-9vQBq3zp.js → imageSource-BMztRu0c.js} +1 -1
- package/dist/assets/{input-CrIvk3HZ.js → input-Biw8aNzR.js} +1 -1
- package/dist/assets/insertionBoundaries-DdwCD4S2.js +1 -0
- package/dist/assets/ja-B9EZwOcw.js +15 -0
- package/dist/assets/ko-Dxk-uJRb.js +15 -0
- package/dist/assets/main-BhlwwJ2c.js +169 -0
- package/dist/assets/main-D2BzpnUt.css +1 -0
- package/dist/assets/main-DSPCkT74.js +2 -0
- package/dist/assets/{markdownHeightCache-BmXVY_gc.js → markdownHeightCache-BOOrGCuG.js} +1 -1
- package/dist/assets/markdownTheme-DM5h1i18.js +1 -0
- package/dist/assets/{mcpQueries-C7jknHV8.js → mcpQueries-CE9tk-Jz.js} +1 -1
- package/dist/assets/miniChat-Ckk9ABql.js +2 -0
- package/dist/assets/mobile-cLMRZqI9.js +2 -0
- package/dist/assets/multirun-DVnFRb1V.js +4 -0
- package/dist/assets/persistence-D0y7226-.js +3 -0
- package/dist/assets/pl-CFhwWLJj.js +15 -0
- package/dist/assets/platform-D_c-T5nO.js +6 -0
- package/dist/assets/projectMeta-CSQgHequ.js +3 -0
- package/dist/assets/pt-BR-Ba6GfZ6J.js +15 -0
- package/dist/assets/renderElectronMiniChatApp-gGFkwoL_.js +2 -0
- package/dist/assets/{renderMobileApp-CGMMqbsO.js → renderMobileApp-B2w50kAc.js} +7 -7
- package/dist/assets/{runtimeConfig-BEANkvOd.js → runtimeConfig-BHH1hJDd.js} +1 -1
- package/dist/assets/runtimeEndpointReset-5uJ4sJk0.js +149 -0
- package/dist/assets/{sessionChangeSummary-qOLrP9fw.js → sessionChangeSummary-spf_atO8.js} +2 -2
- package/dist/assets/{sessionLookup-34XFmP9C.js → sessionLookup-BGjjwiCq.js} +1 -1
- package/dist/assets/{sortable-tabs-strip-DyEV0Heg.js → sortable-tabs-strip-CTY_wG46.js} +1 -1
- package/dist/assets/src-BjEyU6if.css +2 -0
- package/dist/assets/store-DmowK-GO.js +15 -0
- package/dist/assets/{terminalApi-CXaBEoa_.js → terminalApi-Dhqqp4xi.js} +1 -1
- package/dist/assets/{textarea-BBOlRI9O.js → textarea-QxsmcBGj.js} +1 -1
- package/dist/assets/{tooltip-CPI4fr1a.js → tooltip-CpMLl5_k.js} +1 -1
- package/dist/assets/uk-ChchBzy6.js +15 -0
- package/dist/assets/{url-DRDQXjyX.js → url-CGbJbiJq.js} +1 -1
- package/dist/assets/{useAppFontEffects-CiDe42I5.js → useAppFontEffects-B1h7ab_J.js} +56 -51
- package/dist/assets/{useAssistantUIStore-asNG6K7n.js → useAssistantUIStore-Bz4tRflF.js} +1 -1
- package/dist/assets/useDesktopWindowControlsLayout-Cxsx84NO.js +1 -0
- package/dist/assets/{useEffectiveDirectory-C_CouL5v.js → useEffectiveDirectory-DsqhqsJR.js} +1 -1
- package/dist/assets/{useFontPreferences-B0oUZAiB.js → useFontPreferences-Xw1vSkmC.js} +1 -1
- package/dist/assets/useMobileNavigationStore-B1axS4g-.js +414 -0
- package/dist/assets/{useRuntimeAPIs-DwLtO0c_.js → useRuntimeAPIs-DSLXJybW.js} +1 -1
- package/dist/assets/useSessionAutoCleanup-B5FTG_qf.js +2 -0
- package/dist/assets/{useThemeSystem-BlNUWH47.js → useThemeSystem-DMzWVJaW.js} +1 -1
- package/dist/assets/useUIStore-syVmbuDA.js +1 -0
- package/dist/assets/{useWorkerHighlightedLines-CPNYz6Wf.js → useWorkerHighlightedLines-DC_Bc-RV.js} +1 -1
- package/dist/assets/{vendor-base-ui-CDjyofeD.js → vendor-base-ui-CnHfUmDN.js} +1 -1
- package/dist/assets/{vendor-cmdk-BFH0UpXX.js → vendor-cmdk-CeAGfrLo.js} +1 -1
- package/dist/assets/{vendor-dnd-kit-accessibility-BrR3AKvM.js → vendor-dnd-kit-accessibility-BY5LhhUB.js} +1 -1
- package/dist/assets/{vendor-dnd-kit-core-CNfdvk6H.js → vendor-dnd-kit-core-CvWxUPdb.js} +1 -1
- package/dist/assets/{vendor-dnd-kit-sortable-DQUaTeGj.js → vendor-dnd-kit-sortable-hHdP9lLb.js} +1 -1
- package/dist/assets/{vendor-framer-motion-4dogsUk3.js → vendor-framer-motion-DFy_BSVd.js} +1 -1
- package/dist/assets/{vendor-legendapp-list-BU7MdPZR.js → vendor-legendapp-list-WE32lwNk.js} +1 -1
- package/dist/assets/{vendor-markstream-react-qHC2FykU.js → vendor-markstream-react-DX55XLGO.js} +3 -3
- package/dist/assets/vendor-opencode-client-C12gfjag.js +12 -0
- package/dist/assets/{vendor-pierre-diffs-CUfqC003.js → vendor-pierre-diffs-DX7XTPG3.js} +1 -1
- package/dist/assets/{vendor-react-C94A0AHl.js → vendor-react-DLPElpoQ.js} +1 -1
- package/dist/assets/{vendor-react-drawio-FVlWqKVE.js → vendor-react-drawio-ECtEwvbc.js} +1 -1
- package/dist/assets/{vendor-reactuses-core-C2A3EjUi.js → vendor-reactuses-core-B8parU1Y.js} +1 -1
- package/dist/assets/{vendor-sonner-DNqHmUQs.js → vendor-sonner-DVzioF3d.js} +1 -1
- package/dist/assets/{vendor-stream-diffs-CwbKsmlv.js → vendor-stream-diffs-DWy3xSmp.js} +2 -2
- package/dist/assets/{vendor-tanstack-react-query-D0iFK57C.js → vendor-tanstack-react-query-D7pys_c9.js} +1 -1
- package/dist/assets/{vendor-tanstack-react-virtual-B2kV6cnT.js → vendor-tanstack-react-virtual-tlDgH6PG.js} +1 -1
- package/dist/assets/{vendor-zustand-BqLKf6ZY.js → vendor-zustand-B4dlGEM2.js} +1 -1
- package/dist/assets/zh-CN-Z_bkUQk5.js +15 -0
- package/dist/assets/zh-TW-CZeTm7kd.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 +3 -2
- package/server/index.js +38 -7
- 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/notifications/DOCUMENTATION.md +3 -1
- package/server/lib/notifications/runtime.js +110 -26
- package/server/lib/notifications/runtime.test.js +92 -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 +4 -0
- package/server/lib/opencode/ensure-cli.d.ts +8 -0
- package/server/lib/opencode/feature-routes-runtime.js +10 -0
- package/server/lib/opencode/feature-routes-runtime.test.js +7 -0
- package/server/lib/opencode/proxy.js +3 -1
- package/server/lib/opencode/proxy.test.js +3 -3
- package/server/lib/opencode/routes.js +30 -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/session-metadata/DOCUMENTATION.md +27 -17
- package/server/lib/session-metadata/opencode-session-record.d.ts +57 -0
- package/server/lib/session-metadata/opencode-session-record.js +316 -0
- package/server/lib/session-metadata/opencode-session-record.test.js +242 -0
- package/server/lib/session-metadata/routes.js +5 -1
- package/server/lib/session-metadata/session-archive.js +1 -1
- package/server/lib/session-metadata/session-metadata-store.d.ts +2 -0
- package/server/lib/session-metadata/session-metadata-store.js +63 -16
- package/server/lib/session-metadata/session-projection.js +26 -2
- package/dist/assets/Icon-AQPvxMiU.js +0 -1
- package/dist/assets/MobileSurface-xuc_z0_G.js +0 -2
- package/dist/assets/SettingsView-CJ6tcH5_.js +0 -25
- package/dist/assets/TerminalView-Dvr-ysTS.js +0 -8
- package/dist/assets/collapsible-ZmZ6rLWs.js +0 -3
- package/dist/assets/context-menu-B2LXJgXL.js +0 -1
- package/dist/assets/es-CN3E_zXj.js +0 -15
- package/dist/assets/fr-9zZvQZD2.js +0 -13
- package/dist/assets/insertionBoundaries-BiUCSkx-.js +0 -1
- package/dist/assets/ja-Dnjk_82N.js +0 -15
- package/dist/assets/ko-BxCEE99C.js +0 -15
- package/dist/assets/main-C9mIBo77.js +0 -2
- package/dist/assets/main-DiL6w6KF.js +0 -169
- package/dist/assets/main-qSeeJRW1.css +0 -1
- package/dist/assets/markdownTheme-CKac3wKC.js +0 -1
- package/dist/assets/miniChat-CwtUaElU.js +0 -2
- package/dist/assets/mobile-DB-6--ws.js +0 -2
- package/dist/assets/multirun-TAsxFI8e.js +0 -4
- package/dist/assets/persistence-56kq8Z4V.js +0 -3
- package/dist/assets/pl-CNNsWnPM.js +0 -15
- package/dist/assets/platform-C13fMVEs.js +0 -6
- package/dist/assets/pt-BR-BZB2DJMp.js +0 -15
- package/dist/assets/renderElectronMiniChatApp-CGj0MNRU.js +0 -2
- package/dist/assets/runtimeEndpointReset-A_ZBUJFS.js +0 -147
- package/dist/assets/src-ubCWXZhR.css +0 -2
- package/dist/assets/store-DLCiZaA6.js +0 -15
- package/dist/assets/uk-rwQrxeYM.js +0 -15
- package/dist/assets/useDesktopWindowControlsLayout-BlizAlOt.js +0 -1
- package/dist/assets/useMobileNavigationStore-DfPClaW-.js +0 -414
- package/dist/assets/useSessionAutoCleanup-DtrfbxAp.js +0 -2
- package/dist/assets/useUIStore-CQYKJnrg.js +0 -1
- package/dist/assets/vendor-opencode-client-BOe2qpQb.js +0 -12
- package/dist/assets/zh-CN-BvmBDnkD.js +0 -15
- package/dist/assets/zh-TW-9wcHTX0y.js +0 -15
|
@@ -82,3 +82,99 @@ describe('notification template message extraction', () => {
|
|
|
82
82
|
await expect(runtime.fetchLastAssistantMessageText('session-1', 'msg-1')).resolves.toBe('final answer');
|
|
83
83
|
});
|
|
84
84
|
});
|
|
85
|
+
|
|
86
|
+
describe('notification session titles', () => {
|
|
87
|
+
afterEach(() => {
|
|
88
|
+
globalThis.fetch = originalFetch;
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it('reads the OpenCode 2 session title from the data envelope', async () => {
|
|
92
|
+
const runtime = createNotificationTemplateRuntime({
|
|
93
|
+
readSettingsFromDisk: async () => ({}),
|
|
94
|
+
buildOpenCodeUrl: (path) => `http://opencode.test${path}`,
|
|
95
|
+
getOpenCodeAuthHeaders: () => ({ authorization: 'Basic test' }),
|
|
96
|
+
resolveGitBinaryForSpawn: () => 'git',
|
|
97
|
+
});
|
|
98
|
+
globalThis.fetch = vi.fn(async () => new Response(JSON.stringify({
|
|
99
|
+
data: { id: 'ses_1', title: '发布说明' },
|
|
100
|
+
})));
|
|
101
|
+
|
|
102
|
+
const variables = await runtime.buildTemplateVariables({
|
|
103
|
+
type: 'message.updated',
|
|
104
|
+
location: { directory: '/repo' },
|
|
105
|
+
properties: { info: { sessionID: 'ses_1', role: 'assistant', finish: 'stop' } },
|
|
106
|
+
}, 'ses_1');
|
|
107
|
+
|
|
108
|
+
expect(variables.session_name).toBe('发布说明');
|
|
109
|
+
expect(globalThis.fetch).toHaveBeenCalledWith(
|
|
110
|
+
'http://opencode.test/session/ses_1?directory=%2Frepo',
|
|
111
|
+
expect.objectContaining({
|
|
112
|
+
headers: expect.objectContaining({ authorization: 'Basic test' }),
|
|
113
|
+
}),
|
|
114
|
+
);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it('keeps a title learned from session.renamed when the session fetch fails', async () => {
|
|
118
|
+
const runtime = createRuntime();
|
|
119
|
+
globalThis.fetch = vi.fn(async () => new Response(null, { status: 401 }));
|
|
120
|
+
runtime.maybeCacheSessionInfoFromEvent({
|
|
121
|
+
type: 'session.renamed',
|
|
122
|
+
data: { sessionID: 'ses_1', title: '修通知标题' },
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
const variables = await runtime.buildTemplateVariables({
|
|
126
|
+
type: 'session.execution.succeeded',
|
|
127
|
+
data: { sessionID: 'ses_1' },
|
|
128
|
+
}, 'ses_1');
|
|
129
|
+
|
|
130
|
+
expect(variables.session_name).toBe('修通知标题');
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it('reads the latest assistant text from an OpenCode 2 message page', async () => {
|
|
134
|
+
const runtime = createRuntime();
|
|
135
|
+
globalThis.fetch = vi.fn(async () => new Response(JSON.stringify({
|
|
136
|
+
data: [
|
|
137
|
+
{
|
|
138
|
+
id: 'msg_new',
|
|
139
|
+
sessionID: 'ses_1',
|
|
140
|
+
type: 'assistant',
|
|
141
|
+
agent: 'build',
|
|
142
|
+
model: { id: 'gpt-5', providerID: 'openai' },
|
|
143
|
+
content: [
|
|
144
|
+
{ type: 'reasoning', text: 'private' },
|
|
145
|
+
{ type: 'text', text: '发布说明已写好' },
|
|
146
|
+
],
|
|
147
|
+
time: { created: 2 },
|
|
148
|
+
},
|
|
149
|
+
{
|
|
150
|
+
id: 'msg_old',
|
|
151
|
+
sessionID: 'ses_1',
|
|
152
|
+
type: 'assistant',
|
|
153
|
+
content: [{ type: 'text', text: 'older' }],
|
|
154
|
+
finish: 'stop',
|
|
155
|
+
time: { created: 1 },
|
|
156
|
+
},
|
|
157
|
+
],
|
|
158
|
+
})));
|
|
159
|
+
|
|
160
|
+
await expect(runtime.fetchLastAssistantMessageText('ses_1')).resolves.toBe('发布说明已写好');
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
it('fills agent and model names from the OpenCode 2 session record', async () => {
|
|
164
|
+
const runtime = createRuntime();
|
|
165
|
+
globalThis.fetch = vi.fn(async () => new Response(JSON.stringify({
|
|
166
|
+
data: { id: 'ses_1', title: '发布说明', agent: 'build', model: { id: 'gpt-5', providerID: 'openai' } },
|
|
167
|
+
})));
|
|
168
|
+
|
|
169
|
+
const variables = await runtime.buildTemplateVariables({
|
|
170
|
+
type: 'session.execution.succeeded',
|
|
171
|
+
data: { sessionID: 'ses_1' },
|
|
172
|
+
}, 'ses_1');
|
|
173
|
+
|
|
174
|
+
expect(variables).toMatchObject({
|
|
175
|
+
session_name: '发布说明',
|
|
176
|
+
agent_name: 'Build',
|
|
177
|
+
model_name: 'Gpt 5',
|
|
178
|
+
});
|
|
179
|
+
});
|
|
180
|
+
});
|
|
@@ -15,6 +15,7 @@ This module provides OpenCode server integration utilities for the web server ru
|
|
|
15
15
|
- `packages/web/server/lib/opencode/managed-capabilities-runtime.js`: managed-child scheduled-task resources, config injection, rotating bridge identity, and bridge authorization.
|
|
16
16
|
- `packages/web/server/lib/opencode/env-runtime.js`: OpenCode CLI/binary resolution and shell environment runtime.
|
|
17
17
|
- `packages/web/server/lib/opencode/ensure-cli.js`: detect local `opencode` version and install the pinned official V2 into the OpenChamber data dir when missing or too old. Never uses a packaged/bundled binary.
|
|
18
|
+
- `packages/web/server/lib/opencode/upgrade-screen.js`: startup gate for a detected OpenCode below `2.0.15`. It does not own process lifecycle. `GET /api/opencode/compatibility` classifies the version already detected by startup. `POST /api/opencode/install-required` installs `max(pin, 2.0.15)` through `installPinnedOpenCode2Cli`, points the managed binary at that cache, and restarts through the existing lifecycle hooks. Success requires the running serve to report `2.0.15` or newer. External serves are not replaced. Failure returns `upgraded: false` and the error; it does not claim the previous process was restored.
|
|
18
19
|
- `packages/web/server/lib/opencode/env-config.js`: OpenCode-related environment variable parsing and validation (host/port/hostname).
|
|
19
20
|
- `packages/web/server/lib/opencode/hmr-state-runtime.js`: HMR-persistent runtime state initialization, auth-state bootstrap, and HMR sync helpers.
|
|
20
21
|
- `packages/web/server/lib/opencode/bootstrap-runtime.js`: base app bootstrap runtime for status/auth/tts/notification/OpenChamber route wiring.
|
|
@@ -30,6 +31,7 @@ This module provides OpenCode server integration utilities for the web server ru
|
|
|
30
31
|
- `packages/web/server/lib/opencode/static-routes-runtime.js`: static asset/SPA fallback route registration and manifest route wiring. The SPA fallback sends `index.html` relative to `root`, so deep links keep working when the install lives under a dot directory.
|
|
31
32
|
- `packages/web/server/lib/opencode/feature-routes-runtime.js`: feature route composition runtime for dynamic import-backed config/skill/provider route registration.
|
|
32
33
|
- `packages/web/server/lib/conversations/`: combined create-session-and-prompt orchestration (OpenChamber-owned, registered before generic proxy).
|
|
34
|
+
- `packages/web/server/lib/browser-provider/`: host for an installed extension that declares `browser`. Lists reachable providers, sends one agent browser action to the selected provider, and refuses when none is selected. Does not drive the in-app browser pane. See `browser-provider/DOCUMENTATION.md`.
|
|
33
35
|
- `packages/web/server/lib/session-turn-pages/`: turn-window session message pagination (`GET /api/openchamber/sessions/:sessionID/messages`), anchor reconcile (`GET .../messages/reconcile`), Changes L2/L3 (`GET .../changes`), and exact message L1 projection (`GET /api/session/:sessionID/message/:messageID`); loops official OpenCode `session.messages` for turn pages and head→anchor gap recovery with continuation/budgets/`resetRequired`; all registered before generic proxy. L1 first-packet / SSE / exact-message paths keep `summary.diffs` as a slim `{ file, status?, additions, deletions }` list plus additive `diffCount`/`hasDiffs` (patch bodies stay on L3). See `session-turn-pages/DOCUMENTATION.md`.
|
|
34
36
|
- `packages/web/server/lib/opencode/opencode-resolution-runtime.js`: OpenCode binary resolution snapshot runtime for settings routes and diagnostics.
|
|
35
37
|
- `packages/web/server/lib/opencode/tunnel-wiring-runtime.js`: active loopback-port wiring shared by pairing/LAN URLs and the private relay host.
|
|
@@ -87,6 +89,8 @@ This module provides OpenCode server integration utilities for the web server ru
|
|
|
87
89
|
- `PUT /api/behavior/agents-md`
|
|
88
90
|
- `POST /api/opencode/upgrade` (ticket 12): installs the pinned/target `@opencode/cli` into the **owned cache** at `~/.config/openchamber/opencode-cli/<version>/`. Managed owned-cache processes restart on that binary; official shared services use that exact binary's `service restart`, then adopt its registration again. Success requires a fresh running-serve version probe and runtime contract verification. Arbitrary external servers, private processes using global CLI, and private bundled binaries return HTTP 409; user installs are never overwritten. Concurrent upgrades are single-flight (`operation` state). Active sessions require `confirmActiveTasks`/`force`. Failure preserves cached binaries, restores the previous binary selection, and reports the failed phase; it does not claim the old process was restored. Shared-service recovery can subsequently re-ensure the registered service.
|
|
89
91
|
- `GET /api/opencode/upgrade-status`: returns `currentVersion`/`serveVersion`/`cliVersion`, `targetVersion`/`latestVersion` (pin), `supplySource`, `ownership`, `canManage`, `management`, `guidance`, `contract`, and `operation`. Official shared services report `ownership: 'shared-service'`, `processOwnership: 'shared'`, and `canManage: true`. `available` is true only when `canManage` and target > current; unmanaged global/external runtimes report `available: null` plus manual guidance.
|
|
92
|
+
- `GET /api/opencode/compatibility`: startup upgrade-screen status. `state` is `compatible` at OpenCode `2.0.15` or newer, `incompatible` when a detected version is older or not 2.x, and `unavailable` when no version could be read. `canInstall` is true only for a local (managed or bundled) runtime this platform can install into the owned cache. External serves are reported and not replaced.
|
|
93
|
+
- `POST /api/opencode/install-required`: installs the upgrade-screen target (`2.0.15`, or the pin when the pin is newer) into the owned cache and restarts through the existing lifecycle. `upgraded: true` only after the running serve verifies at or above that target. Failure is `upgraded: false` plus `error`. Concurrent installs are single-flight.
|
|
90
94
|
- `GET /api/opencode/contract` (ticket 11): runtime contract admission for the **running serve** (not health alone). Dimensions: reachable / authenticated / protocolCompatible / executionAllowed / capabilities / migration / CLI↔serve mismatch. Floor `2.0.12` (acceptable OpenCode 2.x at or above); below-min 2.x and 1.x block execution with upgrade guidance; no upper version cap.
|
|
91
95
|
- `GET /api/opencode/health`: probes managed OpenCode `GET /api/info` (Basic auth, 4s timeout). `healthy` is reachability/version shape via `evaluateOpenCodeHealthBody` (admits official 2.x `ServerInfo` and classic `{ healthy: true, version }`; rejects 1.x / missing / `healthy: false`). Response also includes `executionAllowed`, `protocolCompatible`, and `contract` so clients do not treat health success as full execution semantics. Upstream non-OK responses pass through status with `{ healthy: false, error, contract }`; transport failures return HTTP 503.
|
|
92
96
|
- `GET /api/opencode/version`: serve + CLI versions, mismatch flag, and contract snapshot
|
|
@@ -19,6 +19,9 @@ import { registerSessionTurnPageRoutes } from '../session-turn-pages/routes.js';
|
|
|
19
19
|
import { registerAssistantRoutes } from '../assistants/routes.js';
|
|
20
20
|
import { registerLlmRoutes } from '../llm/routes.js';
|
|
21
21
|
import { registerMessageQueueRoutes } from '../message-queue/routes.js';
|
|
22
|
+
import { getBrowserProviderHost } from '../browser-provider/host.js';
|
|
23
|
+
import { registerBrowserProviderRoutes } from '../browser-provider/routes.js';
|
|
24
|
+
import { createBrowserProviderSelectionStore } from '../browser-provider/selection-store.js';
|
|
22
25
|
import { registerSkillRoutes } from './skill-routes.js';
|
|
23
26
|
import { registerPluginRoutes } from './plugin-routes.js';
|
|
24
27
|
import { getNpmInfo, clearCache as clearNpmCache } from './npm-registry.js';
|
|
@@ -513,6 +516,13 @@ export const createFeatureRoutesRuntime = (dependencies) => {
|
|
|
513
516
|
openchamberUserConfigRoot,
|
|
514
517
|
openchamberDataDir,
|
|
515
518
|
});
|
|
519
|
+
const browserProviderHost = getBrowserProviderHost();
|
|
520
|
+
if (typeof openchamberDataDir === 'string' && openchamberDataDir) {
|
|
521
|
+
browserProviderHost.attachSelectionStore(createBrowserProviderSelectionStore(
|
|
522
|
+
path.join(openchamberDataDir, 'browser-provider-selection.json'),
|
|
523
|
+
));
|
|
524
|
+
}
|
|
525
|
+
registerBrowserProviderRoutes(app, { host: browserProviderHost });
|
|
516
526
|
};
|
|
517
527
|
|
|
518
528
|
return {
|
|
@@ -54,6 +54,13 @@ describe('feature routes runtime composition', () => {
|
|
|
54
54
|
expect(source).toMatch(/registerScheduledTaskRoutes\(app, \{[\s\S]*runHistoryStore,/);
|
|
55
55
|
});
|
|
56
56
|
|
|
57
|
+
it('registers browser provider routes on the shared host before proxy composition', async () => {
|
|
58
|
+
const source = await fs.readFile(new URL('./feature-routes-runtime.js', import.meta.url), 'utf8');
|
|
59
|
+
expect(source).toContain("import { registerBrowserProviderRoutes } from '../browser-provider/routes.js';");
|
|
60
|
+
expect(source).toContain('registerBrowserProviderRoutes(app, { host: browserProviderHost });');
|
|
61
|
+
expect(source).toContain('createBrowserProviderSelectionStore(');
|
|
62
|
+
});
|
|
63
|
+
|
|
57
64
|
it('registers message queue routes with the injected service before proxy composition', async () => {
|
|
58
65
|
const source = await fs.readFile(new URL('./feature-routes-runtime.js', import.meta.url), 'utf8');
|
|
59
66
|
expect(source).toContain("import { registerMessageQueueRoutes } from '../message-queue/routes.js';");
|
|
@@ -957,7 +957,9 @@ export const registerOpenCodeProxy = (app, deps) => {
|
|
|
957
957
|
req.path === '/opencode/version' ||
|
|
958
958
|
req.path === '/opencode/contract' ||
|
|
959
959
|
req.path === '/opencode/upgrade-status' ||
|
|
960
|
-
req.path === '/opencode/upgrade'
|
|
960
|
+
req.path === '/opencode/upgrade' ||
|
|
961
|
+
req.path === '/opencode/compatibility' ||
|
|
962
|
+
req.path === '/opencode/install-required'
|
|
961
963
|
) {
|
|
962
964
|
return next();
|
|
963
965
|
}
|
|
@@ -518,7 +518,7 @@ describe('registerOpenCodeProxy session metadata overlay', () => {
|
|
|
518
518
|
return app;
|
|
519
519
|
};
|
|
520
520
|
|
|
521
|
-
it('merges
|
|
521
|
+
it('merges side-store gaps onto the OpenCode record without letting the side row win conflicts', async () => {
|
|
522
522
|
globalThis.fetch = vi.fn(async (url) => {
|
|
523
523
|
const path = String(url).split('?')[0];
|
|
524
524
|
if (path.endsWith('/api/session') || path.endsWith('/session')) {
|
|
@@ -566,7 +566,7 @@ describe('registerOpenCodeProxy session metadata overlay', () => {
|
|
|
566
566
|
id: 'ses_1',
|
|
567
567
|
metadata: {
|
|
568
568
|
fromOpenCode: true,
|
|
569
|
-
shared: '
|
|
569
|
+
shared: 'theirs',
|
|
570
570
|
openchamber: { goal: { status: 'active' } },
|
|
571
571
|
},
|
|
572
572
|
});
|
|
@@ -578,7 +578,7 @@ describe('registerOpenCodeProxy session metadata overlay', () => {
|
|
|
578
578
|
id: 'ses_1',
|
|
579
579
|
metadata: {
|
|
580
580
|
fromOpenCode: true,
|
|
581
|
-
shared: '
|
|
581
|
+
shared: 'theirs',
|
|
582
582
|
openchamber: { goal: { status: 'active' } },
|
|
583
583
|
},
|
|
584
584
|
});
|
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
readOpenCode2BinaryVersion,
|
|
12
12
|
resolveOpenChamberDataDir,
|
|
13
13
|
} from './ensure-cli.js';
|
|
14
|
+
import { registerUpgradeScreenRoutes } from './upgrade-screen.js';
|
|
14
15
|
import { evaluateRuntimeContract } from './runtime-contract.js';
|
|
15
16
|
import {
|
|
16
17
|
buildUpgradeStatusSnapshot,
|
|
@@ -253,6 +254,35 @@ export const registerOpenCodeRoutes = (app, dependencies) => {
|
|
|
253
254
|
return fallback;
|
|
254
255
|
};
|
|
255
256
|
|
|
257
|
+
registerUpgradeScreenRoutes(app, {
|
|
258
|
+
resolveOwnership: resolveOwnershipContext,
|
|
259
|
+
readServeVersion: async () => {
|
|
260
|
+
const probe = await readOpenCodeCurrentVersion().catch(() => ({ currentVersion: null }));
|
|
261
|
+
return probe.currentVersion || null;
|
|
262
|
+
},
|
|
263
|
+
readCliVersion: () => {
|
|
264
|
+
const fromRuntime = typeof getOpenCodeCliVersion === 'function' ? getOpenCodeCliVersion() : null;
|
|
265
|
+
if (fromRuntime) return fromRuntime;
|
|
266
|
+
const binaryPath = typeof getResolvedOpenCodeBinary === 'function' ? getResolvedOpenCodeBinary() : null;
|
|
267
|
+
return binaryPath ? readOpenCode2BinaryVersion(binaryPath) : null;
|
|
268
|
+
},
|
|
269
|
+
readConfiguredBinary: async () => {
|
|
270
|
+
const settings = await readSettingsFromDiskMigrated();
|
|
271
|
+
return typeof settings?.opencodeBinary === 'string' ? settings.opencodeBinary : null;
|
|
272
|
+
},
|
|
273
|
+
persistBinary: async (binaryPath) => {
|
|
274
|
+
await persistSettings({ opencodeBinary: binaryPath });
|
|
275
|
+
},
|
|
276
|
+
getResolvedBinary: getResolvedOpenCodeBinary,
|
|
277
|
+
getResolvedBinarySource: getResolvedOpenCodeBinarySource,
|
|
278
|
+
forceBinary: typeof forceResolvedOpenCodeBinary === 'function' ? forceResolvedOpenCodeBinary : null,
|
|
279
|
+
restart: typeof restartOpenCode === 'function' ? restartOpenCode : null,
|
|
280
|
+
waitReady: typeof waitForOpenCodeReady === 'function' ? waitForOpenCodeReady : null,
|
|
281
|
+
isSharedService: () => getIsSharedOpenCodeService() === true,
|
|
282
|
+
install: (options) => installPinnedOpenCode2Cli({ ...options, dataDir: resolveDataDir() }),
|
|
283
|
+
readBinaryVersion: readOpenCode2BinaryVersion,
|
|
284
|
+
});
|
|
285
|
+
|
|
256
286
|
app.get('/api/config/settings', async (req, res) => {
|
|
257
287
|
try {
|
|
258
288
|
const settings = await readSettingsFromDiskMigrated();
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
export const REQUIRED_OPENCODE_VERSION: string;
|
|
2
|
+
|
|
3
|
+
export type UpgradeScreen = {
|
|
4
|
+
state: 'unavailable' | 'compatible' | 'incompatible';
|
|
5
|
+
version: string | null;
|
|
6
|
+
installation: 'managed' | 'external' | 'bundled';
|
|
7
|
+
minimumVersion: string;
|
|
8
|
+
targetVersion: string;
|
|
9
|
+
canInstall: boolean;
|
|
10
|
+
guidance: string | null;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export function describeUpgradeScreen(input?: {
|
|
14
|
+
version?: string | null;
|
|
15
|
+
installation?: 'managed' | 'external' | 'bundled';
|
|
16
|
+
platformCanInstall?: boolean;
|
|
17
|
+
guidance?: string | null;
|
|
18
|
+
pin?: string;
|
|
19
|
+
}): UpgradeScreen;
|
|
20
|
+
|
|
21
|
+
export function platformSupportsUpgradeScreenInstall(platform?: string, arch?: string): boolean;
|
|
22
|
+
|
|
23
|
+
export function installRequiredOpenCode(deps: object): Promise<{
|
|
24
|
+
ok: true;
|
|
25
|
+
upgraded: true;
|
|
26
|
+
version: string;
|
|
27
|
+
targetVersion: string;
|
|
28
|
+
error: null;
|
|
29
|
+
errorCode: null;
|
|
30
|
+
}>;
|
|
@@ -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
|
+
}
|