@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.
- package/README.md +13 -11
- package/bin/cli-opencode.test.js +208 -0
- package/bin/cli.js +62 -9
- package/bin/lib/cli-startup.js +105 -2
- package/bin/lib/cli-startup.test.js +165 -0
- package/dist/assets/{AgentAvatar-NT957MuI.js → AgentAvatar-CLWWNu_Q.js} +1 -1
- package/dist/assets/AssistantView-BTCkm8yk.js +3 -0
- package/dist/assets/{DiagramView-Bh06xinl.js → DiagramView-xuSJTnv7.js} +1 -1
- package/dist/assets/{FadeInOnReveal-CilRl4gc.js → FadeInOnReveal-Ov0D96d0.js} +1 -1
- package/dist/assets/{Icon-i5GiQRLq.js → Icon-DD-BuMfZ.js} +1 -1
- package/dist/assets/MarkdownRenderer-bc_p1H7l.js +2 -0
- package/dist/assets/{MarkdownRendererImpl-f2agCc4q.js → MarkdownRendererImpl-SKi4oG0c.js} +1 -1
- package/dist/assets/{MarkstreamRendererImpl-ChD4WD2v.js → MarkstreamRendererImpl-CnkodGJW.js} +1 -1
- package/dist/assets/MobileDetailNavigation-Bnx59U_J.js +1 -0
- package/dist/assets/{MobileShareBridge-D7SG0ueZ.js → MobileShareBridge-BQlP4J-F.js} +1 -1
- package/dist/assets/MobileSurface-BXLeiZ4d.js +2 -0
- package/dist/assets/{MultiRunWindow-CjCw1X4n.js → MultiRunWindow-ChZGob0v.js} +1 -1
- package/dist/assets/{OnboardingScreen-BGRLk4Wi.js → OnboardingScreen-DtQJxZG-.js} +1 -1
- package/dist/assets/{OverlayScrollbar-NuQI58XD.js → OverlayScrollbar-pMwxaCca.js} +1 -1
- package/dist/assets/SettingsView-B5I0KDTZ.js +25 -0
- package/dist/assets/{SettingsWindow-C2DgbXJ3.js → SettingsWindow-DfnsFa8y.js} +1 -1
- package/dist/assets/{TerminalView-lubDPKMs.js → TerminalView-Dh9MWCpS.js} +1 -1
- package/dist/assets/{ToolOutputDialog-CzgW3-Cb.js → ToolOutputDialog-BejmjrRk.js} +1 -1
- package/dist/assets/{appThemeRegistry-BKc-qXbQ.js → appThemeRegistry-COGfxTFV.js} +1 -1
- package/dist/assets/{button-DtzFDvij.js → button-D-rATN7w.js} +1 -1
- package/dist/assets/{collapsible-D-nzhcyF.js → collapsible-BXRd5_Hr.js} +1 -1
- package/dist/assets/{context-menu-BzVSzHLG.js → context-menu-jGtH6fR9.js} +1 -1
- package/dist/assets/{diagram-2YWalSb5.js → diagram-DQf7Y_uR.js} +1 -1
- package/dist/assets/{dialog-DGV7DsZC.js → dialog-bfbvR4VH.js} +1 -1
- package/dist/assets/es-c52LTHHe.js +15 -0
- package/dist/assets/{fileMentionTouchSelection-BTkP7aYI.js → fileMentionTouchSelection-BvrNCroG.js} +1 -1
- package/dist/assets/{fr-DsBZeN8Y.js → fr-PisFbUE8.js} +3 -3
- package/dist/assets/gitApi-ButwzQ0N.js +9 -0
- package/dist/assets/{input-CbB2HdLg.js → input-8r6F_Fza.js} +1 -1
- package/dist/assets/insertionBoundaries-DGs7S5zv.js +1 -0
- package/dist/assets/ja-C8Ikun3Z.js +15 -0
- package/dist/assets/ko-ClFys5o2.js +15 -0
- package/dist/assets/main-CCE_4y5F.js +2 -0
- package/dist/assets/main-Dn9rpsgl.js +169 -0
- package/dist/assets/{markdownHeightCache-C9ym9Tsb.js → markdownHeightCache-1Qo6y6IJ.js} +1 -1
- package/dist/assets/miniChat-BpjxVqgg.js +2 -0
- package/dist/assets/mobile-nMJCaNq1.js +2 -0
- package/dist/assets/multirun-BvSG94mW.js +4 -0
- package/dist/assets/pl-CnaKWN5F.js +15 -0
- package/dist/assets/platform-CWTIp02q.js +4 -0
- package/dist/assets/projectMeta-DOe49dM_.js +3 -0
- package/dist/assets/pt-BR-s8OpfRI6.js +15 -0
- package/dist/assets/renderElectronMiniChatApp-R6dKh79f.js +2 -0
- package/dist/assets/renderMobileApp-y-S8y8v_.js +28 -0
- package/dist/assets/{runtimeConfig-PpnZiwcW.js → runtimeConfig-BMvqa5we.js} +1 -1
- package/dist/assets/{runtimeEndpointReset-BGjzNuQl.js → runtimeEndpointReset-B6BxhvR0.js} +6 -6
- package/dist/assets/sessionChangeSummary-VVBlEXuh.js +2 -0
- package/dist/assets/{sessionLookup-CbbzalcH.js → sessionLookup-gh-hVRIc.js} +1 -1
- package/dist/assets/{sortable-tabs-strip-CfgEJfB0.js → sortable-tabs-strip-CudoSgV4.js} +1 -1
- package/dist/assets/src-BzX5UI-c.css +2 -0
- package/dist/assets/{store-B6dImdlY.js → store-DJczmczv.js} +4 -4
- package/dist/assets/{terminalApi-xjqssKke.js → terminalApi-DiF6JmBP.js} +1 -1
- package/dist/assets/textarea-3XR8sUfy.js +1 -0
- package/dist/assets/{tooltip-Djn7MSOX.js → tooltip-CQvpK-UV.js} +1 -1
- package/dist/assets/uk-CaX0iqkB.js +15 -0
- package/dist/assets/{useAppFontEffects-BJBcy-84.js → useAppFontEffects-Cpmpo7Wb.js} +45 -45
- package/dist/assets/useDesktopWindowControlsLayout-B1GX59-L.js +1 -0
- package/dist/assets/{useEffectiveDirectory-CzHynoi_.js → useEffectiveDirectory-CN6JoCFw.js} +1 -1
- package/dist/assets/{useFontPreferences-B1JYyzpK.js → useFontPreferences-DcbXQOkc.js} +1 -1
- package/dist/assets/useMobileNavigationStore-BVjFtXSy.js +414 -0
- package/dist/assets/{useSessionAutoCleanup-DzV9N6G-.js → useSessionAutoCleanup-BLjymaFt.js} +1 -1
- package/dist/assets/{useWorkerHighlightedLines-Bi9ASKr_.js → useWorkerHighlightedLines-Bqn4AiBL.js} +1 -1
- package/dist/assets/vendor-opencode-client-BOe2qpQb.js +12 -0
- package/dist/assets/{vendor-yaml-DIcAK3EC.js → vendor-yaml-C9KlJAlj.js} +37 -37
- package/dist/assets/zh-CN-20MYEjha.js +15 -0
- package/dist/assets/{zh-TW-BsOQbrwT.js → zh-TW-DiN04qTw.js} +3 -3
- package/dist/index.html +6 -6
- package/dist/mini-chat.html +6 -6
- package/dist/mobile.html +6 -6
- package/package.json +2 -2
- package/server/index.js +107 -0
- package/server/lib/assistants/DOCUMENTATION.md +18 -18
- package/server/lib/assistants/assign.js +2 -1
- package/server/lib/assistants/contact-store.js +37 -19
- package/server/lib/assistants/projection-history.test.js +207 -0
- package/server/lib/assistants/routes.js +5 -5
- package/server/lib/assistants/service.js +493 -123
- package/server/lib/assistants/service.test.js +203 -133
- package/server/lib/conversations/DOCUMENTATION.md +4 -4
- package/server/lib/conversations/service.js +86 -57
- package/server/lib/conversations/service.test.js +116 -100
- package/server/lib/event-stream/global-hub.js +2 -1
- package/server/lib/llm/DOCUMENTATION.md +71 -84
- package/server/lib/llm/catalog.js +111 -45
- package/server/lib/llm/catalog.test.js +121 -59
- package/server/lib/llm/completions.js +37 -12
- package/server/lib/llm/completions.test.js +45 -4
- package/server/lib/llm/generate.js +421 -450
- package/server/lib/llm/generate.test.js +553 -487
- package/server/lib/llm/routes.js +6 -4
- package/server/lib/message-queue/DOCUMENTATION.md +5 -5
- package/server/lib/message-queue/opencode-adapter.js +191 -104
- package/server/lib/message-queue/opencode-adapter.test.js +328 -58
- package/server/lib/message-queue/runtime.test.js +6 -6
- package/server/lib/message-queue/worker.js +7 -1
- package/server/lib/opencode/DOCUMENTATION.md +28 -5
- package/server/lib/opencode/config-entity-routes.js +84 -13
- package/server/lib/opencode/config-entity-routes.test.js +41 -10
- package/server/lib/opencode/ensure-cli.js +239 -0
- package/server/lib/opencode/ensure-cli.test.js +119 -0
- package/server/lib/opencode/env-runtime.js +193 -110
- package/server/lib/opencode/env-runtime.test.js +221 -17
- package/server/lib/opencode/feature-routes-runtime.js +37 -1
- package/server/lib/opencode/lifecycle.js +204 -70
- package/server/lib/opencode/lifecycle.test.js +376 -6
- package/server/lib/opencode/network-runtime.js +28 -16
- package/server/lib/opencode/network-runtime.test.js +68 -4
- package/server/lib/opencode/opencode2-pin.js +139 -0
- package/server/lib/opencode/opencode2-pin.test.js +87 -0
- package/server/lib/opencode/opencode2-sidecar.e2e.test.js +203 -0
- package/server/lib/opencode/plugin-compatibility.js +15 -0
- package/server/lib/opencode/plugin-compatibility.test.js +21 -0
- package/server/lib/opencode/plugins.js +4 -3
- package/server/lib/opencode/plugins.test.js +12 -0
- package/server/lib/opencode/proxy.js +153 -16
- package/server/lib/opencode/proxy.test.js +120 -0
- package/server/lib/opencode/routes.js +25 -32
- package/server/lib/opencode/routes.test.js +51 -52
- package/server/lib/opencode/server-utils-runtime.js +2 -0
- package/server/lib/opencode/skill-routes.js +6 -7
- package/server/lib/opencode/skill-routes.test.js +36 -5
- package/server/lib/opencode/v1-migration-gate.js +157 -0
- package/server/lib/opencode/v1-migration-gate.test.js +111 -0
- package/server/lib/opencode/v2-client.js +49 -0
- package/server/lib/opencode/watcher.js +2 -1
- package/server/lib/opencode/watcher.test.js +1 -1
- package/server/lib/scheduled-tasks/DOCUMENTATION.md +71 -39
- package/server/lib/scheduled-tasks/managed-tool-route.js +31 -1
- package/server/lib/scheduled-tasks/managed-tool-route.test.js +23 -0
- package/server/lib/scheduled-tasks/routes.js +27 -0
- package/server/lib/scheduled-tasks/routes.test.js +94 -0
- package/server/lib/scheduled-tasks/runtime.js +331 -308
- package/server/lib/scheduled-tasks/runtime.test.js +530 -318
- package/server/lib/session-goal/DOCUMENTATION.md +65 -14
- package/server/lib/session-goal/capability.js +57 -0
- package/server/lib/session-goal/capability.test.js +33 -0
- package/server/lib/session-goal/routes.js +231 -2
- package/server/lib/session-goal/routes.test.js +211 -0
- package/server/lib/session-goal/runtime.js +46 -4
- package/server/lib/session-goal/runtime.test.js +62 -1
- package/server/lib/session-index/sync-runtime.js +20 -17
- package/server/lib/session-index/sync-runtime.test.js +9 -8
- package/server/lib/session-metadata/DOCUMENTATION.md +41 -0
- package/server/lib/session-metadata/routes.js +94 -0
- package/server/lib/session-metadata/routes.test.js +111 -0
- package/server/lib/session-metadata/session-metadata-store.js +235 -0
- package/server/lib/session-metadata/session-metadata-store.test.js +184 -0
- package/server/lib/session-turn-pages/DOCUMENTATION.md +3 -3
- package/server/lib/session-turn-pages/reconcile.service.js +1 -1
- package/server/lib/session-turn-pages/reconcile.service.test.js +16 -0
- package/server/lib/session-turn-pages/routes.js +204 -71
- package/server/lib/session-turn-pages/routes.test.js +285 -0
- package/server/lib/session-turn-pages/service.js +6 -2
- package/server/lib/session-turn-pages/service.test.js +34 -0
- package/server/lib/small-model/DOCUMENTATION.md +5 -4
- package/server/lib/small-model/catalog.js +56 -6
- package/server/lib/small-model/catalog.test.js +47 -0
- package/server/lib/small-model/index.js +3 -3
- package/server/lib/small-model/opencode-session.js +50 -206
- package/server/lib/small-model/opencode-session.test.js +58 -88
- package/server/opencode-proxy.test.js +61 -12
- package/dist/assets/AssistantView-BtGYjSW5.js +0 -3
- package/dist/assets/MarkdownRenderer-AwDKOCvD.js +0 -2
- package/dist/assets/MobileDetailNavigation-B6fGRYsg.js +0 -1
- package/dist/assets/MobileSurface-BY7eErHN.js +0 -2
- package/dist/assets/SettingsView-lcRi35Y3.js +0 -25
- package/dist/assets/es-rqMiRP7-.js +0 -15
- package/dist/assets/gitApi-CcIuekQ3.js +0 -7
- package/dist/assets/insertionBoundaries-BocNNP9Q.js +0 -1
- package/dist/assets/ja-Bgr8-dOg.js +0 -15
- package/dist/assets/ko-B_Gxh-0l.js +0 -15
- package/dist/assets/main-CFLmpNC5.js +0 -169
- package/dist/assets/main-Ckj_IpUy.js +0 -2
- package/dist/assets/miniChat-Dq1CTKNs.js +0 -2
- package/dist/assets/mobile-DlJ0Yr7h.js +0 -2
- package/dist/assets/multirun-BGUKIJys.js +0 -4
- package/dist/assets/pl-DW2XJGC4.js +0 -15
- package/dist/assets/platform-0w_Q9_ji.js +0 -2
- package/dist/assets/projectMeta-Bft2t6GF.js +0 -3
- package/dist/assets/pt-BR-CIsanf4T.js +0 -15
- package/dist/assets/renderElectronMiniChatApp-CTbXbB0Y.js +0 -2
- package/dist/assets/renderMobileApp-DLewUqoE.js +0 -28
- package/dist/assets/sessionChangeSummary-DiqwX3vZ.js +0 -2
- package/dist/assets/src-rS3zCcsC.css +0 -2
- package/dist/assets/textarea-DpK2Z0Gp.js +0 -1
- package/dist/assets/uk-BPmPEUYJ.js +0 -15
- package/dist/assets/useDesktopWindowControlsLayout-3Bm0rXCQ.js +0 -1
- package/dist/assets/useMobileNavigationStore-BIyCYi_z.js +0 -413
- package/dist/assets/vendor-opencode-sdk-C07HQypa.js +0 -7
- 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' });
|