@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
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { readOpenCodeInfo, isSupportedOpenCodeVersion, requireOpenCodeV2, UnsupportedOpenCodeVersionError } from './compatibility.js';
|
|
1
|
+
import { readOpenCodeInfo, readExternalOpenCodeVersion, isSupportedOpenCodeVersion, requireOpenCodeV2, UnsupportedOpenCodeVersionError } from './compatibility.js';
|
|
2
2
|
import { spawn, spawnSync } from 'node:child_process';
|
|
3
3
|
import net from 'node:net';
|
|
4
4
|
import { stripAppImageArgv0Leak } from '../inherited-env.js';
|
|
@@ -55,6 +55,132 @@ const WARMUP_REQUEST_TIMEOUT_MS = 30000;
|
|
|
55
55
|
const MANAGED_STDERR_TAIL_MAX_BYTES = 32 * 1024;
|
|
56
56
|
const HEALTH_FAILURE_DETAIL_MAX_LENGTH = 256;
|
|
57
57
|
|
|
58
|
+
export const resolveOpenCodeConnectionKind = ({ runtime, configuredHost, lifecycleMode }) => {
|
|
59
|
+
if (configuredHost) return 'explicit-external';
|
|
60
|
+
if (lifecycleMode === 'managed' || runtime === 'desktop') return 'managed-owned';
|
|
61
|
+
return 'shared-local';
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
export const createOpenCodeServerComposition = ({
|
|
65
|
+
runtime,
|
|
66
|
+
configuredHost,
|
|
67
|
+
lifecycleMode,
|
|
68
|
+
createSharedRuntime,
|
|
69
|
+
createManagedPluginRuntime = () => null,
|
|
70
|
+
getManagedBaseUrl = () => null,
|
|
71
|
+
getManagedAuthHeaders = () => ({}),
|
|
72
|
+
}) => {
|
|
73
|
+
const kind = resolveOpenCodeConnectionKind({ runtime, configuredHost, lifecycleMode });
|
|
74
|
+
if (kind === 'shared-local' && !createSharedRuntime) {
|
|
75
|
+
throw new Error('Shared OpenCode composition requires a shared runtime factory');
|
|
76
|
+
}
|
|
77
|
+
const sharedRuntime = kind === 'shared-local' ? createSharedRuntime() : null;
|
|
78
|
+
const managedPluginRuntime = kind === 'managed-owned' ? createManagedPluginRuntime() : null;
|
|
79
|
+
const connection = createOpenCodeConnectionAdapter({
|
|
80
|
+
kind,
|
|
81
|
+
sharedRuntime,
|
|
82
|
+
getManagedBaseUrl,
|
|
83
|
+
getManagedAuthHeaders,
|
|
84
|
+
});
|
|
85
|
+
return { kind, sharedRuntime, managedPluginRuntime, connection };
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
export const createOpenCodeRecoveryCallbacks = ({
|
|
89
|
+
resetOpenCodeRuntimeProviders,
|
|
90
|
+
rebindUpstream,
|
|
91
|
+
interruptBusySessionsAfterRestart,
|
|
92
|
+
broadcastUiNotification,
|
|
93
|
+
}) => ({
|
|
94
|
+
onManagedRestarted() {
|
|
95
|
+
resetOpenCodeRuntimeProviders();
|
|
96
|
+
try {
|
|
97
|
+
rebindUpstream();
|
|
98
|
+
} catch (error) {
|
|
99
|
+
console.warn('Failed to rebind message stream after OpenCode restart:', error?.message ?? error);
|
|
100
|
+
}
|
|
101
|
+
try {
|
|
102
|
+
const { sessionIds } = interruptBusySessionsAfterRestart();
|
|
103
|
+
if (sessionIds.length === 0) return;
|
|
104
|
+
const multiple = sessionIds.length > 1;
|
|
105
|
+
broadcastUiNotification({
|
|
106
|
+
title: multiple ? 'Chats interrupted' : 'Chat interrupted',
|
|
107
|
+
body: multiple
|
|
108
|
+
? 'OpenCode restarted during running responses. Send a message in each chat to continue.'
|
|
109
|
+
: 'OpenCode restarted during a running response. Send a message to continue.',
|
|
110
|
+
tag: 'opencode-restart-interrupted',
|
|
111
|
+
kind: 'opencode-restart-interrupted',
|
|
112
|
+
sessionId: sessionIds[0],
|
|
113
|
+
});
|
|
114
|
+
} catch (error) {
|
|
115
|
+
console.warn('Failed to reconcile sessions after OpenCode restart:', error?.message ?? error);
|
|
116
|
+
}
|
|
117
|
+
},
|
|
118
|
+
onSharedRecovered() {
|
|
119
|
+
resetOpenCodeRuntimeProviders();
|
|
120
|
+
rebindUpstream();
|
|
121
|
+
},
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
export const createOpenCodeLifecycleControls = ({
|
|
125
|
+
getKind,
|
|
126
|
+
restartLifecycle,
|
|
127
|
+
triggerLifecycleHealthCheck,
|
|
128
|
+
recoverShared,
|
|
129
|
+
onSharedRecovered,
|
|
130
|
+
}) => {
|
|
131
|
+
let sharedRecoveryPromise = null;
|
|
132
|
+
const recover = () => {
|
|
133
|
+
if (!recoverShared || !onSharedRecovered) {
|
|
134
|
+
return Promise.reject(new Error('Shared OpenCode recovery is not configured'));
|
|
135
|
+
}
|
|
136
|
+
if (!sharedRecoveryPromise) {
|
|
137
|
+
sharedRecoveryPromise = Promise.resolve()
|
|
138
|
+
.then(() => recoverShared())
|
|
139
|
+
.then(() => onSharedRecovered())
|
|
140
|
+
.finally(() => {
|
|
141
|
+
sharedRecoveryPromise = null;
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
return sharedRecoveryPromise;
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
return {
|
|
148
|
+
restart: (...args) => (
|
|
149
|
+
getKind() === 'shared-local' ? recover() : restartLifecycle(...args)
|
|
150
|
+
),
|
|
151
|
+
triggerHealthCheck: (...args) => {
|
|
152
|
+
if (getKind() !== 'shared-local') {
|
|
153
|
+
return triggerLifecycleHealthCheck(...args);
|
|
154
|
+
}
|
|
155
|
+
return recover().catch((error) => {
|
|
156
|
+
console.warn('Failed to recover shared OpenCode service:', error?.message ?? error);
|
|
157
|
+
});
|
|
158
|
+
},
|
|
159
|
+
};
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
export const createOpenCodeConnectionAdapter = ({
|
|
163
|
+
kind,
|
|
164
|
+
sharedRuntime = null,
|
|
165
|
+
getManagedBaseUrl = () => null,
|
|
166
|
+
getManagedAuthHeaders = () => ({}),
|
|
167
|
+
}) => {
|
|
168
|
+
if (kind === 'shared-local' && !sharedRuntime) {
|
|
169
|
+
throw new Error('Shared OpenCode connection requires a shared service runtime');
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
return {
|
|
173
|
+
getOpenCodeConnectionKind: () => kind,
|
|
174
|
+
getOpenCodeBaseUrl: () => (
|
|
175
|
+
kind === 'shared-local' ? sharedRuntime.getBaseUrl() : getManagedBaseUrl()
|
|
176
|
+
),
|
|
177
|
+
getOpenCodeAuthHeaders: () => (
|
|
178
|
+
kind === 'shared-local' ? sharedRuntime.getHeaders() : getManagedAuthHeaders()
|
|
179
|
+
),
|
|
180
|
+
ownsOpenCodeProcess: () => kind === 'managed-owned',
|
|
181
|
+
};
|
|
182
|
+
};
|
|
183
|
+
|
|
58
184
|
const getBoundedTextTail = (value, maxBytes) => {
|
|
59
185
|
const buffer = Buffer.from(String(value ?? ''));
|
|
60
186
|
if (buffer.byteLength <= maxBytes) return buffer.toString();
|
|
@@ -677,6 +803,16 @@ export const createOpenCodeLifecycleRuntime = (deps) => {
|
|
|
677
803
|
}
|
|
678
804
|
};
|
|
679
805
|
|
|
806
|
+
// The version of an explicitly configured server that answered but is not a
|
|
807
|
+
// supported OpenCode (v1, or 2.x below the minimum); null for anything else,
|
|
808
|
+
// including a server that is down.
|
|
809
|
+
const readUnsupportedExternalOpenCodeVersion = async (port, origin) => {
|
|
810
|
+
if (!port || port <= 0) return null;
|
|
811
|
+
const version = await readExternalOpenCodeVersion(origin ?? `http://127.0.0.1:${port}`, getOpenCodeAuthHeaders())
|
|
812
|
+
.catch(() => null);
|
|
813
|
+
return version && !isSupportedOpenCodeVersion(version) ? version : null;
|
|
814
|
+
};
|
|
815
|
+
|
|
680
816
|
const waitForOpenCodePort = async (timeoutMs = 15000) => {
|
|
681
817
|
if (state.openCodePort !== null) {
|
|
682
818
|
return state.openCodePort;
|
|
@@ -991,7 +1127,7 @@ export const createOpenCodeLifecycleRuntime = (deps) => {
|
|
|
991
1127
|
}
|
|
992
1128
|
|
|
993
1129
|
const info = await readOpenCodeInfo(response);
|
|
994
|
-
if (!info) throw new Error('
|
|
1130
|
+
if (!info) throw new Error('The server did not identify itself as OpenCode 2.x. OpenChamber requires OpenCode 2.x; if the server runs an older OpenCode, upgrade it.');
|
|
995
1131
|
if (!isSupportedOpenCodeVersion(info.version)) throw new UnsupportedOpenCodeVersionError(info.version);
|
|
996
1132
|
state.isOpenCodeReady = true;
|
|
997
1133
|
state.lastOpenCodeError = null;
|
|
@@ -1090,6 +1226,7 @@ export const createOpenCodeLifecycleRuntime = (deps) => {
|
|
|
1090
1226
|
const bootstrapOpenCodeAtStartup = async () => {
|
|
1091
1227
|
const bootstrapStartedAt = performance.now();
|
|
1092
1228
|
let bootstrapError = null;
|
|
1229
|
+
let unsupportedExternalVersion = null;
|
|
1093
1230
|
recordStartupPerformance('opencode.bootstrap.start');
|
|
1094
1231
|
try {
|
|
1095
1232
|
// Before doing anything, reap any OpenCode process WE spawned in a prior
|
|
@@ -1130,6 +1267,20 @@ export const createOpenCodeLifecycleRuntime = (deps) => {
|
|
|
1130
1267
|
state.lastOpenCodeError = null;
|
|
1131
1268
|
state.openCodeNotReadySince = 0;
|
|
1132
1269
|
syncToHmrState();
|
|
1270
|
+
} else if (env.ENV_EFFECTIVE_PORT && (unsupportedExternalVersion = await readUnsupportedExternalOpenCodeVersion(
|
|
1271
|
+
env.ENV_EFFECTIVE_PORT,
|
|
1272
|
+
env.ENV_CONFIGURED_OPENCODE_HOST?.origin,
|
|
1273
|
+
))) {
|
|
1274
|
+
// The configured server is an OpenCode this OpenChamber cannot use.
|
|
1275
|
+
// Attach to it anyway, not ready, so the compatibility check reports
|
|
1276
|
+
// its version instead of a managed instance silently replacing it.
|
|
1277
|
+
const label = env.ENV_CONFIGURED_OPENCODE_HOST ? env.ENV_CONFIGURED_OPENCODE_HOST.origin : `http://localhost:${env.ENV_EFFECTIVE_PORT}`;
|
|
1278
|
+
console.warn(`OpenCode ${unsupportedExternalVersion} at ${label} is not supported by this OpenChamber`);
|
|
1279
|
+
state.openCodeBaseUrl = env.ENV_CONFIGURED_OPENCODE_HOST?.origin ?? null;
|
|
1280
|
+
setOpenCodePort(env.ENV_EFFECTIVE_PORT);
|
|
1281
|
+
state.isExternalOpenCode = true;
|
|
1282
|
+
syncToHmrState();
|
|
1283
|
+
throw new UnsupportedOpenCodeVersionError(unsupportedExternalVersion);
|
|
1133
1284
|
} else {
|
|
1134
1285
|
// We never auto-attach to an arbitrary pre-existing OpenCode instance.
|
|
1135
1286
|
// Attaching to an external server requires explicit opt-in via env
|
|
@@ -1156,6 +1307,9 @@ export const createOpenCodeLifecycleRuntime = (deps) => {
|
|
|
1156
1307
|
await waitForOpenCodeReady();
|
|
1157
1308
|
} catch (error) {
|
|
1158
1309
|
bootstrapError = error;
|
|
1310
|
+
// Skip-start mode assumed readiness up front; a server that never
|
|
1311
|
+
// proved itself must not keep reporting ready to startup diagnostics.
|
|
1312
|
+
state.isOpenCodeReady = false;
|
|
1159
1313
|
console.error(`OpenCode readiness check failed: ${error.message}`);
|
|
1160
1314
|
}
|
|
1161
1315
|
} catch (error) {
|
|
@@ -18,7 +18,14 @@ vi.mock('./startup-performance.js', () => ({
|
|
|
18
18
|
recordStartupPerformance: recordStartupPerformanceMock,
|
|
19
19
|
}));
|
|
20
20
|
|
|
21
|
-
const {
|
|
21
|
+
const {
|
|
22
|
+
createOpenCodeConnectionAdapter,
|
|
23
|
+
createOpenCodeLifecycleControls,
|
|
24
|
+
createOpenCodeLifecycleRuntime,
|
|
25
|
+
createOpenCodeRecoveryCallbacks,
|
|
26
|
+
createOpenCodeServerComposition,
|
|
27
|
+
resolveOpenCodeConnectionKind,
|
|
28
|
+
} = await import('./lifecycle.js');
|
|
22
29
|
|
|
23
30
|
const originalOpencodeBinary = process.env.OPENCODE_BINARY;
|
|
24
31
|
const originalPath = process.env.PATH;
|
|
@@ -128,6 +135,226 @@ const createRuntime = (overrides = {}, stateOverrides = {}, envOverrides = {}) =
|
|
|
128
135
|
return runtime;
|
|
129
136
|
};
|
|
130
137
|
|
|
138
|
+
describe('OpenCode connection composition', () => {
|
|
139
|
+
it('selects shared-local for Web without an explicit host', () => {
|
|
140
|
+
expect(resolveOpenCodeConnectionKind({ runtime: 'web', configuredHost: null })).toBe('shared-local');
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it('selects explicit-external when a host is configured', () => {
|
|
144
|
+
expect(resolveOpenCodeConnectionKind({
|
|
145
|
+
runtime: 'web',
|
|
146
|
+
configuredHost: { origin: 'https://opencode.example.com', port: 443 },
|
|
147
|
+
})).toBe('explicit-external');
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it('keeps Desktop on the managed-owned lifecycle', () => {
|
|
151
|
+
expect(resolveOpenCodeConnectionKind({ runtime: 'desktop', configuredHost: null })).toBe('managed-owned');
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it('gives an explicit host priority over the Desktop managed lifecycle', () => {
|
|
155
|
+
expect(resolveOpenCodeConnectionKind({
|
|
156
|
+
runtime: 'desktop',
|
|
157
|
+
configuredHost: { origin: 'https://opencode.example.com', port: 443 },
|
|
158
|
+
})).toBe('explicit-external');
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
it('keeps an embedded managed override on the managed-owned lifecycle', () => {
|
|
162
|
+
expect(resolveOpenCodeConnectionKind({
|
|
163
|
+
runtime: 'web',
|
|
164
|
+
configuredHost: null,
|
|
165
|
+
lifecycleMode: 'managed',
|
|
166
|
+
})).toBe('managed-owned');
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
it('requires a shared runtime factory for shared-local composition', () => {
|
|
170
|
+
expect(() => createOpenCodeServerComposition({
|
|
171
|
+
runtime: 'web',
|
|
172
|
+
configuredHost: null,
|
|
173
|
+
})).toThrow('Shared OpenCode composition requires a shared runtime factory');
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
it('creates shared and managed-only runtimes only for their owning connection kind', () => {
|
|
177
|
+
const sharedRuntime = { getBaseUrl: () => null, getHeaders: () => ({}) };
|
|
178
|
+
const managedPluginRuntime = { buildManagedChildEnv: () => ({}) };
|
|
179
|
+
const createSharedRuntime = vi.fn(() => sharedRuntime);
|
|
180
|
+
const createManagedPluginRuntime = vi.fn(() => managedPluginRuntime);
|
|
181
|
+
|
|
182
|
+
const shared = createOpenCodeServerComposition({
|
|
183
|
+
runtime: 'web',
|
|
184
|
+
configuredHost: null,
|
|
185
|
+
createSharedRuntime,
|
|
186
|
+
createManagedPluginRuntime,
|
|
187
|
+
});
|
|
188
|
+
const external = createOpenCodeServerComposition({
|
|
189
|
+
runtime: 'web',
|
|
190
|
+
configuredHost: { origin: 'https://opencode.example.com', port: 443 },
|
|
191
|
+
createSharedRuntime,
|
|
192
|
+
createManagedPluginRuntime,
|
|
193
|
+
});
|
|
194
|
+
const desktop = createOpenCodeServerComposition({
|
|
195
|
+
runtime: 'desktop',
|
|
196
|
+
configuredHost: null,
|
|
197
|
+
createSharedRuntime,
|
|
198
|
+
createManagedPluginRuntime,
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
expect(shared.sharedRuntime).toBe(sharedRuntime);
|
|
202
|
+
expect(shared.managedPluginRuntime).toBeNull();
|
|
203
|
+
expect(external.sharedRuntime).toBeNull();
|
|
204
|
+
expect(external.managedPluginRuntime).toBeNull();
|
|
205
|
+
expect(desktop.sharedRuntime).toBeNull();
|
|
206
|
+
expect(desktop.managedPluginRuntime).toBe(managedPluginRuntime);
|
|
207
|
+
expect(createSharedRuntime).toHaveBeenCalledTimes(1);
|
|
208
|
+
expect(createManagedPluginRuntime).toHaveBeenCalledTimes(1);
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
it('recovers the shared service through the production callback without interrupting sessions', async () => {
|
|
212
|
+
const restartManaged = vi.fn(async () => {});
|
|
213
|
+
const triggerManagedHealthCheck = vi.fn(async () => {});
|
|
214
|
+
const recoverShared = vi.fn(async () => {});
|
|
215
|
+
const resetOpenCodeRuntimeProviders = vi.fn();
|
|
216
|
+
const rebindUpstream = vi.fn();
|
|
217
|
+
const interruptBusySessionsAfterRestart = vi.fn(() => ({ sessionIds: [] }));
|
|
218
|
+
const broadcastUiNotification = vi.fn();
|
|
219
|
+
const callbacks = createOpenCodeRecoveryCallbacks({
|
|
220
|
+
resetOpenCodeRuntimeProviders,
|
|
221
|
+
rebindUpstream,
|
|
222
|
+
interruptBusySessionsAfterRestart,
|
|
223
|
+
broadcastUiNotification,
|
|
224
|
+
});
|
|
225
|
+
const shared = createOpenCodeLifecycleControls({
|
|
226
|
+
getKind: () => 'shared-local',
|
|
227
|
+
restartLifecycle: restartManaged,
|
|
228
|
+
triggerLifecycleHealthCheck: triggerManagedHealthCheck,
|
|
229
|
+
recoverShared,
|
|
230
|
+
onSharedRecovered: callbacks.onSharedRecovered,
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
await shared.restart('manual');
|
|
234
|
+
|
|
235
|
+
expect(recoverShared).toHaveBeenCalledOnce();
|
|
236
|
+
expect(resetOpenCodeRuntimeProviders).toHaveBeenCalledOnce();
|
|
237
|
+
expect(rebindUpstream).toHaveBeenCalledOnce();
|
|
238
|
+
expect(recoverShared.mock.invocationCallOrder[0]).toBeLessThan(rebindUpstream.mock.invocationCallOrder[0]);
|
|
239
|
+
expect(interruptBusySessionsAfterRestart).not.toHaveBeenCalled();
|
|
240
|
+
expect(broadcastUiNotification).not.toHaveBeenCalled();
|
|
241
|
+
expect(restartManaged).not.toHaveBeenCalled();
|
|
242
|
+
expect(triggerManagedHealthCheck).not.toHaveBeenCalled();
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
it('keeps managed restart interruption behavior in the production callback', () => {
|
|
246
|
+
const resetOpenCodeRuntimeProviders = vi.fn();
|
|
247
|
+
const rebindUpstream = vi.fn();
|
|
248
|
+
const interruptBusySessionsAfterRestart = vi.fn(() => ({ sessionIds: ['session-1'] }));
|
|
249
|
+
const broadcastUiNotification = vi.fn();
|
|
250
|
+
const callbacks = createOpenCodeRecoveryCallbacks({
|
|
251
|
+
resetOpenCodeRuntimeProviders,
|
|
252
|
+
rebindUpstream,
|
|
253
|
+
interruptBusySessionsAfterRestart,
|
|
254
|
+
broadcastUiNotification,
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
callbacks.onManagedRestarted();
|
|
258
|
+
|
|
259
|
+
expect(resetOpenCodeRuntimeProviders).toHaveBeenCalledOnce();
|
|
260
|
+
expect(rebindUpstream).toHaveBeenCalledOnce();
|
|
261
|
+
expect(interruptBusySessionsAfterRestart).toHaveBeenCalledOnce();
|
|
262
|
+
expect(broadcastUiNotification).toHaveBeenCalledWith(expect.objectContaining({
|
|
263
|
+
kind: 'opencode-restart-interrupted',
|
|
264
|
+
sessionId: 'session-1',
|
|
265
|
+
}));
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
it('shares concurrent shared recovery attempts', async () => {
|
|
269
|
+
let finishRecovery;
|
|
270
|
+
const recoverShared = vi.fn(() => new Promise((resolve) => {
|
|
271
|
+
finishRecovery = resolve;
|
|
272
|
+
}));
|
|
273
|
+
const onSharedRecovered = vi.fn(async () => {});
|
|
274
|
+
const shared = createOpenCodeLifecycleControls({
|
|
275
|
+
getKind: () => 'shared-local',
|
|
276
|
+
restartLifecycle: vi.fn(),
|
|
277
|
+
triggerLifecycleHealthCheck: vi.fn(),
|
|
278
|
+
recoverShared,
|
|
279
|
+
onSharedRecovered,
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
const first = shared.triggerHealthCheck();
|
|
283
|
+
const second = shared.triggerHealthCheck();
|
|
284
|
+
await vi.waitFor(() => expect(recoverShared).toHaveBeenCalledOnce());
|
|
285
|
+
finishRecovery();
|
|
286
|
+
await Promise.all([first, second]);
|
|
287
|
+
|
|
288
|
+
expect(recoverShared).toHaveBeenCalledOnce();
|
|
289
|
+
expect(onSharedRecovered).toHaveBeenCalledOnce();
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
it('contains shared health recovery failures and permits a later retry', async () => {
|
|
293
|
+
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
|
294
|
+
const recoverShared = vi.fn()
|
|
295
|
+
.mockRejectedValueOnce(new Error('service unavailable'))
|
|
296
|
+
.mockResolvedValueOnce(undefined);
|
|
297
|
+
const onSharedRecovered = vi.fn(async () => {});
|
|
298
|
+
const shared = createOpenCodeLifecycleControls({
|
|
299
|
+
getKind: () => 'shared-local',
|
|
300
|
+
restartLifecycle: vi.fn(),
|
|
301
|
+
triggerLifecycleHealthCheck: vi.fn(),
|
|
302
|
+
recoverShared,
|
|
303
|
+
onSharedRecovered,
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
await expect(shared.triggerHealthCheck()).resolves.toBeUndefined();
|
|
307
|
+
await expect(shared.restart()).resolves.toBeUndefined();
|
|
308
|
+
|
|
309
|
+
expect(recoverShared).toHaveBeenCalledTimes(2);
|
|
310
|
+
expect(onSharedRecovered).toHaveBeenCalledOnce();
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
it('preserves managed lifecycle controls', async () => {
|
|
314
|
+
const restartManaged = vi.fn(async () => {});
|
|
315
|
+
const triggerManagedHealthCheck = vi.fn(async () => {});
|
|
316
|
+
const managed = createOpenCodeLifecycleControls({
|
|
317
|
+
getKind: () => 'managed-owned',
|
|
318
|
+
restartLifecycle: restartManaged,
|
|
319
|
+
triggerLifecycleHealthCheck: triggerManagedHealthCheck,
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
await managed.restart('manual');
|
|
323
|
+
await managed.triggerHealthCheck();
|
|
324
|
+
|
|
325
|
+
expect(restartManaged).toHaveBeenCalledWith('manual');
|
|
326
|
+
expect(triggerManagedHealthCheck).toHaveBeenCalledTimes(1);
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
it('requires a shared runtime for shared-local composition', () => {
|
|
330
|
+
expect(() => createOpenCodeConnectionAdapter({ kind: 'shared-local' })).toThrow(
|
|
331
|
+
'Shared OpenCode connection requires a shared service runtime',
|
|
332
|
+
);
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
it('reads shared URL and auth from the shared runtime at call time without owning its process', () => {
|
|
336
|
+
let baseUrl = 'http://127.0.0.1:4096';
|
|
337
|
+
let headers = { authorization: 'Basic first' };
|
|
338
|
+
const adapter = createOpenCodeConnectionAdapter({
|
|
339
|
+
kind: 'shared-local',
|
|
340
|
+
sharedRuntime: {
|
|
341
|
+
getBaseUrl: () => baseUrl,
|
|
342
|
+
getHeaders: () => ({ ...headers }),
|
|
343
|
+
},
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
expect(adapter.getOpenCodeConnectionKind()).toBe('shared-local');
|
|
347
|
+
expect(adapter.getOpenCodeBaseUrl()).toBe('http://127.0.0.1:4096');
|
|
348
|
+
expect(adapter.getOpenCodeAuthHeaders()).toEqual({ authorization: 'Basic first' });
|
|
349
|
+
expect(adapter.ownsOpenCodeProcess()).toBe(false);
|
|
350
|
+
|
|
351
|
+
baseUrl = 'http://127.0.0.1:5096';
|
|
352
|
+
headers = { authorization: 'Basic replacement' };
|
|
353
|
+
expect(adapter.getOpenCodeBaseUrl()).toBe('http://127.0.0.1:5096');
|
|
354
|
+
expect(adapter.getOpenCodeAuthHeaders()).toEqual({ authorization: 'Basic replacement' });
|
|
355
|
+
});
|
|
356
|
+
});
|
|
357
|
+
|
|
131
358
|
describe('OpenCode lifecycle', () => {
|
|
132
359
|
it('uses the resolved binary directly on startup and managed restart without an env override', async () => {
|
|
133
360
|
delete process.env.OPENCODE_BINARY;
|
|
@@ -240,6 +467,33 @@ describe('OpenCode lifecycle', () => {
|
|
|
240
467
|
expect(runtime.testState.openCodeBaseUrl).toBe('http://seamus:4095');
|
|
241
468
|
});
|
|
242
469
|
|
|
470
|
+
it('attaches to an OPENCODE_HOST running OpenCode v1 instead of starting a managed server', async () => {
|
|
471
|
+
const requested = [];
|
|
472
|
+
globalThis.fetch = vi.fn(async (url) => {
|
|
473
|
+
const href = String(url);
|
|
474
|
+
requested.push(href);
|
|
475
|
+
if (href.endsWith('/global/health')) return Response.json({ healthy: true, version: '1.18.32' });
|
|
476
|
+
return new Response('<html>OpenCode</html>', { headers: { 'content-type': 'text/html' } });
|
|
477
|
+
});
|
|
478
|
+
const runtime = createRuntime({
|
|
479
|
+
reapManagedOrphanedProcesses: vi.fn(async () => ({ reaped: 0 })),
|
|
480
|
+
}, {}, {
|
|
481
|
+
ENV_CONFIGURED_OPENCODE_PORT: null,
|
|
482
|
+
ENV_CONFIGURED_OPENCODE_HOST: { origin: 'http://seamus:4095', port: 4095 },
|
|
483
|
+
ENV_EFFECTIVE_PORT: 4095,
|
|
484
|
+
});
|
|
485
|
+
|
|
486
|
+
await runtime.bootstrapOpenCodeAtStartup();
|
|
487
|
+
|
|
488
|
+
expect(spawnMock).not.toHaveBeenCalled();
|
|
489
|
+
expect(requested).toContain('http://seamus:4095/global/health');
|
|
490
|
+
expect(runtime.testState.isExternalOpenCode).toBe(true);
|
|
491
|
+
expect(runtime.testState.isOpenCodeReady).toBe(false);
|
|
492
|
+
expect(runtime.testState.openCodeBaseUrl).toBe('http://seamus:4095');
|
|
493
|
+
expect(runtime.testState.openCodePort).toBe(4095);
|
|
494
|
+
expect(runtime.testState.lastOpenCodeError).toContain('1.18.32');
|
|
495
|
+
});
|
|
496
|
+
|
|
243
497
|
it('warms recently used directories after a successful bootstrap', async () => {
|
|
244
498
|
const fetchMock = vi.fn(async () => ({
|
|
245
499
|
ok: true,
|
|
@@ -2,6 +2,17 @@ import { appendManagedPlugin } from './managed-plugin-config.js';
|
|
|
2
2
|
|
|
3
3
|
export const MANAGED_CONFIG_FILE_NAME = 'opencode.managed.json';
|
|
4
4
|
|
|
5
|
+
/**
|
|
6
|
+
* Built-in OpenCode plugins OpenChamber switches off for its managed child.
|
|
7
|
+
*
|
|
8
|
+
* `opencode.browser` only works when OpenCode's own desktop app attaches a
|
|
9
|
+
* browser to the session; OpenChamber does not implement that protocol, so its
|
|
10
|
+
* tools always fail with `browser.disconnected` and agents conclude browsing is
|
|
11
|
+
* unavailable instead of using `openchamber_web`. A project config that lists
|
|
12
|
+
* `opencode.browser` still re-enables it, because that layer sits above ours.
|
|
13
|
+
*/
|
|
14
|
+
const DISABLED_BUILTIN_PLUGINS = ['-opencode.browser'];
|
|
15
|
+
|
|
5
16
|
/**
|
|
6
17
|
* The managed OpenCode config layer.
|
|
7
18
|
*
|
|
@@ -12,7 +23,8 @@ export const MANAGED_CONFIG_FILE_NAME = 'opencode.managed.json';
|
|
|
12
23
|
* process — where `OPENCODE_CONFIG_CONTENT` (an environment variable) could
|
|
13
24
|
* only ever change across a restart.
|
|
14
25
|
*
|
|
15
|
-
* The file is OpenChamber-owned and holds nothing but `plugins
|
|
26
|
+
* The file is OpenChamber-owned and holds nothing but `plugins`: the built-in
|
|
27
|
+
* plugins OpenChamber disables, then its own plugin directories. It sits above
|
|
16
28
|
* the user's global `opencode.json` and below their project config, so it never
|
|
17
29
|
* shadows a project-level choice.
|
|
18
30
|
*
|
|
@@ -46,7 +58,7 @@ export const createManagedConfigRuntime = ({
|
|
|
46
58
|
|
|
47
59
|
const writeConfigFile = async (pluginDirectories) => {
|
|
48
60
|
await fsPromises.mkdir(dataDir, { recursive: true });
|
|
49
|
-
const text = `${JSON.stringify({ plugins: pluginDirectories }, null, 2)}\n`;
|
|
61
|
+
const text = `${JSON.stringify({ plugins: [...DISABLED_BUILTIN_PLUGINS, ...pluginDirectories] }, null, 2)}\n`;
|
|
50
62
|
// Temp + rename: OpenCode reacts to the write immediately, and a partial
|
|
51
63
|
// read would make it drop every managed plugin until the next change.
|
|
52
64
|
const tmp = `${filePath}.tmp-${process.pid}-${Date.now()}`;
|
|
@@ -69,10 +81,11 @@ export const createManagedConfigRuntime = ({
|
|
|
69
81
|
const includeControl = settings?.agentControlToolEnabled !== false;
|
|
70
82
|
const includeWeb = settings?.agentWebToolEnabled !== false;
|
|
71
83
|
const includeMemory = isAgentMemoryAvailable() && settings?.agentMemoryToolEnabled === true;
|
|
84
|
+
const includeNotify = settings?.agentNotifyToolEnabled === true;
|
|
72
85
|
|
|
73
86
|
const directories = [];
|
|
74
|
-
if (agentToolRuntime && (includeControl || includeWeb || includeMemory)) {
|
|
75
|
-
directories.push(await agentToolRuntime.materializePlugin({ includeControl, includeWeb, includeMemory }));
|
|
87
|
+
if (agentToolRuntime && (includeControl || includeWeb || includeMemory || includeNotify)) {
|
|
88
|
+
directories.push(await agentToolRuntime.materializePlugin({ includeControl, includeWeb, includeMemory, includeNotify }));
|
|
76
89
|
}
|
|
77
90
|
return directories;
|
|
78
91
|
};
|
|
@@ -89,9 +102,8 @@ export const createManagedConfigRuntime = ({
|
|
|
89
102
|
await writeConfigFile(directories);
|
|
90
103
|
return { ...childEnv, OPENCODE_CONFIG: filePath };
|
|
91
104
|
}
|
|
92
|
-
if (directories.length === 0) return childEnv;
|
|
93
105
|
let content = env.OPENCODE_CONFIG_CONTENT;
|
|
94
|
-
for (const directory of directories) {
|
|
106
|
+
for (const directory of [...DISABLED_BUILTIN_PLUGINS, ...directories]) {
|
|
95
107
|
content = appendManagedPlugin(content, directory, 'managed plugin');
|
|
96
108
|
}
|
|
97
109
|
return { ...childEnv, OPENCODE_CONFIG_CONTENT: content };
|
|
@@ -52,7 +52,7 @@ describe('managed OpenCode config file', () => {
|
|
|
52
52
|
expect(childEnv.OPENCODE_CONFIG_CONTENT).toBeUndefined();
|
|
53
53
|
expect(childEnv.OPENCHAMBER_AGENT_TOOL_TOKEN).toBe('token');
|
|
54
54
|
expect(await readConfigFile()).toEqual({
|
|
55
|
-
plugins: [path.join(dataDir, 'agent-tool', 'openchamber-agent-tool')],
|
|
55
|
+
plugins: ['-opencode.browser', path.join(dataDir, 'agent-tool', 'openchamber-agent-tool')],
|
|
56
56
|
});
|
|
57
57
|
});
|
|
58
58
|
|
|
@@ -61,7 +61,7 @@ describe('managed OpenCode config file', () => {
|
|
|
61
61
|
|
|
62
62
|
await runtime.buildManagedChildEnv();
|
|
63
63
|
|
|
64
|
-
for (const directory of (await readConfigFile()).plugins) {
|
|
64
|
+
for (const directory of (await readConfigFile()).plugins.filter((entry) => !entry.startsWith('-'))) {
|
|
65
65
|
expect((await fs.stat(directory)).isDirectory()).toBe(true);
|
|
66
66
|
}
|
|
67
67
|
});
|
|
@@ -74,7 +74,7 @@ describe('managed OpenCode config file', () => {
|
|
|
74
74
|
const childEnv = await runtime.buildManagedChildEnv();
|
|
75
75
|
|
|
76
76
|
expect(agentToolRuntime.materializePlugin).not.toHaveBeenCalled();
|
|
77
|
-
expect(await readConfigFile()).toEqual({ plugins: [] });
|
|
77
|
+
expect(await readConfigFile()).toEqual({ plugins: ['-opencode.browser'] });
|
|
78
78
|
// A tool switched on later reaches a process that can already call back.
|
|
79
79
|
expect(childEnv.OPENCHAMBER_AGENT_TOOL_TOKEN).toBe('token');
|
|
80
80
|
});
|
|
@@ -89,7 +89,7 @@ describe('managed OpenCode config file', () => {
|
|
|
89
89
|
expect(await runtime.refreshManagedConfigFile()).toEqual({ updated: true });
|
|
90
90
|
|
|
91
91
|
const { plugins } = await readConfigFile();
|
|
92
|
-
expect(plugins).toEqual([]);
|
|
92
|
+
expect(plugins).toEqual(['-opencode.browser']);
|
|
93
93
|
expect(agentToolRuntime.createChildEnv).toHaveBeenCalledTimes(1);
|
|
94
94
|
});
|
|
95
95
|
|
|
@@ -102,11 +102,27 @@ describe('managed OpenCode config file', () => {
|
|
|
102
102
|
current.settings = { agentControlToolEnabled: false, agentWebToolEnabled: true };
|
|
103
103
|
await runtime.refreshManagedConfigFile();
|
|
104
104
|
|
|
105
|
-
expect((await readConfigFile()).plugins).toHaveLength(
|
|
105
|
+
expect((await readConfigFile()).plugins).toHaveLength(2);
|
|
106
106
|
expect(agentToolRuntime.materializePlugin).toHaveBeenCalledWith({
|
|
107
107
|
includeControl: false,
|
|
108
108
|
includeWeb: true,
|
|
109
109
|
includeMemory: false,
|
|
110
|
+
includeNotify: false,
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it('injects the notify tool only when it is switched on', async () => {
|
|
115
|
+
const { runtime, agentToolRuntime } = await createHarness({
|
|
116
|
+
settings: { agentControlToolEnabled: false, agentWebToolEnabled: false, agentNotifyToolEnabled: true },
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
await runtime.buildManagedChildEnv();
|
|
120
|
+
|
|
121
|
+
expect(agentToolRuntime.materializePlugin).toHaveBeenCalledWith({
|
|
122
|
+
includeControl: false,
|
|
123
|
+
includeWeb: false,
|
|
124
|
+
includeMemory: false,
|
|
125
|
+
includeNotify: true,
|
|
110
126
|
});
|
|
111
127
|
});
|
|
112
128
|
|
|
@@ -121,7 +137,7 @@ describe('managed OpenCode config file', () => {
|
|
|
121
137
|
expect(childEnv.OPENCODE_CONFIG).toBeUndefined();
|
|
122
138
|
expect(JSON.parse(childEnv.OPENCODE_CONFIG_CONTENT)).toEqual({
|
|
123
139
|
model: 'test/model',
|
|
124
|
-
plugins: [path.join(dataDir, 'agent-tool', 'openchamber-agent-tool')],
|
|
140
|
+
plugins: ['-opencode.browser', path.join(dataDir, 'agent-tool', 'openchamber-agent-tool')],
|
|
125
141
|
});
|
|
126
142
|
await expect(fs.stat(path.join(dataDir, MANAGED_CONFIG_FILE_NAME))).rejects.toThrow();
|
|
127
143
|
expect(await runtime.refreshManagedConfigFile()).toEqual({ updated: false, reason: 'external-config' });
|
|
@@ -147,6 +163,6 @@ describe('managed OpenCode config file', () => {
|
|
|
147
163
|
await runtime.buildManagedChildEnv();
|
|
148
164
|
|
|
149
165
|
expect(agentToolRuntime.materializePlugin).not.toHaveBeenCalled();
|
|
150
|
-
expect(await readConfigFile()).toEqual({ plugins: [] });
|
|
166
|
+
expect(await readConfigFile()).toEqual({ plugins: ['-opencode.browser'] });
|
|
151
167
|
});
|
|
152
168
|
});
|
|
@@ -48,9 +48,14 @@ function ensureProjectMcpConfigPath(workingDirectory) {
|
|
|
48
48
|
return path.join(configDir, 'opencode.json');
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
-
/** Same precedence as `getJsonEntrySource`: custom > project > user. */
|
|
51
|
+
/** Same precedence as `getJsonEntrySource`: custom > project > user override > user. */
|
|
52
52
|
function readMcpEntriesAcrossLayers(layers) {
|
|
53
|
-
return readLayeredMcpEntries([
|
|
53
|
+
return readLayeredMcpEntries([
|
|
54
|
+
layers?.userConfig,
|
|
55
|
+
layers?.userOverrideConfig,
|
|
56
|
+
layers?.projectConfig,
|
|
57
|
+
layers?.customConfig,
|
|
58
|
+
]);
|
|
54
59
|
}
|
|
55
60
|
|
|
56
61
|
function listMcpConfigs(workingDirectory) {
|