@vantran-se/openchamber-web 2.0.3 → 2.0.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +20 -28
- package/bin/cli.js +3 -1
- package/bin/cli.test.js +13 -4
- package/bin/lib/DOCUMENTATION.md +4 -1
- package/bin/lib/commands-serve.js +9 -0
- package/dist/assets/{CodeMirrorCommentWidgets-CWhdh0wf.js → CodeMirrorCommentWidgets-DCAQTLMM.js} +1 -1
- package/dist/assets/DiffView-uzNpHBlV.js +4 -0
- package/dist/assets/FilesView-Bkq0sjNx.js +17 -0
- package/dist/assets/{GitView-DKIjpnu4.js → GitView-BmibZC7n.js} +4 -4
- package/dist/assets/{GuestAttachDialog-BIa9jCfL.js → GuestAttachDialog-D7TJG1rt.js} +1 -1
- package/dist/assets/GuestSurfacePane-PAcYaFzP.js +1 -0
- package/dist/assets/{JsonTreeView-CtI1Mqwo.js → JsonTreeView-BNT2yUVu.js} +1 -1
- package/dist/assets/{LinearIssuesView-BD2zLu-r.js → LinearIssuesView-D3Azksju.js} +1 -1
- package/dist/assets/MarkdownImageGallery-BGY6tuiK.js +1 -0
- package/dist/assets/MarkdownRendererImpl-DJmhQ37f.js +12 -0
- package/dist/assets/{OnboardingScreen-B8TxloXZ.js → OnboardingScreen-D_41H2qC.js} +1 -1
- package/dist/assets/{PatchDiff--Civs1xY.js → PatchDiff-C0RSqa9W.js} +1 -1
- package/dist/assets/{PierreDiffViewer-BlDmw3-A.js → PierreDiffViewer-BBE7tTY2.js} +3 -3
- package/dist/assets/{PlanView-DyH2SyCP.js → PlanView-Bqc-xuDe.js} +4 -4
- package/dist/assets/PluginPane-DQ0k41Q-.js +3 -0
- package/dist/assets/SettingsView-BAiJU5hJ.js +30 -0
- package/dist/assets/{SettingsWindow-y9WpZqK6.js → SettingsWindow-DuC4ecLw.js} +1 -1
- package/dist/assets/{SubmoduleDiffSummary-DeFmhPFZ.js → SubmoduleDiffSummary-0HgmcMXz.js} +1 -1
- package/dist/assets/{ToolOutputDialog-8DmF7_PZ.js → ToolOutputDialog-DdHkQJEE.js} +6 -6
- package/dist/assets/{ToolPartDiffPreview-Ca15DKE1.js → ToolPartDiffPreview-DWdN0r5K.js} +2 -2
- package/dist/assets/{VirtualizedFileDiff-BfEBad09.js → VirtualizedFileDiff-ClDezf2d.js} +1 -1
- package/dist/assets/{WalkthroughView-Cj3K8Xz6.js → WalkthroughView-BJicp0RC.js} +2 -2
- package/dist/assets/{appThemeRegistry-DAdawJZn.js → appThemeRegistry-CiuyzfSW.js} +1 -1
- package/dist/assets/changesTree-CePHbFcj.js +1 -0
- package/dist/assets/commitAndPush-DTKb6IwT.js +1 -0
- package/dist/assets/{computeVirtualFileMetrics-RF-KjfUH.js → computeVirtualFileMetrics-kM_zJNMt.js} +2 -2
- package/dist/assets/de-BMnfXm-G.js +10 -0
- package/dist/assets/es-DKEvCCL6.js +16 -0
- package/dist/assets/fr-D9fgE2xE.js +14 -0
- package/dist/assets/index-DUke1nme.css +1 -0
- package/dist/assets/{index-CL7ZQ94-.js → index-DWB8bR_g.js} +1 -1
- package/dist/assets/index-Di6zUe5h.js +41 -0
- package/dist/assets/{isDiffPlainText-D2yJ9fO6.js → isDiffPlainText-DOoIsGqs.js} +1 -1
- package/dist/assets/{isFilePlainText-BwMEq1Ey.js → isFilePlainText-bt5VHvcd.js} +1 -1
- package/dist/assets/ja-BEXad-KW.js +16 -0
- package/dist/assets/ko-DGrQ0lEe.js +16 -0
- package/dist/assets/{main-quLSA7jc.js → main-BpLQbZC2.js} +2 -2
- package/dist/assets/main-CI9DtHJv.js +896 -0
- package/dist/assets/{markdown-shiki.worker-CahLvHIq.js → markdown-shiki.worker-Bg8SoAJJ.js} +8 -5
- package/dist/assets/markdownCore-DK2KK5RW.js +7 -0
- package/dist/assets/{miniChat-D-lJJibr.js → miniChat-CIWaR8wG.js} +2 -2
- package/dist/assets/{mobile-VTtphbW6.js → mobile-CnWKEYIP.js} +2 -2
- package/dist/assets/patchFileDiff-Cqn0C0az.js +13 -0
- package/dist/assets/pl-BlhkmR5b.js +16 -0
- package/dist/assets/pt-BR-Bs96Ew9I.js +16 -0
- package/dist/assets/{renderElectronMiniChatApp-DF7ch5eM.js → renderElectronMiniChatApp-D9t0-9nV.js} +2 -2
- package/dist/assets/renderMobileApp-CW7fVs_s.js +25 -0
- package/dist/assets/runtimeEndpointReset-D0PcCb3K.js +27 -0
- package/dist/assets/scheduledTasksApi-Dahv3ws0.js +1 -0
- package/dist/assets/{shikiHighlight-_HeCCHx1.js → shikiHighlight-l-MbaNS4.js} +1 -1
- package/dist/assets/{textMateThemeFromAppTheme-C95XdFGj.js → textMateThemeFromAppTheme-C-kGcOpD.js} +1 -1
- package/dist/assets/tr-CehUK1M9.js +7 -0
- package/dist/assets/uk-Bno5WGNz.js +16 -0
- package/dist/assets/{useAppFontEffects-ZM9ij19-.js → useAppFontEffects-DDjuwVqi.js} +160 -156
- package/dist/assets/{useInlineCommentController-CkfxMJPy.js → useInlineCommentController-Clg8EO3A.js} +2 -2
- package/dist/assets/{useRootScrollLock-BtiAC1fj.js → useRootScrollLock-nDcRrgJ1.js} +2 -2
- package/dist/assets/{vendor-codemirror-language-data-U-lXzXNK.js → vendor-codemirror-language-data-oKF0MAwd.js} +2 -2
- package/dist/assets/vendor-opencode-client-DSZl7jSi.js +12 -0
- package/dist/assets/vendor-zod-BSoePkpP.js +39 -0
- package/dist/assets/{web-components-C583Pmfm.js → web-components-Bc1WPd4i.js} +1 -1
- package/dist/assets/zh-CN-DnAweIWo.js +16 -0
- package/dist/assets/zh-TW-BbBBK25p.js +16 -0
- package/dist/index.html +5 -5
- package/dist/mini-chat.html +5 -5
- package/dist/mobile.html +5 -5
- package/package.json +3 -3
- package/server/index.d.ts +2 -0
- package/server/index.js +196 -52
- package/server/lib/agent-tool/DOCUMENTATION.md +5 -0
- package/server/lib/agent-tool/runtime.js +89 -51
- package/server/lib/agent-tool/runtime.test.js +17 -0
- package/server/lib/browser-control/DOCUMENTATION.md +14 -2
- package/server/lib/event-stream/DOCUMENTATION.md +2 -2
- package/server/lib/event-stream/global-hub.js +45 -7
- package/server/lib/event-stream/global-hub.test.js +128 -2
- package/server/lib/event-stream/global-ws-bridge.js +3 -1
- package/server/lib/event-stream/rebind.test.js +1 -0
- package/server/lib/event-stream/runtime.js +1 -2
- package/server/lib/event-stream/upstream-reader.js +3 -2
- package/server/lib/git/DOCUMENTATION.md +1 -1
- package/server/lib/git/service.js +22 -0
- package/server/lib/git/service.test.js +8 -0
- package/server/lib/guests/DOCUMENTATION.md +4 -3
- package/server/lib/guests/catalog.js +19 -1
- package/server/lib/guests/catalog.test.js +22 -0
- package/server/lib/guests/html-styles.js +5 -3
- package/server/lib/guests/html-styles.test.js +3 -1
- package/server/lib/guests/routes.js +11 -3
- package/server/lib/guests/service.js +5 -0
- package/server/lib/guests/surface.js +55 -9
- package/server/lib/guests/surface.test.js +56 -6
- package/server/lib/notifications/DOCUMENTATION.md +10 -0
- package/server/lib/notifications/emit-route.js +118 -0
- package/server/lib/notifications/emit-route.test.js +112 -0
- package/server/lib/openchamber-control/actions.js +16 -2
- package/server/lib/openchamber-control/service.js +23 -0
- package/server/lib/openchamber-control/service.test.js +31 -0
- package/server/lib/openchamber-sessions/fork-inheritance.js +103 -0
- package/server/lib/openchamber-sessions/fork-inheritance.test.js +69 -0
- package/server/lib/openchamber-sessions/routes.js +11 -0
- package/server/lib/openchamber-sessions/routes.test.js +18 -0
- package/server/lib/opencode/DOCUMENTATION.md +23 -23
- package/server/lib/opencode/bootstrap-runtime.js +14 -1
- package/server/lib/opencode/compatibility.js +6 -3
- package/server/lib/opencode/compatibility.test.js +8 -0
- package/server/lib/opencode/config-entity-v2.test.js +27 -0
- package/server/lib/opencode/config-paths.test.js +12 -0
- package/server/lib/opencode/config-v2.d.ts +2 -0
- package/server/lib/opencode/config-v2.js +42 -0
- package/server/lib/opencode/config-v2.test.js +17 -0
- package/server/lib/opencode/core-routes.js +20 -4
- package/server/lib/opencode/core-routes.test.js +41 -0
- package/server/lib/opencode/env-runtime.js +1 -8
- package/server/lib/opencode/feature-routes-runtime.js +2 -1
- package/server/lib/opencode/lifecycle.js +156 -2
- package/server/lib/opencode/lifecycle.test.js +255 -1
- package/server/lib/opencode/managed-config-file.js +18 -6
- package/server/lib/opencode/managed-config-file.test.js +23 -7
- package/server/lib/opencode/mcp.js +7 -2
- package/server/lib/opencode/network-runtime.js +16 -7
- package/server/lib/opencode/network-runtime.test.js +29 -1
- package/server/lib/opencode/npm-registry-config.d.ts +1 -0
- package/server/lib/opencode/npm-registry-config.js +123 -0
- package/server/lib/opencode/npm-registry-config.test.js +167 -0
- package/server/lib/opencode/npm-registry.js +5 -6
- package/server/lib/opencode/npm-registry.test.js +30 -0
- package/server/lib/opencode/openchamber-routes.js +29 -11
- package/server/lib/opencode/openchamber-routes.test.js +18 -0
- package/server/lib/opencode/project-directory-runtime.js +8 -0
- package/server/lib/opencode/project-directory-runtime.test.js +13 -0
- package/server/lib/opencode/provider-source-route.test.js +59 -0
- package/server/lib/opencode/providers.js +3 -0
- package/server/lib/opencode/providers.test.js +62 -0
- package/server/lib/opencode/proxy.js +53 -16
- package/server/lib/opencode/routes-upgrade.test.js +1 -1
- package/server/lib/opencode/routes.js +36 -1
- package/server/lib/opencode/server-startup-runtime.js +25 -0
- package/server/lib/opencode/server-startup-runtime.test.js +67 -1
- package/server/lib/opencode/server-utils-runtime.js +5 -0
- package/server/lib/opencode/settings-helpers.js +10 -0
- package/server/lib/opencode/settings-helpers.test.js +2 -2
- package/server/lib/opencode/settings-registry.json +17 -0
- package/server/lib/opencode/settings-runtime.js +1 -0
- package/server/lib/opencode/shared-service-runtime.js +155 -0
- package/server/lib/opencode/shared-service-runtime.test.js +180 -0
- package/server/lib/opencode/shared.js +49 -4
- package/server/lib/opencode/shared.test.js +78 -1
- package/server/lib/opencode/shutdown-runtime.js +14 -5
- package/server/lib/opencode/shutdown-runtime.test.js +77 -2
- package/server/lib/opencode/skill-routes.js +28 -12
- package/server/lib/opencode/skill-routes.test.js +121 -1
- package/server/lib/opencode/startup-pipeline-runtime.js +5 -4
- package/server/lib/opencode/startup-pipeline-runtime.test.js +18 -7
- package/server/lib/opencode/upgrade-capability.js +0 -14
- package/server/lib/opencode/v1-migration-topup.js +21 -15
- package/server/lib/opencode/v1-migration-topup.test.js +14 -6
- package/server/lib/opencode/v2-install.d.ts +1 -0
- package/server/lib/opencode/v2-install.js +8 -7
- package/server/lib/opencode/v2-install.test.js +13 -4
- package/server/lib/opencode/watcher.js +3 -1
- package/server/lib/opencode/watcher.test.js +54 -0
- package/server/lib/opencode/websearch-config.js +13 -1
- package/server/lib/quota/providers/kimi.js +1 -1
- package/server/lib/realtime-proxy.js +5 -1
- package/server/lib/realtime-proxy.test.js +29 -0
- package/server/lib/relay/tunnel-host.js +4 -1
- package/server/lib/relay/tunnel-host.test.js +11 -0
- package/server/lib/session-assist/DOCUMENTATION.md +17 -5
- package/server/lib/session-assist/runtime.js +19 -2
- package/server/lib/session-assist/runtime.test.js +8 -0
- package/server/lib/session-goal/DOCUMENTATION.md +11 -0
- package/server/lib/small-model/DOCUMENTATION.md +11 -10
- package/server/lib/small-model/index.js +27 -32
- package/server/lib/small-model/index.test.js +28 -16
- package/server/lib/spaces/DOCUMENTATION.md +404 -32
- package/server/lib/spaces/code-in-bait.js +145 -6
- package/server/lib/spaces/code-in.js +10 -150
- package/server/lib/spaces/code-in.test.js +7 -83
- package/server/lib/spaces/code-out.js +1819 -0
- package/server/lib/spaces/code-out.test.js +3945 -0
- package/server/lib/spaces/code-transfer.js +173 -0
- package/server/lib/spaces/command-stream.test.js +181 -0
- package/server/lib/spaces/dispatcher.js +670 -0
- package/server/lib/spaces/dispatcher.test.js +582 -0
- package/server/lib/spaces/gatekeeper-channel.js +61 -7
- package/server/lib/spaces/gatekeeper-channel.test.js +43 -7
- package/server/lib/spaces/gatekeeper-program.cjs +68 -14
- package/server/lib/spaces/gatekeeper-program.test.js +118 -3
- package/server/lib/spaces/hardening.js +20 -4
- package/server/lib/spaces/hardening.test.js +18 -3
- package/server/lib/spaces/host-git.js +7 -4
- package/server/lib/spaces/host-git.test.js +6 -2
- package/server/lib/spaces/host.js +310 -0
- package/server/lib/spaces/host.test.js +273 -0
- package/server/lib/spaces/journey.js +552 -0
- package/server/lib/spaces/journey.test.js +589 -0
- package/server/lib/spaces/layout.js +42 -8
- package/server/lib/spaces/manager.js +4 -3
- package/server/lib/spaces/manager.test.js +6 -0
- package/server/lib/spaces/places/code-out.docker.live.test.js +412 -0
- package/server/lib/spaces/places/contract-suite.js +36 -0
- package/server/lib/spaces/places/dispatcher.docker.live.test.js +296 -0
- package/server/lib/spaces/places/docker-live-support.js +27 -4
- package/server/lib/spaces/places/docker.js +33 -5
- package/server/lib/spaces/places/docker.test.js +42 -3
- package/server/lib/spaces/places/escape-suite.js +118 -14
- package/server/lib/spaces/places/fake-docker.js +3 -1
- package/server/lib/spaces/places/grants.docker.live.test.js +197 -0
- package/server/lib/spaces/places/memory-place.js +24 -1
- package/server/lib/spaces/places/registry.js +1 -1
- package/server/lib/spaces/places/server.docker.live.test.js +69 -33
- package/server/lib/spaces/places/sessions.docker.live.test.js +222 -0
- package/server/lib/spaces/routes.js +216 -0
- package/server/lib/spaces/routes.test.js +213 -0
- package/server/lib/spaces/run-command.js +261 -8
- package/server/lib/spaces/run-command.test.js +193 -3
- package/server/lib/spaces/space-events.js +153 -0
- package/server/lib/spaces/space-events.test.js +160 -0
- package/server/lib/spaces/space-opencode.js +52 -0
- package/server/lib/spaces/space-opencode.test.js +41 -0
- package/server/lib/spaces/space-records.js +150 -0
- package/server/lib/spaces/space-records.test.js +113 -0
- package/server/lib/spaces/space-sessions.js +210 -0
- package/server/lib/spaces/space-sessions.test.js +161 -0
- package/server/lib/spaces/websocket.js +329 -0
- package/server/lib/spaces/websocket.test.js +345 -0
- package/server/lib/ui-auth/ui-auth.js +8 -2
- package/server/lib/ui-auth/ui-auth.test.js +29 -0
- package/server/opencode-proxy.test.js +271 -0
- package/server/proxy-headers.js +8 -2
- package/server/proxy-headers.test.js +20 -0
- package/dist/assets/DiffView-CX3wAX2S.js +0 -4
- package/dist/assets/FilesView-OL3CON3r.js +0 -17
- package/dist/assets/GuestSurfacePane-B_cqX5I5.js +0 -1
- package/dist/assets/MarkdownImageGallery-DFiTFZgj.js +0 -1
- package/dist/assets/MarkdownRendererImpl-DZm5sf1W.js +0 -12
- package/dist/assets/PluginPane-BibJit86.js +0 -3
- package/dist/assets/SettingsView-h-VpVO47.js +0 -29
- package/dist/assets/commitAndPush-BIPfOMXY.js +0 -1
- package/dist/assets/de-BqCNGROu.js +0 -10
- package/dist/assets/es-BTPpTRgN.js +0 -16
- package/dist/assets/fr-hxbzMdAg.js +0 -14
- package/dist/assets/index-CLcM8HKx.css +0 -1
- package/dist/assets/index-CvLgnuNI.js +0 -41
- package/dist/assets/ja-_zLwwYiy.js +0 -16
- package/dist/assets/ko-CFn3Dzhc.js +0 -16
- package/dist/assets/main-BLJaug9I.js +0 -896
- package/dist/assets/markdownCore-DS9FdwrI.js +0 -5
- package/dist/assets/patchFileDiff-D9iBcTEi.js +0 -13
- package/dist/assets/pl-DFHE7xac.js +0 -16
- package/dist/assets/pt-BR-CUjZF179.js +0 -16
- package/dist/assets/renderMobileApp-CNQhdXsi.js +0 -25
- package/dist/assets/runtimeEndpointReset-DeaERc0M.js +0 -27
- package/dist/assets/scheduledTasksApi-BD6Vx9pu.js +0 -1
- package/dist/assets/tr-BB6sA_zH.js +0 -7
- package/dist/assets/uk-CB4o8g-o.js +0 -16
- package/dist/assets/vendor-opencode-client-_BAHQo4R.js +0 -12
- package/dist/assets/vendor-zod-Gea9uBlY.js +0 -39
- package/dist/assets/zh-CN-BRyDuWhc.js +0 -16
- package/dist/assets/zh-TW-BDGbWA1Q.js +0 -16
|
@@ -16,18 +16,20 @@ const createRuntime = (server, overrides = {}) => {
|
|
|
16
16
|
openCodeWatcherRuntime: { stop: vi.fn() },
|
|
17
17
|
sessionRuntime: { dispose: vi.fn() },
|
|
18
18
|
scheduledTasksRuntime: { stop: vi.fn() },
|
|
19
|
+
globalEventHub: { stop: vi.fn() },
|
|
19
20
|
getHealthCheckInterval: () => null,
|
|
20
21
|
clearHealthCheckInterval: vi.fn(),
|
|
21
22
|
getTerminalRuntime: () => null,
|
|
22
23
|
setTerminalRuntime: vi.fn(),
|
|
23
24
|
getMessageStreamRuntime: () => null,
|
|
24
25
|
setMessageStreamRuntime: vi.fn(),
|
|
25
|
-
|
|
26
|
+
getOpenCodeConnectionKind: () => 'explicit-external',
|
|
26
27
|
getOpenCodePort: () => null,
|
|
27
28
|
getOpenCodeProcess: () => null,
|
|
28
29
|
setOpenCodeProcess: vi.fn(),
|
|
29
30
|
killProcessOnPort: vi.fn(),
|
|
30
31
|
waitForPortRelease: vi.fn(async () => true),
|
|
32
|
+
disposeSharedOpenCodeService: vi.fn(async () => {}),
|
|
31
33
|
getServer: () => server,
|
|
32
34
|
getUiAuthController: () => null,
|
|
33
35
|
setUiAuthController: vi.fn(),
|
|
@@ -151,7 +153,7 @@ describe('graceful shutdown runtime', () => {
|
|
|
151
153
|
throw new Error('fixture cleanup failure');
|
|
152
154
|
},
|
|
153
155
|
getTerminalRuntime: () => ({ shutdown: terminalShutdown }),
|
|
154
|
-
|
|
156
|
+
getOpenCodeConnectionKind: () => 'managed-owned',
|
|
155
157
|
getOpenCodeProcess: () => ({ close: processClose }),
|
|
156
158
|
});
|
|
157
159
|
await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve));
|
|
@@ -187,6 +189,70 @@ describe('graceful shutdown runtime', () => {
|
|
|
187
189
|
}
|
|
188
190
|
});
|
|
189
191
|
|
|
192
|
+
it('only managed-owned closes and kills OpenCode before waiting for its port', async () => {
|
|
193
|
+
const close = vi.fn(async () => {});
|
|
194
|
+
const killProcessOnPort = vi.fn();
|
|
195
|
+
const waitForPortRelease = vi.fn(async () => true);
|
|
196
|
+
const disposeSharedOpenCodeService = vi.fn(async () => {});
|
|
197
|
+
const runtime = createRuntime(null, {
|
|
198
|
+
getOpenCodeConnectionKind: () => 'managed-owned',
|
|
199
|
+
getOpenCodePort: () => 4096,
|
|
200
|
+
getOpenCodeProcess: () => ({ close }),
|
|
201
|
+
killProcessOnPort,
|
|
202
|
+
waitForPortRelease,
|
|
203
|
+
disposeSharedOpenCodeService,
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
await runtime.gracefulShutdown({ exitProcess: false });
|
|
207
|
+
|
|
208
|
+
expect(close).toHaveBeenCalledOnce();
|
|
209
|
+
expect(killProcessOnPort).toHaveBeenCalledWith(4096);
|
|
210
|
+
expect(waitForPortRelease).toHaveBeenCalledWith(4096, 5000);
|
|
211
|
+
expect(disposeSharedOpenCodeService).not.toHaveBeenCalled();
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
it('shared-local disposes connection state without touching the shared service process', async () => {
|
|
215
|
+
const close = vi.fn();
|
|
216
|
+
const killProcessOnPort = vi.fn();
|
|
217
|
+
const waitForPortRelease = vi.fn();
|
|
218
|
+
const disposeSharedOpenCodeService = vi.fn(async () => {});
|
|
219
|
+
const runtime = createRuntime(null, {
|
|
220
|
+
getOpenCodeConnectionKind: () => 'shared-local',
|
|
221
|
+
getOpenCodeProcess: () => ({ close }),
|
|
222
|
+
killProcessOnPort,
|
|
223
|
+
waitForPortRelease,
|
|
224
|
+
disposeSharedOpenCodeService,
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
await runtime.gracefulShutdown({ exitProcess: false });
|
|
228
|
+
|
|
229
|
+
expect(disposeSharedOpenCodeService).toHaveBeenCalledOnce();
|
|
230
|
+
expect(close).not.toHaveBeenCalled();
|
|
231
|
+
expect(killProcessOnPort).not.toHaveBeenCalled();
|
|
232
|
+
expect(waitForPortRelease).not.toHaveBeenCalled();
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
it('explicit-external leaves OpenCode and shared state untouched', async () => {
|
|
236
|
+
const close = vi.fn();
|
|
237
|
+
const killProcessOnPort = vi.fn();
|
|
238
|
+
const waitForPortRelease = vi.fn();
|
|
239
|
+
const disposeSharedOpenCodeService = vi.fn();
|
|
240
|
+
const runtime = createRuntime(null, {
|
|
241
|
+
getOpenCodeConnectionKind: () => 'explicit-external',
|
|
242
|
+
getOpenCodeProcess: () => ({ close }),
|
|
243
|
+
killProcessOnPort,
|
|
244
|
+
waitForPortRelease,
|
|
245
|
+
disposeSharedOpenCodeService,
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
await runtime.gracefulShutdown({ exitProcess: false });
|
|
249
|
+
|
|
250
|
+
expect(close).not.toHaveBeenCalled();
|
|
251
|
+
expect(killProcessOnPort).not.toHaveBeenCalled();
|
|
252
|
+
expect(waitForPortRelease).not.toHaveBeenCalled();
|
|
253
|
+
expect(disposeSharedOpenCodeService).not.toHaveBeenCalled();
|
|
254
|
+
});
|
|
255
|
+
|
|
190
256
|
it('stops guest services during shutdown', async () => {
|
|
191
257
|
const server = {
|
|
192
258
|
close: vi.fn((callback) => {
|
|
@@ -255,6 +321,15 @@ describe('graceful shutdown runtime', () => {
|
|
|
255
321
|
expect(vi.getTimerCount()).toBe(0);
|
|
256
322
|
});
|
|
257
323
|
|
|
324
|
+
it('stops the application-owned global event hub during embedded shutdown', async () => {
|
|
325
|
+
const globalEventHub = { stop: vi.fn() };
|
|
326
|
+
const runtime = createRuntime(null, { globalEventHub });
|
|
327
|
+
|
|
328
|
+
await runtime.gracefulShutdown({ exitProcess: false });
|
|
329
|
+
|
|
330
|
+
expect(globalEventHub.stop).toHaveBeenCalledOnce();
|
|
331
|
+
});
|
|
332
|
+
|
|
258
333
|
it('isolates failed viewer and relay cleanup and still drains guests and exits', async () => {
|
|
259
334
|
const stop = vi.fn(() => { throw new Error('stop failed'); });
|
|
260
335
|
const stopAllGuestServices = vi.fn(async () => {});
|
|
@@ -125,9 +125,12 @@ export const registerSkillRoutes = (app, dependencies) => {
|
|
|
125
125
|
return { scope: SKILL_SCOPE.USER, source };
|
|
126
126
|
};
|
|
127
127
|
|
|
128
|
+
// Returns null when OpenCode's list could not be read (not running, error,
|
|
129
|
+
// timeout, malformed payload) so callers can tell a failed fetch from a
|
|
130
|
+
// genuinely empty list.
|
|
128
131
|
const fetchOpenCodeDiscoveredSkills = async (workingDirectory) => {
|
|
129
132
|
if (!getOpenCodePort()) {
|
|
130
|
-
return
|
|
133
|
+
return null;
|
|
131
134
|
}
|
|
132
135
|
|
|
133
136
|
try {
|
|
@@ -145,13 +148,19 @@ export const registerSkillRoutes = (app, dependencies) => {
|
|
|
145
148
|
const response = await client.skill.list();
|
|
146
149
|
const payload = response?.data;
|
|
147
150
|
if (!Array.isArray(payload)) {
|
|
148
|
-
return
|
|
151
|
+
return null;
|
|
149
152
|
}
|
|
150
153
|
|
|
151
154
|
return payload
|
|
152
155
|
.map((item) => {
|
|
153
156
|
const name = typeof item?.name === 'string' ? item.name.trim() : '';
|
|
154
|
-
|
|
157
|
+
// OpenCode v1's skill payload used `location`; v2 renamed the field
|
|
158
|
+
// to `path`. Accept both, or the whole authoritative list is dropped
|
|
159
|
+
// and the panel falls back to the (smaller) local disk scan.
|
|
160
|
+
const rawLocation = typeof item?.path === 'string' ? item.path : (typeof item?.location === 'string' ? item.location : '');
|
|
161
|
+
// v1 marked built-in skills with `<built-in>`; v2 gives them a synthetic
|
|
162
|
+
// `/builtin/<id>.md` path. Normalize so they stay read-only in the panel.
|
|
163
|
+
const location = rawLocation.startsWith('/builtin/') ? '<built-in>' : rawLocation;
|
|
155
164
|
const description = typeof item?.description === 'string' ? item.description : '';
|
|
156
165
|
const content = typeof item?.content === 'string' ? item.content : '';
|
|
157
166
|
if (!name || !location) {
|
|
@@ -183,7 +192,7 @@ export const registerSkillRoutes = (app, dependencies) => {
|
|
|
183
192
|
.filter(Boolean);
|
|
184
193
|
} catch (error) {
|
|
185
194
|
console.error('Failed to list OpenCode skills:', error);
|
|
186
|
-
return
|
|
195
|
+
return null;
|
|
187
196
|
}
|
|
188
197
|
};
|
|
189
198
|
|
|
@@ -244,7 +253,7 @@ export const registerSkillRoutes = (app, dependencies) => {
|
|
|
244
253
|
}
|
|
245
254
|
const openCodeSkills = await fetchOpenCodeDiscoveredSkills(directory);
|
|
246
255
|
const localSkills = discoverSkills(directory);
|
|
247
|
-
const skills = mergeDiscoveredSkills(openCodeSkills, localSkills);
|
|
256
|
+
const skills = mergeDiscoveredSkills(openCodeSkills ?? [], localSkills);
|
|
248
257
|
|
|
249
258
|
const enrichedSkills = skills.map((skill) => {
|
|
250
259
|
const sources = getSkillSources(skill.name, directory, skill);
|
|
@@ -268,7 +277,7 @@ export const registerSkillRoutes = (app, dependencies) => {
|
|
|
268
277
|
// OpenCode's own skill-list endpoint is not usable for this: on 1.18.14
|
|
269
278
|
// it returns only global and builtin skills, omitting the project
|
|
270
279
|
// `.agents`/`.claude` skills the agent demonstrably has.
|
|
271
|
-
|
|
280
|
+
const body = {
|
|
272
281
|
skills: enrichedSkills,
|
|
273
282
|
externalSkills: {
|
|
274
283
|
// `OPENCODE_DISABLE_CLAUDE_CODE` is the broad switch; the specific
|
|
@@ -277,7 +286,14 @@ export const registerSkillRoutes = (app, dependencies) => {
|
|
|
277
286
|
|| isEnvFlagEnabled(process.env.OPENCODE_DISABLE_CLAUDE_CODE_SKILLS),
|
|
278
287
|
allDisabled: isEnvFlagEnabled(process.env.OPENCODE_DISABLE_EXTERNAL_SKILLS),
|
|
279
288
|
},
|
|
280
|
-
}
|
|
289
|
+
};
|
|
290
|
+
// The list is only the disk scan when OpenCode's own list failed:
|
|
291
|
+
// built-in skills and anything OpenCode finds only through its config
|
|
292
|
+
// are missing, so the client must not treat this as complete.
|
|
293
|
+
if (openCodeSkills === null) {
|
|
294
|
+
body.openCodeSkillsUnavailable = true;
|
|
295
|
+
}
|
|
296
|
+
res.json(body);
|
|
281
297
|
} catch (error) {
|
|
282
298
|
console.error('Failed to list skills:', error);
|
|
283
299
|
res.status(500).json({ error: 'Failed to list skills' });
|
|
@@ -366,7 +382,7 @@ export const registerSkillRoutes = (app, dependencies) => {
|
|
|
366
382
|
}
|
|
367
383
|
|
|
368
384
|
const resolvedDiscovered = mergeDiscoveredSkills(
|
|
369
|
-
await fetchOpenCodeDiscoveredSkills(directory),
|
|
385
|
+
(await fetchOpenCodeDiscoveredSkills(directory)) ?? [],
|
|
370
386
|
discoverSkills(directory),
|
|
371
387
|
);
|
|
372
388
|
const installedByName = new Map(resolvedDiscovered.map((s) => [s.name, s]));
|
|
@@ -538,7 +554,7 @@ export const registerSkillRoutes = (app, dependencies) => {
|
|
|
538
554
|
if (error) {
|
|
539
555
|
return res.status(400).json({ error });
|
|
540
556
|
}
|
|
541
|
-
const discoveredSkill = (await fetchOpenCodeDiscoveredSkills(directory))
|
|
557
|
+
const discoveredSkill = ((await fetchOpenCodeDiscoveredSkills(directory)) ?? [])
|
|
542
558
|
.find((skill) => skill.name === skillName) || null;
|
|
543
559
|
const sources = getSkillSources(skillName, directory, discoveredSkill);
|
|
544
560
|
|
|
@@ -567,7 +583,7 @@ export const registerSkillRoutes = (app, dependencies) => {
|
|
|
567
583
|
return res.status(400).json({ error });
|
|
568
584
|
}
|
|
569
585
|
|
|
570
|
-
const discoveredSkill = (await fetchOpenCodeDiscoveredSkills(directory))
|
|
586
|
+
const discoveredSkill = ((await fetchOpenCodeDiscoveredSkills(directory)) ?? [])
|
|
571
587
|
.find((skill) => skill.name === skillName) || null;
|
|
572
588
|
const sources = getSkillSources(skillName, directory, discoveredSkill);
|
|
573
589
|
if (!sources.md.exists || !sources.md.dir) {
|
|
@@ -661,7 +677,7 @@ export const registerSkillRoutes = (app, dependencies) => {
|
|
|
661
677
|
return res.status(400).json({ error });
|
|
662
678
|
}
|
|
663
679
|
|
|
664
|
-
const discoveredSkill = (await fetchOpenCodeDiscoveredSkills(directory))
|
|
680
|
+
const discoveredSkill = ((await fetchOpenCodeDiscoveredSkills(directory)) ?? [])
|
|
665
681
|
.find((skill) => skill.name === skillName) || null;
|
|
666
682
|
const sources = getSkillSources(skillName, directory, discoveredSkill);
|
|
667
683
|
if (!sources.md.exists || !sources.md.dir) {
|
|
@@ -695,7 +711,7 @@ export const registerSkillRoutes = (app, dependencies) => {
|
|
|
695
711
|
return res.status(400).json({ error });
|
|
696
712
|
}
|
|
697
713
|
|
|
698
|
-
const discoveredSkill = (await fetchOpenCodeDiscoveredSkills(directory))
|
|
714
|
+
const discoveredSkill = ((await fetchOpenCodeDiscoveredSkills(directory)) ?? [])
|
|
699
715
|
.find((skill) => skill.name === skillName) || null;
|
|
700
716
|
const sources = getSkillSources(skillName, directory, discoveredSkill);
|
|
701
717
|
if (!sources.md.exists || !sources.md.dir) {
|
|
@@ -28,7 +28,7 @@ const createTempProject = () => {
|
|
|
28
28
|
return projectRoot;
|
|
29
29
|
};
|
|
30
30
|
|
|
31
|
-
const startSkillsApp = ({ projectRoot }) => {
|
|
31
|
+
const startSkillsApp = ({ projectRoot, overrides = {} }) => {
|
|
32
32
|
const app = express();
|
|
33
33
|
app.use(express.json());
|
|
34
34
|
|
|
@@ -76,6 +76,7 @@ const startSkillsApp = ({ projectRoot }) => {
|
|
|
76
76
|
fetchGitHubRepoMetas: async () => ({}),
|
|
77
77
|
getProfiles: () => [],
|
|
78
78
|
getProfile: () => null,
|
|
79
|
+
...overrides,
|
|
79
80
|
});
|
|
80
81
|
|
|
81
82
|
const server = app.listen(0);
|
|
@@ -213,4 +214,123 @@ describe('skill-routes directory soft fallback', () => {
|
|
|
213
214
|
});
|
|
214
215
|
}
|
|
215
216
|
});
|
|
217
|
+
|
|
218
|
+
it('lists OpenCode skills reported with the v2 `path` field and keeps the v1 `location` fallback', async () => {
|
|
219
|
+
projectRoot = createTempProject();
|
|
220
|
+
|
|
221
|
+
// OpenCode v2's skill payload renamed `location` to `path`. The route maps
|
|
222
|
+
// the fetch result to { name, path, ... }; with the old field only, the
|
|
223
|
+
// whole authoritative list was dropped and the panel fell back to the
|
|
224
|
+
// smaller local disk scan. Serve a v2-style payload from a stub.
|
|
225
|
+
const stub = express();
|
|
226
|
+
stub.get('/api/skill', (_req, res) => {
|
|
227
|
+
res.json({
|
|
228
|
+
location: { directory: projectRoot },
|
|
229
|
+
data: [
|
|
230
|
+
{
|
|
231
|
+
id: 'v2-path-skill',
|
|
232
|
+
name: 'v2-path-skill',
|
|
233
|
+
path: path.join(projectRoot, '.agents', 'skills', 'v2-path-skill'),
|
|
234
|
+
description: 'Delivered with the v2 path field',
|
|
235
|
+
},
|
|
236
|
+
{
|
|
237
|
+
id: 'v1-location-skill',
|
|
238
|
+
name: 'v1-location-skill',
|
|
239
|
+
location: path.join(projectRoot, '.agents', 'skills', 'v1-location-skill'),
|
|
240
|
+
description: 'Delivered with the legacy location field',
|
|
241
|
+
},
|
|
242
|
+
{
|
|
243
|
+
id: 'opencode',
|
|
244
|
+
name: 'OpenCode',
|
|
245
|
+
path: '/builtin/opencode.md',
|
|
246
|
+
description: 'v2 built-in skill with a synthetic path',
|
|
247
|
+
},
|
|
248
|
+
{
|
|
249
|
+
id: 'unlocated-skill',
|
|
250
|
+
name: 'unlocated-skill',
|
|
251
|
+
description: 'Has neither field; must be dropped',
|
|
252
|
+
},
|
|
253
|
+
],
|
|
254
|
+
});
|
|
255
|
+
});
|
|
256
|
+
const stubServer = await new Promise((resolve) => {
|
|
257
|
+
const server = stub.listen(0, () => resolve(server));
|
|
258
|
+
});
|
|
259
|
+
const stubPort = stubServer.address().port;
|
|
260
|
+
|
|
261
|
+
try {
|
|
262
|
+
appHandle = startSkillsApp({
|
|
263
|
+
projectRoot,
|
|
264
|
+
overrides: {
|
|
265
|
+
buildOpenCodeUrl: () => `http://127.0.0.1:${stubPort}/`,
|
|
266
|
+
getOpenCodePort: () => stubPort,
|
|
267
|
+
getOpenCodeAuthHeaders: () => ({}),
|
|
268
|
+
},
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
const listResponse = await fetch(
|
|
272
|
+
`${appHandle.baseUrl}/api/config/skills?directory=${encodeURIComponent(projectRoot)}`,
|
|
273
|
+
);
|
|
274
|
+
expect(listResponse.status).toBe(200);
|
|
275
|
+
const payload = await listResponse.json();
|
|
276
|
+
const byName = new Map(payload.skills.map((skill) => [skill.name, skill]));
|
|
277
|
+
|
|
278
|
+
expect(byName.has('v2-path-skill')).toBe(true);
|
|
279
|
+
expect(byName.get('v2-path-skill').path).toContain('v2-path-skill');
|
|
280
|
+
|
|
281
|
+
expect(byName.has('v1-location-skill')).toBe(true);
|
|
282
|
+
expect(byName.get('v1-location-skill').path).toContain('v1-location-skill');
|
|
283
|
+
|
|
284
|
+
expect(byName.get('OpenCode')?.path).toBe('<built-in>');
|
|
285
|
+
expect(byName.get('OpenCode')?.renamable).toBe(false);
|
|
286
|
+
|
|
287
|
+
expect(byName.has('unlocated-skill')).toBe(false);
|
|
288
|
+
} finally {
|
|
289
|
+
stubServer.close();
|
|
290
|
+
}
|
|
291
|
+
});
|
|
292
|
+
it('flags the list as partial when OpenCode skill list fails, and not when it succeeds', async () => {
|
|
293
|
+
projectRoot = createTempProject();
|
|
294
|
+
fs.mkdirSync(path.join(projectRoot, '.agents', 'skills', 'disk-skill'), { recursive: true });
|
|
295
|
+
fs.writeFileSync(
|
|
296
|
+
path.join(projectRoot, '.agents', 'skills', 'disk-skill', 'SKILL.md'),
|
|
297
|
+
'---\nname: disk-skill\ndescription: On disk\n---\nBody\n',
|
|
298
|
+
);
|
|
299
|
+
|
|
300
|
+
let failing = true;
|
|
301
|
+
const stub = express();
|
|
302
|
+
stub.get('/api/skill', (_req, res) => {
|
|
303
|
+
if (failing) {
|
|
304
|
+
res.status(500).json({ error: 'boom' });
|
|
305
|
+
return;
|
|
306
|
+
}
|
|
307
|
+
res.json({ data: [] });
|
|
308
|
+
});
|
|
309
|
+
const stubServer = await new Promise((resolve) => {
|
|
310
|
+
const server = stub.listen(0, () => resolve(server));
|
|
311
|
+
});
|
|
312
|
+
const stubPort = stubServer.address().port;
|
|
313
|
+
|
|
314
|
+
try {
|
|
315
|
+
appHandle = startSkillsApp({
|
|
316
|
+
projectRoot,
|
|
317
|
+
overrides: {
|
|
318
|
+
buildOpenCodeUrl: () => `http://127.0.0.1:${stubPort}/`,
|
|
319
|
+
getOpenCodePort: () => stubPort,
|
|
320
|
+
},
|
|
321
|
+
});
|
|
322
|
+
const url = `${appHandle.baseUrl}/api/config/skills?directory=${encodeURIComponent(projectRoot)}`;
|
|
323
|
+
|
|
324
|
+
const failed = await (await fetch(url)).json();
|
|
325
|
+
expect(failed.openCodeSkillsUnavailable).toBe(true);
|
|
326
|
+
expect(failed.skills.map((skill) => skill.name)).toContain('disk-skill');
|
|
327
|
+
|
|
328
|
+
failing = false;
|
|
329
|
+
const complete = await (await fetch(url)).json();
|
|
330
|
+
expect(complete.openCodeSkillsUnavailable).toBeUndefined();
|
|
331
|
+
expect(complete.skills.map((skill) => skill.name)).toContain('disk-skill');
|
|
332
|
+
} finally {
|
|
333
|
+
stubServer.close();
|
|
334
|
+
}
|
|
335
|
+
});
|
|
216
336
|
});
|
|
@@ -34,7 +34,6 @@ export const createStartupPipelineRuntime = (dependencies) => {
|
|
|
34
34
|
terminalRebindWindowMs,
|
|
35
35
|
terminalMaxRebindsPerWindow,
|
|
36
36
|
setupProxy,
|
|
37
|
-
scheduleOpenCodeApiDetection,
|
|
38
37
|
bootstrapOpenCodeAtStartup,
|
|
39
38
|
staticRoutesRuntime,
|
|
40
39
|
process,
|
|
@@ -132,13 +131,15 @@ export const createStartupPipelineRuntime = (dependencies) => {
|
|
|
132
131
|
bindHost,
|
|
133
132
|
startupTunnelRequest,
|
|
134
133
|
onTunnelReady,
|
|
134
|
+
afterListening: async ({ activePort }) => {
|
|
135
|
+
tunnelRuntimeContext.setActivePort(activePort);
|
|
136
|
+
await bootstrapOpenCodeAtStartup();
|
|
137
|
+
},
|
|
135
138
|
});
|
|
136
139
|
recordStartupPerformance('web.listener.ready', {
|
|
137
140
|
durationMs: performance.now() - pipelineStartedAt,
|
|
138
141
|
});
|
|
139
|
-
|
|
140
|
-
scheduleOpenCodeApiDetection();
|
|
141
|
-
void bootstrapOpenCodeAtStartup();
|
|
142
|
+
|
|
142
143
|
|
|
143
144
|
serverStartupRuntime.attachProcessHandlers({ attachSignals });
|
|
144
145
|
|
|
@@ -3,23 +3,27 @@ import { describe, expect, it, vi } from 'vitest';
|
|
|
3
3
|
import { createStartupPipelineRuntime } from './startup-pipeline-runtime.js';
|
|
4
4
|
|
|
5
5
|
describe('startup pipeline runtime', () => {
|
|
6
|
-
it('publishes the listening port before
|
|
6
|
+
it('publishes the listening port before awaiting OpenCode bootstrap', async () => {
|
|
7
7
|
const order = [];
|
|
8
|
+
let releaseBootstrap;
|
|
9
|
+
const bootstrapGate = new Promise((resolve) => { releaseBootstrap = resolve; });
|
|
8
10
|
const runtime = createStartupPipelineRuntime({
|
|
9
11
|
createTerminalRuntime: () => ({}),
|
|
10
12
|
createDictationRuntime: () => ({}),
|
|
11
13
|
createMessageStreamWsRuntime: () => ({}),
|
|
12
14
|
createServerStartupRuntime: () => ({
|
|
13
15
|
resolveBindHost: () => '127.0.0.1',
|
|
14
|
-
startListeningAndMaybeTunnel: async () => {
|
|
16
|
+
startListeningAndMaybeTunnel: async ({ afterListening }) => {
|
|
15
17
|
order.push('listen');
|
|
18
|
+
await afterListening({ activePort: 3901, bindHost: '127.0.0.1' });
|
|
19
|
+
order.push('ready');
|
|
16
20
|
return { activePort: 3901 };
|
|
17
21
|
},
|
|
18
22
|
attachProcessHandlers: vi.fn(),
|
|
19
23
|
}),
|
|
20
24
|
});
|
|
21
25
|
|
|
22
|
-
|
|
26
|
+
const run = runtime.run({
|
|
23
27
|
app: {},
|
|
24
28
|
setupProxy: vi.fn(),
|
|
25
29
|
staticRoutesRuntime: { registerStaticRoutes: vi.fn() },
|
|
@@ -27,14 +31,21 @@ describe('startup pipeline runtime', () => {
|
|
|
27
31
|
tunnelRuntimeContext: {
|
|
28
32
|
setActivePort: (port) => order.push(`port:${port}`),
|
|
29
33
|
},
|
|
30
|
-
|
|
31
|
-
|
|
34
|
+
bootstrapOpenCodeAtStartup: async () => {
|
|
35
|
+
order.push('bootstrap');
|
|
36
|
+
await bootstrapGate;
|
|
37
|
+
},
|
|
32
38
|
process: {},
|
|
33
39
|
crypto: {},
|
|
34
|
-
server: {},
|
|
40
|
+
server: { close: (callback) => callback() },
|
|
35
41
|
attachSignals: false,
|
|
36
42
|
});
|
|
37
43
|
|
|
38
|
-
|
|
44
|
+
await Promise.resolve();
|
|
45
|
+
expect(order).toEqual(['listen', 'port:3901', 'bootstrap']);
|
|
46
|
+
releaseBootstrap();
|
|
47
|
+
await run;
|
|
48
|
+
|
|
49
|
+
expect(order).toEqual(['listen', 'port:3901', 'bootstrap', 'ready']);
|
|
39
50
|
});
|
|
40
51
|
});
|
|
@@ -1,23 +1,9 @@
|
|
|
1
|
-
// TEMPORARY WORKAROUND — Windows ARM64: native opencode.exe fails with a Bun
|
|
2
|
-
// FFI/TinyCC dlopen error (https://github.com/anomalyco/opencode/issues/19130).
|
|
3
|
-
// Disable OpenCode self-upgrade on ARM64 so it can't overwrite the working x64
|
|
4
|
-
// binary with the broken ARM64 build. Remove when the upstream issue is resolved.
|
|
5
|
-
const isWindowsArm64 = () => process.platform === 'win32' && process.arch === 'arm64';
|
|
6
|
-
|
|
7
1
|
export const resolveOpenCodeUpgradeCapability = ({
|
|
8
2
|
isExternal,
|
|
9
3
|
hasManagedProcess,
|
|
10
4
|
activeBinary,
|
|
11
5
|
isBundledBinary,
|
|
12
6
|
}) => {
|
|
13
|
-
if (isWindowsArm64()) {
|
|
14
|
-
return {
|
|
15
|
-
supported: false,
|
|
16
|
-
manager: 'openchamber',
|
|
17
|
-
reason: 'windows-arm64-workaround',
|
|
18
|
-
};
|
|
19
|
-
}
|
|
20
|
-
|
|
21
7
|
if (isExternal) {
|
|
22
8
|
return {
|
|
23
9
|
supported: false,
|
|
@@ -21,14 +21,16 @@
|
|
|
21
21
|
* seen v2 activity. Every visited session gets its `session_message` rows
|
|
22
22
|
* deleted and replaced by the V1 transform, so a migrated session that was
|
|
23
23
|
* continued in v2 would lose that conversation.
|
|
24
|
-
* 3.
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
24
|
+
* 3. Only 1.x activity since the last completed import triggers a new one.
|
|
25
|
+
* A v2 delete removes the `session_v2` row but leaves the legacy `session`
|
|
26
|
+
* row behind, so "missing from v2" alone would bring deleted sessions back
|
|
27
|
+
* on every start. A legacy session counts as missing only when its
|
|
28
|
+
* `time_updated` is newer than the completion stamp OpenCode writes on
|
|
29
|
+
* `migration.v1-v2`: only OpenCode 1.x writes that table, so an unchanged
|
|
30
|
+
* table means nobody ran 1.x since and the top-up skips. Decided by the
|
|
31
|
+
* maintainer (2026-09-24). When 1.x was used again, OpenCode still walks
|
|
32
|
+
* every id below the cursor, so a deleted session sorting below a fresh one
|
|
33
|
+
* comes back with it; that needs an import route taking an explicit list.
|
|
32
34
|
*
|
|
33
35
|
* The loop OpenCode runs is `SELECT id FROM session WHERE id < cursor ORDER BY
|
|
34
36
|
* id DESC LIMIT 1`, one session at a time, so the cursor has to be strictly
|
|
@@ -196,11 +198,15 @@ const findRevisitedSessionsWithV2Activity = (db, cursor, completedAt) =>
|
|
|
196
198
|
return parsed.success ? [parsed.data.id] : [];
|
|
197
199
|
});
|
|
198
200
|
|
|
199
|
-
/**
|
|
200
|
-
|
|
201
|
+
/**
|
|
202
|
+
* A legacy session with no `session_v2` twin that 1.x touched after the last
|
|
203
|
+
* completed import (see rule 3). Bound parameter: the completion timestamp.
|
|
204
|
+
*/
|
|
205
|
+
const MISSING_CONDITION = 'id NOT IN (SELECT id FROM session_v2) AND time_updated > ?';
|
|
201
206
|
|
|
202
|
-
const countMissingSessions = (db) =>
|
|
203
|
-
firstRow(countRowSchema, db.all(`SELECT COUNT(*) AS value FROM session WHERE ${MISSING_CONDITION}
|
|
207
|
+
const countMissingSessions = (db, completedAt) =>
|
|
208
|
+
firstRow(countRowSchema, db.all(`SELECT COUNT(*) AS value FROM session WHERE ${MISSING_CONDITION}`, [completedAt]))
|
|
209
|
+
?.value ?? 0;
|
|
204
210
|
|
|
205
211
|
const countRevisitedSessions = (db, cursor) =>
|
|
206
212
|
firstRow(
|
|
@@ -244,12 +250,12 @@ export const topUpV1Migration = (options = {}) => {
|
|
|
244
250
|
const migration = readCompletedMigration(db);
|
|
245
251
|
if (!migration) return outcome('skipped', 'migration-not-completed');
|
|
246
252
|
|
|
247
|
-
const missingCount = countMissingSessions(db);
|
|
253
|
+
const missingCount = countMissingSessions(db, migration.completedAt);
|
|
248
254
|
if (missingCount === 0) return outcome('skipped', 'nothing-missing');
|
|
249
255
|
|
|
250
256
|
const maxMissing = firstRow(
|
|
251
257
|
nullableIdRowSchema,
|
|
252
|
-
db.all(`SELECT MAX(id) AS id FROM session WHERE ${MISSING_CONDITION}
|
|
258
|
+
db.all(`SELECT MAX(id) AS id FROM session WHERE ${MISSING_CONDITION}`, [migration.completedAt]),
|
|
253
259
|
)?.id;
|
|
254
260
|
if (!maxMissing) return outcome('skipped', 'nothing-missing');
|
|
255
261
|
|
|
@@ -271,7 +277,7 @@ export const topUpV1Migration = (options = {}) => {
|
|
|
271
277
|
MIGRATION_STATE_KEY,
|
|
272
278
|
]);
|
|
273
279
|
logger.log(
|
|
274
|
-
`[OpenCode] Scheduled the import of ${missingCount} OpenCode 1.x session(s)
|
|
280
|
+
`[OpenCode] Scheduled the import of ${missingCount} OpenCode 1.x session(s) changed since the last v2 import; ` +
|
|
275
281
|
`OpenCode will re-import ${revisitedCount} untouched session(s) on the way.`,
|
|
276
282
|
);
|
|
277
283
|
return outcome('scheduled', undefined, missingCount, revisitedCount);
|
|
@@ -226,13 +226,21 @@ describe.skipIf(!sqlite)('topUpV1Migration', () => {
|
|
|
226
226
|
});
|
|
227
227
|
});
|
|
228
228
|
|
|
229
|
-
it('
|
|
230
|
-
// ses_a was
|
|
231
|
-
//
|
|
232
|
-
// back over losing ses_d: deleting again is cheap, a lost session is not.
|
|
229
|
+
it('does not bring back a session deleted in v2 when 1.x was not used since', () => {
|
|
230
|
+
// ses_a was imported and then deleted in v2: only its legacy row is left,
|
|
231
|
+
// untouched since the last completed import.
|
|
233
232
|
createDatabase();
|
|
234
|
-
seed({ v1: [{ id: 'ses_a', timeCreated: COMPLETED_AT - 1 }
|
|
235
|
-
expect(run()).toEqual({ status: '
|
|
233
|
+
seed({ v1: [{ id: 'ses_a', timeCreated: COMPLETED_AT - 1 }] });
|
|
234
|
+
expect(run()).toEqual({ status: 'skipped', missing: 0, revisited: 0, reason: 'nothing-missing' });
|
|
235
|
+
expect(readMigrationRow().state).toEqual({ phase: 'completed' });
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
it('imports only sessions 1.x changed since the last import, cursor included', () => {
|
|
239
|
+
// ses_z (deleted in v2, old) sorts above ses_d (fresh from 1.x): the
|
|
240
|
+
// cursor must stop at ses_d so ses_z stays gone.
|
|
241
|
+
createDatabase();
|
|
242
|
+
seed({ v1: [{ id: 'ses_z', timeCreated: COMPLETED_AT - 1 }, 'ses_d'] });
|
|
243
|
+
expect(run()).toEqual({ status: 'scheduled', missing: 1, revisited: 0 });
|
|
236
244
|
expect(readMigrationRow().state).toEqual({ phase: 'sessions', cursor: computeResumeCursor('ses_d') });
|
|
237
245
|
});
|
|
238
246
|
|
|
@@ -18,10 +18,10 @@ const packageDistSchema = z.object({
|
|
|
18
18
|
});
|
|
19
19
|
|
|
20
20
|
// OpenCode's installer is a bash script, so Windows installs the npm platform
|
|
21
|
-
// package that script downloads
|
|
22
|
-
//
|
|
23
|
-
//
|
|
24
|
-
const
|
|
21
|
+
// package that script downloads, the same one the desktop bundle picks in
|
|
22
|
+
// packages/electron/scripts/prepare-opencode-cli.mjs. x64 takes the baseline
|
|
23
|
+
// build so hosts without AVX2 still run it.
|
|
24
|
+
const windowsPackage = (arch) => (arch === 'arm64' ? '@opencode/cli-windows-arm64' : '@opencode/cli-windows-x64-baseline');
|
|
25
25
|
|
|
26
26
|
export const supportsOpenCodeV2Install = (platform = process.platform) =>
|
|
27
27
|
(platform === 'darwin' || platform === 'linux' || platform === 'win32') && (process.arch === 'x64' || process.arch === 'arm64');
|
|
@@ -72,8 +72,8 @@ const prepareInstallerScript = async ({ version, workDirectory, env, fetchImpl }
|
|
|
72
72
|
* Downloads the Windows platform package, checks it against the integrity npm
|
|
73
73
|
* publishes for it, and unpacks it. Returns the step that replaces the binary.
|
|
74
74
|
*/
|
|
75
|
-
const prepareWindowsBinary = async ({ version, directory, workDirectory, fetchImpl, tarCommand }) => {
|
|
76
|
-
const metadataResponse = await fetchImpl(`${NPM_REGISTRY}/${
|
|
75
|
+
const prepareWindowsBinary = async ({ version, arch, directory, workDirectory, fetchImpl, tarCommand }) => {
|
|
76
|
+
const metadataResponse = await fetchImpl(`${NPM_REGISTRY}/${windowsPackage(arch).replace('/', '%2F')}/${version}`, { signal: AbortSignal.timeout(15_000) });
|
|
77
77
|
if (!metadataResponse.ok) throw new Error('Could not resolve the OpenCode v2 package.');
|
|
78
78
|
const { dist } = packageDistSchema.parse(await metadataResponse.json());
|
|
79
79
|
if (new URL(dist.tarball).origin !== NPM_REGISTRY) throw new Error('The OpenCode v2 package is not served by the npm registry.');
|
|
@@ -100,6 +100,7 @@ export const installOpenCodeV2 = async ({
|
|
|
100
100
|
env = process.env,
|
|
101
101
|
fetchImpl = fetch,
|
|
102
102
|
platform = process.platform,
|
|
103
|
+
arch = process.arch,
|
|
103
104
|
// Windows' own bsdtar. Git Bash's GNU tar on PATH reads `C:\...` as a remote host.
|
|
104
105
|
tarCommand = path.join(process.env.SystemRoot || 'C:\\Windows', 'System32', 'tar.exe'),
|
|
105
106
|
} = {}) => {
|
|
@@ -119,7 +120,7 @@ export const installOpenCodeV2 = async ({
|
|
|
119
120
|
if (!releaseResponse.ok) throw new Error('Could not resolve the OpenCode v2 release.');
|
|
120
121
|
const { version } = releaseSchema.parse(await releaseResponse.json());
|
|
121
122
|
const replaceBinary = windows
|
|
122
|
-
? await prepareWindowsBinary({ version, directory, workDirectory: lock, fetchImpl, tarCommand })
|
|
123
|
+
? await prepareWindowsBinary({ version, arch, directory, workDirectory: lock, fetchImpl, tarCommand })
|
|
123
124
|
: await prepareInstallerScript({ version, workDirectory: lock, env, fetchImpl });
|
|
124
125
|
for (const name of binaryNames) {
|
|
125
126
|
const target = path.join(directory, name);
|