@openchambery/web 2.0.0-beta.8 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +14 -0
- package/dist/assets/{AgentAvatar-CzTtVMnI.js → AgentAvatar-C1MGZ7qC.js} +1 -1
- package/dist/assets/AssistantView-vqCFvVo0.js +3 -0
- package/dist/assets/{DiagramView-BBPsLqLq.js → DiagramView-CYO5iVVm.js} +1 -1
- package/dist/assets/{FadeInOnReveal-DCqrQkkz.js → FadeInOnReveal-BMSk_tY-.js} +1 -1
- package/dist/assets/Icon-BdzPRCSy.js +1 -0
- package/dist/assets/MarkdownRenderer-FMynjkGR.js +2 -0
- package/dist/assets/{MarkdownRendererImpl-CD0RYNWX.js → MarkdownRendererImpl-DoWvw5jj.js} +3 -3
- package/dist/assets/{MarkstreamRendererImpl-CECVYTH6.js → MarkstreamRendererImpl-DoeYajmw.js} +1 -1
- package/dist/assets/{MobileDetailNavigation-DOwW-F0i.js → MobileDetailNavigation-BzBz-co_.js} +1 -1
- package/dist/assets/{MobileShareBridge-d0hQ2FSQ.js → MobileShareBridge-D1Uoj6l_.js} +3 -3
- package/dist/assets/MobileSurface-8YpZ51Qo.js +2 -0
- package/dist/assets/{MultiRunWindow-DOdFEgMg.js → MultiRunWindow-BxcYUPta.js} +1 -1
- package/dist/assets/{OnboardingScreen-O4cGdz5p.js → OnboardingScreen-yRdQTR1M.js} +1 -1
- package/dist/assets/{OverlayScrollbar-S-7EePsU.js → OverlayScrollbar-BUjtqnxn.js} +1 -1
- package/dist/assets/SettingsView-D7KrvqvH.js +25 -0
- package/dist/assets/{SettingsWindow-vKn-MWGU.js → SettingsWindow-DX2w-cDc.js} +1 -1
- package/dist/assets/{TerminalView-BgPjY0CO.js → TerminalView-CiUcCQPm.js} +1 -1
- package/dist/assets/{ToolOutputDialog-DtT3wbie.js → ToolOutputDialog-DQvXtK71.js} +1 -1
- package/dist/assets/appThemeRegistry-BXC6cIDE.js +1 -0
- package/dist/assets/{assistant-attachment-cache-DphJleHr.js → assistant-attachment-cache-sG8YQsVs.js} +1 -1
- package/dist/assets/{button-DIIrsMjW.js → button-CUwjpv-i.js} +1 -1
- package/dist/assets/{collapsible-CnvUeYst.js → collapsible-paItOH3A.js} +1 -1
- package/dist/assets/{context-menu-BGBUpZEQ.js → context-menu-K6zNumqN.js} +1 -1
- package/dist/assets/{desktopHosts-JGFqhlaM.js → desktopHosts-DuftEj8B.js} +1 -1
- package/dist/assets/desktopRelayRestore-g378BdZf.js +1 -0
- package/dist/assets/{diagram-rvn7_qBM.js → diagram-D6nY5hbD.js} +1 -1
- package/dist/assets/dialog-PjKWayIU.js +1 -0
- package/dist/assets/dropdown-menu.styles-ayLWk2gf.js +1 -0
- package/dist/assets/{es-Dsmc0A_3.js → es-BBbon7SL.js} +4 -4
- package/dist/assets/fileMentionTouchSelection-78v5mNTE.js +2 -0
- package/dist/assets/{fileQueries-DN6gbLhM.js → fileQueries-DuF89nFO.js} +1 -1
- package/dist/assets/{fr-C_ZlZDI2.js → fr-BsKj2CTi.js} +4 -4
- package/dist/assets/{gitApi-D9ygh31C.js → gitApi-eoKSJcvW.js} +4 -4
- package/dist/assets/{imageSource-BMztRu0c.js → imageSource-BpUsiwFd.js} +1 -1
- package/dist/assets/{input-BVu-iykl.js → input-BeVw4D0W.js} +1 -1
- package/dist/assets/insertionBoundaries-BI3RHOpf.js +1 -0
- package/dist/assets/{ja-BqqTaWTi.js → ja-1mgxunoM.js} +4 -4
- package/dist/assets/{ko-DP2XPM37.js → ko-Ra9znPcL.js} +4 -4
- package/dist/assets/main-CQpUPjPK.js +169 -0
- package/dist/assets/{main-B97oNWoh.js → main-MN0YKJ0_.js} +2 -2
- package/dist/assets/main-qSeeJRW1.css +1 -0
- package/dist/assets/{markdownHeightCache-D9-B1wdY.js → markdownHeightCache-CGCWmHgq.js} +1 -1
- package/dist/assets/{mcpQueries-zC4-V5b8.js → mcpQueries-X424Iq2z.js} +1 -1
- package/dist/assets/miniChat-DFIl76ku.js +2 -0
- package/dist/assets/mobile-C3fWXGQJ.js +2 -0
- package/dist/assets/multirun-DuSX3YRs.js +3 -0
- package/dist/assets/persistence-Dnj1QOdX.js +3 -0
- package/dist/assets/pl-B714sdql.js +15 -0
- package/dist/assets/platform-jCZ3KfMQ.js +6 -0
- package/dist/assets/projectMeta-Cn08pTNi.js +1 -0
- package/dist/assets/{pt-BR-W09Q2pvn.js → pt-BR-DVoT_I7_.js} +4 -4
- package/dist/assets/{relay-image-stream-DrdVj99c.js → relay-image-stream-D2XooXKZ.js} +1 -1
- package/dist/assets/renderElectronMiniChatApp-CRPdctIn.js +2 -0
- package/dist/assets/renderMobileApp-CVURQbeh.js +28 -0
- package/dist/assets/{runtime-fetch-Fs9ZV2V8.js → runtime-fetch-CiqQd__Y.js} +1 -1
- package/dist/assets/runtime-switch-BJM2k_uH.js +1 -0
- package/dist/assets/runtimeConfig-xivE1wxC.js +2 -0
- package/dist/assets/runtimeEndpointReset-B8DzKzwg.js +148 -0
- package/dist/assets/runtimeEndpointReset-D_vfNOQz.css +1 -0
- package/dist/assets/sessionChangeSummary-yBsR2pyd.js +2 -0
- package/dist/assets/{sessionLookup-DcyGP3xg.js → sessionLookup-1Z1FWZRB.js} +1 -1
- package/dist/assets/{sortable-tabs-strip-BsObV7Pl.js → sortable-tabs-strip-D611LPyd.js} +1 -1
- package/dist/assets/src-TBM7scxy.css +2 -0
- package/dist/assets/{store-jaP79mqI.js → store-l_X_jQNz.js} +4 -4
- package/dist/assets/{terminalApi-Dhqqp4xi.js → terminalApi-BZW_rAc3.js} +1 -1
- package/dist/assets/{textarea-BoXpPjTd.js → textarea-QtZjf4L8.js} +1 -1
- package/dist/assets/{tooltip-DYeQ_Hvy.js → tooltip-DDNnmcXI.js} +1 -1
- package/dist/assets/{uk-BgFzz8m2.js → uk-ChNnTgKP.js} +4 -4
- package/dist/assets/{url-CGbJbiJq.js → url-DNRMq_0R.js} +1 -1
- package/dist/assets/{useAppFontEffects-BauzQXF0.js → useAppFontEffects-DJy8P5By.js} +51 -51
- package/dist/assets/{useAssistantUIStore-Bz4tRflF.js → useAssistantUIStore-DBwfg9nX.js} +1 -1
- package/dist/assets/useDesktopWindowControlsLayout-DlwSomVK.js +1 -0
- package/dist/assets/{useEffectiveDirectory-qDoROBC_.js → useEffectiveDirectory-eJorVTT9.js} +1 -1
- package/dist/assets/{useFontPreferences-Xw1vSkmC.js → useFontPreferences-Bjgt_lzl.js} +1 -1
- package/dist/assets/useMobileNavigationStore-A_ZNTfpl.js +415 -0
- package/dist/assets/useSessionAutoCleanup-CqC3dEou.js +2 -0
- package/dist/assets/{useUIStore-syVmbuDA.js → useUIStore-Dfx6TKvi.js} +1 -1
- package/dist/assets/{useWorkerHighlightedLines-D8luxgem.js → useWorkerHighlightedLines-B0vSL6jr.js} +1 -1
- package/dist/assets/vendor-opencode-client-MF2GBrFx.js +12 -0
- package/dist/assets/{vendor-zustand-B4dlGEM2.js → vendor-zustand-DzRqcymP.js} +1 -1
- package/dist/assets/{zh-CN-BhqSLgUY.js → zh-CN-DHEFf4u1.js} +4 -4
- package/dist/assets/{zh-TW-BsaBgY-p.js → zh-TW-rdSa8atW.js} +4 -4
- package/dist/index.html +13 -13
- package/dist/mini-chat.html +13 -13
- package/dist/mobile.html +13 -13
- package/package.json +3 -3
- package/server/index.js +8 -20
- package/server/lib/client-auth/remote-clients.js +15 -0
- package/server/lib/client-auth/remote-clients.test.js +42 -1
- package/server/lib/git/DOCUMENTATION.md +1 -0
- package/server/lib/git/routes.js +7 -4
- package/server/lib/git/routes.test.js +34 -0
- package/server/lib/git/service.js +3 -1
- package/server/lib/llm/DOCUMENTATION.md +6 -4
- package/server/lib/llm/generate.js +8 -9
- package/server/lib/llm/generate.test.js +33 -6
- package/server/lib/message-queue/service.js +1 -1
- package/server/lib/opencode/DOCUMENTATION.md +33 -22
- package/server/lib/opencode/agents.js +13 -22
- package/server/lib/opencode/auth-state-runtime.js +3 -3
- package/server/lib/opencode/auth-state-runtime.test.js +59 -0
- package/server/lib/opencode/auth.d.ts +13 -0
- package/server/lib/opencode/auth.js +74 -64
- package/server/lib/opencode/auth.test.js +57 -0
- package/server/lib/opencode/commands.js +1 -1
- package/server/lib/opencode/config-entities-layering.test.js +72 -0
- package/server/lib/opencode/config-entity-routes.js +10 -123
- package/server/lib/opencode/config-entity-routes.test.js +23 -11
- package/server/lib/opencode/config-files.d.ts +17 -0
- package/server/lib/opencode/config-files.js +187 -0
- package/server/lib/opencode/config-files.test.js +201 -0
- package/server/lib/opencode/core-routes.js +17 -1
- package/server/lib/opencode/core-routes.test.js +43 -15
- package/server/lib/opencode/credential-boundary.d.ts +1 -0
- package/server/lib/opencode/credential-boundary.js +11 -0
- package/server/lib/opencode/ensure-cli.js +4 -1
- package/server/lib/opencode/env-config.js +4 -1
- package/server/lib/opencode/env-config.test.js +28 -0
- package/server/lib/opencode/env-runtime.test.js +18 -1
- package/server/lib/opencode/feature-routes-runtime.js +9 -7
- package/server/lib/opencode/hmr-state-runtime.js +4 -4
- package/server/lib/opencode/lifecycle.js +1 -49
- package/server/lib/opencode/lifecycle.test.js +30 -4
- package/server/lib/opencode/location-release.d.ts +8 -0
- package/server/lib/opencode/location-release.js +32 -0
- package/server/lib/opencode/location-release.test.js +43 -0
- package/server/lib/opencode/mcp.js +1 -1
- package/server/lib/opencode/opencode2-pin.js +2 -1
- package/server/lib/opencode/owned-runtime-upgrade.test.js +5 -5
- package/server/lib/opencode/plugin-routes.test.js +4 -2
- package/server/lib/opencode/plugins.js +12 -60
- package/server/lib/opencode/plugins.test.js +5 -3
- package/server/lib/opencode/providers.js +6 -3
- package/server/lib/opencode/proxy.js +7 -0
- package/server/lib/opencode/proxy.test.js +13 -0
- package/server/lib/opencode/remote-upgrade.integration.test.js +66 -0
- package/server/lib/opencode/routes.js +10 -8
- package/server/lib/opencode/routes.test.js +51 -10
- package/server/lib/opencode/runtime-contract.js +3 -2
- package/server/lib/opencode/runtime-contract.test.js +12 -12
- package/server/lib/opencode/settings-normalization-runtime.js +3 -1
- package/server/lib/opencode/settings-normalization-runtime.test.js +36 -0
- package/server/lib/opencode/settings-runtime.test.js +58 -1
- package/server/lib/opencode/shared.js +6 -175
- package/server/lib/opencode/update-discovery.js +39 -0
- package/server/lib/opencode/update-discovery.test.js +39 -0
- package/server/lib/opencode/upgrade-screen.js +4 -11
- package/server/lib/opencode/upgrade-screen.test.js +25 -25
- package/server/lib/opencode/version-alignment.test.js +40 -0
- package/server/lib/question-auto-delegate/DOCUMENTATION.md +21 -6
- package/server/lib/question-auto-delegate/core.d.ts +5 -1
- package/server/lib/question-auto-delegate/core.js +21 -13
- package/server/lib/question-auto-delegate/form-io.d.ts +6 -0
- package/server/lib/question-auto-delegate/form-io.js +39 -0
- package/server/lib/question-auto-delegate/runtime.js +4 -24
- package/server/lib/question-auto-delegate/runtime.test.js +138 -0
- package/server/lib/quota/DOCUMENTATION.md +9 -0
- package/server/lib/quota/provider-credentials.test.js +29 -0
- package/server/lib/quota/providers/claude.js +4 -4
- package/server/lib/quota/providers/codex.js +4 -4
- package/server/lib/quota/providers/codex.test.js +1 -1
- package/server/lib/quota/providers/copilot.js +5 -5
- package/server/lib/quota/providers/google/auth.js +3 -3
- package/server/lib/quota/providers/google/index.js +2 -2
- package/server/lib/quota/providers/index.js +3 -7
- package/server/lib/quota/providers/index.test.js +2 -2
- package/server/lib/quota/providers/kimi.js +4 -4
- package/server/lib/quota/providers/minimax-shared.js +4 -4
- package/server/lib/quota/providers/nanogpt.js +4 -4
- package/server/lib/quota/providers/openai.js +4 -4
- package/server/lib/quota/providers/openrouter.js +4 -4
- package/server/lib/quota/providers/wafer.js +4 -4
- package/server/lib/quota/providers/zai.js +4 -4
- package/server/lib/quota/providers/zhipuai-coding-plan.js +6 -20
- package/server/lib/quota/routes.js +1 -1
- package/server/lib/relay/DOCUMENTATION.md +3 -1
- package/server/lib/relay/service.js +1 -0
- package/server/lib/relay/service.test.js +13 -0
- package/server/lib/scheduled-tasks/DOCUMENTATION.md +8 -1
- package/server/lib/scheduled-tasks/runtime.js +12 -4
- package/server/lib/scheduled-tasks/runtime.test.js +81 -6
- package/server/lib/session-goal/DOCUMENTATION.md +11 -0
- package/server/lib/session-goal/runtime.js +4 -4
- package/server/lib/session-goal/runtime.test.js +32 -3
- package/server/lib/session-index/DOCUMENTATION.md +7 -0
- package/server/lib/session-index/event-ingest.js +13 -0
- package/server/lib/session-index/event-ingest.test.js +7 -0
- package/server/lib/session-index/service.js +45 -0
- package/server/lib/session-index/service.test.js +47 -0
- package/server/lib/session-metadata/DOCUMENTATION.md +6 -0
- package/server/lib/session-metadata/session-metadata-store.test.js +23 -0
- package/server/lib/session-metadata/session-projection.d.ts +1 -0
- package/server/lib/session-metadata/session-projection.js +23 -0
- package/server/lib/session-metadata/session-projection.test.js +35 -0
- package/server/lib/session-title/DOCUMENTATION.md +52 -14
- package/server/lib/session-title/native-runtime.test.js +59 -5
- package/server/lib/session-title/runtime.js +63 -17
- package/server/lib/session-title/session-access.js +108 -3
- package/server/lib/session-title/session-access.test.js +125 -7
- package/server/lib/tts/DOCUMENTATION.md +7 -5
- package/server/lib/tts/credential-source.test.js +23 -0
- package/server/lib/tts/routes.js +2 -2
- package/server/lib/tts/service.js +16 -37
- package/dist/assets/AssistantView-CxAzKd78.js +0 -3
- package/dist/assets/Icon-B8fSTLdP.js +0 -1
- package/dist/assets/MarkdownRenderer-yq2DPZUG.js +0 -2
- package/dist/assets/MobileSurface-qalyCN7X.js +0 -2
- package/dist/assets/SettingsView-DVxMbeKC.js +0 -25
- package/dist/assets/appThemeRegistry-DC5NRV_i.js +0 -1
- package/dist/assets/desktopRelayRestore-CHEwrI93.js +0 -1
- package/dist/assets/dialog-CQ63sc9E.js +0 -1
- package/dist/assets/dropdown-menu.styles-Bzo6L3FH.js +0 -1
- package/dist/assets/fileMentionTouchSelection-B8Blr6EI.js +0 -3
- package/dist/assets/insertionBoundaries-DYu8VdgE.js +0 -1
- package/dist/assets/main-D2BzpnUt.css +0 -1
- package/dist/assets/main-SXMSe99Y.js +0 -169
- package/dist/assets/miniChat-CIi4PmQO.js +0 -2
- package/dist/assets/mobile-BK0Ov-k5.js +0 -2
- package/dist/assets/multirun-UIu_EbC2.js +0 -4
- package/dist/assets/persistence-D0y7226-.js +0 -3
- package/dist/assets/pl-BCf5uLlK.js +0 -15
- package/dist/assets/platform-D_c-T5nO.js +0 -6
- package/dist/assets/projectMeta-BPi16qGY.js +0 -3
- package/dist/assets/renderElectronMiniChatApp-CnTCJpBd.js +0 -2
- package/dist/assets/renderMobileApp-BFDrOvfv.js +0 -28
- package/dist/assets/runtime-switch-LvWDU-SL.js +0 -1
- package/dist/assets/runtimeConfig-023C-il6.js +0 -2
- package/dist/assets/runtimeEndpointReset-Bdx3CutT.js +0 -149
- package/dist/assets/sessionChangeSummary-B8xrgW5-.js +0 -2
- package/dist/assets/src-lS1keDVV.css +0 -2
- package/dist/assets/useDesktopWindowControlsLayout-kZ4wtPnO.js +0 -1
- package/dist/assets/useMobileNavigationStore-BDXu5FSG.js +0 -414
- package/dist/assets/useSessionAutoCleanup-BHloMhRp.js +0 -2
- package/dist/assets/vendor-opencode-client-C12gfjag.js +0 -12
- package/server/lib/opencode/snippets.js +0 -242
- package/server/lib/opencode/snippets.test.js +0 -97
|
@@ -10,6 +10,8 @@ vi.mock('node:child_process', () => ({
|
|
|
10
10
|
|
|
11
11
|
const { createOpenCodeLifecycleRuntime } = await import('./lifecycle.js');
|
|
12
12
|
const { createOpenCodeNetworkRuntime } = await import('./network-runtime.js');
|
|
13
|
+
const { resolveOpenCodeEnvConfig } = await import('./env-config.js');
|
|
14
|
+
const { RUNTIME_CONTRACT_MIN_VERIFIED } = await import('./runtime-contract.js');
|
|
13
15
|
|
|
14
16
|
const originalOpencodeBinary = process.env.OPENCODE_BINARY;
|
|
15
17
|
const originalPath = process.env.PATH;
|
|
@@ -106,6 +108,7 @@ const createRuntime = (overrides = {}, stateRef = null) => {
|
|
|
106
108
|
getManagedOpenCodeShellEnvSnapshot: vi.fn(() => ({
|
|
107
109
|
PATH: '/home/user/.bun/bin:/usr/local/bin:/usr/bin',
|
|
108
110
|
SHELL_ONLY: 'yes',
|
|
111
|
+
OPENCODE_PASSWORD: 'shell-v2-password',
|
|
109
112
|
OPENCODE_SERVER_PASSWORD: 'shell-password',
|
|
110
113
|
})),
|
|
111
114
|
...overrides,
|
|
@@ -160,6 +163,28 @@ const startListeningChild = () => {
|
|
|
160
163
|
};
|
|
161
164
|
|
|
162
165
|
describe('OpenCode lifecycle', () => {
|
|
166
|
+
it.each(['http://example.test:80', 'https://example.test:443', 'http://[::1]:80'])('attaches explicit default-port host %s without spawning', async (host) => {
|
|
167
|
+
const config = resolveOpenCodeEnvConfig({ env: { OPENCODE_HOST: host, OPENCODE_PORT: '9999' } });
|
|
168
|
+
const stateRef = {};
|
|
169
|
+
const runtime = createRuntime({
|
|
170
|
+
env: {
|
|
171
|
+
ENV_CONFIGURED_OPENCODE_PORT: config.configuredOpenCodePort,
|
|
172
|
+
ENV_CONFIGURED_OPENCODE_HOST: config.configuredOpenCodeHost,
|
|
173
|
+
ENV_EFFECTIVE_PORT: config.effectivePort,
|
|
174
|
+
ENV_CONFIGURED_OPENCODE_HOSTNAME: '127.0.0.1',
|
|
175
|
+
ENV_SKIP_OPENCODE_START: true,
|
|
176
|
+
},
|
|
177
|
+
buildOpenCodeUrl: (route) => new URL(route, config.configuredOpenCodeHost.origin).href,
|
|
178
|
+
}, stateRef);
|
|
179
|
+
const fetchMock = stubOpenCodeFetch({ health: { ...DEFAULT_V2_HEALTH, version: '2.0.23' } });
|
|
180
|
+
await runtime.bootstrapOpenCodeAtStartup();
|
|
181
|
+
await runtime.waitForOpenCodeReady(1000, 1);
|
|
182
|
+
expect(stateRef.current.isExternalOpenCode).toBe(true);
|
|
183
|
+
expect(stateRef.current.openCodePort).toBe(config.effectivePort);
|
|
184
|
+
expect(fetchMock.mock.calls.some(([url]) => String(url) === `${config.configuredOpenCodeHost.origin}/api/info`)).toBe(true);
|
|
185
|
+
expect(spawnMock).not.toHaveBeenCalled();
|
|
186
|
+
});
|
|
187
|
+
|
|
163
188
|
it('launches managed OpenCode with the managed PATH', async () => {
|
|
164
189
|
delete process.env.OPENCODE_BINARY;
|
|
165
190
|
const child = createMockChild();
|
|
@@ -179,6 +204,7 @@ describe('OpenCode lifecycle', () => {
|
|
|
179
204
|
expect(options.env.PATH).toBe('/home/user/.bun/bin:/usr/local/bin:/usr/bin');
|
|
180
205
|
expect(options.env.SHELL_ONLY).toBe('yes');
|
|
181
206
|
expect(options.env.OPENCODE_SERVER_PASSWORD).toBe('password');
|
|
207
|
+
expect(options.env.OPENCODE_PASSWORD).toBe('password');
|
|
182
208
|
|
|
183
209
|
await server.close();
|
|
184
210
|
});
|
|
@@ -847,7 +873,7 @@ describe('OpenCode lifecycle', () => {
|
|
|
847
873
|
stateRef.current.runtimeContract = {
|
|
848
874
|
executionAllowed: true,
|
|
849
875
|
phase: 'ready',
|
|
850
|
-
serveVersion:
|
|
876
|
+
serveVersion: RUNTIME_CONTRACT_MIN_VERIFIED,
|
|
851
877
|
instanceGeneration: 1,
|
|
852
878
|
};
|
|
853
879
|
|
|
@@ -861,7 +887,7 @@ describe('OpenCode lifecycle', () => {
|
|
|
861
887
|
runtime.refreshRuntimeContractFromProbe(
|
|
862
888
|
{
|
|
863
889
|
ok: true,
|
|
864
|
-
version:
|
|
890
|
+
version: RUNTIME_CONTRACT_MIN_VERIFIED,
|
|
865
891
|
authenticated: true,
|
|
866
892
|
healthOk: true,
|
|
867
893
|
},
|
|
@@ -875,7 +901,7 @@ describe('OpenCode lifecycle', () => {
|
|
|
875
901
|
runtime.refreshRuntimeContractFromProbe(
|
|
876
902
|
{
|
|
877
903
|
ok: true,
|
|
878
|
-
version:
|
|
904
|
+
version: RUNTIME_CONTRACT_MIN_VERIFIED,
|
|
879
905
|
authenticated: true,
|
|
880
906
|
healthOk: true,
|
|
881
907
|
},
|
|
@@ -884,7 +910,7 @@ describe('OpenCode lifecycle', () => {
|
|
|
884
910
|
);
|
|
885
911
|
expect(stateRef.current.runtimeContract.executionAllowed).toBe(true);
|
|
886
912
|
expect(stateRef.current.runtimeContract.instanceGeneration).toBe(2);
|
|
887
|
-
expect(stateRef.current.runtimeContract.serveVersion).toBe(
|
|
913
|
+
expect(stateRef.current.runtimeContract.serveVersion).toBe(RUNTIME_CONTRACT_MIN_VERIFIED);
|
|
888
914
|
});
|
|
889
915
|
|
|
890
916
|
it('does not mark external skip-start ready before version and migration admit', async () => {
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { OpenCodeClient } from '@opencode/client';
|
|
2
|
+
|
|
3
|
+
export type LocationReleaseResult = { state: 'released' | 'timeout' | 'failed' | 'unavailable' };
|
|
4
|
+
export function releaseDeletedWorktreeLocation(input: {
|
|
5
|
+
getClient: () => OpenCodeClient | null;
|
|
6
|
+
directory: string;
|
|
7
|
+
timeoutMs?: number;
|
|
8
|
+
}): Promise<LocationReleaseResult>;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Explicit worktree deletion only. An eviction acknowledgement detaches cached
|
|
3
|
+
* services; it does not prove that borrowers have released every OS handle.
|
|
4
|
+
* Never use this operation for automatic navigation cleanup on a shared serve.
|
|
5
|
+
*
|
|
6
|
+
* @param {{ getClient: () => import('@opencode/client').OpenCodeClient | null, directory: string, timeoutMs?: number }} input
|
|
7
|
+
* @returns {Promise<{ state: 'released' | 'timeout' | 'failed' | 'unavailable' }>}
|
|
8
|
+
*/
|
|
9
|
+
export async function releaseDeletedWorktreeLocation({ getClient, directory, timeoutMs = 2000 }) {
|
|
10
|
+
const controller = new AbortController();
|
|
11
|
+
let timer;
|
|
12
|
+
try {
|
|
13
|
+
if (!directory || !directory.trim()) return { state: 'unavailable' };
|
|
14
|
+
const client = getClient();
|
|
15
|
+
if (!client) return { state: 'unavailable' };
|
|
16
|
+
const deadline = new Promise((resolve) => {
|
|
17
|
+
timer = setTimeout(() => {
|
|
18
|
+
controller.abort();
|
|
19
|
+
resolve({ state: 'timeout' });
|
|
20
|
+
}, timeoutMs);
|
|
21
|
+
});
|
|
22
|
+
const request = Promise.resolve().then(() => client.debug.location.evict(
|
|
23
|
+
{ location: { directory } },
|
|
24
|
+
{ signal: controller.signal },
|
|
25
|
+
)).then(() => ({ state: 'released' }), () => ({ state: controller.signal.aborted ? 'timeout' : 'failed' }));
|
|
26
|
+
return await Promise.race([request, deadline]);
|
|
27
|
+
} catch {
|
|
28
|
+
return { state: 'failed' };
|
|
29
|
+
} finally {
|
|
30
|
+
clearTimeout(timer);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import { OpenCode } from '@opencode/client';
|
|
3
|
+
import { releaseDeletedWorktreeLocation } from './location-release.js';
|
|
4
|
+
|
|
5
|
+
afterEach(() => vi.useRealTimers());
|
|
6
|
+
|
|
7
|
+
describe('explicit worktree location release', () => {
|
|
8
|
+
it('uses the official SDK exact target, current origin and auth', async () => {
|
|
9
|
+
const requests = [];
|
|
10
|
+
let origin = 'https://old.invalid';
|
|
11
|
+
const getClient = () => OpenCode.make({ baseUrl: origin, headers: { Authorization: 'test-auth' }, fetch: async (input, init) => {
|
|
12
|
+
requests.push(new Request(input, init));
|
|
13
|
+
return new Response(null, { status: 204 });
|
|
14
|
+
} });
|
|
15
|
+
origin = 'https://current.invalid';
|
|
16
|
+
expect(await releaseDeletedWorktreeLocation({ getClient, directory: '/repo/work tree' })).toEqual({ state: 'released' });
|
|
17
|
+
expect(requests).toHaveLength(1);
|
|
18
|
+
const url = new URL(requests[0].url);
|
|
19
|
+
expect(url.origin + url.pathname).toBe('https://current.invalid/api/debug/location');
|
|
20
|
+
expect(url.searchParams.get('location[directory]')).toBe('/repo/work tree');
|
|
21
|
+
expect(requests[0].method).toBe('DELETE');
|
|
22
|
+
expect(requests[0].headers.get('Authorization')).toBe('test-auth');
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('reports failure without claiming successful disposal', async () => {
|
|
26
|
+
const getClient = () => ({ debug: { location: { evict: async () => { throw new Error('unavailable'); } } } });
|
|
27
|
+
expect(await releaseDeletedWorktreeLocation({ getClient, directory: '/repo' })).toEqual({ state: 'failed' });
|
|
28
|
+
expect(await releaseDeletedWorktreeLocation({ getClient: () => null, directory: '/repo' })).toEqual({ state: 'unavailable' });
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it('bounds even a transport that ignores abort and never settles', async () => {
|
|
32
|
+
vi.useFakeTimers();
|
|
33
|
+
let signal;
|
|
34
|
+
const getClient = () => ({ debug: { location: { evict: (_input, options) => {
|
|
35
|
+
signal = options.signal;
|
|
36
|
+
return new Promise(() => {});
|
|
37
|
+
} } } });
|
|
38
|
+
const result = releaseDeletedWorktreeLocation({ getClient, directory: '/repo', timeoutMs: 50 });
|
|
39
|
+
await vi.advanceTimersByTimeAsync(50);
|
|
40
|
+
expect(await result).toEqual({ state: 'timeout' });
|
|
41
|
+
expect(signal.aborted).toBe(true);
|
|
42
|
+
});
|
|
43
|
+
});
|
|
@@ -29,7 +29,7 @@ function validateMcpName(name) {
|
|
|
29
29
|
*/
|
|
30
30
|
function resolveMcpScopeFromPath(layers, sourcePath) {
|
|
31
31
|
if (!sourcePath) return null;
|
|
32
|
-
return
|
|
32
|
+
return layers.projectLayers.some((layer) => layer.path === sourcePath) ? AGENT_SCOPE.PROJECT : AGENT_SCOPE.USER;
|
|
33
33
|
}
|
|
34
34
|
|
|
35
35
|
function ensureProjectMcpConfigPath(workingDirectory) {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// Pinned desktop/runtime opencode2. Upgrade and prepare must not fall back to 1.18.x.
|
|
2
|
-
export const PINNED_OPENCODE2_VERSION = '2.0.
|
|
2
|
+
export const PINNED_OPENCODE2_VERSION = '2.0.23';
|
|
3
|
+
export const MINIMUM_OPENCODE2_VERSION = '2.0.20';
|
|
3
4
|
|
|
4
5
|
// Global npm/bun package that installs the opencode v2 binary (not 1.x opencode-ai).
|
|
5
6
|
export const OPENCODE2_NPM_PACKAGE = '@opencode/cli';
|
|
@@ -86,15 +86,15 @@ describe('owned runtime upgrade (ticket 12)', () => {
|
|
|
86
86
|
|
|
87
87
|
it('verifies running serve identity before upgrade success', () => {
|
|
88
88
|
const contractOk = evaluateRuntimeContract({
|
|
89
|
-
serveVersion: '2.0.
|
|
89
|
+
serveVersion: '2.0.23',
|
|
90
90
|
reachable: true,
|
|
91
91
|
authenticated: true,
|
|
92
92
|
healthOk: true,
|
|
93
93
|
migrationAdmitTranscript: true,
|
|
94
94
|
});
|
|
95
95
|
expect(evaluateOwnedUpgradeResult({
|
|
96
|
-
targetVersion: '2.0.
|
|
97
|
-
serveVersion: '2.0.
|
|
96
|
+
targetVersion: '2.0.23',
|
|
97
|
+
serveVersion: '2.0.23',
|
|
98
98
|
binaryPath: ownedBinary,
|
|
99
99
|
expectedBinaryPath: ownedBinary,
|
|
100
100
|
contract: contractOk,
|
|
@@ -116,8 +116,8 @@ describe('owned runtime upgrade (ticket 12)', () => {
|
|
|
116
116
|
});
|
|
117
117
|
|
|
118
118
|
expect(evaluateOwnedUpgradeResult({
|
|
119
|
-
targetVersion: '2.0.
|
|
120
|
-
serveVersion: '2.0.
|
|
119
|
+
targetVersion: '2.0.23',
|
|
120
|
+
serveVersion: '2.0.23',
|
|
121
121
|
binaryPath: '/usr/bin/opencode',
|
|
122
122
|
expectedBinaryPath: ownedBinary,
|
|
123
123
|
contract: contractOk,
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, mock, test } from 'bun:test';
|
|
2
|
+
import { vi } from 'vitest';
|
|
2
3
|
import express from 'express';
|
|
3
4
|
import fs from 'fs';
|
|
4
5
|
import os from 'os';
|
|
@@ -67,7 +68,8 @@ describe('opencode plugin routes', () => {
|
|
|
67
68
|
beforeAll(async () => {
|
|
68
69
|
rootDir = fs.mkdtempSync(path.join(os.tmpdir(), 'openchamber-plugin-routes-'));
|
|
69
70
|
userConfigPath = path.join(rootDir, 'user-opencode.json');
|
|
70
|
-
|
|
71
|
+
vi.stubEnv('OPENCODE_CONFIG', userConfigPath);
|
|
72
|
+
vi.stubEnv('OPENCODE_CONFIG_DIR', path.join(rootDir, 'global-config'));
|
|
71
73
|
plugins = await import('./plugins.js');
|
|
72
74
|
});
|
|
73
75
|
|
|
@@ -91,7 +93,7 @@ describe('opencode plugin routes', () => {
|
|
|
91
93
|
|
|
92
94
|
afterAll(() => {
|
|
93
95
|
fs.rmSync(rootDir, { recursive: true, force: true });
|
|
94
|
-
|
|
96
|
+
vi.unstubAllEnvs();
|
|
95
97
|
});
|
|
96
98
|
|
|
97
99
|
test('GET /api/config/plugins empty returns entries and files arrays', async () => {
|
|
@@ -4,6 +4,8 @@ import path from 'path';
|
|
|
4
4
|
import {
|
|
5
5
|
AGENT_SCOPE,
|
|
6
6
|
readConfigFile,
|
|
7
|
+
readConfigLayers,
|
|
8
|
+
getConfigForPath,
|
|
7
9
|
writeConfig,
|
|
8
10
|
} from './shared.js';
|
|
9
11
|
import { isPathSpec } from './plugin-spec.js';
|
|
@@ -74,54 +76,8 @@ function getActiveOpencodeConfigDir() {
|
|
|
74
76
|
return path.join(os.homedir(), '.config', 'opencode');
|
|
75
77
|
}
|
|
76
78
|
|
|
77
|
-
function getActiveUserConfigPaths() {
|
|
78
|
-
const configDir = getActiveOpencodeConfigDir();
|
|
79
|
-
return [
|
|
80
|
-
path.join(configDir, 'config.json'),
|
|
81
|
-
path.join(configDir, 'opencode.json'),
|
|
82
|
-
path.join(configDir, 'opencode.jsonc'),
|
|
83
|
-
];
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
function getActiveCustomConfigPath() {
|
|
87
|
-
return process.env.OPENCODE_CONFIG ? path.resolve(process.env.OPENCODE_CONFIG) : null;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
function getPrimaryUserConfigPath() {
|
|
91
|
-
const [defaultPath, ...fallbackPaths] = getActiveUserConfigPaths();
|
|
92
|
-
for (const userPath of [defaultPath, ...fallbackPaths]) {
|
|
93
|
-
if (fs.existsSync(userPath)) {
|
|
94
|
-
return userPath;
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
return defaultPath;
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
function getProjectConfigPath(workingDirectory) {
|
|
101
|
-
if (!workingDirectory) return null;
|
|
102
|
-
const candidates = [
|
|
103
|
-
path.join(workingDirectory, 'opencode.json'),
|
|
104
|
-
path.join(workingDirectory, 'opencode.jsonc'),
|
|
105
|
-
path.join(workingDirectory, '.opencode', 'opencode.json'),
|
|
106
|
-
path.join(workingDirectory, '.opencode', 'opencode.jsonc'),
|
|
107
|
-
];
|
|
108
|
-
return candidates.find((candidate) => fs.existsSync(candidate)) || candidates[0];
|
|
109
|
-
}
|
|
110
|
-
|
|
111
79
|
function readPluginConfigLayers(workingDirectory) {
|
|
112
|
-
|
|
113
|
-
const userPath = getPrimaryUserConfigPath();
|
|
114
|
-
const projectPath = getProjectConfigPath(workingDirectory);
|
|
115
|
-
return {
|
|
116
|
-
userConfig: readConfigFile(userPath),
|
|
117
|
-
projectConfig: readConfigFile(projectPath),
|
|
118
|
-
customConfig: readConfigFile(customPath),
|
|
119
|
-
paths: {
|
|
120
|
-
userPath,
|
|
121
|
-
projectPath,
|
|
122
|
-
customPath,
|
|
123
|
-
},
|
|
124
|
-
};
|
|
80
|
+
return readConfigLayers(workingDirectory);
|
|
125
81
|
}
|
|
126
82
|
|
|
127
83
|
function validateFileName(fileName) {
|
|
@@ -144,16 +100,10 @@ function ensureProjectConfigPath(workingDirectory) {
|
|
|
144
100
|
}
|
|
145
101
|
|
|
146
102
|
function configSources(layers) {
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
}
|
|
151
|
-
sources.push({ config: layers.userConfig, filePath: layers.paths.userPath, scope: AGENT_SCOPE.USER });
|
|
152
|
-
}
|
|
153
|
-
if (layers.paths.projectPath) {
|
|
154
|
-
sources.push({ config: layers.projectConfig, filePath: layers.paths.projectPath, scope: AGENT_SCOPE.PROJECT });
|
|
155
|
-
}
|
|
156
|
-
return sources;
|
|
103
|
+
return [...layers.documents.filter((layer) => layer.scope !== 'project').reverse(), ...layers.projectLayers.toReversed()].map((layer) => ({
|
|
104
|
+
config: layer.config, filePath: layer.path,
|
|
105
|
+
scope: layer.scope === 'project' ? AGENT_SCOPE.PROJECT : AGENT_SCOPE.USER,
|
|
106
|
+
}));
|
|
157
107
|
}
|
|
158
108
|
|
|
159
109
|
function splitScopedValue(value) {
|
|
@@ -175,7 +125,9 @@ function getPluginTarget(id, workingDirectory) {
|
|
|
175
125
|
const { scope, value: spec } = splitScopedValue(decoded.value);
|
|
176
126
|
validateScope(scope);
|
|
177
127
|
const layers = readPluginConfigLayers(workingDirectory);
|
|
178
|
-
const source = configSources(layers).find((candidate) => candidate.scope === scope
|
|
128
|
+
const source = configSources(layers).find((candidate) => candidate.scope === scope
|
|
129
|
+
&& Array.isArray(candidate.config?.plugin)
|
|
130
|
+
&& candidate.config.plugin.some((raw) => parsePluginRaw(raw).spec === spec));
|
|
179
131
|
const plugin = Array.isArray(source?.config?.plugin) ? source.config.plugin : [];
|
|
180
132
|
const index = plugin.findIndex((raw) => parsePluginRaw(raw).spec === spec);
|
|
181
133
|
if (!source || index === -1) {
|
|
@@ -281,14 +233,14 @@ function createPluginEntry(entry, workingDirectory) {
|
|
|
281
233
|
throw codedError(`Plugin "${spec}" already exists`, 'ENTRY_EXISTS');
|
|
282
234
|
}
|
|
283
235
|
|
|
284
|
-
let targetPath =
|
|
236
|
+
let targetPath = layers.paths.userPath;
|
|
285
237
|
let config = {};
|
|
286
238
|
if (scope === AGENT_SCOPE.PROJECT) {
|
|
287
239
|
targetPath = ensureProjectConfigPath(workingDirectory);
|
|
288
240
|
config = fs.existsSync(targetPath) ? readConfigFile(targetPath) : {};
|
|
289
241
|
} else {
|
|
290
242
|
targetPath = layers.paths.customPath || layers.paths.userPath;
|
|
291
|
-
config = layers
|
|
243
|
+
config = getConfigForPath(layers, targetPath);
|
|
292
244
|
}
|
|
293
245
|
|
|
294
246
|
if (!Array.isArray(config.plugin)) {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { afterAll, beforeAll, beforeEach, describe, expect, test } from 'bun:test';
|
|
2
|
+
import { vi } from 'vitest';
|
|
2
3
|
import fs from 'fs';
|
|
3
4
|
import os from 'os';
|
|
4
5
|
import path from 'path';
|
|
@@ -30,19 +31,20 @@ describe('opencode plugins data layer', () => {
|
|
|
30
31
|
beforeAll(async () => {
|
|
31
32
|
rootDir = fs.mkdtempSync(path.join(os.tmpdir(), 'openchamber-plugins-'));
|
|
32
33
|
userConfigPath = path.join(rootDir, 'user-opencode.json');
|
|
33
|
-
|
|
34
|
+
vi.stubEnv('OPENCODE_CONFIG', userConfigPath);
|
|
35
|
+
vi.stubEnv('OPENCODE_CONFIG_DIR', path.join(rootDir, 'global-config'));
|
|
34
36
|
plugins = await import('./plugins.js');
|
|
35
37
|
});
|
|
36
38
|
|
|
37
39
|
beforeEach(() => {
|
|
38
|
-
|
|
40
|
+
vi.stubEnv('OPENCODE_CONFIG', userConfigPath);
|
|
39
41
|
projectDir = fs.mkdtempSync(path.join(rootDir, 'project-'));
|
|
40
42
|
fs.rmSync(userConfigPath, { force: true });
|
|
41
43
|
});
|
|
42
44
|
|
|
43
45
|
afterAll(() => {
|
|
44
46
|
fs.rmSync(rootDir, { recursive: true, force: true });
|
|
45
|
-
|
|
47
|
+
vi.unstubAllEnvs();
|
|
46
48
|
});
|
|
47
49
|
|
|
48
50
|
test('parses raw plugin entries', () => {
|
|
@@ -30,8 +30,8 @@ function getProviderSources(providerId, workingDirectory) {
|
|
|
30
30
|
return {
|
|
31
31
|
sources: {
|
|
32
32
|
auth: { exists: false },
|
|
33
|
-
user: { exists: userExists, path: paths.userPath },
|
|
34
|
-
project: { exists: projectExists, path: paths.projectPath || null },
|
|
33
|
+
user: { exists: userExists, path: layers.userLayers.findLast((layer) => layer.config?.provider?.[providerId] !== undefined || layer.config?.providers?.[providerId] !== undefined)?.path || paths.userPath },
|
|
34
|
+
project: { exists: projectExists, path: layers.projectLayers.findLast((layer) => layer.config?.provider?.[providerId] !== undefined || layer.config?.providers?.[providerId] !== undefined)?.path || paths.projectPath || null },
|
|
35
35
|
custom: { exists: customExists, path: paths.customPath }
|
|
36
36
|
}
|
|
37
37
|
};
|
|
@@ -44,17 +44,20 @@ function removeProviderConfig(providerId, workingDirectory, scope = 'user') {
|
|
|
44
44
|
|
|
45
45
|
const layers = readConfigLayers(workingDirectory);
|
|
46
46
|
let targetPath = layers.paths.userPath;
|
|
47
|
+
const sources = getProviderSources(providerId, workingDirectory).sources;
|
|
47
48
|
|
|
48
49
|
if (scope === 'project') {
|
|
49
50
|
if (!workingDirectory) {
|
|
50
51
|
throw new Error('Working directory is required for project scope');
|
|
51
52
|
}
|
|
52
|
-
targetPath =
|
|
53
|
+
targetPath = sources.project.path || targetPath;
|
|
53
54
|
} else if (scope === 'custom') {
|
|
54
55
|
if (!layers.paths.customPath) {
|
|
55
56
|
return false;
|
|
56
57
|
}
|
|
57
58
|
targetPath = layers.paths.customPath;
|
|
59
|
+
} else {
|
|
60
|
+
targetPath = sources.user.path;
|
|
58
61
|
}
|
|
59
62
|
|
|
60
63
|
const targetConfig = getConfigForPath(layers, targetPath);
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { createProxyMiddleware } from 'http-proxy-middleware';
|
|
2
|
+
import { isSensitiveCredentialResponse } from './credential-boundary.js';
|
|
2
3
|
|
|
3
4
|
import {
|
|
4
5
|
applyForwardProxyResponseHeaders,
|
|
@@ -244,6 +245,12 @@ export const resolveSessionTurnAdmissionRequest = (req) => {
|
|
|
244
245
|
};
|
|
245
246
|
|
|
246
247
|
export const registerOpenCodeProxy = (app, deps) => {
|
|
248
|
+
app.use((req, res, next) => {
|
|
249
|
+
if (isSensitiveCredentialResponse(req.method, req.originalUrl || req.url)) {
|
|
250
|
+
return res.status(403).json({ code: 'credential_response_refused', error: 'Provider credential values are host-only.' });
|
|
251
|
+
}
|
|
252
|
+
return next();
|
|
253
|
+
});
|
|
247
254
|
const {
|
|
248
255
|
fs,
|
|
249
256
|
os,
|
|
@@ -57,6 +57,19 @@ const mountProxy = () => {
|
|
|
57
57
|
return app;
|
|
58
58
|
};
|
|
59
59
|
|
|
60
|
+
describe('credential response boundary', () => {
|
|
61
|
+
it.each(['/api/credential', '/api/credential/', '/api/%63redential?directory=/repo'])('refuses raw credential list %s before upstream', async (path) => {
|
|
62
|
+
const fetchSpy = vi.fn();
|
|
63
|
+
globalThis.fetch = fetchSpy;
|
|
64
|
+
const response = await request(mountProxy()).get(path).expect(403);
|
|
65
|
+
expect(response.body.code).toBe('credential_response_refused');
|
|
66
|
+
expect(fetchSpy).not.toHaveBeenCalled();
|
|
67
|
+
});
|
|
68
|
+
it('refuses credential create replies which echo values', async () => {
|
|
69
|
+
await request(mountProxy()).post('/api/credential').send({}).expect(403);
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
|
|
60
73
|
const sseBody = (blocks) => {
|
|
61
74
|
const encoder = new TextEncoder();
|
|
62
75
|
const text = blocks.join('');
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { expect, test } from 'vitest';
|
|
2
|
+
import express from 'express';
|
|
3
|
+
import request from 'supertest';
|
|
4
|
+
import { execFile, spawn } from 'node:child_process';
|
|
5
|
+
import { promisify } from 'node:util';
|
|
6
|
+
import fs from 'node:fs/promises';
|
|
7
|
+
import os from 'node:os';
|
|
8
|
+
import path from 'node:path';
|
|
9
|
+
import net from 'node:net';
|
|
10
|
+
import { createOpenCodeUpdateDiscovery } from './update-discovery.js';
|
|
11
|
+
import { installPinnedOpenCode2Cli } from './ensure-cli.js';
|
|
12
|
+
import { createSharedOpenCodeService } from './shared-service.js';
|
|
13
|
+
import { registerOpenCodeRoutes } from './routes.js';
|
|
14
|
+
|
|
15
|
+
// Downloads real official binaries; never touches the user's shared service.
|
|
16
|
+
test.skipIf(process.env.OPENCHAMBER_TEST_REMOTE_UPGRADE !== '1')('remote discovery upgrades an isolated shared service and verifies its running version', async () => {
|
|
17
|
+
const root = await fs.mkdtemp(path.join(os.tmpdir(), 'opencode-upgrade-'));
|
|
18
|
+
const env = {
|
|
19
|
+
PATH: process.env.PATH, HOME: root, TMPDIR: os.tmpdir(),
|
|
20
|
+
XDG_CONFIG_HOME: path.join(root, 'config'), XDG_DATA_HOME: path.join(root, 'data'),
|
|
21
|
+
XDG_STATE_HOME: path.join(root, 'state'), XDG_CACHE_HOME: path.join(root, 'cache'),
|
|
22
|
+
OPENCODE_CONFIG_DIR: path.join(root, 'config', 'opencode'), OPENCODE_DISABLE_MODELS_FETCH: '1',
|
|
23
|
+
};
|
|
24
|
+
await fs.mkdir(env.OPENCODE_CONFIG_DIR, { recursive: true });
|
|
25
|
+
const listener = net.createServer();
|
|
26
|
+
await new Promise((resolve) => listener.listen(0, '127.0.0.1', resolve));
|
|
27
|
+
const port = listener.address().port;
|
|
28
|
+
await new Promise((resolve) => listener.close(resolve));
|
|
29
|
+
await fs.writeFile(path.join(env.OPENCODE_CONFIG_DIR, 'service.json'), JSON.stringify({ hostname: '127.0.0.1', port }));
|
|
30
|
+
const service = createSharedOpenCodeService({ envLike: env, spawnImpl: (command, args, options) => spawn(command, args, { ...options, cwd: root }) });
|
|
31
|
+
let binary;
|
|
32
|
+
let registration;
|
|
33
|
+
try {
|
|
34
|
+
const target = await createOpenCodeUpdateDiscovery()();
|
|
35
|
+
expect(target).not.toBe('2.0.16');
|
|
36
|
+
binary = await installPinnedOpenCode2Cli({ version: '2.0.16', dataDir: root });
|
|
37
|
+
await service.start({ binary, env });
|
|
38
|
+
registration = await service.readRegistration();
|
|
39
|
+
expect(registration.version).toBe('2.0.16');
|
|
40
|
+
const app = express();
|
|
41
|
+
app.use(express.json());
|
|
42
|
+
registerOpenCodeRoutes(app, {
|
|
43
|
+
getIsExternalOpenCode: () => true, getIsSharedOpenCodeService: () => true,
|
|
44
|
+
getResolvedOpenCodeBinary: () => binary, getResolvedOpenCodeBinarySource: () => 'installed',
|
|
45
|
+
readSettingsFromDiskMigrated: async () => ({}),
|
|
46
|
+
getOpenCodeResolutionSnapshot: async () => ({ source: 'installed', resolved: binary }),
|
|
47
|
+
buildOpenCodeUrl: (pathname) => `${registration.origin}${pathname}`,
|
|
48
|
+
getOpenCodeAuthHeaders: () => ({ Authorization: `Basic ${Buffer.from(`opencode:${registration.password}`).toString('base64')}` }),
|
|
49
|
+
forceResolvedOpenCodeBinary: (value) => { binary = value; },
|
|
50
|
+
restartOpenCode: async ({ binaryPath }) => {
|
|
51
|
+
await service.start({ binary: binaryPath, env, replace: true });
|
|
52
|
+
registration = await service.readRegistration();
|
|
53
|
+
},
|
|
54
|
+
openchamberDataDir: root,
|
|
55
|
+
});
|
|
56
|
+
const before = await request(app).get('/api/opencode/upgrade-status').expect(200);
|
|
57
|
+
expect(before.body).toMatchObject({ available: true, targetVersion: target });
|
|
58
|
+
const upgrade = await request(app).post('/api/opencode/upgrade').send({ target }).expect(200);
|
|
59
|
+
expect(upgrade.body).toMatchObject({ success: true, restarted: true, version: target });
|
|
60
|
+
const after = await request(app).get('/api/opencode/upgrade-status').expect(200);
|
|
61
|
+
expect(after.body).toMatchObject({ available: false, serveVersion: target });
|
|
62
|
+
} finally {
|
|
63
|
+
if (binary) await promisify(execFile)(binary, ['service', 'stop'], { env, cwd: root, timeout: 20_000, windowsHide: true });
|
|
64
|
+
await fs.rm(root, { recursive: true, force: true });
|
|
65
|
+
}
|
|
66
|
+
}, 180_000);
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { createProjectIdFromPath } from '../projects/project-id.js';
|
|
2
2
|
import { projectBootstrapSettingsResponse } from './settings-helpers.js';
|
|
3
3
|
import {
|
|
4
|
-
PINNED_OPENCODE2_VERSION,
|
|
5
4
|
evaluateOpenCodeHealthBody,
|
|
6
5
|
isOpenCode1xVersion,
|
|
7
6
|
resolveOpenCode2UpgradeTarget,
|
|
@@ -13,6 +12,7 @@ import {
|
|
|
13
12
|
} from './ensure-cli.js';
|
|
14
13
|
import { registerUpgradeScreenRoutes } from './upgrade-screen.js';
|
|
15
14
|
import { evaluateRuntimeContract } from './runtime-contract.js';
|
|
15
|
+
import { createOpenCodeUpdateDiscovery } from './update-discovery.js';
|
|
16
16
|
import {
|
|
17
17
|
buildUpgradeStatusSnapshot,
|
|
18
18
|
classifyRuntimeOwnership,
|
|
@@ -54,6 +54,7 @@ export const registerOpenCodeRoutes = (app, dependencies) => {
|
|
|
54
54
|
getResolvedOpenCodeBinarySource = () => null,
|
|
55
55
|
getActiveSessionCount = () => 0,
|
|
56
56
|
openchamberDataDir = null,
|
|
57
|
+
discoverOpenCodeUpdate = createOpenCodeUpdateDiscovery(),
|
|
57
58
|
} = dependencies;
|
|
58
59
|
|
|
59
60
|
const upgradeOperation = createUpgradeOperationState();
|
|
@@ -156,12 +157,13 @@ export const registerOpenCodeRoutes = (app, dependencies) => {
|
|
|
156
157
|
const buildUpgradeStatus = async () => {
|
|
157
158
|
const ownership = await resolveOwnershipContext();
|
|
158
159
|
const contract = await buildLiveContract();
|
|
160
|
+
const targetVersion = await discoverOpenCodeUpdate();
|
|
159
161
|
const activeCount = typeof getActiveSessionCount === 'function' ? Number(getActiveSessionCount()) || 0 : 0;
|
|
160
162
|
return buildUpgradeStatusSnapshot({
|
|
161
163
|
ownership,
|
|
162
164
|
serveVersion: contract.serveVersion,
|
|
163
165
|
cliVersion: contract.cliVersion,
|
|
164
|
-
targetVersion
|
|
166
|
+
targetVersion,
|
|
165
167
|
contract,
|
|
166
168
|
operation: upgradeOperation.getState(),
|
|
167
169
|
hasActiveTasks: activeCount > 0,
|
|
@@ -342,7 +344,7 @@ export const registerOpenCodeRoutes = (app, dependencies) => {
|
|
|
342
344
|
errorCode: 'OPENCODE_UPGRADE_1X_REFUSED',
|
|
343
345
|
});
|
|
344
346
|
}
|
|
345
|
-
const target = resolveOpenCode2UpgradeTarget(rawTarget);
|
|
347
|
+
const target = resolveOpenCode2UpgradeTarget(rawTarget ?? (await buildUpgradeStatus()).targetVersion);
|
|
346
348
|
const confirmActive = req.body?.confirmActiveTasks === true || req.body?.force === true;
|
|
347
349
|
const activeCount = typeof getActiveSessionCount === 'function' ? Number(getActiveSessionCount()) || 0 : 0;
|
|
348
350
|
if (activeCount > 0 && !confirmActive) {
|
|
@@ -707,7 +709,7 @@ export const registerOpenCodeRoutes = (app, dependencies) => {
|
|
|
707
709
|
|
|
708
710
|
const sources = getProviderSources(providerId, directory);
|
|
709
711
|
const { getProviderAuth } = await getAuthLibrary();
|
|
710
|
-
const auth = getProviderAuth(providerId);
|
|
712
|
+
const auth = await getProviderAuth(providerId);
|
|
711
713
|
sources.sources.auth.exists = Boolean(auth);
|
|
712
714
|
|
|
713
715
|
return res.json({
|
|
@@ -751,12 +753,12 @@ export const registerOpenCodeRoutes = (app, dependencies) => {
|
|
|
751
753
|
let removed = false;
|
|
752
754
|
if (scope === 'auth') {
|
|
753
755
|
const { removeProviderAuth } = await getAuthLibrary();
|
|
754
|
-
removed = removeProviderAuth(providerId);
|
|
756
|
+
removed = await removeProviderAuth(providerId);
|
|
755
757
|
} else if (scope === 'user' || scope === 'project' || scope === 'custom') {
|
|
756
758
|
removed = removeProviderConfig(providerId, directory, scope);
|
|
757
759
|
} else if (scope === 'all') {
|
|
758
760
|
const { removeProviderAuth } = await getAuthLibrary();
|
|
759
|
-
const authRemoved = removeProviderAuth(providerId);
|
|
761
|
+
const authRemoved = await removeProviderAuth(providerId);
|
|
760
762
|
const userRemoved = removeProviderConfig(providerId, directory, 'user');
|
|
761
763
|
const projectRemoved = directory ? removeProviderConfig(providerId, directory, 'project') : false;
|
|
762
764
|
const customRemoved = removeProviderConfig(providerId, directory, 'custom');
|
|
@@ -765,14 +767,14 @@ export const registerOpenCodeRoutes = (app, dependencies) => {
|
|
|
765
767
|
return res.status(400).json({ error: 'Invalid scope' });
|
|
766
768
|
}
|
|
767
769
|
|
|
768
|
-
if (removed) {
|
|
770
|
+
if (removed && scope !== 'auth') {
|
|
769
771
|
await refreshOpenCodeAfterConfigChange(`provider ${providerId} disconnected (${scope})`);
|
|
770
772
|
}
|
|
771
773
|
|
|
772
774
|
return res.json({
|
|
773
775
|
success: true,
|
|
774
776
|
removed,
|
|
775
|
-
requiresReload: removed,
|
|
777
|
+
requiresReload: removed && scope !== 'auth',
|
|
776
778
|
message: removed ? 'Provider disconnected successfully' : 'Provider was not connected',
|
|
777
779
|
reloadDelayMs: removed ? clientReloadDelayMs : undefined,
|
|
778
780
|
});
|