@openchambery/web 1.19.21-beta.1 → 2.0.0-beta.1

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.
Files changed (195) hide show
  1. package/README.md +13 -11
  2. package/bin/cli-opencode.test.js +208 -0
  3. package/bin/cli.js +62 -9
  4. package/bin/lib/cli-startup.js +105 -2
  5. package/bin/lib/cli-startup.test.js +165 -0
  6. package/dist/assets/{AgentAvatar-NT957MuI.js → AgentAvatar-CLWWNu_Q.js} +1 -1
  7. package/dist/assets/AssistantView-BTCkm8yk.js +3 -0
  8. package/dist/assets/{DiagramView-Bh06xinl.js → DiagramView-xuSJTnv7.js} +1 -1
  9. package/dist/assets/{FadeInOnReveal-CilRl4gc.js → FadeInOnReveal-Ov0D96d0.js} +1 -1
  10. package/dist/assets/{Icon-i5GiQRLq.js → Icon-DD-BuMfZ.js} +1 -1
  11. package/dist/assets/MarkdownRenderer-bc_p1H7l.js +2 -0
  12. package/dist/assets/{MarkdownRendererImpl-f2agCc4q.js → MarkdownRendererImpl-SKi4oG0c.js} +1 -1
  13. package/dist/assets/{MarkstreamRendererImpl-ChD4WD2v.js → MarkstreamRendererImpl-CnkodGJW.js} +1 -1
  14. package/dist/assets/MobileDetailNavigation-Bnx59U_J.js +1 -0
  15. package/dist/assets/{MobileShareBridge-D7SG0ueZ.js → MobileShareBridge-BQlP4J-F.js} +1 -1
  16. package/dist/assets/MobileSurface-BXLeiZ4d.js +2 -0
  17. package/dist/assets/{MultiRunWindow-CjCw1X4n.js → MultiRunWindow-ChZGob0v.js} +1 -1
  18. package/dist/assets/{OnboardingScreen-BGRLk4Wi.js → OnboardingScreen-DtQJxZG-.js} +1 -1
  19. package/dist/assets/{OverlayScrollbar-NuQI58XD.js → OverlayScrollbar-pMwxaCca.js} +1 -1
  20. package/dist/assets/SettingsView-B5I0KDTZ.js +25 -0
  21. package/dist/assets/{SettingsWindow-C2DgbXJ3.js → SettingsWindow-DfnsFa8y.js} +1 -1
  22. package/dist/assets/{TerminalView-lubDPKMs.js → TerminalView-Dh9MWCpS.js} +1 -1
  23. package/dist/assets/{ToolOutputDialog-CzgW3-Cb.js → ToolOutputDialog-BejmjrRk.js} +1 -1
  24. package/dist/assets/{appThemeRegistry-BKc-qXbQ.js → appThemeRegistry-COGfxTFV.js} +1 -1
  25. package/dist/assets/{button-DtzFDvij.js → button-D-rATN7w.js} +1 -1
  26. package/dist/assets/{collapsible-D-nzhcyF.js → collapsible-BXRd5_Hr.js} +1 -1
  27. package/dist/assets/{context-menu-BzVSzHLG.js → context-menu-jGtH6fR9.js} +1 -1
  28. package/dist/assets/{diagram-2YWalSb5.js → diagram-DQf7Y_uR.js} +1 -1
  29. package/dist/assets/{dialog-DGV7DsZC.js → dialog-bfbvR4VH.js} +1 -1
  30. package/dist/assets/es-c52LTHHe.js +15 -0
  31. package/dist/assets/{fileMentionTouchSelection-BTkP7aYI.js → fileMentionTouchSelection-BvrNCroG.js} +1 -1
  32. package/dist/assets/{fr-DsBZeN8Y.js → fr-PisFbUE8.js} +3 -3
  33. package/dist/assets/gitApi-ButwzQ0N.js +9 -0
  34. package/dist/assets/{input-CbB2HdLg.js → input-8r6F_Fza.js} +1 -1
  35. package/dist/assets/insertionBoundaries-DGs7S5zv.js +1 -0
  36. package/dist/assets/ja-C8Ikun3Z.js +15 -0
  37. package/dist/assets/ko-ClFys5o2.js +15 -0
  38. package/dist/assets/main-CCE_4y5F.js +2 -0
  39. package/dist/assets/main-Dn9rpsgl.js +169 -0
  40. package/dist/assets/{markdownHeightCache-C9ym9Tsb.js → markdownHeightCache-1Qo6y6IJ.js} +1 -1
  41. package/dist/assets/miniChat-BpjxVqgg.js +2 -0
  42. package/dist/assets/mobile-nMJCaNq1.js +2 -0
  43. package/dist/assets/multirun-BvSG94mW.js +4 -0
  44. package/dist/assets/pl-CnaKWN5F.js +15 -0
  45. package/dist/assets/platform-CWTIp02q.js +4 -0
  46. package/dist/assets/projectMeta-DOe49dM_.js +3 -0
  47. package/dist/assets/pt-BR-s8OpfRI6.js +15 -0
  48. package/dist/assets/renderElectronMiniChatApp-R6dKh79f.js +2 -0
  49. package/dist/assets/renderMobileApp-y-S8y8v_.js +28 -0
  50. package/dist/assets/{runtimeConfig-PpnZiwcW.js → runtimeConfig-BMvqa5we.js} +1 -1
  51. package/dist/assets/{runtimeEndpointReset-BGjzNuQl.js → runtimeEndpointReset-B6BxhvR0.js} +6 -6
  52. package/dist/assets/sessionChangeSummary-VVBlEXuh.js +2 -0
  53. package/dist/assets/{sessionLookup-CbbzalcH.js → sessionLookup-gh-hVRIc.js} +1 -1
  54. package/dist/assets/{sortable-tabs-strip-CfgEJfB0.js → sortable-tabs-strip-CudoSgV4.js} +1 -1
  55. package/dist/assets/src-BzX5UI-c.css +2 -0
  56. package/dist/assets/{store-B6dImdlY.js → store-DJczmczv.js} +4 -4
  57. package/dist/assets/{terminalApi-xjqssKke.js → terminalApi-DiF6JmBP.js} +1 -1
  58. package/dist/assets/textarea-3XR8sUfy.js +1 -0
  59. package/dist/assets/{tooltip-Djn7MSOX.js → tooltip-CQvpK-UV.js} +1 -1
  60. package/dist/assets/uk-CaX0iqkB.js +15 -0
  61. package/dist/assets/{useAppFontEffects-BJBcy-84.js → useAppFontEffects-Cpmpo7Wb.js} +45 -45
  62. package/dist/assets/useDesktopWindowControlsLayout-B1GX59-L.js +1 -0
  63. package/dist/assets/{useEffectiveDirectory-CzHynoi_.js → useEffectiveDirectory-CN6JoCFw.js} +1 -1
  64. package/dist/assets/{useFontPreferences-B1JYyzpK.js → useFontPreferences-DcbXQOkc.js} +1 -1
  65. package/dist/assets/useMobileNavigationStore-BVjFtXSy.js +414 -0
  66. package/dist/assets/{useSessionAutoCleanup-DzV9N6G-.js → useSessionAutoCleanup-BLjymaFt.js} +1 -1
  67. package/dist/assets/{useWorkerHighlightedLines-Bi9ASKr_.js → useWorkerHighlightedLines-Bqn4AiBL.js} +1 -1
  68. package/dist/assets/vendor-opencode-client-BOe2qpQb.js +12 -0
  69. package/dist/assets/{vendor-yaml-DIcAK3EC.js → vendor-yaml-C9KlJAlj.js} +37 -37
  70. package/dist/assets/zh-CN-20MYEjha.js +15 -0
  71. package/dist/assets/{zh-TW-BsOQbrwT.js → zh-TW-DiN04qTw.js} +3 -3
  72. package/dist/index.html +6 -6
  73. package/dist/mini-chat.html +6 -6
  74. package/dist/mobile.html +6 -6
  75. package/package.json +2 -2
  76. package/server/index.js +107 -0
  77. package/server/lib/assistants/DOCUMENTATION.md +18 -18
  78. package/server/lib/assistants/assign.js +2 -1
  79. package/server/lib/assistants/contact-store.js +37 -19
  80. package/server/lib/assistants/projection-history.test.js +207 -0
  81. package/server/lib/assistants/routes.js +5 -5
  82. package/server/lib/assistants/service.js +493 -123
  83. package/server/lib/assistants/service.test.js +203 -133
  84. package/server/lib/conversations/DOCUMENTATION.md +4 -4
  85. package/server/lib/conversations/service.js +86 -57
  86. package/server/lib/conversations/service.test.js +116 -100
  87. package/server/lib/event-stream/global-hub.js +2 -1
  88. package/server/lib/llm/DOCUMENTATION.md +71 -84
  89. package/server/lib/llm/catalog.js +111 -45
  90. package/server/lib/llm/catalog.test.js +121 -59
  91. package/server/lib/llm/completions.js +37 -12
  92. package/server/lib/llm/completions.test.js +45 -4
  93. package/server/lib/llm/generate.js +421 -450
  94. package/server/lib/llm/generate.test.js +553 -487
  95. package/server/lib/llm/routes.js +6 -4
  96. package/server/lib/message-queue/DOCUMENTATION.md +5 -5
  97. package/server/lib/message-queue/opencode-adapter.js +191 -104
  98. package/server/lib/message-queue/opencode-adapter.test.js +328 -58
  99. package/server/lib/message-queue/runtime.test.js +6 -6
  100. package/server/lib/message-queue/worker.js +7 -1
  101. package/server/lib/opencode/DOCUMENTATION.md +28 -5
  102. package/server/lib/opencode/config-entity-routes.js +84 -13
  103. package/server/lib/opencode/config-entity-routes.test.js +41 -10
  104. package/server/lib/opencode/ensure-cli.js +239 -0
  105. package/server/lib/opencode/ensure-cli.test.js +119 -0
  106. package/server/lib/opencode/env-runtime.js +193 -110
  107. package/server/lib/opencode/env-runtime.test.js +221 -17
  108. package/server/lib/opencode/feature-routes-runtime.js +37 -1
  109. package/server/lib/opencode/lifecycle.js +204 -70
  110. package/server/lib/opencode/lifecycle.test.js +376 -6
  111. package/server/lib/opencode/network-runtime.js +28 -16
  112. package/server/lib/opencode/network-runtime.test.js +68 -4
  113. package/server/lib/opencode/opencode2-pin.js +139 -0
  114. package/server/lib/opencode/opencode2-pin.test.js +87 -0
  115. package/server/lib/opencode/opencode2-sidecar.e2e.test.js +203 -0
  116. package/server/lib/opencode/plugin-compatibility.js +15 -0
  117. package/server/lib/opencode/plugin-compatibility.test.js +21 -0
  118. package/server/lib/opencode/plugins.js +4 -3
  119. package/server/lib/opencode/plugins.test.js +12 -0
  120. package/server/lib/opencode/proxy.js +153 -16
  121. package/server/lib/opencode/proxy.test.js +120 -0
  122. package/server/lib/opencode/routes.js +25 -32
  123. package/server/lib/opencode/routes.test.js +51 -52
  124. package/server/lib/opencode/server-utils-runtime.js +2 -0
  125. package/server/lib/opencode/skill-routes.js +6 -7
  126. package/server/lib/opencode/skill-routes.test.js +36 -5
  127. package/server/lib/opencode/v1-migration-gate.js +157 -0
  128. package/server/lib/opencode/v1-migration-gate.test.js +111 -0
  129. package/server/lib/opencode/v2-client.js +49 -0
  130. package/server/lib/opencode/watcher.js +2 -1
  131. package/server/lib/opencode/watcher.test.js +1 -1
  132. package/server/lib/scheduled-tasks/DOCUMENTATION.md +71 -39
  133. package/server/lib/scheduled-tasks/managed-tool-route.js +31 -1
  134. package/server/lib/scheduled-tasks/managed-tool-route.test.js +23 -0
  135. package/server/lib/scheduled-tasks/routes.js +27 -0
  136. package/server/lib/scheduled-tasks/routes.test.js +94 -0
  137. package/server/lib/scheduled-tasks/runtime.js +331 -308
  138. package/server/lib/scheduled-tasks/runtime.test.js +530 -318
  139. package/server/lib/session-goal/DOCUMENTATION.md +65 -14
  140. package/server/lib/session-goal/capability.js +57 -0
  141. package/server/lib/session-goal/capability.test.js +33 -0
  142. package/server/lib/session-goal/routes.js +231 -2
  143. package/server/lib/session-goal/routes.test.js +211 -0
  144. package/server/lib/session-goal/runtime.js +46 -4
  145. package/server/lib/session-goal/runtime.test.js +62 -1
  146. package/server/lib/session-index/sync-runtime.js +20 -17
  147. package/server/lib/session-index/sync-runtime.test.js +9 -8
  148. package/server/lib/session-metadata/DOCUMENTATION.md +41 -0
  149. package/server/lib/session-metadata/routes.js +94 -0
  150. package/server/lib/session-metadata/routes.test.js +111 -0
  151. package/server/lib/session-metadata/session-metadata-store.js +235 -0
  152. package/server/lib/session-metadata/session-metadata-store.test.js +184 -0
  153. package/server/lib/session-turn-pages/DOCUMENTATION.md +3 -3
  154. package/server/lib/session-turn-pages/reconcile.service.js +1 -1
  155. package/server/lib/session-turn-pages/reconcile.service.test.js +16 -0
  156. package/server/lib/session-turn-pages/routes.js +204 -71
  157. package/server/lib/session-turn-pages/routes.test.js +285 -0
  158. package/server/lib/session-turn-pages/service.js +6 -2
  159. package/server/lib/session-turn-pages/service.test.js +34 -0
  160. package/server/lib/small-model/DOCUMENTATION.md +5 -4
  161. package/server/lib/small-model/catalog.js +56 -6
  162. package/server/lib/small-model/catalog.test.js +47 -0
  163. package/server/lib/small-model/index.js +3 -3
  164. package/server/lib/small-model/opencode-session.js +50 -206
  165. package/server/lib/small-model/opencode-session.test.js +58 -88
  166. package/server/opencode-proxy.test.js +61 -12
  167. package/dist/assets/AssistantView-BtGYjSW5.js +0 -3
  168. package/dist/assets/MarkdownRenderer-AwDKOCvD.js +0 -2
  169. package/dist/assets/MobileDetailNavigation-B6fGRYsg.js +0 -1
  170. package/dist/assets/MobileSurface-BY7eErHN.js +0 -2
  171. package/dist/assets/SettingsView-lcRi35Y3.js +0 -25
  172. package/dist/assets/es-rqMiRP7-.js +0 -15
  173. package/dist/assets/gitApi-CcIuekQ3.js +0 -7
  174. package/dist/assets/insertionBoundaries-BocNNP9Q.js +0 -1
  175. package/dist/assets/ja-Bgr8-dOg.js +0 -15
  176. package/dist/assets/ko-B_Gxh-0l.js +0 -15
  177. package/dist/assets/main-CFLmpNC5.js +0 -169
  178. package/dist/assets/main-Ckj_IpUy.js +0 -2
  179. package/dist/assets/miniChat-Dq1CTKNs.js +0 -2
  180. package/dist/assets/mobile-DlJ0Yr7h.js +0 -2
  181. package/dist/assets/multirun-BGUKIJys.js +0 -4
  182. package/dist/assets/pl-DW2XJGC4.js +0 -15
  183. package/dist/assets/platform-0w_Q9_ji.js +0 -2
  184. package/dist/assets/projectMeta-Bft2t6GF.js +0 -3
  185. package/dist/assets/pt-BR-CIsanf4T.js +0 -15
  186. package/dist/assets/renderElectronMiniChatApp-CTbXbB0Y.js +0 -2
  187. package/dist/assets/renderMobileApp-DLewUqoE.js +0 -28
  188. package/dist/assets/sessionChangeSummary-DiqwX3vZ.js +0 -2
  189. package/dist/assets/src-rS3zCcsC.css +0 -2
  190. package/dist/assets/textarea-DpK2Z0Gp.js +0 -1
  191. package/dist/assets/uk-BPmPEUYJ.js +0 -15
  192. package/dist/assets/useDesktopWindowControlsLayout-3Bm0rXCQ.js +0 -1
  193. package/dist/assets/useMobileNavigationStore-BIyCYi_z.js +0 -413
  194. package/dist/assets/vendor-opencode-sdk-C07HQypa.js +0 -7
  195. package/dist/assets/zh-CN-R8-mDcIp.js +0 -15
