@openchambery/web 2.0.0-beta.26 → 2.0.0-beta.28
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/assets/{AgentAvatar-FyTdEOMr.js → AgentAvatar-CqD6Pfom.js} +1 -1
- package/dist/assets/{AssistantView-DvOO2Kf5.js → AssistantView-BW9f9vF2.js} +3 -3
- package/dist/assets/{DiagramView-BqjeSOIg.js → DiagramView-BP3WDmFe.js} +1 -1
- package/dist/assets/{FadeInOnReveal-DHDffzHs.js → FadeInOnReveal-ezW_e-yz.js} +1 -1
- package/dist/assets/{Icon-D7NdMmbi.js → Icon-ChOAx1jX.js} +1 -1
- package/dist/assets/{MarkdownRenderer-kpkRqS42.js → MarkdownRenderer-BBHZa1Ts.js} +2 -2
- package/dist/assets/{MarkdownRendererImpl-CzBos-Gt.js → MarkdownRendererImpl-DfxEsttZ.js} +2 -2
- package/dist/assets/{MarkstreamRendererImpl-RY_rAgKD.js → MarkstreamRendererImpl-6k-n071l.js} +1 -1
- package/dist/assets/{MobileDetailNavigation-DgtRh0zu.js → MobileDetailNavigation-s48GkQY9.js} +1 -1
- package/dist/assets/{MobileShareBridge-BM3cN-gG.js → MobileShareBridge-DAgShkFE.js} +2 -2
- package/dist/assets/MobileSurface-DZug2f4O.js +2 -0
- package/dist/assets/{MultiRunWindow-Bq4gz78C.js → MultiRunWindow-CIhHw9mo.js} +1 -1
- package/dist/assets/{OnboardingScreen-DMbPMwlp.js → OnboardingScreen-ozxwbzDw.js} +1 -1
- package/dist/assets/{OverlayScrollbar-mI9WVa7E.js → OverlayScrollbar-BL-QnnrN.js} +1 -1
- package/dist/assets/SettingsView-DmUdWSji.js +25 -0
- package/dist/assets/{SettingsWindow-CHPjbEnI.js → SettingsWindow-CuIZwGhM.js} +1 -1
- package/dist/assets/{TerminalView-DCJuL2nP.js → TerminalView-BIJ-ILgl.js} +1 -1
- package/dist/assets/{ToolOutputDialog-Cg_Q1t3X.js → ToolOutputDialog-B6KIxVUo.js} +1 -1
- package/dist/assets/{appThemeRegistry-BxuflaQt.js → appThemeRegistry-BuMGUGUK.js} +1 -1
- package/dist/assets/{button-e8fYWIMM.js → button-DoDvdeG-.js} +1 -1
- package/dist/assets/{collapsible-B6wLXHi3.js → collapsible-CZkyFVK0.js} +1 -1
- package/dist/assets/{context-menu-Bq7W1d7y.js → context-menu-DTjON-Ta.js} +1 -1
- package/dist/assets/{desktopHosts-C8vf9vRY.js → desktopHosts-CP1YHQ4Z.js} +1 -1
- package/dist/assets/{desktopRelayRestore-DDJtljeu.js → desktopRelayRestore-BeOGO7oA.js} +1 -1
- package/dist/assets/{diagram-BBL-LfsC.js → diagram-DUi801df.js} +1 -1
- package/dist/assets/{dialog-MRsJpHzu.js → dialog-DGzGqOhG.js} +1 -1
- package/dist/assets/{es-DvPwAj0y.js → es-DCjl24dV.js} +3 -3
- package/dist/assets/fileMentionTouchSelection-C3A6XKRM.js +2 -0
- package/dist/assets/{fileQueries-CBu-ytXS.js → fileQueries-CAH8-p27.js} +1 -1
- package/dist/assets/{fr-XQsoaNKY.js → fr-DfJ5j0t5.js} +3 -3
- package/dist/assets/{gitApi-CuIUGt04.js → gitApi-B1Nyw0PD.js} +3 -3
- package/dist/assets/{input-DbOe1mdT.js → input-Ct2uaiDc.js} +1 -1
- package/dist/assets/insertionBoundaries-BypTPUfp.js +1 -0
- package/dist/assets/{ja-DslzO96s.js → ja-B_9_w2Xf.js} +3 -3
- package/dist/assets/{ko-C_9XOAL0.js → ko-DlGbo3kV.js} +3 -3
- package/dist/assets/main-BREqmQUm.js +169 -0
- package/dist/assets/{main-Sd4Auh_x.js → main-BodZWzei.js} +2 -2
- package/dist/assets/main-qSeeJRW1.css +1 -0
- package/dist/assets/{markdownHeightCache-B5VSLl79.js → markdownHeightCache-CMoWScss.js} +1 -1
- package/dist/assets/{mcpQueries-BYNrkehM.js → mcpQueries-nScpWEqu.js} +1 -1
- package/dist/assets/{miniChat-Hp9bNmS0.js → miniChat-Ca-kdWf0.js} +2 -2
- package/dist/assets/{mobile-fBLEOx9_.js → mobile-BWi6F8Lj.js} +2 -2
- package/dist/assets/multirun-BdSrR6jN.js +3 -0
- package/dist/assets/persistence-B3_CPgm1.js +3 -0
- package/dist/assets/{pl-Dst9Wk53.js → pl-DjUsS8Ei.js} +3 -3
- package/dist/assets/platform-Bxdc4SbV.js +6 -0
- package/dist/assets/projectMeta-CIKLMweH.js +1 -0
- package/dist/assets/{pt-BR-Dq394BPb.js → pt-BR-B8sV7qo1.js} +3 -3
- package/dist/assets/{renderElectronMiniChatApp-D1xipCO5.js → renderElectronMiniChatApp-DtYCIW4n.js} +2 -2
- package/dist/assets/renderMobileApp-D47vojsP.js +28 -0
- package/dist/assets/{runtimeConfig-pTYamMLy.js → runtimeConfig-u9DrZmkS.js} +1 -1
- package/dist/assets/runtimeEndpointReset-Bn4vJ-gl.js +148 -0
- package/dist/assets/runtimeEndpointReset-D1_Y5I4F.css +1 -0
- package/dist/assets/{sessionChangeSummary-DcnZyuWt.js → sessionChangeSummary-C0OxmHed.js} +2 -2
- package/dist/assets/{sessionLookup-B3t-FerN.js → sessionLookup-D1NHRgWf.js} +1 -1
- package/dist/assets/{sortable-tabs-strip-B0sZ7mm6.js → sortable-tabs-strip-Cp5i5hI-.js} +1 -1
- package/dist/assets/{store-D8fmTWK3.js → store-BTqPn8Uq.js} +3 -3
- package/dist/assets/{terminalApi-CJyWCuc8.js → terminalApi-CqyW3_W3.js} +1 -1
- package/dist/assets/{textarea-B5GMntHk.js → textarea-jEbc6gqf.js} +1 -1
- package/dist/assets/{tooltip-C64SdRvR.js → tooltip-DKYT7xYe.js} +1 -1
- package/dist/assets/{uk-8XAR75Qh.js → uk-Bh7HXyLp.js} +3 -3
- package/dist/assets/{url-DF1NFwt_.js → url-hhmiocZY.js} +1 -1
- package/dist/assets/{useAppFontEffects-BQklk6XB.js → useAppFontEffects-BBX1KjLV.js} +53 -53
- package/dist/assets/{useDesktopWindowControlsLayout-CrzYwZDQ.js → useDesktopWindowControlsLayout-uKYzUX-m.js} +1 -1
- package/dist/assets/{useEffectiveDirectory-Ddx7sZ5A.js → useEffectiveDirectory-BOolveMp.js} +1 -1
- package/dist/assets/{useFontPreferences-CrteQiAp.js → useFontPreferences-BMJQP6t6.js} +1 -1
- package/dist/assets/useMobileNavigationStore-CC8QT7VC.js +415 -0
- package/dist/assets/useSessionAutoCleanup-CydMAlLf.js +2 -0
- package/dist/assets/{useWorkerHighlightedLines-VOtVUGkO.js → useWorkerHighlightedLines-BF1ClmMe.js} +1 -1
- package/dist/assets/vendor-opencode-client-MF2GBrFx.js +12 -0
- package/dist/assets/{zh-CN-B-ILfrqC.js → zh-CN-Uzia0gk4.js} +3 -3
- package/dist/assets/{zh-TW-5HeTVP4z.js → zh-TW-CiI7DLdB.js} +3 -3
- package/dist/index.html +8 -8
- package/dist/mini-chat.html +8 -8
- package/dist/mobile.html +8 -8
- package/package.json +3 -3
- package/server/index.js +2 -20
- package/server/lib/git/DOCUMENTATION.md +1 -0
- package/server/lib/git/routes.js +7 -4
- package/server/lib/git/routes.test.js +34 -0
- package/server/lib/git/service.js +3 -1
- package/server/lib/llm/DOCUMENTATION.md +6 -4
- package/server/lib/llm/generate.js +8 -9
- package/server/lib/llm/generate.test.js +33 -6
- package/server/lib/opencode/DOCUMENTATION.md +24 -20
- package/server/lib/opencode/agents.js +13 -22
- package/server/lib/opencode/auth-state-runtime.js +3 -3
- package/server/lib/opencode/auth-state-runtime.test.js +59 -0
- package/server/lib/opencode/auth.d.ts +13 -0
- package/server/lib/opencode/auth.js +74 -64
- package/server/lib/opencode/auth.test.js +57 -0
- package/server/lib/opencode/commands.js +1 -1
- package/server/lib/opencode/config-entities-layering.test.js +72 -0
- package/server/lib/opencode/config-entity-routes.js +9 -122
- package/server/lib/opencode/config-entity-routes.test.js +7 -11
- package/server/lib/opencode/config-files.d.ts +17 -0
- package/server/lib/opencode/config-files.js +187 -0
- package/server/lib/opencode/config-files.test.js +201 -0
- package/server/lib/opencode/core-routes.js +0 -1
- package/server/lib/opencode/core-routes.test.js +0 -15
- package/server/lib/opencode/credential-boundary.d.ts +1 -0
- package/server/lib/opencode/credential-boundary.js +11 -0
- package/server/lib/opencode/ensure-cli.js +4 -1
- package/server/lib/opencode/env-config.js +4 -1
- package/server/lib/opencode/env-config.test.js +28 -0
- package/server/lib/opencode/env-runtime.test.js +2 -1
- package/server/lib/opencode/feature-routes-runtime.js +9 -7
- package/server/lib/opencode/hmr-state-runtime.js +4 -4
- package/server/lib/opencode/lifecycle.js +1 -49
- package/server/lib/opencode/lifecycle.test.js +30 -4
- package/server/lib/opencode/location-release.d.ts +8 -0
- package/server/lib/opencode/location-release.js +32 -0
- package/server/lib/opencode/location-release.test.js +43 -0
- package/server/lib/opencode/mcp.js +1 -1
- package/server/lib/opencode/opencode2-pin.js +2 -1
- package/server/lib/opencode/owned-runtime-upgrade.test.js +5 -5
- package/server/lib/opencode/plugins.js +12 -60
- package/server/lib/opencode/providers.js +6 -3
- package/server/lib/opencode/proxy.js +7 -0
- package/server/lib/opencode/proxy.test.js +13 -0
- package/server/lib/opencode/routes.js +5 -5
- package/server/lib/opencode/routes.test.js +18 -18
- package/server/lib/opencode/runtime-contract.js +3 -2
- package/server/lib/opencode/runtime-contract.test.js +12 -12
- package/server/lib/opencode/shared.js +6 -175
- package/server/lib/opencode/upgrade-screen.js +4 -11
- package/server/lib/opencode/upgrade-screen.test.js +25 -25
- package/server/lib/opencode/version-alignment.test.js +40 -0
- package/server/lib/quota/DOCUMENTATION.md +9 -0
- package/server/lib/quota/provider-credentials.test.js +29 -0
- package/server/lib/quota/providers/claude.js +4 -4
- package/server/lib/quota/providers/codex.js +4 -4
- package/server/lib/quota/providers/codex.test.js +1 -1
- package/server/lib/quota/providers/copilot.js +5 -5
- package/server/lib/quota/providers/google/auth.js +3 -3
- package/server/lib/quota/providers/google/index.js +2 -2
- package/server/lib/quota/providers/index.js +3 -7
- package/server/lib/quota/providers/index.test.js +2 -2
- package/server/lib/quota/providers/kimi.js +4 -4
- package/server/lib/quota/providers/minimax-shared.js +4 -4
- package/server/lib/quota/providers/nanogpt.js +4 -4
- package/server/lib/quota/providers/openai.js +4 -4
- package/server/lib/quota/providers/openrouter.js +4 -4
- package/server/lib/quota/providers/wafer.js +4 -4
- package/server/lib/quota/providers/zai.js +4 -4
- package/server/lib/quota/providers/zhipuai-coding-plan.js +6 -20
- package/server/lib/quota/routes.js +1 -1
- package/server/lib/scheduled-tasks/DOCUMENTATION.md +1 -0
- package/server/lib/scheduled-tasks/runtime.js +2 -3
- package/server/lib/session-index/DOCUMENTATION.md +7 -0
- package/server/lib/session-index/event-ingest.js +13 -0
- package/server/lib/session-index/event-ingest.test.js +7 -0
- package/server/lib/session-index/service.js +45 -0
- package/server/lib/session-index/service.test.js +47 -0
- package/server/lib/tts/DOCUMENTATION.md +7 -5
- package/server/lib/tts/credential-source.test.js +23 -0
- package/server/lib/tts/routes.js +2 -2
- package/server/lib/tts/service.js +16 -37
- package/dist/assets/MobileSurface-ocVWenHA.js +0 -2
- package/dist/assets/SettingsView-CIZz40Jg.js +0 -25
- package/dist/assets/fileMentionTouchSelection-Bj67pPo7.js +0 -3
- package/dist/assets/insertionBoundaries-nzs1Q3YR.js +0 -1
- package/dist/assets/main-D2BzpnUt.css +0 -1
- package/dist/assets/main-DjilfYN7.js +0 -169
- package/dist/assets/multirun-D7W7oCj4.js +0 -4
- package/dist/assets/persistence-CMWO3jFo.js +0 -3
- package/dist/assets/platform-B0p43squ.js +0 -6
- package/dist/assets/projectMeta-DrUKrYt0.js +0 -3
- package/dist/assets/renderMobileApp-DSk7Qs-_.js +0 -28
- package/dist/assets/runtimeEndpointReset-B5HEMuiB.js +0 -149
- package/dist/assets/useMobileNavigationStore-i4cXNoOf.js +0 -414
- package/dist/assets/useSessionAutoCleanup-Dzd0fGSe.js +0 -2
- package/dist/assets/vendor-opencode-client-C12gfjag.js +0 -12
- package/server/lib/opencode/snippets.js +0 -242
- package/server/lib/opencode/snippets.test.js +0 -97
|
@@ -29,8 +29,6 @@ const OPENCODE_CHILD_SIGTERM_GRACE_MS = 2500;
|
|
|
29
29
|
const OPENCODE_CHILD_SIGKILL_WAIT_MS = 1000;
|
|
30
30
|
// Last-used directory plus recently opened projects — deeper tails are unlikely
|
|
31
31
|
// to be the user's first click and just add background work.
|
|
32
|
-
const WARMUP_DIRECTORY_LIMIT = 4;
|
|
33
|
-
const WARMUP_REQUEST_TIMEOUT_MS = 30000;
|
|
34
32
|
|
|
35
33
|
export const createOpenCodeLifecycleRuntime = (deps) => {
|
|
36
34
|
const {
|
|
@@ -57,7 +55,6 @@ export const createOpenCodeLifecycleRuntime = (deps) => {
|
|
|
57
55
|
getManagedOpenCodeShellEnvSnapshot,
|
|
58
56
|
managedCapabilitiesRuntime = null,
|
|
59
57
|
getActiveSessionCount = () => 0,
|
|
60
|
-
getWarmupDirectories = async () => [],
|
|
61
58
|
// Official background service (shared-service.js). null keeps the private
|
|
62
59
|
// managed `opencode serve` path.
|
|
63
60
|
sharedService = null,
|
|
@@ -796,6 +793,7 @@ export const createOpenCodeLifecycleRuntime = (deps) => {
|
|
|
796
793
|
...shellEnv,
|
|
797
794
|
...process.env,
|
|
798
795
|
PATH: envPath,
|
|
796
|
+
OPENCODE_PASSWORD: openCodePassword,
|
|
799
797
|
OPENCODE_SERVER_PASSWORD: openCodePassword,
|
|
800
798
|
};
|
|
801
799
|
const managedEnv = managedCapabilitiesRuntime
|
|
@@ -1253,11 +1251,6 @@ export const createOpenCodeLifecycleRuntime = (deps) => {
|
|
|
1253
1251
|
await waitForOpenCodePort();
|
|
1254
1252
|
try {
|
|
1255
1253
|
await waitForOpenCodeReady();
|
|
1256
|
-
// Warm only after readiness admits; a failed gate must not kick off
|
|
1257
|
-
// directory-scoped traffic against an unready upstream.
|
|
1258
|
-
if (state.isOpenCodeReady) {
|
|
1259
|
-
void warmOpenCodeDirectories();
|
|
1260
|
-
}
|
|
1261
1254
|
} catch (error) {
|
|
1262
1255
|
console.error(`OpenCode readiness check failed: ${error.message}`);
|
|
1263
1256
|
}
|
|
@@ -1268,47 +1261,6 @@ export const createOpenCodeLifecycleRuntime = (deps) => {
|
|
|
1268
1261
|
}
|
|
1269
1262
|
};
|
|
1270
1263
|
|
|
1271
|
-
// OpenCode initializes each project directory lazily on its first
|
|
1272
|
-
// directory-scoped request, and that initialization takes seconds on large
|
|
1273
|
-
// session stores. Without warming, the user's first session open pays it
|
|
1274
|
-
// interactively. Warm the most recently used directories right after
|
|
1275
|
-
// readiness so the work overlaps UI startup instead. Sequential and
|
|
1276
|
-
// best-effort: a failed or slow directory never blocks the others for long,
|
|
1277
|
-
// and a restart invalidates the pass via the port/readiness guard.
|
|
1278
|
-
const warmOpenCodeDirectories = async () => {
|
|
1279
|
-
let directories = [];
|
|
1280
|
-
try {
|
|
1281
|
-
directories = await getWarmupDirectories();
|
|
1282
|
-
} catch {
|
|
1283
|
-
return;
|
|
1284
|
-
}
|
|
1285
|
-
if (!Array.isArray(directories) || directories.length === 0) return;
|
|
1286
|
-
|
|
1287
|
-
const warmedPort = state.openCodePort;
|
|
1288
|
-
for (const directory of directories.slice(0, WARMUP_DIRECTORY_LIMIT)) {
|
|
1289
|
-
if (typeof directory !== 'string' || !directory) continue;
|
|
1290
|
-
if (!state.isOpenCodeReady || state.openCodePort !== warmedPort) return;
|
|
1291
|
-
let timeout = null;
|
|
1292
|
-
try {
|
|
1293
|
-
const controller = new AbortController();
|
|
1294
|
-
timeout = setTimeout(() => controller.abort(), WARMUP_REQUEST_TIMEOUT_MS);
|
|
1295
|
-
// Warming a directory is the point, not the answer: any directory-scoped
|
|
1296
|
-
// read makes OpenCode initialise it. `/api/session` is the cheapest one
|
|
1297
|
-
// that takes a directory.
|
|
1298
|
-
const url = `${buildOpenCodeUrl('/api/session', '')}?directory=${encodeURIComponent(directory)}&limit=1`;
|
|
1299
|
-
await fetch(url, {
|
|
1300
|
-
method: 'GET',
|
|
1301
|
-
headers: { Accept: 'application/json', ...getOpenCodeAuthHeaders() },
|
|
1302
|
-
signal: controller.signal,
|
|
1303
|
-
});
|
|
1304
|
-
} catch {
|
|
1305
|
-
// Best-effort — the directory stays lazy and the UI's own request warms it.
|
|
1306
|
-
} finally {
|
|
1307
|
-
if (timeout) clearTimeout(timeout);
|
|
1308
|
-
}
|
|
1309
|
-
}
|
|
1310
|
-
};
|
|
1311
|
-
|
|
1312
1264
|
const bootstrapOpenCodeAtStartup = () => {
|
|
1313
1265
|
if (!bootstrapOpenCodePromise) {
|
|
1314
1266
|
bootstrapOpenCodePromise = runBootstrapOpenCodeAtStartup().finally(() => {
|
|
@@ -10,6 +10,8 @@ vi.mock('node:child_process', () => ({
|
|
|
10
10
|
|
|
11
11
|
const { createOpenCodeLifecycleRuntime } = await import('./lifecycle.js');
|
|
12
12
|
const { createOpenCodeNetworkRuntime } = await import('./network-runtime.js');
|
|
13
|
+
const { resolveOpenCodeEnvConfig } = await import('./env-config.js');
|
|
14
|
+
const { RUNTIME_CONTRACT_MIN_VERIFIED } = await import('./runtime-contract.js');
|
|
13
15
|
|
|
14
16
|
const originalOpencodeBinary = process.env.OPENCODE_BINARY;
|
|
15
17
|
const originalPath = process.env.PATH;
|
|
@@ -106,6 +108,7 @@ const createRuntime = (overrides = {}, stateRef = null) => {
|
|
|
106
108
|
getManagedOpenCodeShellEnvSnapshot: vi.fn(() => ({
|
|
107
109
|
PATH: '/home/user/.bun/bin:/usr/local/bin:/usr/bin',
|
|
108
110
|
SHELL_ONLY: 'yes',
|
|
111
|
+
OPENCODE_PASSWORD: 'shell-v2-password',
|
|
109
112
|
OPENCODE_SERVER_PASSWORD: 'shell-password',
|
|
110
113
|
})),
|
|
111
114
|
...overrides,
|
|
@@ -160,6 +163,28 @@ const startListeningChild = () => {
|
|
|
160
163
|
};
|
|
161
164
|
|
|
162
165
|
describe('OpenCode lifecycle', () => {
|
|
166
|
+
it.each(['http://example.test:80', 'https://example.test:443', 'http://[::1]:80'])('attaches explicit default-port host %s without spawning', async (host) => {
|
|
167
|
+
const config = resolveOpenCodeEnvConfig({ env: { OPENCODE_HOST: host, OPENCODE_PORT: '9999' } });
|
|
168
|
+
const stateRef = {};
|
|
169
|
+
const runtime = createRuntime({
|
|
170
|
+
env: {
|
|
171
|
+
ENV_CONFIGURED_OPENCODE_PORT: config.configuredOpenCodePort,
|
|
172
|
+
ENV_CONFIGURED_OPENCODE_HOST: config.configuredOpenCodeHost,
|
|
173
|
+
ENV_EFFECTIVE_PORT: config.effectivePort,
|
|
174
|
+
ENV_CONFIGURED_OPENCODE_HOSTNAME: '127.0.0.1',
|
|
175
|
+
ENV_SKIP_OPENCODE_START: true,
|
|
176
|
+
},
|
|
177
|
+
buildOpenCodeUrl: (route) => new URL(route, config.configuredOpenCodeHost.origin).href,
|
|
178
|
+
}, stateRef);
|
|
179
|
+
const fetchMock = stubOpenCodeFetch({ health: { ...DEFAULT_V2_HEALTH, version: '2.0.23' } });
|
|
180
|
+
await runtime.bootstrapOpenCodeAtStartup();
|
|
181
|
+
await runtime.waitForOpenCodeReady(1000, 1);
|
|
182
|
+
expect(stateRef.current.isExternalOpenCode).toBe(true);
|
|
183
|
+
expect(stateRef.current.openCodePort).toBe(config.effectivePort);
|
|
184
|
+
expect(fetchMock.mock.calls.some(([url]) => String(url) === `${config.configuredOpenCodeHost.origin}/api/info`)).toBe(true);
|
|
185
|
+
expect(spawnMock).not.toHaveBeenCalled();
|
|
186
|
+
});
|
|
187
|
+
|
|
163
188
|
it('launches managed OpenCode with the managed PATH', async () => {
|
|
164
189
|
delete process.env.OPENCODE_BINARY;
|
|
165
190
|
const child = createMockChild();
|
|
@@ -179,6 +204,7 @@ describe('OpenCode lifecycle', () => {
|
|
|
179
204
|
expect(options.env.PATH).toBe('/home/user/.bun/bin:/usr/local/bin:/usr/bin');
|
|
180
205
|
expect(options.env.SHELL_ONLY).toBe('yes');
|
|
181
206
|
expect(options.env.OPENCODE_SERVER_PASSWORD).toBe('password');
|
|
207
|
+
expect(options.env.OPENCODE_PASSWORD).toBe('password');
|
|
182
208
|
|
|
183
209
|
await server.close();
|
|
184
210
|
});
|
|
@@ -847,7 +873,7 @@ describe('OpenCode lifecycle', () => {
|
|
|
847
873
|
stateRef.current.runtimeContract = {
|
|
848
874
|
executionAllowed: true,
|
|
849
875
|
phase: 'ready',
|
|
850
|
-
serveVersion:
|
|
876
|
+
serveVersion: RUNTIME_CONTRACT_MIN_VERIFIED,
|
|
851
877
|
instanceGeneration: 1,
|
|
852
878
|
};
|
|
853
879
|
|
|
@@ -861,7 +887,7 @@ describe('OpenCode lifecycle', () => {
|
|
|
861
887
|
runtime.refreshRuntimeContractFromProbe(
|
|
862
888
|
{
|
|
863
889
|
ok: true,
|
|
864
|
-
version:
|
|
890
|
+
version: RUNTIME_CONTRACT_MIN_VERIFIED,
|
|
865
891
|
authenticated: true,
|
|
866
892
|
healthOk: true,
|
|
867
893
|
},
|
|
@@ -875,7 +901,7 @@ describe('OpenCode lifecycle', () => {
|
|
|
875
901
|
runtime.refreshRuntimeContractFromProbe(
|
|
876
902
|
{
|
|
877
903
|
ok: true,
|
|
878
|
-
version:
|
|
904
|
+
version: RUNTIME_CONTRACT_MIN_VERIFIED,
|
|
879
905
|
authenticated: true,
|
|
880
906
|
healthOk: true,
|
|
881
907
|
},
|
|
@@ -884,7 +910,7 @@ describe('OpenCode lifecycle', () => {
|
|
|
884
910
|
);
|
|
885
911
|
expect(stateRef.current.runtimeContract.executionAllowed).toBe(true);
|
|
886
912
|
expect(stateRef.current.runtimeContract.instanceGeneration).toBe(2);
|
|
887
|
-
expect(stateRef.current.runtimeContract.serveVersion).toBe(
|
|
913
|
+
expect(stateRef.current.runtimeContract.serveVersion).toBe(RUNTIME_CONTRACT_MIN_VERIFIED);
|
|
888
914
|
});
|
|
889
915
|
|
|
890
916
|
it('does not mark external skip-start ready before version and migration admit', async () => {
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { OpenCodeClient } from '@opencode/client';
|
|
2
|
+
|
|
3
|
+
export type LocationReleaseResult = { state: 'released' | 'timeout' | 'failed' | 'unavailable' };
|
|
4
|
+
export function releaseDeletedWorktreeLocation(input: {
|
|
5
|
+
getClient: () => OpenCodeClient | null;
|
|
6
|
+
directory: string;
|
|
7
|
+
timeoutMs?: number;
|
|
8
|
+
}): Promise<LocationReleaseResult>;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Explicit worktree deletion only. An eviction acknowledgement detaches cached
|
|
3
|
+
* services; it does not prove that borrowers have released every OS handle.
|
|
4
|
+
* Never use this operation for automatic navigation cleanup on a shared serve.
|
|
5
|
+
*
|
|
6
|
+
* @param {{ getClient: () => import('@opencode/client').OpenCodeClient | null, directory: string, timeoutMs?: number }} input
|
|
7
|
+
* @returns {Promise<{ state: 'released' | 'timeout' | 'failed' | 'unavailable' }>}
|
|
8
|
+
*/
|
|
9
|
+
export async function releaseDeletedWorktreeLocation({ getClient, directory, timeoutMs = 2000 }) {
|
|
10
|
+
const controller = new AbortController();
|
|
11
|
+
let timer;
|
|
12
|
+
try {
|
|
13
|
+
if (!directory || !directory.trim()) return { state: 'unavailable' };
|
|
14
|
+
const client = getClient();
|
|
15
|
+
if (!client) return { state: 'unavailable' };
|
|
16
|
+
const deadline = new Promise((resolve) => {
|
|
17
|
+
timer = setTimeout(() => {
|
|
18
|
+
controller.abort();
|
|
19
|
+
resolve({ state: 'timeout' });
|
|
20
|
+
}, timeoutMs);
|
|
21
|
+
});
|
|
22
|
+
const request = Promise.resolve().then(() => client.debug.location.evict(
|
|
23
|
+
{ location: { directory } },
|
|
24
|
+
{ signal: controller.signal },
|
|
25
|
+
)).then(() => ({ state: 'released' }), () => ({ state: controller.signal.aborted ? 'timeout' : 'failed' }));
|
|
26
|
+
return await Promise.race([request, deadline]);
|
|
27
|
+
} catch {
|
|
28
|
+
return { state: 'failed' };
|
|
29
|
+
} finally {
|
|
30
|
+
clearTimeout(timer);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import { OpenCode } from '@opencode/client';
|
|
3
|
+
import { releaseDeletedWorktreeLocation } from './location-release.js';
|
|
4
|
+
|
|
5
|
+
afterEach(() => vi.useRealTimers());
|
|
6
|
+
|
|
7
|
+
describe('explicit worktree location release', () => {
|
|
8
|
+
it('uses the official SDK exact target, current origin and auth', async () => {
|
|
9
|
+
const requests = [];
|
|
10
|
+
let origin = 'https://old.invalid';
|
|
11
|
+
const getClient = () => OpenCode.make({ baseUrl: origin, headers: { Authorization: 'test-auth' }, fetch: async (input, init) => {
|
|
12
|
+
requests.push(new Request(input, init));
|
|
13
|
+
return new Response(null, { status: 204 });
|
|
14
|
+
} });
|
|
15
|
+
origin = 'https://current.invalid';
|
|
16
|
+
expect(await releaseDeletedWorktreeLocation({ getClient, directory: '/repo/work tree' })).toEqual({ state: 'released' });
|
|
17
|
+
expect(requests).toHaveLength(1);
|
|
18
|
+
const url = new URL(requests[0].url);
|
|
19
|
+
expect(url.origin + url.pathname).toBe('https://current.invalid/api/debug/location');
|
|
20
|
+
expect(url.searchParams.get('location[directory]')).toBe('/repo/work tree');
|
|
21
|
+
expect(requests[0].method).toBe('DELETE');
|
|
22
|
+
expect(requests[0].headers.get('Authorization')).toBe('test-auth');
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('reports failure without claiming successful disposal', async () => {
|
|
26
|
+
const getClient = () => ({ debug: { location: { evict: async () => { throw new Error('unavailable'); } } } });
|
|
27
|
+
expect(await releaseDeletedWorktreeLocation({ getClient, directory: '/repo' })).toEqual({ state: 'failed' });
|
|
28
|
+
expect(await releaseDeletedWorktreeLocation({ getClient: () => null, directory: '/repo' })).toEqual({ state: 'unavailable' });
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it('bounds even a transport that ignores abort and never settles', async () => {
|
|
32
|
+
vi.useFakeTimers();
|
|
33
|
+
let signal;
|
|
34
|
+
const getClient = () => ({ debug: { location: { evict: (_input, options) => {
|
|
35
|
+
signal = options.signal;
|
|
36
|
+
return new Promise(() => {});
|
|
37
|
+
} } } });
|
|
38
|
+
const result = releaseDeletedWorktreeLocation({ getClient, directory: '/repo', timeoutMs: 50 });
|
|
39
|
+
await vi.advanceTimersByTimeAsync(50);
|
|
40
|
+
expect(await result).toEqual({ state: 'timeout' });
|
|
41
|
+
expect(signal.aborted).toBe(true);
|
|
42
|
+
});
|
|
43
|
+
});
|
|
@@ -29,7 +29,7 @@ function validateMcpName(name) {
|
|
|
29
29
|
*/
|
|
30
30
|
function resolveMcpScopeFromPath(layers, sourcePath) {
|
|
31
31
|
if (!sourcePath) return null;
|
|
32
|
-
return
|
|
32
|
+
return layers.projectLayers.some((layer) => layer.path === sourcePath) ? AGENT_SCOPE.PROJECT : AGENT_SCOPE.USER;
|
|
33
33
|
}
|
|
34
34
|
|
|
35
35
|
function ensureProjectMcpConfigPath(workingDirectory) {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// Pinned desktop/runtime opencode2. Upgrade and prepare must not fall back to 1.18.x.
|
|
2
|
-
export const PINNED_OPENCODE2_VERSION = '2.0.
|
|
2
|
+
export const PINNED_OPENCODE2_VERSION = '2.0.23';
|
|
3
|
+
export const MINIMUM_OPENCODE2_VERSION = '2.0.20';
|
|
3
4
|
|
|
4
5
|
// Global npm/bun package that installs the opencode v2 binary (not 1.x opencode-ai).
|
|
5
6
|
export const OPENCODE2_NPM_PACKAGE = '@opencode/cli';
|
|
@@ -86,15 +86,15 @@ describe('owned runtime upgrade (ticket 12)', () => {
|
|
|
86
86
|
|
|
87
87
|
it('verifies running serve identity before upgrade success', () => {
|
|
88
88
|
const contractOk = evaluateRuntimeContract({
|
|
89
|
-
serveVersion: '2.0.
|
|
89
|
+
serveVersion: '2.0.23',
|
|
90
90
|
reachable: true,
|
|
91
91
|
authenticated: true,
|
|
92
92
|
healthOk: true,
|
|
93
93
|
migrationAdmitTranscript: true,
|
|
94
94
|
});
|
|
95
95
|
expect(evaluateOwnedUpgradeResult({
|
|
96
|
-
targetVersion: '2.0.
|
|
97
|
-
serveVersion: '2.0.
|
|
96
|
+
targetVersion: '2.0.23',
|
|
97
|
+
serveVersion: '2.0.23',
|
|
98
98
|
binaryPath: ownedBinary,
|
|
99
99
|
expectedBinaryPath: ownedBinary,
|
|
100
100
|
contract: contractOk,
|
|
@@ -116,8 +116,8 @@ describe('owned runtime upgrade (ticket 12)', () => {
|
|
|
116
116
|
});
|
|
117
117
|
|
|
118
118
|
expect(evaluateOwnedUpgradeResult({
|
|
119
|
-
targetVersion: '2.0.
|
|
120
|
-
serveVersion: '2.0.
|
|
119
|
+
targetVersion: '2.0.23',
|
|
120
|
+
serveVersion: '2.0.23',
|
|
121
121
|
binaryPath: '/usr/bin/opencode',
|
|
122
122
|
expectedBinaryPath: ownedBinary,
|
|
123
123
|
contract: contractOk,
|
|
@@ -4,6 +4,8 @@ import path from 'path';
|
|
|
4
4
|
import {
|
|
5
5
|
AGENT_SCOPE,
|
|
6
6
|
readConfigFile,
|
|
7
|
+
readConfigLayers,
|
|
8
|
+
getConfigForPath,
|
|
7
9
|
writeConfig,
|
|
8
10
|
} from './shared.js';
|
|
9
11
|
import { isPathSpec } from './plugin-spec.js';
|
|
@@ -74,54 +76,8 @@ function getActiveOpencodeConfigDir() {
|
|
|
74
76
|
return path.join(os.homedir(), '.config', 'opencode');
|
|
75
77
|
}
|
|
76
78
|
|
|
77
|
-
function getActiveUserConfigPaths() {
|
|
78
|
-
const configDir = getActiveOpencodeConfigDir();
|
|
79
|
-
return [
|
|
80
|
-
path.join(configDir, 'config.json'),
|
|
81
|
-
path.join(configDir, 'opencode.json'),
|
|
82
|
-
path.join(configDir, 'opencode.jsonc'),
|
|
83
|
-
];
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
function getActiveCustomConfigPath() {
|
|
87
|
-
return process.env.OPENCODE_CONFIG ? path.resolve(process.env.OPENCODE_CONFIG) : null;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
function getPrimaryUserConfigPath() {
|
|
91
|
-
const [defaultPath, ...fallbackPaths] = getActiveUserConfigPaths();
|
|
92
|
-
for (const userPath of [defaultPath, ...fallbackPaths]) {
|
|
93
|
-
if (fs.existsSync(userPath)) {
|
|
94
|
-
return userPath;
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
return defaultPath;
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
function getProjectConfigPath(workingDirectory) {
|
|
101
|
-
if (!workingDirectory) return null;
|
|
102
|
-
const candidates = [
|
|
103
|
-
path.join(workingDirectory, 'opencode.json'),
|
|
104
|
-
path.join(workingDirectory, 'opencode.jsonc'),
|
|
105
|
-
path.join(workingDirectory, '.opencode', 'opencode.json'),
|
|
106
|
-
path.join(workingDirectory, '.opencode', 'opencode.jsonc'),
|
|
107
|
-
];
|
|
108
|
-
return candidates.find((candidate) => fs.existsSync(candidate)) || candidates[0];
|
|
109
|
-
}
|
|
110
|
-
|
|
111
79
|
function readPluginConfigLayers(workingDirectory) {
|
|
112
|
-
|
|
113
|
-
const userPath = getPrimaryUserConfigPath();
|
|
114
|
-
const projectPath = getProjectConfigPath(workingDirectory);
|
|
115
|
-
return {
|
|
116
|
-
userConfig: readConfigFile(userPath),
|
|
117
|
-
projectConfig: readConfigFile(projectPath),
|
|
118
|
-
customConfig: readConfigFile(customPath),
|
|
119
|
-
paths: {
|
|
120
|
-
userPath,
|
|
121
|
-
projectPath,
|
|
122
|
-
customPath,
|
|
123
|
-
},
|
|
124
|
-
};
|
|
80
|
+
return readConfigLayers(workingDirectory);
|
|
125
81
|
}
|
|
126
82
|
|
|
127
83
|
function validateFileName(fileName) {
|
|
@@ -144,16 +100,10 @@ function ensureProjectConfigPath(workingDirectory) {
|
|
|
144
100
|
}
|
|
145
101
|
|
|
146
102
|
function configSources(layers) {
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
}
|
|
151
|
-
sources.push({ config: layers.userConfig, filePath: layers.paths.userPath, scope: AGENT_SCOPE.USER });
|
|
152
|
-
}
|
|
153
|
-
if (layers.paths.projectPath) {
|
|
154
|
-
sources.push({ config: layers.projectConfig, filePath: layers.paths.projectPath, scope: AGENT_SCOPE.PROJECT });
|
|
155
|
-
}
|
|
156
|
-
return sources;
|
|
103
|
+
return [...layers.documents.filter((layer) => layer.scope !== 'project').reverse(), ...layers.projectLayers.toReversed()].map((layer) => ({
|
|
104
|
+
config: layer.config, filePath: layer.path,
|
|
105
|
+
scope: layer.scope === 'project' ? AGENT_SCOPE.PROJECT : AGENT_SCOPE.USER,
|
|
106
|
+
}));
|
|
157
107
|
}
|
|
158
108
|
|
|
159
109
|
function splitScopedValue(value) {
|
|
@@ -175,7 +125,9 @@ function getPluginTarget(id, workingDirectory) {
|
|
|
175
125
|
const { scope, value: spec } = splitScopedValue(decoded.value);
|
|
176
126
|
validateScope(scope);
|
|
177
127
|
const layers = readPluginConfigLayers(workingDirectory);
|
|
178
|
-
const source = configSources(layers).find((candidate) => candidate.scope === scope
|
|
128
|
+
const source = configSources(layers).find((candidate) => candidate.scope === scope
|
|
129
|
+
&& Array.isArray(candidate.config?.plugin)
|
|
130
|
+
&& candidate.config.plugin.some((raw) => parsePluginRaw(raw).spec === spec));
|
|
179
131
|
const plugin = Array.isArray(source?.config?.plugin) ? source.config.plugin : [];
|
|
180
132
|
const index = plugin.findIndex((raw) => parsePluginRaw(raw).spec === spec);
|
|
181
133
|
if (!source || index === -1) {
|
|
@@ -281,14 +233,14 @@ function createPluginEntry(entry, workingDirectory) {
|
|
|
281
233
|
throw codedError(`Plugin "${spec}" already exists`, 'ENTRY_EXISTS');
|
|
282
234
|
}
|
|
283
235
|
|
|
284
|
-
let targetPath =
|
|
236
|
+
let targetPath = layers.paths.userPath;
|
|
285
237
|
let config = {};
|
|
286
238
|
if (scope === AGENT_SCOPE.PROJECT) {
|
|
287
239
|
targetPath = ensureProjectConfigPath(workingDirectory);
|
|
288
240
|
config = fs.existsSync(targetPath) ? readConfigFile(targetPath) : {};
|
|
289
241
|
} else {
|
|
290
242
|
targetPath = layers.paths.customPath || layers.paths.userPath;
|
|
291
|
-
config = layers
|
|
243
|
+
config = getConfigForPath(layers, targetPath);
|
|
292
244
|
}
|
|
293
245
|
|
|
294
246
|
if (!Array.isArray(config.plugin)) {
|
|
@@ -30,8 +30,8 @@ function getProviderSources(providerId, workingDirectory) {
|
|
|
30
30
|
return {
|
|
31
31
|
sources: {
|
|
32
32
|
auth: { exists: false },
|
|
33
|
-
user: { exists: userExists, path: paths.userPath },
|
|
34
|
-
project: { exists: projectExists, path: paths.projectPath || null },
|
|
33
|
+
user: { exists: userExists, path: layers.userLayers.findLast((layer) => layer.config?.provider?.[providerId] !== undefined || layer.config?.providers?.[providerId] !== undefined)?.path || paths.userPath },
|
|
34
|
+
project: { exists: projectExists, path: layers.projectLayers.findLast((layer) => layer.config?.provider?.[providerId] !== undefined || layer.config?.providers?.[providerId] !== undefined)?.path || paths.projectPath || null },
|
|
35
35
|
custom: { exists: customExists, path: paths.customPath }
|
|
36
36
|
}
|
|
37
37
|
};
|
|
@@ -44,17 +44,20 @@ function removeProviderConfig(providerId, workingDirectory, scope = 'user') {
|
|
|
44
44
|
|
|
45
45
|
const layers = readConfigLayers(workingDirectory);
|
|
46
46
|
let targetPath = layers.paths.userPath;
|
|
47
|
+
const sources = getProviderSources(providerId, workingDirectory).sources;
|
|
47
48
|
|
|
48
49
|
if (scope === 'project') {
|
|
49
50
|
if (!workingDirectory) {
|
|
50
51
|
throw new Error('Working directory is required for project scope');
|
|
51
52
|
}
|
|
52
|
-
targetPath =
|
|
53
|
+
targetPath = sources.project.path || targetPath;
|
|
53
54
|
} else if (scope === 'custom') {
|
|
54
55
|
if (!layers.paths.customPath) {
|
|
55
56
|
return false;
|
|
56
57
|
}
|
|
57
58
|
targetPath = layers.paths.customPath;
|
|
59
|
+
} else {
|
|
60
|
+
targetPath = sources.user.path;
|
|
58
61
|
}
|
|
59
62
|
|
|
60
63
|
const targetConfig = getConfigForPath(layers, targetPath);
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { createProxyMiddleware } from 'http-proxy-middleware';
|
|
2
|
+
import { isSensitiveCredentialResponse } from './credential-boundary.js';
|
|
2
3
|
|
|
3
4
|
import {
|
|
4
5
|
applyForwardProxyResponseHeaders,
|
|
@@ -244,6 +245,12 @@ export const resolveSessionTurnAdmissionRequest = (req) => {
|
|
|
244
245
|
};
|
|
245
246
|
|
|
246
247
|
export const registerOpenCodeProxy = (app, deps) => {
|
|
248
|
+
app.use((req, res, next) => {
|
|
249
|
+
if (isSensitiveCredentialResponse(req.method, req.originalUrl || req.url)) {
|
|
250
|
+
return res.status(403).json({ code: 'credential_response_refused', error: 'Provider credential values are host-only.' });
|
|
251
|
+
}
|
|
252
|
+
return next();
|
|
253
|
+
});
|
|
247
254
|
const {
|
|
248
255
|
fs,
|
|
249
256
|
os,
|
|
@@ -57,6 +57,19 @@ const mountProxy = () => {
|
|
|
57
57
|
return app;
|
|
58
58
|
};
|
|
59
59
|
|
|
60
|
+
describe('credential response boundary', () => {
|
|
61
|
+
it.each(['/api/credential', '/api/credential/', '/api/%63redential?directory=/repo'])('refuses raw credential list %s before upstream', async (path) => {
|
|
62
|
+
const fetchSpy = vi.fn();
|
|
63
|
+
globalThis.fetch = fetchSpy;
|
|
64
|
+
const response = await request(mountProxy()).get(path).expect(403);
|
|
65
|
+
expect(response.body.code).toBe('credential_response_refused');
|
|
66
|
+
expect(fetchSpy).not.toHaveBeenCalled();
|
|
67
|
+
});
|
|
68
|
+
it('refuses credential create replies which echo values', async () => {
|
|
69
|
+
await request(mountProxy()).post('/api/credential').send({}).expect(403);
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
|
|
60
73
|
const sseBody = (blocks) => {
|
|
61
74
|
const encoder = new TextEncoder();
|
|
62
75
|
const text = blocks.join('');
|
|
@@ -709,7 +709,7 @@ export const registerOpenCodeRoutes = (app, dependencies) => {
|
|
|
709
709
|
|
|
710
710
|
const sources = getProviderSources(providerId, directory);
|
|
711
711
|
const { getProviderAuth } = await getAuthLibrary();
|
|
712
|
-
const auth = getProviderAuth(providerId);
|
|
712
|
+
const auth = await getProviderAuth(providerId);
|
|
713
713
|
sources.sources.auth.exists = Boolean(auth);
|
|
714
714
|
|
|
715
715
|
return res.json({
|
|
@@ -753,12 +753,12 @@ export const registerOpenCodeRoutes = (app, dependencies) => {
|
|
|
753
753
|
let removed = false;
|
|
754
754
|
if (scope === 'auth') {
|
|
755
755
|
const { removeProviderAuth } = await getAuthLibrary();
|
|
756
|
-
removed = removeProviderAuth(providerId);
|
|
756
|
+
removed = await removeProviderAuth(providerId);
|
|
757
757
|
} else if (scope === 'user' || scope === 'project' || scope === 'custom') {
|
|
758
758
|
removed = removeProviderConfig(providerId, directory, scope);
|
|
759
759
|
} else if (scope === 'all') {
|
|
760
760
|
const { removeProviderAuth } = await getAuthLibrary();
|
|
761
|
-
const authRemoved = removeProviderAuth(providerId);
|
|
761
|
+
const authRemoved = await removeProviderAuth(providerId);
|
|
762
762
|
const userRemoved = removeProviderConfig(providerId, directory, 'user');
|
|
763
763
|
const projectRemoved = directory ? removeProviderConfig(providerId, directory, 'project') : false;
|
|
764
764
|
const customRemoved = removeProviderConfig(providerId, directory, 'custom');
|
|
@@ -767,14 +767,14 @@ export const registerOpenCodeRoutes = (app, dependencies) => {
|
|
|
767
767
|
return res.status(400).json({ error: 'Invalid scope' });
|
|
768
768
|
}
|
|
769
769
|
|
|
770
|
-
if (removed) {
|
|
770
|
+
if (removed && scope !== 'auth') {
|
|
771
771
|
await refreshOpenCodeAfterConfigChange(`provider ${providerId} disconnected (${scope})`);
|
|
772
772
|
}
|
|
773
773
|
|
|
774
774
|
return res.json({
|
|
775
775
|
success: true,
|
|
776
776
|
removed,
|
|
777
|
-
requiresReload: removed,
|
|
777
|
+
requiresReload: removed && scope !== 'auth',
|
|
778
778
|
message: removed ? 'Provider disconnected successfully' : 'Provider was not connected',
|
|
779
779
|
reloadDelayMs: removed ? clientReloadDelayMs : undefined,
|
|
780
780
|
});
|
|
@@ -233,39 +233,39 @@ describe('opencode2 upgrade pin (ticket 12)', () => {
|
|
|
233
233
|
});
|
|
234
234
|
|
|
235
235
|
it('upgrades a shared service using the owned cache and verifies the running version', async () => {
|
|
236
|
-
const binary = '/cache/opencode-cli/2.0.
|
|
236
|
+
const binary = '/cache/opencode-cli/2.0.23/opencode';
|
|
237
237
|
vi.mocked(installPinnedOpenCode2Cli).mockResolvedValueOnce(binary);
|
|
238
|
-
vi.mocked(readOpenCode2BinaryVersion).mockReturnValueOnce('2.0.
|
|
239
|
-
vi.stubGlobal('fetch', vi.fn(async () => new Response(JSON.stringify({ version: '2.0.
|
|
238
|
+
vi.mocked(readOpenCode2BinaryVersion).mockReturnValueOnce('2.0.23');
|
|
239
|
+
vi.stubGlobal('fetch', vi.fn(async () => new Response(JSON.stringify({ version: '2.0.23', pid: 42 }))));
|
|
240
240
|
let resolvedBinary = '/usr/local/bin/opencode';
|
|
241
241
|
const restartOpenCode = vi.fn(async () => {});
|
|
242
242
|
const { app } = createUpgradeApp({
|
|
243
243
|
getIsExternalOpenCode: () => true,
|
|
244
244
|
getIsSharedOpenCodeService: () => true,
|
|
245
245
|
getResolvedOpenCodeBinary: () => resolvedBinary,
|
|
246
|
-
getOpenCodeCliVersion: () => '2.0.
|
|
246
|
+
getOpenCodeCliVersion: () => '2.0.23',
|
|
247
247
|
forceResolvedOpenCodeBinary: (value) => { resolvedBinary = value; },
|
|
248
248
|
restartOpenCode,
|
|
249
249
|
waitForOpenCodeReady: vi.fn(),
|
|
250
250
|
openchamberDataDir: '/cache',
|
|
251
251
|
});
|
|
252
252
|
|
|
253
|
-
const response = await request(app).post('/api/opencode/upgrade').send({ target: '2.0.
|
|
253
|
+
const response = await request(app).post('/api/opencode/upgrade').send({ target: '2.0.23' }).expect(200);
|
|
254
254
|
expect(restartOpenCode).toHaveBeenCalledWith({ binaryPath: binary });
|
|
255
|
-
expect(response.body).toMatchObject({ success: true, version: '2.0.
|
|
255
|
+
expect(response.body).toMatchObject({ success: true, version: '2.0.23', ownership: 'shared-service' });
|
|
256
256
|
});
|
|
257
257
|
|
|
258
|
-
it('discovers the remote release and updates a shared 2.0.16 service to 2.0.
|
|
258
|
+
it('discovers the remote release and updates a shared 2.0.16 service to 2.0.23', async () => {
|
|
259
259
|
let serveVersion = '2.0.16';
|
|
260
260
|
let binary = '/usr/local/bin/opencode';
|
|
261
|
-
const installed = '/cache/opencode-cli/2.0.
|
|
261
|
+
const installed = '/cache/opencode-cli/2.0.23/opencode';
|
|
262
262
|
const fetchMock = vi.fn(async (url) => new Response(JSON.stringify({
|
|
263
|
-
version: String(url).includes('registry.npmjs.org') ? '2.0.
|
|
263
|
+
version: String(url).includes('registry.npmjs.org') ? '2.0.23' : serveVersion,
|
|
264
264
|
})));
|
|
265
265
|
vi.stubGlobal('fetch', fetchMock);
|
|
266
|
-
vi.mocked(readOpenCode2BinaryVersion).mockReturnValue('2.0.
|
|
266
|
+
vi.mocked(readOpenCode2BinaryVersion).mockReturnValue('2.0.23');
|
|
267
267
|
vi.mocked(installPinnedOpenCode2Cli).mockResolvedValueOnce(installed);
|
|
268
|
-
const restartOpenCode = vi.fn(async () => { serveVersion = '2.0.
|
|
268
|
+
const restartOpenCode = vi.fn(async () => { serveVersion = '2.0.23'; });
|
|
269
269
|
const { app } = createUpgradeApp({
|
|
270
270
|
discoverOpenCodeUpdate: createOpenCodeUpdateDiscovery(),
|
|
271
271
|
getIsExternalOpenCode: () => true,
|
|
@@ -277,12 +277,12 @@ describe('opencode2 upgrade pin (ticket 12)', () => {
|
|
|
277
277
|
openchamberDataDir: '/cache',
|
|
278
278
|
});
|
|
279
279
|
const before = await request(app).get('/api/opencode/upgrade-status').expect(200);
|
|
280
|
-
expect(before.body).toMatchObject({ available: true, serveVersion: '2.0.16', targetVersion: '2.0.
|
|
280
|
+
expect(before.body).toMatchObject({ available: true, serveVersion: '2.0.16', targetVersion: '2.0.23', canManage: true });
|
|
281
281
|
const result = await request(app).post('/api/opencode/upgrade').send({}).expect(200);
|
|
282
|
-
expect(result.body).toMatchObject({ success: true, restarted: true, version: '2.0.
|
|
282
|
+
expect(result.body).toMatchObject({ success: true, restarted: true, version: '2.0.23' });
|
|
283
283
|
expect(restartOpenCode).toHaveBeenCalledWith({ binaryPath: installed });
|
|
284
284
|
const after = await request(app).get('/api/opencode/upgrade-status').expect(200);
|
|
285
|
-
expect(after.body).toMatchObject({ available: false, serveVersion: '2.0.
|
|
285
|
+
expect(after.body).toMatchObject({ available: false, serveVersion: '2.0.23' });
|
|
286
286
|
expect(fetchMock.mock.calls.filter(([url]) => String(url).includes('registry.npmjs.org'))).toHaveLength(1);
|
|
287
287
|
vi.mocked(readOpenCode2BinaryVersion).mockReset();
|
|
288
288
|
});
|
|
@@ -385,7 +385,7 @@ describe('runtime contract routes (ticket 11)', () => {
|
|
|
385
385
|
ok: true,
|
|
386
386
|
status: 200,
|
|
387
387
|
statusText: 'OK',
|
|
388
|
-
json: async () => ({ version: '2.0.
|
|
388
|
+
json: async () => ({ version: '2.0.20', pid: 1 }),
|
|
389
389
|
})));
|
|
390
390
|
const { app } = createContractApp();
|
|
391
391
|
|
|
@@ -395,7 +395,7 @@ describe('runtime contract routes (ticket 11)', () => {
|
|
|
395
395
|
expect(health.body.executionAllowed).toBe(true);
|
|
396
396
|
|
|
397
397
|
const contract = await request(app).get('/api/opencode/contract').expect(200);
|
|
398
|
-
expect(contract.body.serveVersion).toBe('2.0.
|
|
398
|
+
expect(contract.body.serveVersion).toBe('2.0.20');
|
|
399
399
|
expect(contract.body.capabilities['core.protocol'].available).toBe(true);
|
|
400
400
|
});
|
|
401
401
|
|
|
@@ -439,7 +439,7 @@ describe('GET /api/opencode/health', () => {
|
|
|
439
439
|
status: 200,
|
|
440
440
|
statusText: 'OK',
|
|
441
441
|
json: async () => ({
|
|
442
|
-
version: '2.0.
|
|
442
|
+
version: '2.0.20',
|
|
443
443
|
pid: 4242,
|
|
444
444
|
urls: { api: 'http://127.0.0.1:4096' },
|
|
445
445
|
paths: { config: '/tmp/config', data: '/tmp/data' },
|
|
@@ -453,7 +453,7 @@ describe('GET /api/opencode/health', () => {
|
|
|
453
453
|
|
|
454
454
|
expect(response.body).toMatchObject({
|
|
455
455
|
healthy: true,
|
|
456
|
-
version: '2.0.
|
|
456
|
+
version: '2.0.20',
|
|
457
457
|
executionAllowed: true,
|
|
458
458
|
protocolCompatible: true,
|
|
459
459
|
});
|