@openchambery/web 2.0.0-beta.9 → 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-oeXj4YHn.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-CahUctxC.js → MarkdownRendererImpl-DoWvw5jj.js} +3 -3
- package/dist/assets/{MarkstreamRendererImpl-5JUaTSdl.js → MarkstreamRendererImpl-DoeYajmw.js} +1 -1
- package/dist/assets/{MobileDetailNavigation-agQ-Yhpd.js → MobileDetailNavigation-BzBz-co_.js} +1 -1
- package/dist/assets/{MobileShareBridge-yCN95dYg.js → MobileShareBridge-D1Uoj6l_.js} +3 -3
- package/dist/assets/MobileSurface-8YpZ51Qo.js +2 -0
- package/dist/assets/{MultiRunWindow-C0HqMFpp.js → MultiRunWindow-BxcYUPta.js} +1 -1
- package/dist/assets/{OnboardingScreen-O4cGdz5p.js → OnboardingScreen-yRdQTR1M.js} +1 -1
- package/dist/assets/{OverlayScrollbar-CRfGtcmG.js → OverlayScrollbar-BUjtqnxn.js} +1 -1
- package/dist/assets/SettingsView-D7KrvqvH.js +25 -0
- package/dist/assets/{SettingsWindow-B_d5Aqq3.js → SettingsWindow-DX2w-cDc.js} +1 -1
- package/dist/assets/{TerminalView-DdNQQb8W.js → TerminalView-CiUcCQPm.js} +1 -1
- package/dist/assets/{ToolOutputDialog-4DC5OE1J.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-da8etTTe.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-KwlAomA8.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-3GTWeMU-.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-0Z4e-J1g.js → sessionLookup-1Z1FWZRB.js} +1 -1
- package/dist/assets/{sortable-tabs-strip-CE7W7lJ9.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-mDR2jXTo.js → terminalApi-BZW_rAc3.js} +1 -1
- package/dist/assets/{textarea-XD2oX_Ee.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-y_GTVXhJ.js → useAppFontEffects-DJy8P5By.js} +55 -55
- package/dist/assets/{useAssistantUIStore-Bz4tRflF.js → useAssistantUIStore-DBwfg9nX.js} +1 -1
- package/dist/assets/useDesktopWindowControlsLayout-DlwSomVK.js +1 -0
- package/dist/assets/{useEffectiveDirectory-BW9jNyuK.js → useEffectiveDirectory-eJorVTT9.js} +1 -1
- package/dist/assets/{useFontPreferences-Bjx8zkGM.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-BUcSLKlL.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-CLFPj5Ki.js +0 -3
- package/dist/assets/Icon-B8fSTLdP.js +0 -1
- package/dist/assets/MarkdownRenderer-BK69MEHQ.js +0 -2
- package/dist/assets/MobileSurface-D0HP_lTS.js +0 -2
- package/dist/assets/SettingsView-xLYb22wV.js +0 -25
- package/dist/assets/appThemeRegistry-LkVH62uF.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-CBzDuQxP.js +0 -3
- package/dist/assets/insertionBoundaries-DQrE5qG2.js +0 -1
- package/dist/assets/main-D2BzpnUt.css +0 -1
- package/dist/assets/main-DYISNvWY.js +0 -169
- package/dist/assets/miniChat-DYIqdWi7.js +0 -2
- package/dist/assets/mobile-CwiCcmWt.js +0 -2
- package/dist/assets/multirun-C3snjTFs.js +0 -4
- package/dist/assets/persistence-D0y7226-.js +0 -3
- package/dist/assets/pl-BCf5uLlK.js +0 -15
- package/dist/assets/platform-CurqO28D.js +0 -6
- package/dist/assets/projectMeta-BkhYFMaM.js +0 -3
- package/dist/assets/renderElectronMiniChatApp-7iyh_419.js +0 -2
- package/dist/assets/renderMobileApp-y0_NeckR.js +0 -28
- package/dist/assets/runtime-switch-LvWDU-SL.js +0 -1
- package/dist/assets/runtimeConfig-12C5j02f.js +0 -2
- package/dist/assets/runtimeEndpointReset-DTY2ZXXb.js +0 -149
- package/dist/assets/sessionChangeSummary-CwPBIb3Z.js +0 -2
- package/dist/assets/src-lS1keDVV.css +0 -2
- package/dist/assets/useDesktopWindowControlsLayout-CR_DcY77.js +0 -1
- package/dist/assets/useMobileNavigationStore-DwupvLSA.js +0 -414
- package/dist/assets/useSessionAutoCleanup-DkGO_usj.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
|
@@ -15,8 +15,10 @@ vi.mock('./ensure-cli.js', async (importOriginal) => {
|
|
|
15
15
|
|
|
16
16
|
import { registerOpenCodeRoutes } from './routes.js';
|
|
17
17
|
import { isOpenCode1xVersion } from './opencode2-pin.js';
|
|
18
|
+
import { createOpenCodeUpdateDiscovery } from './update-discovery.js';
|
|
18
19
|
|
|
19
20
|
const createDependencies = ({ formatSettingsResponse }) => ({
|
|
21
|
+
discoverOpenCodeUpdate: async () => '2.0.12',
|
|
20
22
|
crypto: {},
|
|
21
23
|
clientReloadDelayMs: 0,
|
|
22
24
|
getOpenCodeResolutionSnapshot: vi.fn(),
|
|
@@ -231,26 +233,65 @@ describe('opencode2 upgrade pin (ticket 12)', () => {
|
|
|
231
233
|
});
|
|
232
234
|
|
|
233
235
|
it('upgrades a shared service using the owned cache and verifies the running version', async () => {
|
|
234
|
-
const binary = '/cache/opencode-cli/2.0.
|
|
236
|
+
const binary = '/cache/opencode-cli/2.0.23/opencode';
|
|
235
237
|
vi.mocked(installPinnedOpenCode2Cli).mockResolvedValueOnce(binary);
|
|
236
|
-
vi.mocked(readOpenCode2BinaryVersion).mockReturnValueOnce('2.0.
|
|
237
|
-
vi.stubGlobal('fetch', vi.fn(async () => new Response(JSON.stringify({ version: '2.0.
|
|
238
|
+
vi.mocked(readOpenCode2BinaryVersion).mockReturnValueOnce('2.0.23');
|
|
239
|
+
vi.stubGlobal('fetch', vi.fn(async () => new Response(JSON.stringify({ version: '2.0.23', pid: 42 }))));
|
|
238
240
|
let resolvedBinary = '/usr/local/bin/opencode';
|
|
239
241
|
const restartOpenCode = vi.fn(async () => {});
|
|
240
242
|
const { app } = createUpgradeApp({
|
|
241
243
|
getIsExternalOpenCode: () => true,
|
|
242
244
|
getIsSharedOpenCodeService: () => true,
|
|
243
245
|
getResolvedOpenCodeBinary: () => resolvedBinary,
|
|
244
|
-
getOpenCodeCliVersion: () => '2.0.
|
|
246
|
+
getOpenCodeCliVersion: () => '2.0.23',
|
|
245
247
|
forceResolvedOpenCodeBinary: (value) => { resolvedBinary = value; },
|
|
246
248
|
restartOpenCode,
|
|
247
249
|
waitForOpenCodeReady: vi.fn(),
|
|
248
250
|
openchamberDataDir: '/cache',
|
|
249
251
|
});
|
|
250
252
|
|
|
251
|
-
const response = await request(app).post('/api/opencode/upgrade').send({ target: '2.0.
|
|
253
|
+
const response = await request(app).post('/api/opencode/upgrade').send({ target: '2.0.23' }).expect(200);
|
|
252
254
|
expect(restartOpenCode).toHaveBeenCalledWith({ binaryPath: binary });
|
|
253
|
-
expect(response.body).toMatchObject({ success: true, version: '2.0.
|
|
255
|
+
expect(response.body).toMatchObject({ success: true, version: '2.0.23', ownership: 'shared-service' });
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
it('discovers the remote release and updates a shared 2.0.16 service to 2.0.23', async () => {
|
|
259
|
+
let serveVersion = '2.0.16';
|
|
260
|
+
let binary = '/usr/local/bin/opencode';
|
|
261
|
+
const installed = '/cache/opencode-cli/2.0.23/opencode';
|
|
262
|
+
const fetchMock = vi.fn(async (url) => new Response(JSON.stringify({
|
|
263
|
+
version: String(url).includes('registry.npmjs.org') ? '2.0.23' : serveVersion,
|
|
264
|
+
})));
|
|
265
|
+
vi.stubGlobal('fetch', fetchMock);
|
|
266
|
+
vi.mocked(readOpenCode2BinaryVersion).mockReturnValue('2.0.23');
|
|
267
|
+
vi.mocked(installPinnedOpenCode2Cli).mockResolvedValueOnce(installed);
|
|
268
|
+
const restartOpenCode = vi.fn(async () => { serveVersion = '2.0.23'; });
|
|
269
|
+
const { app } = createUpgradeApp({
|
|
270
|
+
discoverOpenCodeUpdate: createOpenCodeUpdateDiscovery(),
|
|
271
|
+
getIsExternalOpenCode: () => true,
|
|
272
|
+
getIsSharedOpenCodeService: () => true,
|
|
273
|
+
getResolvedOpenCodeBinary: () => binary,
|
|
274
|
+
forceResolvedOpenCodeBinary: (value) => { binary = value; },
|
|
275
|
+
restartOpenCode,
|
|
276
|
+
waitForOpenCodeReady: vi.fn(),
|
|
277
|
+
openchamberDataDir: '/cache',
|
|
278
|
+
});
|
|
279
|
+
const before = await request(app).get('/api/opencode/upgrade-status').expect(200);
|
|
280
|
+
expect(before.body).toMatchObject({ available: true, serveVersion: '2.0.16', targetVersion: '2.0.23', canManage: true });
|
|
281
|
+
const result = await request(app).post('/api/opencode/upgrade').send({}).expect(200);
|
|
282
|
+
expect(result.body).toMatchObject({ success: true, restarted: true, version: '2.0.23' });
|
|
283
|
+
expect(restartOpenCode).toHaveBeenCalledWith({ binaryPath: installed });
|
|
284
|
+
const after = await request(app).get('/api/opencode/upgrade-status').expect(200);
|
|
285
|
+
expect(after.body).toMatchObject({ available: false, serveVersion: '2.0.23' });
|
|
286
|
+
expect(fetchMock.mock.calls.filter(([url]) => String(url).includes('registry.npmjs.org'))).toHaveLength(1);
|
|
287
|
+
vi.mocked(readOpenCode2BinaryVersion).mockReset();
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
it('reports registry failure as unknown rather than up-to-date', async () => {
|
|
291
|
+
vi.stubGlobal('fetch', vi.fn(async () => new Response(JSON.stringify({ version: '2.0.16' }))));
|
|
292
|
+
const { app } = createUpgradeApp({ discoverOpenCodeUpdate: async () => { throw new Error('registry unavailable'); } });
|
|
293
|
+
const result = await request(app).get('/api/opencode/upgrade-status').expect(500);
|
|
294
|
+
expect(result.body).toMatchObject({ available: null, error: 'registry unavailable' });
|
|
254
295
|
});
|
|
255
296
|
|
|
256
297
|
it('does not report a failed shared replacement as success and restores binary selection', async () => {
|
|
@@ -344,7 +385,7 @@ describe('runtime contract routes (ticket 11)', () => {
|
|
|
344
385
|
ok: true,
|
|
345
386
|
status: 200,
|
|
346
387
|
statusText: 'OK',
|
|
347
|
-
json: async () => ({ version: '2.0.
|
|
388
|
+
json: async () => ({ version: '2.0.20', pid: 1 }),
|
|
348
389
|
})));
|
|
349
390
|
const { app } = createContractApp();
|
|
350
391
|
|
|
@@ -354,7 +395,7 @@ describe('runtime contract routes (ticket 11)', () => {
|
|
|
354
395
|
expect(health.body.executionAllowed).toBe(true);
|
|
355
396
|
|
|
356
397
|
const contract = await request(app).get('/api/opencode/contract').expect(200);
|
|
357
|
-
expect(contract.body.serveVersion).toBe('2.0.
|
|
398
|
+
expect(contract.body.serveVersion).toBe('2.0.20');
|
|
358
399
|
expect(contract.body.capabilities['core.protocol'].available).toBe(true);
|
|
359
400
|
});
|
|
360
401
|
|
|
@@ -398,7 +439,7 @@ describe('GET /api/opencode/health', () => {
|
|
|
398
439
|
status: 200,
|
|
399
440
|
statusText: 'OK',
|
|
400
441
|
json: async () => ({
|
|
401
|
-
version: '2.0.
|
|
442
|
+
version: '2.0.20',
|
|
402
443
|
pid: 4242,
|
|
403
444
|
urls: { api: 'http://127.0.0.1:4096' },
|
|
404
445
|
paths: { config: '/tmp/config', data: '/tmp/data' },
|
|
@@ -412,7 +453,7 @@ describe('GET /api/opencode/health', () => {
|
|
|
412
453
|
|
|
413
454
|
expect(response.body).toMatchObject({
|
|
414
455
|
healthy: true,
|
|
415
|
-
version: '2.0.
|
|
456
|
+
version: '2.0.20',
|
|
416
457
|
executionAllowed: true,
|
|
417
458
|
protocolCompatible: true,
|
|
418
459
|
});
|
|
@@ -7,20 +7,21 @@
|
|
|
7
7
|
* execution semantics.
|
|
8
8
|
*
|
|
9
9
|
* Evidence bounds (ticket 11):
|
|
10
|
-
* - MIN_VERIFIED:
|
|
10
|
+
* - MIN_VERIFIED: credential API availability (`2.0.20`)
|
|
11
11
|
* Any acceptable OpenCode 2.x at or above MIN_VERIFIED may execute. Below-min
|
|
12
12
|
* 2.x and 1.x are refused with upgrade guidance; there is no upper version cap.
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
15
|
import {
|
|
16
16
|
PINNED_OPENCODE2_VERSION,
|
|
17
|
+
MINIMUM_OPENCODE2_VERSION,
|
|
17
18
|
compareOpenCode2Versions,
|
|
18
19
|
isAcceptableOpenCode2HealthVersion,
|
|
19
20
|
isOpenCode1xVersion,
|
|
20
21
|
} from './opencode2-pin.js';
|
|
21
22
|
|
|
22
23
|
/** Lowest version with documented OpenChamber + OpenCode 2 contract tests. */
|
|
23
|
-
export const RUNTIME_CONTRACT_MIN_VERIFIED =
|
|
24
|
+
export const RUNTIME_CONTRACT_MIN_VERIFIED = MINIMUM_OPENCODE2_VERSION;
|
|
24
25
|
|
|
25
26
|
/**
|
|
26
27
|
* Optional / core capabilities and the minimum serve version that admits them.
|
|
@@ -13,14 +13,14 @@ import {
|
|
|
13
13
|
} from './runtime-contract.js';
|
|
14
14
|
|
|
15
15
|
describe('runtime contract admission (ticket 11)', () => {
|
|
16
|
-
it('records
|
|
17
|
-
expect(RUNTIME_CONTRACT_MIN_VERIFIED).toBe('2.0.
|
|
16
|
+
it('records the credential API floor independently of the install pin', () => {
|
|
17
|
+
expect(RUNTIME_CONTRACT_MIN_VERIFIED).toBe('2.0.20');
|
|
18
18
|
});
|
|
19
19
|
|
|
20
20
|
it('classifies version bands with a 2.x floor only', () => {
|
|
21
|
-
expect(classifyRuntimeVersionBand('2.0.
|
|
22
|
-
expect(classifyRuntimeVersionBand('2.0.
|
|
23
|
-
expect(classifyRuntimeVersionBand('2.0.
|
|
21
|
+
expect(classifyRuntimeVersionBand('2.0.19')).toBe('below-min');
|
|
22
|
+
expect(classifyRuntimeVersionBand('2.0.20')).toBe('verified');
|
|
23
|
+
expect(classifyRuntimeVersionBand('2.0.23')).toBe('verified');
|
|
24
24
|
expect(classifyRuntimeVersionBand('2.1.0')).toBe('verified');
|
|
25
25
|
expect(classifyRuntimeVersionBand('2.0.5')).toBe('below-min');
|
|
26
26
|
expect(classifyRuntimeVersionBand('1.18.18')).toBe('1x');
|
|
@@ -30,8 +30,8 @@ describe('runtime contract admission (ticket 11)', () => {
|
|
|
30
30
|
|
|
31
31
|
it('admits verified serve for core protocol and optional caps', () => {
|
|
32
32
|
const result = evaluateRuntimeContract({
|
|
33
|
-
serveVersion: '2.0.
|
|
34
|
-
cliVersion: '2.0.
|
|
33
|
+
serveVersion: '2.0.20',
|
|
34
|
+
cliVersion: '2.0.20',
|
|
35
35
|
reachable: true,
|
|
36
36
|
authenticated: true,
|
|
37
37
|
healthOk: true,
|
|
@@ -65,7 +65,7 @@ describe('runtime contract admission (ticket 11)', () => {
|
|
|
65
65
|
|
|
66
66
|
it('admits newer 2.x above the documented floor for execution', () => {
|
|
67
67
|
const result = evaluateRuntimeContract({
|
|
68
|
-
serveVersion: '2.0.
|
|
68
|
+
serveVersion: '2.0.23',
|
|
69
69
|
reachable: true,
|
|
70
70
|
authenticated: true,
|
|
71
71
|
healthOk: true,
|
|
@@ -105,12 +105,12 @@ describe('runtime contract admission (ticket 11)', () => {
|
|
|
105
105
|
expect(evaluateRuntimeContract({
|
|
106
106
|
reachable: true,
|
|
107
107
|
authenticated: false,
|
|
108
|
-
serveVersion: '2.0.
|
|
108
|
+
serveVersion: '2.0.20',
|
|
109
109
|
healthOk: true,
|
|
110
110
|
}).phase).toBe('unauthenticated');
|
|
111
111
|
|
|
112
112
|
const migration = evaluateRuntimeContract({
|
|
113
|
-
serveVersion: '2.0.
|
|
113
|
+
serveVersion: '2.0.20',
|
|
114
114
|
reachable: true,
|
|
115
115
|
authenticated: true,
|
|
116
116
|
healthOk: true,
|
|
@@ -122,7 +122,7 @@ describe('runtime contract admission (ticket 11)', () => {
|
|
|
122
122
|
expect(migration.migrationExecutable).toBe(false);
|
|
123
123
|
|
|
124
124
|
const mismatch = evaluateRuntimeContract({
|
|
125
|
-
serveVersion: '2.0.
|
|
125
|
+
serveVersion: '2.0.20',
|
|
126
126
|
cliVersion: '2.0.14',
|
|
127
127
|
reachable: true,
|
|
128
128
|
authenticated: true,
|
|
@@ -179,7 +179,7 @@ describe('runtime contract admission (ticket 11)', () => {
|
|
|
179
179
|
migrationAdmitTranscript: true,
|
|
180
180
|
});
|
|
181
181
|
const body = runtimeContractExecutionBlockedBody(contract);
|
|
182
|
-
expect(body.error).toContain('2.0.
|
|
182
|
+
expect(body.error).toContain('2.0.20');
|
|
183
183
|
expect(body.error).toMatch(/Settings → About/i);
|
|
184
184
|
expect(formatRuntimeContractBlockedError(contract)).toBe(body.error);
|
|
185
185
|
});
|
|
@@ -44,7 +44,9 @@ export const createSettingsNormalizationRuntime = (dependencies) => {
|
|
|
44
44
|
return value;
|
|
45
45
|
}
|
|
46
46
|
try {
|
|
47
|
-
|
|
47
|
+
// The JS implementation preserves input casing on case-insensitive volumes.
|
|
48
|
+
// Native realpath supplies the filesystem spelling used by Git and OpenCode.
|
|
49
|
+
return (realpathSync.native ?? realpathSync)(value);
|
|
48
50
|
} catch {
|
|
49
51
|
return value;
|
|
50
52
|
}
|
|
@@ -22,6 +22,42 @@ const createTestRuntime = (overrides = {}) => {
|
|
|
22
22
|
|
|
23
23
|
describe('settings normalization runtime - symlink resolution', () => {
|
|
24
24
|
describe('normalizePathForPersistence', () => {
|
|
25
|
+
it('uses native realpath to recover filesystem casing instead of preserving the input spelling', () => {
|
|
26
|
+
const realpathSync = Object.assign((p) => p, {
|
|
27
|
+
native: (p) => p.replace('/code/', '/Code/'),
|
|
28
|
+
});
|
|
29
|
+
const runtime = createTestRuntime({ realpathSync, processLike: { platform: 'darwin' } });
|
|
30
|
+
|
|
31
|
+
expect(runtime.normalizePathForPersistence('/workspace/code/project')).toBe('/workspace/Code/project');
|
|
32
|
+
const result = runtime.normalizeSettingsPaths({
|
|
33
|
+
projects: [{ id: 'project', path: '/workspace/code/project' }],
|
|
34
|
+
lastDirectory: '/workspace/code/project',
|
|
35
|
+
pinnedDirectories: ['/workspace/code/project'],
|
|
36
|
+
});
|
|
37
|
+
expect(result.changed).toBe(true);
|
|
38
|
+
expect(result.settings.projects[0].path).toBe('/workspace/Code/project');
|
|
39
|
+
expect(result.settings.lastDirectory).toBe('/workspace/Code/project');
|
|
40
|
+
expect(result.settings.pinnedDirectories).toEqual(['/workspace/Code/project']);
|
|
41
|
+
expect(runtime.normalizeSettingsPaths(result.settings).changed).toBe(false);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('preserves distinct directory casing when native realpath reports distinct locations', () => {
|
|
45
|
+
const runtime = createTestRuntime({ realpathSync: Object.assign((p) => p, { native: (p) => p }) });
|
|
46
|
+
expect(runtime.sanitizeProjects([
|
|
47
|
+
{ id: 'lower', path: '/workspace/code/project' },
|
|
48
|
+
{ id: 'upper', path: '/workspace/Code/project' },
|
|
49
|
+
])).toHaveLength(2);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('keeps unavailable paths when native realpath fails', () => {
|
|
53
|
+
const runtime = createTestRuntime({
|
|
54
|
+
realpathSync: Object.assign(() => '/incorrect-fallback', {
|
|
55
|
+
native: () => { throw new Error('EACCES'); },
|
|
56
|
+
}),
|
|
57
|
+
});
|
|
58
|
+
expect(runtime.normalizePathForPersistence('/unavailable/project')).toBe('/unavailable/project');
|
|
59
|
+
});
|
|
60
|
+
|
|
25
61
|
it('resolves symlinks via realpathSync', () => {
|
|
26
62
|
const runtime = createTestRuntime({
|
|
27
63
|
realpathSync: (p) =>
|
|
@@ -5,8 +5,9 @@ import os from 'os';
|
|
|
5
5
|
import path from 'path';
|
|
6
6
|
import { createProjectIdFromPath } from '../projects/project-id.js';
|
|
7
7
|
import { createSettingsRuntime } from './settings-runtime.js';
|
|
8
|
+
import { createSettingsNormalizationRuntime } from './settings-normalization-runtime.js';
|
|
8
9
|
|
|
9
|
-
const createRuntime = async () => {
|
|
10
|
+
const createRuntime = async (overrides = {}) => {
|
|
10
11
|
const tempRoot = await fsPromises.mkdtemp(path.join(os.tmpdir(), 'oc-settings-runtime-'));
|
|
11
12
|
const settingsFilePath = path.join(tempRoot, 'settings.json');
|
|
12
13
|
const runtime = createSettingsRuntime({
|
|
@@ -21,6 +22,7 @@ const createRuntime = async () => {
|
|
|
21
22
|
normalizeStringArray: (values) => Array.isArray(values) ? values.filter((value) => typeof value === 'string') : [],
|
|
22
23
|
formatSettingsResponse: (settings) => settings,
|
|
23
24
|
resolveDirectoryCandidate: (value) => value,
|
|
25
|
+
...overrides,
|
|
24
26
|
});
|
|
25
27
|
|
|
26
28
|
return {
|
|
@@ -34,6 +36,61 @@ const createRuntime = async () => {
|
|
|
34
36
|
};
|
|
35
37
|
|
|
36
38
|
describe('settings runtime', () => {
|
|
39
|
+
it('repairs existing project casing, migrates project data, and persists new paths canonically', async () => {
|
|
40
|
+
const normalization = createSettingsNormalizationRuntime({
|
|
41
|
+
os, path, processLike: process,
|
|
42
|
+
realpathSync: Object.assign((value) => value, {
|
|
43
|
+
native: (value) => value.replace(`${path.sep}code${path.sep}`, `${path.sep}Code${path.sep}`),
|
|
44
|
+
}),
|
|
45
|
+
});
|
|
46
|
+
let failSettingsWrite = true;
|
|
47
|
+
const { runtime, settingsFilePath, tempRoot, cleanup } = await createRuntime({
|
|
48
|
+
...normalization,
|
|
49
|
+
fsPromises: {
|
|
50
|
+
...fsPromises,
|
|
51
|
+
rename: async (source, target) => {
|
|
52
|
+
if (failSettingsWrite && path.basename(target) === 'settings.json') {
|
|
53
|
+
throw Object.assign(new Error('Simulated settings write failure'), { code: 'EIO' });
|
|
54
|
+
}
|
|
55
|
+
return fsPromises.rename(source, target);
|
|
56
|
+
},
|
|
57
|
+
},
|
|
58
|
+
});
|
|
59
|
+
try {
|
|
60
|
+
const legacyPath = path.join(tempRoot, 'code', 'project');
|
|
61
|
+
const canonicalPath = path.join(tempRoot, 'Code', 'project');
|
|
62
|
+
const oldId = createProjectIdFromPath(legacyPath);
|
|
63
|
+
const newId = createProjectIdFromPath(canonicalPath);
|
|
64
|
+
const projectsRoot = path.join(tempRoot, 'projects');
|
|
65
|
+
await fsPromises.mkdir(canonicalPath, { recursive: true });
|
|
66
|
+
await fsPromises.mkdir(projectsRoot, { recursive: true });
|
|
67
|
+
await fsPromises.writeFile(path.join(projectsRoot, `${oldId}.json`), JSON.stringify({ projectNotes: 'Keep these notes' }));
|
|
68
|
+
await fsPromises.writeFile(settingsFilePath, JSON.stringify({
|
|
69
|
+
projects: [{ id: oldId, path: legacyPath, label: 'My project' }],
|
|
70
|
+
activeProjectId: oldId,
|
|
71
|
+
lastDirectory: legacyPath,
|
|
72
|
+
}));
|
|
73
|
+
|
|
74
|
+
const original = await fsPromises.readFile(settingsFilePath, 'utf8');
|
|
75
|
+
await expect(runtime.readSettingsFromDiskMigrated()).rejects.toThrow('Simulated settings write failure');
|
|
76
|
+
expect(await fsPromises.readFile(settingsFilePath, 'utf8')).toBe(original);
|
|
77
|
+
failSettingsWrite = false;
|
|
78
|
+
const settings = await runtime.readSettingsFromDiskMigrated();
|
|
79
|
+
expect(settings.projects).toMatchObject([{ id: newId, path: canonicalPath, label: 'My project' }]);
|
|
80
|
+
expect(settings.activeProjectId).toBe(newId);
|
|
81
|
+
expect(settings.lastDirectory).toBe(canonicalPath);
|
|
82
|
+
expect(JSON.parse(await fsPromises.readFile(path.join(projectsRoot, `${newId}.json`), 'utf8')).projectNotes).toBe('Keep these notes');
|
|
83
|
+
expect(JSON.parse(await fsPromises.readFile(settingsFilePath, 'utf8'))).toEqual(settings);
|
|
84
|
+
expect(await runtime.readSettingsFromDiskMigrated()).toEqual(settings);
|
|
85
|
+
|
|
86
|
+
const saved = await runtime.persistSettings({ ...settings, projects: [{ id: oldId, path: legacyPath }] });
|
|
87
|
+
expect(saved.projects).toMatchObject([{ id: newId, path: canonicalPath }]);
|
|
88
|
+
expect(saved.activeProjectId).toBe(newId);
|
|
89
|
+
} finally {
|
|
90
|
+
await cleanup();
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
|
|
37
94
|
it('migrates absent notification toggles to their defaults', async () => {
|
|
38
95
|
const { runtime, settingsFilePath, cleanup } = await createRuntime();
|
|
39
96
|
try {
|
|
@@ -2,18 +2,18 @@ import fs from 'fs';
|
|
|
2
2
|
import path from 'path';
|
|
3
3
|
import os from 'os';
|
|
4
4
|
import yaml from 'yaml';
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
getGlobalConfigDirectory, readConfigFile, isPlainObject, readConfigLayers,
|
|
7
|
+
readConfig, getConfigForPath, writeConfig, getJsonEntrySource, getJsonWriteTarget,
|
|
8
|
+
} from './config-files.js';
|
|
6
9
|
|
|
7
10
|
// ============== PATH CONSTANTS ==============
|
|
8
11
|
|
|
9
|
-
const OPENCODE_CONFIG_DIR =
|
|
12
|
+
const OPENCODE_CONFIG_DIR = getGlobalConfigDirectory();
|
|
10
13
|
const AGENT_DIR = path.join(OPENCODE_CONFIG_DIR, 'agents');
|
|
11
14
|
const COMMAND_DIR = path.join(OPENCODE_CONFIG_DIR, 'commands');
|
|
12
15
|
const SKILL_DIR = path.join(OPENCODE_CONFIG_DIR, 'skills');
|
|
13
|
-
const CONFIG_FILE = path.join(OPENCODE_CONFIG_DIR, '
|
|
14
|
-
const CUSTOM_CONFIG_FILE = process.env.OPENCODE_CONFIG
|
|
15
|
-
? path.resolve(process.env.OPENCODE_CONFIG)
|
|
16
|
-
: null;
|
|
16
|
+
const CONFIG_FILE = path.join(OPENCODE_CONFIG_DIR, 'opencode.jsonc');
|
|
17
17
|
const PROMPT_FILE_PATTERN = /^\{file:(.+)\}$/i;
|
|
18
18
|
|
|
19
19
|
// ============== SCOPE TYPE CONSTANTS ==============
|
|
@@ -89,175 +89,6 @@ function writeMdFile(filePath, frontmatter, body) {
|
|
|
89
89
|
|
|
90
90
|
// ============== CONFIG FILE OPERATIONS ==============
|
|
91
91
|
|
|
92
|
-
function getProjectConfigCandidates(workingDirectory) {
|
|
93
|
-
if (!workingDirectory) return [];
|
|
94
|
-
return [
|
|
95
|
-
path.join(workingDirectory, 'opencode.json'),
|
|
96
|
-
path.join(workingDirectory, 'opencode.jsonc'),
|
|
97
|
-
path.join(workingDirectory, '.opencode', 'opencode.json'),
|
|
98
|
-
path.join(workingDirectory, '.opencode', 'opencode.jsonc'),
|
|
99
|
-
];
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
function getProjectConfigPath(workingDirectory) {
|
|
103
|
-
if (!workingDirectory) return null;
|
|
104
|
-
|
|
105
|
-
const candidates = getProjectConfigCandidates(workingDirectory);
|
|
106
|
-
|
|
107
|
-
for (const candidate of candidates) {
|
|
108
|
-
if (fs.existsSync(candidate)) {
|
|
109
|
-
return candidate;
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
return candidates[0];
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
function getConfigPaths(workingDirectory) {
|
|
117
|
-
return {
|
|
118
|
-
userPaths: [
|
|
119
|
-
path.join(OPENCODE_CONFIG_DIR, 'config.json'),
|
|
120
|
-
path.join(OPENCODE_CONFIG_DIR, 'opencode.json'),
|
|
121
|
-
path.join(OPENCODE_CONFIG_DIR, 'opencode.jsonc'),
|
|
122
|
-
],
|
|
123
|
-
projectPath: getProjectConfigPath(workingDirectory),
|
|
124
|
-
customPath: CUSTOM_CONFIG_FILE
|
|
125
|
-
};
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
function getPrimaryUserConfigPath(userPaths) {
|
|
129
|
-
for (const userPath of userPaths) {
|
|
130
|
-
if (fs.existsSync(userPath)) {
|
|
131
|
-
return userPath;
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
return CONFIG_FILE;
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
function readConfigFile(filePath) {
|
|
139
|
-
if (!filePath || !fs.existsSync(filePath)) {
|
|
140
|
-
return {};
|
|
141
|
-
}
|
|
142
|
-
try {
|
|
143
|
-
const content = fs.readFileSync(filePath, 'utf8');
|
|
144
|
-
const normalized = content.trim();
|
|
145
|
-
if (!normalized) {
|
|
146
|
-
return {};
|
|
147
|
-
}
|
|
148
|
-
return parseJsonc(normalized, [], { allowTrailingComma: true });
|
|
149
|
-
} catch (error) {
|
|
150
|
-
console.error(`Failed to read config file: ${filePath}`, error);
|
|
151
|
-
throw new Error('Failed to read OpenCode configuration');
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
function isPlainObject(value) {
|
|
156
|
-
return value && typeof value === 'object' && !Array.isArray(value);
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
function mergeConfigs(base, override) {
|
|
160
|
-
if (!isPlainObject(base) || !isPlainObject(override)) {
|
|
161
|
-
return override;
|
|
162
|
-
}
|
|
163
|
-
const result = { ...base };
|
|
164
|
-
for (const [key, value] of Object.entries(override)) {
|
|
165
|
-
if (key in result) {
|
|
166
|
-
const baseValue = result[key];
|
|
167
|
-
if (isPlainObject(baseValue) && isPlainObject(value)) {
|
|
168
|
-
result[key] = mergeConfigs(baseValue, value);
|
|
169
|
-
} else {
|
|
170
|
-
result[key] = value;
|
|
171
|
-
}
|
|
172
|
-
} else {
|
|
173
|
-
result[key] = value;
|
|
174
|
-
}
|
|
175
|
-
}
|
|
176
|
-
return result;
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
function readConfigLayers(workingDirectory) {
|
|
180
|
-
const { userPaths, projectPath, customPath } = getConfigPaths(workingDirectory);
|
|
181
|
-
const userPath = getPrimaryUserConfigPath(userPaths);
|
|
182
|
-
const userConfig = readConfigFile(userPath);
|
|
183
|
-
const projectConfig = readConfigFile(projectPath);
|
|
184
|
-
const customConfig = readConfigFile(customPath);
|
|
185
|
-
const mergedConfig = mergeConfigs(mergeConfigs(userConfig, projectConfig), customConfig);
|
|
186
|
-
|
|
187
|
-
return {
|
|
188
|
-
userConfig,
|
|
189
|
-
projectConfig,
|
|
190
|
-
customConfig,
|
|
191
|
-
mergedConfig,
|
|
192
|
-
paths: { userPath, projectPath, customPath }
|
|
193
|
-
};
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
function readConfig(workingDirectory) {
|
|
197
|
-
return readConfigLayers(workingDirectory).mergedConfig;
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
function getConfigForPath(layers, targetPath) {
|
|
201
|
-
if (!targetPath) {
|
|
202
|
-
return layers.userConfig;
|
|
203
|
-
}
|
|
204
|
-
if (layers.paths.customPath && targetPath === layers.paths.customPath) {
|
|
205
|
-
return layers.customConfig;
|
|
206
|
-
}
|
|
207
|
-
if (layers.paths.projectPath && targetPath === layers.paths.projectPath) {
|
|
208
|
-
return layers.projectConfig;
|
|
209
|
-
}
|
|
210
|
-
return layers.userConfig;
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
function writeConfig(config, filePath = CONFIG_FILE) {
|
|
214
|
-
try {
|
|
215
|
-
if (fs.existsSync(filePath)) {
|
|
216
|
-
const backupFile = `${filePath}.openchamber.backup`;
|
|
217
|
-
fs.copyFileSync(filePath, backupFile);
|
|
218
|
-
console.log(`Created config backup: ${backupFile}`);
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
222
|
-
fs.writeFileSync(filePath, JSON.stringify(config, null, 2), 'utf8');
|
|
223
|
-
console.log(`Successfully wrote config file: ${filePath}`);
|
|
224
|
-
} catch (error) {
|
|
225
|
-
console.error(`Failed to write config file: ${filePath}`, error);
|
|
226
|
-
throw new Error('Failed to write OpenCode configuration');
|
|
227
|
-
}
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
function getJsonEntrySource(layers, sectionKey, entryName) {
|
|
231
|
-
const { userConfig, projectConfig, customConfig, paths } = layers;
|
|
232
|
-
const customSection = customConfig?.[sectionKey]?.[entryName];
|
|
233
|
-
if (customSection !== undefined) {
|
|
234
|
-
return { section: customSection, config: customConfig, path: paths.customPath, exists: true };
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
const projectSection = projectConfig?.[sectionKey]?.[entryName];
|
|
238
|
-
if (projectSection !== undefined) {
|
|
239
|
-
return { section: projectSection, config: projectConfig, path: paths.projectPath, exists: true };
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
const userSection = userConfig?.[sectionKey]?.[entryName];
|
|
243
|
-
if (userSection !== undefined) {
|
|
244
|
-
return { section: userSection, config: userConfig, path: paths.userPath, exists: true };
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
return { section: null, config: null, path: null, exists: false };
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
function getJsonWriteTarget(layers, preferredScope) {
|
|
251
|
-
const { userConfig, projectConfig, customConfig, paths } = layers;
|
|
252
|
-
if (paths.customPath) {
|
|
253
|
-
return { config: customConfig, path: paths.customPath };
|
|
254
|
-
}
|
|
255
|
-
if (preferredScope === AGENT_SCOPE.PROJECT && paths.projectPath) {
|
|
256
|
-
return { config: projectConfig, path: paths.projectPath };
|
|
257
|
-
}
|
|
258
|
-
return { config: userConfig, path: paths.userPath };
|
|
259
|
-
}
|
|
260
|
-
|
|
261
92
|
// ============== GIT/WORKTREE HELPERS ==============
|
|
262
93
|
|
|
263
94
|
function getAncestors(startDir, stopDir) {
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { OPENCODE2_NPM_PACKAGE } from './opencode2-pin.js';
|
|
2
|
+
|
|
3
|
+
const stableVersion = (value) => typeof value === 'string' && /^2\.\d+\.\d+$/.test(value) ? value : null;
|
|
4
|
+
|
|
5
|
+
// One bounded registry snapshot per host, shared by all connected clients.
|
|
6
|
+
export function createOpenCodeUpdateDiscovery({ fetchImpl = (...args) => fetch(...args), now = Date.now } = {}) {
|
|
7
|
+
let snapshot = null;
|
|
8
|
+
let expiresAt = 0;
|
|
9
|
+
let pending = null;
|
|
10
|
+
|
|
11
|
+
const registry = () => {
|
|
12
|
+
if (snapshot && now() < expiresAt) return Promise.resolve(snapshot);
|
|
13
|
+
if (pending) return pending;
|
|
14
|
+
pending = (async () => {
|
|
15
|
+
try {
|
|
16
|
+
const response = await fetchImpl(`https://registry.npmjs.org/${encodeURIComponent(OPENCODE2_NPM_PACKAGE)}/latest`, {
|
|
17
|
+
headers: { Accept: 'application/json' },
|
|
18
|
+
signal: AbortSignal.timeout(5000),
|
|
19
|
+
});
|
|
20
|
+
if (!response.ok) throw new Error(`OpenCode registry returned HTTP ${response.status}`);
|
|
21
|
+
const version = stableVersion((await response.json()).version);
|
|
22
|
+
if (!version) throw new Error('OpenCode registry did not return a stable 2.x version');
|
|
23
|
+
snapshot = { version, error: null };
|
|
24
|
+
expiresAt = now() + 5 * 60_000;
|
|
25
|
+
} catch (error) {
|
|
26
|
+
snapshot = { version: snapshot?.version ?? null, error: error instanceof Error ? error.message : 'OpenCode registry check failed' };
|
|
27
|
+
expiresAt = now() + 30_000;
|
|
28
|
+
}
|
|
29
|
+
return snapshot;
|
|
30
|
+
})().finally(() => { pending = null; });
|
|
31
|
+
return pending;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
return async () => {
|
|
35
|
+
const remote = await registry();
|
|
36
|
+
if (remote.error) throw new Error(remote.error);
|
|
37
|
+
return remote.version;
|
|
38
|
+
};
|
|
39
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import { createOpenCodeUpdateDiscovery } from './update-discovery.js';
|
|
3
|
+
|
|
4
|
+
describe('OpenCode remote update discovery', () => {
|
|
5
|
+
it('shares concurrent requests, caches for five minutes and refreshes after expiry', async () => {
|
|
6
|
+
let time = 0;
|
|
7
|
+
const fetchImpl = vi.fn(async () => Response.json({ version: '2.0.18' }));
|
|
8
|
+
const discover = createOpenCodeUpdateDiscovery({ fetchImpl, now: () => time });
|
|
9
|
+
expect(await Promise.all([discover(), discover(), discover()])).toEqual(['2.0.18', '2.0.18', '2.0.18']);
|
|
10
|
+
expect(fetchImpl).toHaveBeenCalledTimes(1);
|
|
11
|
+
time = 299_999;
|
|
12
|
+
await discover();
|
|
13
|
+
expect(fetchImpl).toHaveBeenCalledTimes(1);
|
|
14
|
+
time = 300_000;
|
|
15
|
+
fetchImpl.mockResolvedValueOnce(Response.json({ version: '2.0.19' }));
|
|
16
|
+
expect(await discover()).toBe('2.0.19');
|
|
17
|
+
expect(fetchImpl).toHaveBeenCalledTimes(2);
|
|
18
|
+
expect(fetchImpl.mock.calls[0][1].signal).toBeInstanceOf(AbortSignal);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it.each(['1.18.4', '3.0.0', '2.0.19-beta.1', 'latest', undefined])('rejects an invalid stable release %s', async (version) => {
|
|
22
|
+
const discover = createOpenCodeUpdateDiscovery({ fetchImpl: async () => Response.json({ version }) });
|
|
23
|
+
await expect(discover()).rejects.toThrow('stable 2.x');
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('backs off errors without reporting stale success, then recovers', async () => {
|
|
27
|
+
let time = 0;
|
|
28
|
+
const fetchImpl = vi.fn().mockResolvedValueOnce(Response.json({ version: '2.0.18' }))
|
|
29
|
+
.mockRejectedValueOnce(new Error('offline')).mockResolvedValueOnce(Response.json({ version: '2.0.19' }));
|
|
30
|
+
const discover = createOpenCodeUpdateDiscovery({ fetchImpl, now: () => time });
|
|
31
|
+
await discover();
|
|
32
|
+
time = 300_000;
|
|
33
|
+
await expect(discover()).rejects.toThrow('offline');
|
|
34
|
+
await expect(discover()).rejects.toThrow('offline');
|
|
35
|
+
expect(fetchImpl).toHaveBeenCalledTimes(2);
|
|
36
|
+
time += 30_000;
|
|
37
|
+
expect(await discover()).toBe('2.0.19');
|
|
38
|
+
});
|
|
39
|
+
});
|