@@ -0,0 +1,139 @@
1
+ // Pinned desktop/runtime opencode2. Upgrade and prepare must not fall back to 1.18.x.
2
+ export const PINNED_OPENCODE2_VERSION = '2.0.12';
3
+
4
+ // Global npm/bun package that installs the opencode2 binary (not 1.x opencode-ai).
5
+ export const OPENCODE2_NPM_PACKAGE = '@opencode/cli';
6
+
7
+ export function isOpenCode1xVersion(value) {
8
+ if (typeof value !== 'string') return false;
9
+ const normalized = value.trim().replace(/^v/i, '');
10
+ if (!normalized) return false;
11
+ return /^1(?:\.|$)/.test(normalized);
12
+ }
13
+
14
+ /**
15
+ * Health admission for managed/external opencode2.
16
+ * healthy alone is not enough: reject 1.x and missing/unknown version strings.
17
+ */
18
+ export function isAcceptableOpenCode2HealthVersion(value) {
19
+ if (typeof value !== 'string') return false;
20
+ const normalized = value.trim().replace(/^v/i, '');
21
+ if (!normalized) return false;
22
+ if (isOpenCode1xVersion(normalized)) return false;
23
+ // Require a version-like token (semver or next pin), not free-form noise.
24
+ return /^\d+(?:\.\d+)*(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/.test(normalized);
25
+ }
26
+
27
+ /**
28
+ * Pure health/info body gate used by lifecycle and VS Code sidecar.
29
+ * Accepts classic `{ healthy: true, version }` and official 2.x `ServerInfo`
30
+ * (`GET /api/info` → `{ version, pid, urls, paths }` without `healthy`).
31
+ * @param {{ healthy?: unknown, version?: unknown } | null | undefined} body
32
+ * @returns {{ ok: boolean, version: string | null, reason?: string }}
33
+ */
34
+ export function evaluateOpenCodeHealthBody(body) {
35
+ if (!body || typeof body !== 'object' || Array.isArray(body)) {
36
+ return { ok: false, version: null, reason: 'invalid-body' };
37
+ }
38
+ // Classic /api/health required healthy:true; /api/info omits healthy entirely.
39
+ if (Object.prototype.hasOwnProperty.call(body, 'healthy') && body.healthy !== true) {
40
+ return { ok: false, version: null, reason: 'unhealthy' };
41
+ }
42
+ const raw = typeof body.version === 'string' ? body.version.trim() : '';
43
+ const version = raw ? raw.replace(/^v/i, '') : null;
44
+ if (!isAcceptableOpenCode2HealthVersion(version)) {
45
+ if (isOpenCode1xVersion(version)) {
46
+ return { ok: false, version, reason: '1x-version' };
47
+ }
48
+ return { ok: false, version, reason: 'unknown-version' };
49
+ }
50
+ return { ok: true, version };
51
+ }
52
+
53
+ export function rejectOpenCode1xUpgradeTarget(target) {
54
+ if (isOpenCode1xVersion(target)) {
55
+ const error = new Error(`OpenCode upgrade refuses 1.x target: ${target}`);
56
+ error.code = 'OPENCODE_UPGRADE_1X_REFUSED';
57
+ throw error;
58
+ }
59
+ return target;
60
+ }
61
+
62
+ export function resolveOpenCode2UpgradeTarget(target) {
63
+ const trimmed = typeof target === 'string' ? target.trim() : '';
64
+ if (trimmed) {
65
+ rejectOpenCode1xUpgradeTarget(trimmed);
66
+ return trimmed;
67
+ }
68
+ return PINNED_OPENCODE2_VERSION;
69
+ }
70
+
71
+ /**
72
+ * Parse `opencode2 --version` / `opencode --version` stdout.
73
+ * v2: `opencode2 v2.0.12` (name then version). 1.x: `1.18.18`.
74
+ */
75
+ export function parseOpenCode2VersionOutput(stdout) {
76
+ const tokens = String(stdout || '').trim().split(/\s+/);
77
+ const versionToken = tokens.find((token) => /^v?\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/.test(token));
78
+ if (!versionToken) return '';
79
+ return versionToken.replace(/^v/, '');
80
+ }
81
+
82
+ export function openCodeBinaryName(platform = process.platform) {
83
+ return platform === 'win32' ? 'opencode.exe' : 'opencode';
84
+ }
85
+
86
+ /** @deprecated Packaging leftover. Runtime install uses `openCodeBinaryName`. */
87
+ export function openCode2BinaryName(platform = process.platform) {
88
+ return platform === 'win32' ? 'opencode2.exe' : 'opencode2';
89
+ }
90
+
91
+ export function resolveOpenCode2NpmArchitecture(arch = process.arch) {
92
+ const normalized = String(arch || '').trim().toLowerCase();
93
+ if (normalized === 'x64' || normalized === 'amd64' || normalized === 'x86_64') return 'x64';
94
+ if (normalized === 'arm64' || normalized === 'aarch64') return 'arm64';
95
+ throw new Error(`Unsupported OpenCode architecture ${JSON.stringify(arch)}`);
96
+ }
97
+
98
+ /**
99
+ * Official 2.x ships as `@opencode/cli-<os>-<arch>[-baseline]`.
100
+ * `targetArchitecture` may be `{ opencode }` (Electron prepare) or an arch string.
101
+ */
102
+ export function npmPackageForOpenCode2(platform = process.platform, targetArchitecture = process.arch) {
103
+ const rawArch = targetArchitecture && typeof targetArchitecture === 'object'
104
+ ? (targetArchitecture.opencode ?? targetArchitecture.node)
105
+ : targetArchitecture;
106
+ const arch = resolveOpenCode2NpmArchitecture(rawArch);
107
+ const os = { darwin: 'darwin', win32: 'windows', linux: 'linux' }[platform];
108
+ if (!os) throw new Error(`No opencode2 npm package mapping for platform ${platform}`);
109
+ const suffix = arch === 'x64' ? '-baseline' : '';
110
+ return `@opencode/cli-${os}-${arch}${suffix}`;
111
+ }
112
+
113
+ function parseVersionForComparison(value) {
114
+ const normalized = String(value || '').replace(/^v/i, '').split('+')[0];
115
+ const prereleaseIndex = normalized.indexOf('-');
116
+ const core = prereleaseIndex >= 0 ? normalized.slice(0, prereleaseIndex) : normalized;
117
+ const parts = core.split('.').map((part) => {
118
+ const parsed = Number.parseInt(part || '0', 10);
119
+ return Number.isFinite(parsed) ? parsed : 0;
120
+ });
121
+ return { parts, prerelease: prereleaseIndex >= 0 };
122
+ }
123
+
124
+ export function compareOpenCode2Versions(left, right) {
125
+ const a = parseVersionForComparison(left);
126
+ const b = parseVersionForComparison(right);
127
+ const length = Math.max(a.parts.length, b.parts.length);
128
+ for (let index = 0; index < length; index += 1) {
129
+ const diff = (a.parts[index] || 0) - (b.parts[index] || 0);
130
+ if (diff !== 0) return diff;
131
+ }
132
+ if (a.prerelease !== b.prerelease) return a.prerelease ? -1 : 1;
133
+ return 0;
134
+ }
135
+
136
+ export function isOpenCode2VersionAtLeast(version, minimum = PINNED_OPENCODE2_VERSION) {
137
+ if (!isAcceptableOpenCode2HealthVersion(version)) return false;
138
+ return compareOpenCode2Versions(version, minimum) >= 0;
139
+ }
@@ -0,0 +1,87 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import {
3
+ OPENCODE2_NPM_PACKAGE,
4
+ PINNED_OPENCODE2_VERSION,
5
+ compareOpenCode2Versions,
6
+ evaluateOpenCodeHealthBody,
7
+ isAcceptableOpenCode2HealthVersion,
8
+ isOpenCode1xVersion,
9
+ isOpenCode2VersionAtLeast,
10
+ npmPackageForOpenCode2,
11
+ parseOpenCode2VersionOutput,
12
+ rejectOpenCode1xUpgradeTarget,
13
+ resolveOpenCode2UpgradeTarget,
14
+ } from './opencode2-pin.js';
15
+
16
+ describe('opencode2 pin (ticket 12)', () => {
17
+ it('rejects 1.x version strings and accepts the pinned v2', () => {
18
+ expect(isOpenCode1xVersion('1.18.4')).toBe(true);
19
+ expect(isOpenCode1xVersion('v1.18.18')).toBe(true);
20
+ expect(isOpenCode1xVersion('1.18.x')).toBe(true);
21
+ expect(isOpenCode1xVersion('1')).toBe(true);
22
+ expect(isOpenCode1xVersion(PINNED_OPENCODE2_VERSION)).toBe(false);
23
+ expect(isOpenCode1xVersion('2.0.12')).toBe(false);
24
+ expect(isOpenCode1xVersion('')).toBe(false);
25
+ expect(isOpenCode1xVersion(null)).toBe(false);
26
+ });
27
+
28
+ it('throws on 1.x upgrade targets and defaults empty to the pin', () => {
29
+ expect(() => rejectOpenCode1xUpgradeTarget('1.18.4')).toThrow(/1\.x/);
30
+ expect(() => rejectOpenCode1xUpgradeTarget('1.18.4')).toThrow(expect.objectContaining({ code: 'OPENCODE_UPGRADE_1X_REFUSED' }));
31
+ expect(resolveOpenCode2UpgradeTarget(undefined)).toBe(PINNED_OPENCODE2_VERSION);
32
+ expect(resolveOpenCode2UpgradeTarget('')).toBe(PINNED_OPENCODE2_VERSION);
33
+ expect(resolveOpenCode2UpgradeTarget(PINNED_OPENCODE2_VERSION)).toBe(PINNED_OPENCODE2_VERSION);
34
+ expect(() => resolveOpenCode2UpgradeTarget('1.18.18')).toThrow(/1\.x/);
35
+ });
36
+
37
+ it('pins the opencode2 npm package name (not 1.x opencode-ai)', () => {
38
+ expect(OPENCODE2_NPM_PACKAGE).toBe('@opencode/cli');
39
+ expect(OPENCODE2_NPM_PACKAGE).not.toBe('opencode-ai');
40
+ expect(OPENCODE2_NPM_PACKAGE).not.toBe('@opencode-ai/cli');
41
+ });
42
+
43
+ it('accepts authoritative v2 health/info versions and rejects 1.x / missing / noise', () => {
44
+ expect(isAcceptableOpenCode2HealthVersion(PINNED_OPENCODE2_VERSION)).toBe(true);
45
+ expect(isAcceptableOpenCode2HealthVersion('v2.0.12')).toBe(true);
46
+ expect(isAcceptableOpenCode2HealthVersion('1.15.0')).toBe(false);
47
+ expect(isAcceptableOpenCode2HealthVersion('v1.18.18')).toBe(false);
48
+ expect(isAcceptableOpenCode2HealthVersion('')).toBe(false);
49
+ expect(isAcceptableOpenCode2HealthVersion(null)).toBe(false);
50
+ expect(isAcceptableOpenCode2HealthVersion('not-a-version')).toBe(false);
51
+
52
+ expect(evaluateOpenCodeHealthBody({ healthy: true, version: PINNED_OPENCODE2_VERSION })).toEqual({
53
+ ok: true,
54
+ version: PINNED_OPENCODE2_VERSION,
55
+ });
56
+ // Official 2.x ServerInfo from GET /api/info has version without healthy.
57
+ expect(evaluateOpenCodeHealthBody({ version: PINNED_OPENCODE2_VERSION, pid: 1 })).toEqual({
58
+ ok: true,
59
+ version: PINNED_OPENCODE2_VERSION,
60
+ });
61
+ expect(evaluateOpenCodeHealthBody({ healthy: true, version: '1.15.0' })).toMatchObject({
62
+ ok: false,
63
+ version: '1.15.0',
64
+ reason: '1x-version',
65
+ });
66
+ expect(evaluateOpenCodeHealthBody({ healthy: true })).toMatchObject({
67
+ ok: false,
68
+ reason: 'unknown-version',
69
+ });
70
+ expect(evaluateOpenCodeHealthBody({ healthy: false, version: PINNED_OPENCODE2_VERSION })).toMatchObject({
71
+ ok: false,
72
+ reason: 'unhealthy',
73
+ });
74
+ expect(evaluateOpenCodeHealthBody(null)).toMatchObject({ ok: false, reason: 'invalid-body' });
75
+ });
76
+
77
+ it('parses --version output and compares against the pin', () => {
78
+ expect(parseOpenCode2VersionOutput('opencode2 v2.0.12\n')).toBe('2.0.12');
79
+ expect(parseOpenCode2VersionOutput('1.18.18\n')).toBe('1.18.18');
80
+ expect(compareOpenCode2Versions('2.0.12', '2.0.1')).toBeGreaterThan(0);
81
+ expect(isOpenCode2VersionAtLeast('2.0.12', '2.0.12')).toBe(true);
82
+ expect(isOpenCode2VersionAtLeast('2.0.1', '2.0.12')).toBe(false);
83
+ expect(isOpenCode2VersionAtLeast('1.18.18', '2.0.12')).toBe(false);
84
+ expect(npmPackageForOpenCode2('darwin', 'arm64')).toBe('@opencode/cli-darwin-arm64');
85
+ expect(npmPackageForOpenCode2('darwin', { opencode: 'x64' })).toBe('@opencode/cli-darwin-x64-baseline');
86
+ });
87
+ });
@@ -0,0 +1,203 @@
1
+ import { afterEach, describe, expect, it } from 'bun:test';
2
+ import net from 'node:net';
3
+ import path from 'node:path';
4
+
5
+ import { createOpenCodeEnvRuntime } from './env-runtime.js';
6
+ import { createOpenCodeLifecycleRuntime } from './lifecycle.js';
7
+ import { createOpenCodeNetworkRuntime } from './network-runtime.js';
8
+ import { fetchV1MigrationGate } from './v1-migration-gate.js';
9
+
10
+ const E2E_PASSWORD = 'openchamber-opencode2-e2e';
11
+ const basicAuth = `Basic ${Buffer.from(`opencode:${E2E_PASSWORD}`, 'utf8').toString('base64')}`;
12
+ const OPENCODE2_HEALTH_VERSION = /^(?:v)?(?:0\.0\.0-(?:next|beta)(?:-\d+)?|2(?:\.\d+)*)/i;
13
+ const OPENCODE_1X_VERSION = /^v?1(?:\.|$)/;
14
+
15
+ const freePort = () => new Promise((resolve, reject) => {
16
+ const server = net.createServer();
17
+ server.once('error', reject);
18
+ server.listen(0, '127.0.0.1', () => {
19
+ const address = server.address();
20
+ server.close((error) => (error ? reject(error) : resolve(address.port)));
21
+ });
22
+ });
23
+
24
+ const jsonGet = async (url, pathname) => {
25
+ const response = await fetch(`${url.replace(/\/+$/, '')}${pathname}`, {
26
+ headers: {
27
+ Accept: 'application/json',
28
+ Authorization: basicAuth,
29
+ },
30
+ });
31
+ const body = await response.json().catch(() => null);
32
+ return { status: response.status, body };
33
+ };
34
+
35
+ const jsonPost = async (url, pathname, payload) => {
36
+ const response = await fetch(`${url.replace(/\/+$/, '')}${pathname}`, {
37
+ method: 'POST',
38
+ headers: {
39
+ Accept: 'application/json',
40
+ Authorization: basicAuth,
41
+ 'content-type': 'application/json',
42
+ },
43
+ body: JSON.stringify(payload ?? {}),
44
+ });
45
+ const body = await response.json().catch(() => null);
46
+ return { status: response.status, body };
47
+ };
48
+
49
+ describe('opencode2 sidecar e2e', () => {
50
+ let server = null;
51
+ const previousBinary = process.env.OPENCODE_BINARY;
52
+ const previousPassword = process.env.OPENCODE_PASSWORD;
53
+
54
+ afterEach(async () => {
55
+ if (server?.close) {
56
+ await server.close().catch(() => {});
57
+ server = null;
58
+ }
59
+ if (typeof previousBinary === 'string') {
60
+ process.env.OPENCODE_BINARY = previousBinary;
61
+ } else {
62
+ delete process.env.OPENCODE_BINARY;
63
+ }
64
+ if (typeof previousPassword === 'string') {
65
+ process.env.OPENCODE_PASSWORD = previousPassword;
66
+ } else {
67
+ delete process.env.OPENCODE_PASSWORD;
68
+ }
69
+ });
70
+
71
+ it('starts managed opencode2 and serves health, migration, session, and message projection', async () => {
72
+ delete process.env.OPENCODE_BINARY;
73
+ delete process.env.OPENCODE_PATH;
74
+ delete process.env.OPENCHAMBER_OPENCODE_PATH;
75
+ delete process.env.OPENCHAMBER_OPENCODE_BIN;
76
+
77
+ const envRuntime = createOpenCodeEnvRuntime({
78
+ state: {
79
+ cachedLoginShellEnvSnapshot: null,
80
+ resolvedOpencodeBinary: null,
81
+ resolvedOpencodeBinarySource: null,
82
+ useWslForOpencode: false,
83
+ resolvedWslBinary: null,
84
+ resolvedWslOpencodePath: null,
85
+ resolvedWslDistro: null,
86
+ resolvedNodeBinary: null,
87
+ resolvedBunBinary: null,
88
+ managedOpenCodeShellEnvSnapshot: null,
89
+ },
90
+ normalizeDirectoryPath: (value) => value,
91
+ readSettingsFromDiskMigrated: async () => ({}),
92
+ });
93
+
94
+ const resolved = envRuntime.resolveOpencodeCliPath();
95
+ expect(resolved).toBeTruthy();
96
+ expect(path.basename(resolved).replace(/\.exe$/i, '')).toBe('opencode2');
97
+
98
+ process.env.OPENCODE_BINARY = resolved;
99
+ process.env.OPENCODE_PASSWORD = E2E_PASSWORD;
100
+
101
+ const port = await freePort();
102
+ const state = {
103
+ openCodeWorkingDirectory: process.cwd(),
104
+ openCodeProcess: null,
105
+ openCodePort: null,
106
+ openCodeBaseUrl: null,
107
+ currentRestartPromise: null,
108
+ isRestartingOpenCode: false,
109
+ openCodeApiPrefix: '',
110
+ openCodeApiPrefixDetected: false,
111
+ openCodeApiDetectionTimer: null,
112
+ lastOpenCodeError: null,
113
+ isOpenCodeReady: false,
114
+ v1Migration: null,
115
+ openCodeNotReadySince: 0,
116
+ isExternalOpenCode: false,
117
+ isShuttingDown: false,
118
+ healthCheckInterval: null,
119
+ expressApp: null,
120
+ useWslForOpencode: false,
121
+ resolvedWslBinary: null,
122
+ resolvedWslOpencodePath: null,
123
+ resolvedWslDistro: null,
124
+ };
125
+
126
+ const getOpenCodeAuthHeaders = () => ({ Authorization: basicAuth });
127
+ const network = createOpenCodeNetworkRuntime({
128
+ state,
129
+ getOpenCodeAuthHeaders,
130
+ configuredOpenCodeHostname: '127.0.0.1',
131
+ });
132
+
133
+ const runtime = createOpenCodeLifecycleRuntime({
134
+ state,
135
+ env: {
136
+ ENV_CONFIGURED_OPENCODE_PORT: port,
137
+ ENV_CONFIGURED_OPENCODE_HOST: null,
138
+ ENV_EFFECTIVE_PORT: port,
139
+ ENV_CONFIGURED_OPENCODE_HOSTNAME: '127.0.0.1',
140
+ ENV_SKIP_OPENCODE_START: false,
141
+ },
142
+ syncToHmrState: () => {},
143
+ syncFromHmrState: () => {},
144
+ getOpenCodeAuthHeaders,
145
+ buildOpenCodeUrl: (route) => `http://127.0.0.1:${state.openCodePort || port}${route}`,
146
+ waitForReady: network.waitForReady,
147
+ normalizeApiPrefix: network.normalizeApiPrefix,
148
+ applyOpencodeBinaryFromSettings: async () => resolved,
149
+ ensureOpencodeCliEnv: () => {},
150
+ ensureLocalOpenCodeServerPassword: async () => E2E_PASSWORD,
151
+ resolveManagedOpenCodeLaunchSpec: envRuntime.resolveManagedOpenCodeLaunchSpec,
152
+ setOpenCodePort: (nextPort) => {
153
+ state.openCodePort = nextPort;
154
+ },
155
+ setDetectedOpenCodeApiPrefix: network.setDetectedOpenCodeApiPrefix,
156
+ setupProxy: () => {},
157
+ ensureOpenCodeApiPrefix: () => {},
158
+ clearResolvedOpenCodeBinary: envRuntime.clearResolvedOpenCodeBinary,
159
+ buildAugmentedPath: () => process.env.PATH,
160
+ buildManagedOpenCodePath: () => process.env.PATH,
161
+ getManagedOpenCodeShellEnvSnapshot: () => ({
162
+ PATH: process.env.PATH,
163
+ OPENCODE_PASSWORD: E2E_PASSWORD,
164
+ OPENCODE_SERVER_PASSWORD: E2E_PASSWORD,
165
+ }),
166
+ });
167
+
168
+ server = await runtime.startOpenCode();
169
+ expect(server.url).toMatch(/^http:\/\/127\.0\.0\.1:\d+$/);
170
+ expect(String(state.lastOpenCodeLaunchDiagnostics?.binary || '')).toContain('opencode2');
171
+
172
+ const health = await jsonGet(server.url, '/api/info');
173
+ expect(health.status).toBe(200);
174
+ expect(health.body).toMatchObject({
175
+ healthy: true,
176
+ version: expect.stringMatching(OPENCODE2_HEALTH_VERSION),
177
+ });
178
+ expect(String(health.body?.version ?? '')).not.toMatch(OPENCODE_1X_VERSION);
179
+
180
+ const unauthenticated = await fetch(`${server.url}/api/info`);
181
+ expect(unauthenticated.status).toBe(401);
182
+
183
+ const gate = await fetchV1MigrationGate({
184
+ url: `${server.url}/api/experimental/migration/v1`,
185
+ headers: getOpenCodeAuthHeaders(),
186
+ });
187
+ expect(gate.admitTranscript).toBe(true);
188
+ expect(['completed', 'absent']).toContain(gate.phase);
189
+
190
+ const created = await jsonPost(server.url, '/api/session', {});
191
+ expect(created.status).toBe(200);
192
+ const sessionID = created.body?.data?.id ?? created.body?.id;
193
+ expect(sessionID).toMatch(/^ses_/);
194
+
195
+ const messages = await jsonGet(
196
+ server.url,
197
+ `/api/session/${encodeURIComponent(sessionID)}/message?limit=20&order=desc`,
198
+ );
199
+ expect(messages.status).toBe(200);
200
+ const page = messages.body?.data ?? messages.body;
201
+ expect(Array.isArray(page)).toBe(true);
202
+ }, 60_000);
203
+ });
@@ -0,0 +1,15 @@
1
+ // V1 plugin API cannot run on opencode2. Settings must mark this explicitly
2
+ // instead of letting load fail silently.
3
+ export const PLUGIN_COMPATIBILITY_V1_INCOMPATIBLE = 'v1-incompatible';
4
+
5
+ export function pluginCompatibilityForV2() {
6
+ return {
7
+ compatibility: PLUGIN_COMPATIBILITY_V1_INCOMPATIBLE,
8
+ compatible: false,
9
+ };
10
+ }
11
+
12
+ export function withV1PluginIncompatibility(record) {
13
+ if (!record || typeof record !== 'object') return record;
14
+ return { ...record, ...pluginCompatibilityForV2() };
15
+ }
@@ -0,0 +1,21 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import {
3
+ PLUGIN_COMPATIBILITY_V1_INCOMPATIBLE,
4
+ pluginCompatibilityForV2,
5
+ withV1PluginIncompatibility,
6
+ } from './plugin-compatibility.js';
7
+
8
+ describe('V1 plugin compatibility (ticket 12)', () => {
9
+ it('marks V1 plugins incompatible instead of a silent load failure', () => {
10
+ expect(pluginCompatibilityForV2()).toEqual({
11
+ compatibility: PLUGIN_COMPATIBILITY_V1_INCOMPATIBLE,
12
+ compatible: false,
13
+ });
14
+ expect(withV1PluginIncompatibility({ id: 'config:user:oh-my-opencode', spec: 'oh-my-opencode' })).toEqual({
15
+ id: 'config:user:oh-my-opencode',
16
+ spec: 'oh-my-opencode',
17
+ compatibility: 'v1-incompatible',
18
+ compatible: false,
19
+ });
20
+ });
21
+ });
@@ -7,6 +7,7 @@ import {
7
7
  writeConfig,
8
8
  } from './shared.js';
9
9
  import { isPathSpec } from './plugin-spec.js';
10
+ import { withV1PluginIncompatibility } from './plugin-compatibility.js';
10
11
 
11
12
  const PLUGIN_FILE_NAME_PATTERN = /^[a-z0-9][a-z0-9-_.]*\.(js|ts|mjs|cjs)$/;
12
13
 
@@ -248,7 +249,7 @@ function listPluginEntries(workingDirectory) {
248
249
  }
249
250
  return source.config.plugin.map((raw) => {
250
251
  const parsed = parsePluginRaw(raw);
251
- return {
252
+ return withV1PluginIncompatibility({
252
253
  id: encodePluginId('config', `${source.scope}:${parsed.spec}`),
253
254
  spec: parsed.spec,
254
255
  ...(parsed.options !== undefined ? { options: parsed.options } : {}),
@@ -256,7 +257,7 @@ function listPluginEntries(workingDirectory) {
256
257
  kind: 'config',
257
258
  parsedKind: parsedKindForSpec(parsed.spec),
258
259
  sourcePath: source.filePath,
259
- };
260
+ });
260
261
  });
261
262
  });
262
263
  }
@@ -333,7 +334,7 @@ function listPluginDirFiles(workingDirectory) {
333
334
  }
334
335
  return fs.readdirSync(dir, { withFileTypes: true })
335
336
  .filter((entry) => entry.isFile() && PLUGIN_FILE_NAME_PATTERN.test(entry.name) && !entry.name.includes('..'))
336
- .map((entry) => ({
337
+ .map((entry) => withV1PluginIncompatibility({
337
338
  id: encodePluginId('file', `${scope}:${entry.name}`),
338
339
  fileName: entry.name,
339
340
  scope,
@@ -138,6 +138,18 @@ describe('opencode plugins data layer', () => {
138
138
  ]);
139
139
  });
140
140
 
141
+ test('lists V1 plugins as incompatible instead of a silent load failure', () => {
142
+ writeJson(userConfigPath, { plugin: ['oh-my-opencode'] });
143
+ const entries = plugins.listPluginEntries(projectDir);
144
+ expect(entries).toEqual([
145
+ expect.objectContaining({
146
+ spec: 'oh-my-opencode',
147
+ compatibility: 'v1-incompatible',
148
+ compatible: false,
149
+ }),
150
+ ]);
151
+ });
152
+
141
153
  test('encodes and decodes ids', () => {
142
154
  const id = plugins.encodePluginId('config', 'user:oh-my-openagent@4.3.0');
143
155
  expect(plugins.decodePluginId(id)).toEqual({ prefix: 'config', value: 'user:oh-my-openagent@4.3.0' });