@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
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
[](https://github.com/yee94/openchamber/releases/latest)
|
|
5
5
|
[](https://discord.gg/ZYRSdnwwKA)
|
|
6
6
|
|
|
7
|
-
Run [OpenCode](https://opencode.ai) in your browser. Install the CLI, open `localhost:3000`, done. Works on desktop browsers, tablets, and phones as a PWA.
|
|
7
|
+
Run [OpenCode](https://opencode.ai) in your browser. Install the `opencode2` CLI, open `localhost:3000`, done. Works on desktop browsers, tablets, and phones as a PWA.
|
|
8
8
|
|
|
9
9
|
Full project overview, screenshots, and all features: [github.com/yee94/openchamber](https://github.com/yee94/openchamber)
|
|
10
10
|
|
|
@@ -16,7 +16,7 @@ curl -fsSL https://raw.githubusercontent.com/yee94/openchamber/main/scripts/inst
|
|
|
16
16
|
|
|
17
17
|
Or install manually: `bun add -g @openchambery/web` (or npm, pnpm, yarn).
|
|
18
18
|
|
|
19
|
-
> **Prerequisites:** [
|
|
19
|
+
> **Prerequisites:** [`opencode2`](https://opencode.ai) CLI installed (PATH or `OPENCODE_BINARY`; `opencode` / `opencode.exe` / `opencode.cmd` are rejected), Node.js 22+.
|
|
20
20
|
|
|
21
21
|
## Usage
|
|
22
22
|
|
|
@@ -33,7 +33,7 @@ openchamber connect-url --port 3000 # Add this server to OpenChamber Desktop
|
|
|
33
33
|
openchamber connect-url --server http://host:3000 --qr
|
|
34
34
|
openchamber connect-url --port 3000 --qr
|
|
35
35
|
openchamber logs # Follow latest instance logs
|
|
36
|
-
OPENCODE_PORT=4096 OPENCODE_SKIP_START=true openchamber # Connect to external
|
|
36
|
+
OPENCODE_PORT=4096 OPENCODE_SKIP_START=true openchamber # Connect to external opencode2 server
|
|
37
37
|
OPENCODE_HOST=https://myhost:4096 OPENCODE_SKIP_START=true openchamber # Connect via custom host/HTTPS
|
|
38
38
|
openchamber stop # Stop server
|
|
39
39
|
openchamber update # Update to latest version
|
|
@@ -109,6 +109,8 @@ Generating a client token does not automatically password-protect the hosted bro
|
|
|
109
109
|
<details>
|
|
110
110
|
<summary>Connect to external OpenCode server</summary>
|
|
111
111
|
|
|
112
|
+
The external host must be **opencode2**. OpenChamber still runs v2 health (`GET /api/health`) and V1 migration (`GET /api/experimental/migration/v1`) checks; a 1.x `opencode` server will not pass. `OPENCODE_BINARY` and `settings.opencodeBinary` must resolve to `opencode2`, not `opencode` / `opencode.exe` / `opencode.cmd`.
|
|
113
|
+
|
|
112
114
|
```bash
|
|
113
115
|
OPENCODE_PORT=4096 OPENCODE_SKIP_START=true openchamber
|
|
114
116
|
OPENCODE_HOST=https://myhost:4096 OPENCODE_SKIP_START=true openchamber
|
|
@@ -116,8 +118,8 @@ OPENCODE_HOST=https://myhost:4096 OPENCODE_SKIP_START=true openchamber
|
|
|
116
118
|
|
|
117
119
|
| Variable | Description |
|
|
118
120
|
|----------|-------------|
|
|
119
|
-
| `OPENCODE_HOST` | Full base URL of external server (overrides `OPENCODE_PORT`) |
|
|
120
|
-
| `OPENCODE_PORT` | Port of external server |
|
|
121
|
+
| `OPENCODE_HOST` | Full base URL of external opencode2 server (overrides `OPENCODE_PORT`) |
|
|
122
|
+
| `OPENCODE_PORT` | Port of external opencode2 server |
|
|
121
123
|
| `OPENCODE_SKIP_START` | Skip starting embedded OpenCode server |
|
|
122
124
|
| `OPENCHAMBER_OPENCODE_HOSTNAME` | Bind hostname for managed OpenCode server (default: `127.0.0.1`, use `0.0.0.0` for LAN/remote access — trusted networks only) |
|
|
123
125
|
| `OPENCHAMBER_HOST` | Bind hostname for the OpenChamber web server (default: `127.0.0.1`; use `0.0.0.0` for LAN/remote access — trusted networks only) |
|
|
@@ -166,16 +168,16 @@ openchamber stop # Stop background server
|
|
|
166
168
|
<details>
|
|
167
169
|
<summary>systemd service (VPN / LAN access)</summary>
|
|
168
170
|
|
|
169
|
-
Use `--foreground` to keep the CLI process alive so systemd (or any other process manager) can track and restart it. Combine with `OPENCODE_HOST` to connect to an
|
|
171
|
+
Use `--foreground` to keep the CLI process alive so systemd (or any other process manager) can track and restart it. Combine with `OPENCODE_HOST` to connect to an opencode2 instance running as a separate service.
|
|
170
172
|
|
|
171
|
-
**`~/.config/systemd/user/
|
|
173
|
+
**`~/.config/systemd/user/opencode2.service`**
|
|
172
174
|
```ini
|
|
173
175
|
[Unit]
|
|
174
|
-
Description=OpenCode Server
|
|
176
|
+
Description=OpenCode 2 Server
|
|
175
177
|
|
|
176
178
|
[Service]
|
|
177
179
|
Type=simple
|
|
178
|
-
ExecStart=
|
|
180
|
+
ExecStart=opencode2 serve --port 4095
|
|
179
181
|
Environment="PATH=/home/linuxbrew/.linuxbrew/bin:/home/linuxbrew/.linuxbrew/sbin:/home/YOU/.local/bin:/home/YOU/.npm-global/bin:/usr/local/bin:/usr/bin:/bin"
|
|
180
182
|
Environment=SSH_AUTH_SOCK=%t/ssh-agent.socket
|
|
181
183
|
Restart=on-failure
|
|
@@ -195,7 +197,7 @@ WantedBy=default.target
|
|
|
195
197
|
```ini
|
|
196
198
|
[Unit]
|
|
197
199
|
Description=OpenChamber Web Server
|
|
198
|
-
After=
|
|
200
|
+
After=opencode2.service
|
|
199
201
|
|
|
200
202
|
[Service]
|
|
201
203
|
Type=simple
|
|
@@ -211,7 +213,7 @@ WantedBy=default.target
|
|
|
211
213
|
|
|
212
214
|
```bash
|
|
213
215
|
systemctl --user daemon-reload
|
|
214
|
-
systemctl --user enable --now
|
|
216
|
+
systemctl --user enable --now opencode2 openchamber
|
|
215
217
|
```
|
|
216
218
|
|
|
217
219
|
`--host 0.0.0.0` is required to listen on all interfaces (the default is `127.0.0.1`). Use `--host <ip>` or `OPENCHAMBER_HOST=<ip>` to bind to a specific interface instead.
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import os from 'os';
|
|
4
|
+
import path from 'path';
|
|
5
|
+
|
|
6
|
+
import { checkOpenCodeCLI } from './cli.js';
|
|
7
|
+
|
|
8
|
+
function createTempDir(prefix) {
|
|
9
|
+
return fs.mkdtempSync(path.join(os.tmpdir(), prefix));
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function writeVersionBinary(filePath, version) {
|
|
13
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
14
|
+
if (process.platform === 'win32') {
|
|
15
|
+
fs.writeFileSync(filePath, `@echo off\r\necho ${version}\r\n`);
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
fs.writeFileSync(filePath, `#!${process.execPath}\nconsole.log(${JSON.stringify(version)});\n`);
|
|
19
|
+
fs.chmodSync(filePath, 0o755);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
async function withIsolatedOpenCodeEnv(fn) {
|
|
23
|
+
const previousBinary = process.env.OPENCODE_BINARY;
|
|
24
|
+
const previousPath = process.env.PATH;
|
|
25
|
+
const previousDataDir = process.env.OPENCHAMBER_DATA_DIR;
|
|
26
|
+
const previousAutoInstall = process.env.OPENCHAMBER_OPENCODE2_AUTO_INSTALL;
|
|
27
|
+
const dataDir = createTempDir('openchamber-cli-opencode-data-');
|
|
28
|
+
process.env.OPENCHAMBER_DATA_DIR = dataDir;
|
|
29
|
+
process.env.OPENCHAMBER_OPENCODE2_AUTO_INSTALL = '0';
|
|
30
|
+
try {
|
|
31
|
+
return await fn({ dataDir });
|
|
32
|
+
} finally {
|
|
33
|
+
if (typeof previousBinary === 'string') {
|
|
34
|
+
process.env.OPENCODE_BINARY = previousBinary;
|
|
35
|
+
} else {
|
|
36
|
+
delete process.env.OPENCODE_BINARY;
|
|
37
|
+
}
|
|
38
|
+
if (typeof previousPath === 'string') {
|
|
39
|
+
process.env.PATH = previousPath;
|
|
40
|
+
} else {
|
|
41
|
+
delete process.env.PATH;
|
|
42
|
+
}
|
|
43
|
+
if (typeof previousDataDir === 'string') {
|
|
44
|
+
process.env.OPENCHAMBER_DATA_DIR = previousDataDir;
|
|
45
|
+
} else {
|
|
46
|
+
delete process.env.OPENCHAMBER_DATA_DIR;
|
|
47
|
+
}
|
|
48
|
+
if (typeof previousAutoInstall === 'string') {
|
|
49
|
+
process.env.OPENCHAMBER_OPENCODE2_AUTO_INSTALL = previousAutoInstall;
|
|
50
|
+
} else {
|
|
51
|
+
delete process.env.OPENCHAMBER_OPENCODE2_AUTO_INSTALL;
|
|
52
|
+
}
|
|
53
|
+
fs.rmSync(dataDir, { recursive: true, force: true });
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
describe('checkOpenCodeCLI v2 gate', () => {
|
|
58
|
+
it('uses the same binary validation for TTY, non-TTY, --quiet, and --json notice styles', async () => {
|
|
59
|
+
await withIsolatedOpenCodeEnv(async () => {
|
|
60
|
+
const dir = createTempDir('openchamber-cli-mode-parity-');
|
|
61
|
+
const binary = path.join(dir, process.platform === 'win32' ? 'opencode.exe' : 'opencode');
|
|
62
|
+
writeVersionBinary(binary, '1.18.4');
|
|
63
|
+
process.env.OPENCODE_BINARY = binary;
|
|
64
|
+
const pathDir = createTempDir('openchamber-cli-mode-path-');
|
|
65
|
+
writeVersionBinary(path.join(pathDir, process.platform === 'win32' ? 'opencode2.cmd' : 'opencode2'), '2.0.12');
|
|
66
|
+
process.env.PATH = pathDir;
|
|
67
|
+
|
|
68
|
+
const notices = [];
|
|
69
|
+
const modes = [
|
|
70
|
+
{ json: true, quiet: false },
|
|
71
|
+
{ json: false, quiet: true },
|
|
72
|
+
{ json: false, quiet: false },
|
|
73
|
+
];
|
|
74
|
+
try {
|
|
75
|
+
for (const mode of modes) {
|
|
76
|
+
await expect(checkOpenCodeCLI((notice) => notices.push({ mode, notice }))).rejects.toMatchObject({
|
|
77
|
+
code: 'OPENCODE_BINARY_INVALID',
|
|
78
|
+
message: expect.stringMatching(/1\.x.*OpenCode v2/s),
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
expect(notices).toEqual([]);
|
|
82
|
+
} finally {
|
|
83
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
84
|
+
fs.rmSync(pathDir, { recursive: true, force: true });
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it('fails closed for opencode / opencode.exe / opencode.cmd and does not fall back to PATH', async () => {
|
|
90
|
+
await withIsolatedOpenCodeEnv(async () => {
|
|
91
|
+
const names = process.platform === 'win32'
|
|
92
|
+
? ['opencode.exe', 'opencode.cmd']
|
|
93
|
+
: ['opencode', 'opencode.exe', 'opencode.cmd'];
|
|
94
|
+
const pathDir = createTempDir('openchamber-cli-legacy-path-');
|
|
95
|
+
const v2 = path.join(pathDir, process.platform === 'win32' ? 'opencode2.cmd' : 'opencode2');
|
|
96
|
+
writeVersionBinary(v2, '2.0.12');
|
|
97
|
+
process.env.PATH = pathDir;
|
|
98
|
+
|
|
99
|
+
try {
|
|
100
|
+
for (const name of names) {
|
|
101
|
+
const dir = createTempDir(`openchamber-cli-legacy-${name}-`);
|
|
102
|
+
const binary = path.join(dir, name);
|
|
103
|
+
writeVersionBinary(binary, '1.18.4');
|
|
104
|
+
process.env.OPENCODE_BINARY = binary;
|
|
105
|
+
try {
|
|
106
|
+
await expect(checkOpenCodeCLI()).rejects.toMatchObject({
|
|
107
|
+
code: 'OPENCODE_BINARY_INVALID',
|
|
108
|
+
message: expect.stringMatching(/1\.x.*OpenCode v2/s),
|
|
109
|
+
});
|
|
110
|
+
expect(process.env.OPENCODE_BINARY).toBe(binary);
|
|
111
|
+
} finally {
|
|
112
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
} finally {
|
|
116
|
+
fs.rmSync(pathDir, { recursive: true, force: true });
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it('discovers opencode2 from PATH and ignores a sibling 1.x opencode', async () => {
|
|
122
|
+
await withIsolatedOpenCodeEnv(async () => {
|
|
123
|
+
const pathDir = createTempDir('openchamber-cli-path-opencode2-');
|
|
124
|
+
const legacy = path.join(pathDir, process.platform === 'win32' ? 'opencode.exe' : 'opencode');
|
|
125
|
+
const binary = path.join(pathDir, process.platform === 'win32' ? 'opencode2.cmd' : 'opencode2');
|
|
126
|
+
writeVersionBinary(legacy, '1.18.4');
|
|
127
|
+
writeVersionBinary(binary, '2.0.12');
|
|
128
|
+
process.env.PATH = pathDir;
|
|
129
|
+
delete process.env.OPENCODE_BINARY;
|
|
130
|
+
|
|
131
|
+
try {
|
|
132
|
+
await expect(checkOpenCodeCLI()).resolves.toBe(binary);
|
|
133
|
+
expect(process.env.OPENCODE_BINARY).toBe(binary);
|
|
134
|
+
} finally {
|
|
135
|
+
fs.rmSync(pathDir, { recursive: true, force: true });
|
|
136
|
+
}
|
|
137
|
+
});
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it('fails closed when settings.opencodeBinary is a 1.x basename', async () => {
|
|
141
|
+
await withIsolatedOpenCodeEnv(async ({ dataDir }) => {
|
|
142
|
+
const dir = createTempDir('openchamber-cli-settings-1x-');
|
|
143
|
+
const binary = path.join(dir, process.platform === 'win32' ? 'opencode.exe' : 'opencode');
|
|
144
|
+
writeVersionBinary(binary, '1.18.4');
|
|
145
|
+
fs.writeFileSync(path.join(dataDir, 'settings.json'), JSON.stringify({ opencodeBinary: binary }));
|
|
146
|
+
delete process.env.OPENCODE_BINARY;
|
|
147
|
+
const pathDir = createTempDir('openchamber-cli-settings-path-');
|
|
148
|
+
writeVersionBinary(path.join(pathDir, process.platform === 'win32' ? 'opencode2.cmd' : 'opencode2'), '2.0.12');
|
|
149
|
+
process.env.PATH = pathDir;
|
|
150
|
+
try {
|
|
151
|
+
await expect(checkOpenCodeCLI()).rejects.toMatchObject({
|
|
152
|
+
code: 'OPENCODE_BINARY_INVALID',
|
|
153
|
+
message: expect.stringMatching(/1\.x.*OpenCode v2/s),
|
|
154
|
+
});
|
|
155
|
+
} finally {
|
|
156
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
157
|
+
fs.rmSync(pathDir, { recursive: true, force: true });
|
|
158
|
+
}
|
|
159
|
+
});
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
it('accepts a configured opencode2 path after the v2 version gate', async () => {
|
|
163
|
+
await withIsolatedOpenCodeEnv(async ({ dataDir }) => {
|
|
164
|
+
const dir = createTempDir('openchamber-cli-settings-v2-');
|
|
165
|
+
const binary = path.join(dir, process.platform === 'win32' ? 'opencode2.cmd' : 'opencode2');
|
|
166
|
+
writeVersionBinary(binary, '2.0.12');
|
|
167
|
+
fs.writeFileSync(path.join(dataDir, 'settings.json'), JSON.stringify({ opencodeBinary: binary }));
|
|
168
|
+
delete process.env.OPENCODE_BINARY;
|
|
169
|
+
process.env.PATH = createTempDir('openchamber-cli-settings-empty-');
|
|
170
|
+
try {
|
|
171
|
+
await expect(checkOpenCodeCLI()).resolves.toBe(binary);
|
|
172
|
+
expect(process.env.OPENCODE_BINARY).toBe(binary);
|
|
173
|
+
} finally {
|
|
174
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
175
|
+
}
|
|
176
|
+
});
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
it('names opencode2 when PATH lookup finds nothing', async () => {
|
|
180
|
+
await withIsolatedOpenCodeEnv(async () => {
|
|
181
|
+
delete process.env.OPENCODE_BINARY;
|
|
182
|
+
process.env.PATH = createTempDir('openchamber-cli-missing-');
|
|
183
|
+
await expect(checkOpenCodeCLI()).rejects.toThrow(/opencode/);
|
|
184
|
+
});
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
it('installs pinned opencode2 when PATH has no usable binary', async () => {
|
|
188
|
+
await withIsolatedOpenCodeEnv(async ({ dataDir }) => {
|
|
189
|
+
delete process.env.OPENCODE_BINARY;
|
|
190
|
+
process.env.PATH = createTempDir('openchamber-cli-install-missing-');
|
|
191
|
+
const installed = path.join(dataDir, 'opencode-cli', '2.0.12', process.platform === 'win32' ? 'opencode2.exe' : 'opencode2');
|
|
192
|
+
writeVersionBinary(installed, '2.0.12');
|
|
193
|
+
const notices = [];
|
|
194
|
+
await expect(checkOpenCodeCLI((notice) => notices.push(notice), {
|
|
195
|
+
ensurePinnedOpenCode2Cli: async () => ({
|
|
196
|
+
path: installed,
|
|
197
|
+
version: '2.0.12',
|
|
198
|
+
source: 'installed',
|
|
199
|
+
installed: true,
|
|
200
|
+
}),
|
|
201
|
+
})).resolves.toBe(installed);
|
|
202
|
+
expect(process.env.OPENCODE_BINARY).toBe(installed);
|
|
203
|
+
expect(notices).toEqual([
|
|
204
|
+
expect.objectContaining({ code: 'OPENCODE_CLI_INSTALLED' }),
|
|
205
|
+
]);
|
|
206
|
+
});
|
|
207
|
+
});
|
|
208
|
+
});
|
package/bin/cli.js
CHANGED
|
@@ -18,7 +18,12 @@ import {
|
|
|
18
18
|
findClosestMatch,
|
|
19
19
|
} from './lib/cli-args.js';
|
|
20
20
|
import { readDesktopLocalPortFromSettings } from './lib/cli-paths.js';
|
|
21
|
-
import {
|
|
21
|
+
import { searchPathFor } from './lib/cli-executables.js';
|
|
22
|
+
import {
|
|
23
|
+
assertOpenCode2Binary,
|
|
24
|
+
readConfiguredOpenCodeBinary,
|
|
25
|
+
} from './lib/cli-startup.js';
|
|
26
|
+
import { ensurePinnedOpenCode2Cli } from '../server/lib/opencode/ensure-cli.js';
|
|
22
27
|
import { startupCommand } from './lib/commands-startup.js';
|
|
23
28
|
import { logsCommand } from './lib/commands-logs.js';
|
|
24
29
|
import { statusCommand } from './lib/commands-status.js';
|
|
@@ -75,14 +80,16 @@ function importFromFilePath(filePath) {
|
|
|
75
80
|
return import(pathToFileURL(filePath).href);
|
|
76
81
|
}
|
|
77
82
|
|
|
78
|
-
|
|
83
|
+
// Binary validation is policy, not presentation: TTY, non-TTY, --quiet, and
|
|
84
|
+
// --json all run this same check before serve starts.
|
|
85
|
+
async function checkOpenCodeCLI(onNotice, options = {}) {
|
|
79
86
|
if (process.env.OPENCODE_BINARY) {
|
|
80
|
-
const override =
|
|
87
|
+
const override = assertOpenCode2Binary(process.env.OPENCODE_BINARY);
|
|
81
88
|
if (override) {
|
|
82
89
|
process.env.OPENCODE_BINARY = override;
|
|
83
90
|
return override;
|
|
84
91
|
}
|
|
85
|
-
const message = `OPENCODE_BINARY="${process.env.OPENCODE_BINARY}" is not
|
|
92
|
+
const message = `OPENCODE_BINARY="${process.env.OPENCODE_BINARY}" is not a usable OpenCode v2 CLI. Falling back to PATH lookup.`;
|
|
86
93
|
if (typeof onNotice === 'function') {
|
|
87
94
|
onNotice({ level: 'warning', code: 'OPENCODE_BINARY_INVALID', message });
|
|
88
95
|
} else {
|
|
@@ -90,15 +97,60 @@ async function checkOpenCodeCLI(onNotice) {
|
|
|
90
97
|
}
|
|
91
98
|
}
|
|
92
99
|
|
|
93
|
-
const
|
|
94
|
-
if (
|
|
95
|
-
|
|
96
|
-
|
|
100
|
+
const configured = readConfiguredOpenCodeBinary();
|
|
101
|
+
if (configured) {
|
|
102
|
+
const fromConfig = assertOpenCode2Binary(configured);
|
|
103
|
+
if (fromConfig) {
|
|
104
|
+
process.env.OPENCODE_BINARY = fromConfig;
|
|
105
|
+
return fromConfig;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
let discovered = '';
|
|
110
|
+
for (const name of ['opencode', 'opencode2']) {
|
|
111
|
+
const resolvedFromPath = searchPathFor(name);
|
|
112
|
+
if (!resolvedFromPath) continue;
|
|
113
|
+
try {
|
|
114
|
+
const verified = assertOpenCode2Binary(resolvedFromPath);
|
|
115
|
+
if (verified) {
|
|
116
|
+
discovered = verified;
|
|
117
|
+
break;
|
|
118
|
+
}
|
|
119
|
+
} catch (error) {
|
|
120
|
+
if (error?.code === 'OPENCODE_BINARY_INVALID') continue;
|
|
121
|
+
throw error;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
const ensureCli = typeof options.ensurePinnedOpenCode2Cli === 'function'
|
|
125
|
+
? options.ensurePinnedOpenCode2Cli
|
|
126
|
+
: ensurePinnedOpenCode2Cli;
|
|
127
|
+
|
|
128
|
+
try {
|
|
129
|
+
const ensured = await ensureCli({ discoveredPath: discovered });
|
|
130
|
+
if (ensured?.path) {
|
|
131
|
+
if (typeof onNotice === 'function' && ensured.installed) {
|
|
132
|
+
onNotice({
|
|
133
|
+
level: 'info',
|
|
134
|
+
code: 'OPENCODE_CLI_INSTALLED',
|
|
135
|
+
message: `Installed opencode ${ensured.version} to ${ensured.path}`,
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
process.env.OPENCODE_BINARY = ensured.path;
|
|
139
|
+
return ensured.path;
|
|
140
|
+
}
|
|
141
|
+
} catch (error) {
|
|
142
|
+
if (error?.code === 'OPENCODE_CLI_MISSING') {
|
|
143
|
+
throw new Error(
|
|
144
|
+
`Unable to locate the opencode CLI on PATH (${process.env.PATH || '<empty>'}). ` +
|
|
145
|
+
'Ensure OpenCode v2 is installed and reachable, or set OPENCODE_BINARY to its full path.'
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
throw error;
|
|
97
149
|
}
|
|
98
150
|
|
|
99
151
|
throw new Error(
|
|
100
152
|
`Unable to locate the opencode CLI on PATH (${process.env.PATH || '<empty>'}). ` +
|
|
101
|
-
'Ensure
|
|
153
|
+
'Ensure OpenCode v2 is installed and reachable, or set OPENCODE_BINARY to its full path.'
|
|
102
154
|
);
|
|
103
155
|
}
|
|
104
156
|
|
|
@@ -304,4 +356,5 @@ export {
|
|
|
304
356
|
findClosestMatch,
|
|
305
357
|
TunnelCliError,
|
|
306
358
|
EXIT_CODE,
|
|
359
|
+
checkOpenCodeCLI,
|
|
307
360
|
};
|
package/bin/lib/cli-startup.js
CHANGED
|
@@ -6,10 +6,107 @@ import { DEFAULT_PORT } from './cli-args.js';
|
|
|
6
6
|
import { EXIT_CODE, TunnelCliError } from './cli-errors.js';
|
|
7
7
|
import { getDataDir } from './cli-paths.js';
|
|
8
8
|
import { hasUiPasswordConfigured } from './cli-network.js';
|
|
9
|
-
import { searchPathFor } from './cli-executables.js';
|
|
9
|
+
import { resolveExplicitBinary, searchPathFor } from './cli-executables.js';
|
|
10
|
+
import { isAcceptableOpenCode2HealthVersion, isOpenCode1xVersion } from '../../server/lib/opencode/opencode2-pin.js';
|
|
10
11
|
|
|
11
12
|
const STARTUP_SERVICE_ID = 'dev.openchamber.web';
|
|
12
13
|
|
|
14
|
+
function createOpenCode1xVersionError(candidate, version) {
|
|
15
|
+
const error = new Error(
|
|
16
|
+
`OpenCode 1.x is not supported (${candidate} reports ${version}). Install OpenCode v2 (opencode).`
|
|
17
|
+
);
|
|
18
|
+
error.code = 'OPENCODE_BINARY_INVALID';
|
|
19
|
+
return error;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function extractOpenCodeVersion(output) {
|
|
23
|
+
if (typeof output !== 'string' || output.trim().length === 0) {
|
|
24
|
+
return '';
|
|
25
|
+
}
|
|
26
|
+
const match = output.match(/v?\d+\.\d+[\w.+-]*/i);
|
|
27
|
+
return match ? match[0] : '';
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function readOpenCodeBinaryVersion(binaryPath) {
|
|
31
|
+
const result = spawnSync(binaryPath, ['--version'], {
|
|
32
|
+
encoding: 'utf8',
|
|
33
|
+
timeout: 8000,
|
|
34
|
+
windowsHide: true,
|
|
35
|
+
});
|
|
36
|
+
return `${result.stdout || ''}\n${result.stderr || ''}`.trim();
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// OPENCODE_BINARY and settings.opencodeBinary share the v2 version gate.
|
|
40
|
+
function assertOpenCode2Binary(candidate) {
|
|
41
|
+
const trimmed = typeof candidate === 'string' ? candidate.trim() : '';
|
|
42
|
+
if (!trimmed) {
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
const resolved = resolveExplicitBinary(trimmed) || trimmed;
|
|
46
|
+
const version = extractOpenCodeVersion(readOpenCodeBinaryVersion(resolved));
|
|
47
|
+
if (isOpenCode1xVersion(version)) {
|
|
48
|
+
throw createOpenCode1xVersionError(resolved, version);
|
|
49
|
+
}
|
|
50
|
+
if (!isAcceptableOpenCode2HealthVersion(version)) {
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
return resolveExplicitBinary(trimmed);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function readConfiguredOpenCodeBinary() {
|
|
57
|
+
try {
|
|
58
|
+
const raw = fs.readFileSync(path.join(getDataDir(), 'settings.json'), 'utf8');
|
|
59
|
+
const parsed = JSON.parse(raw);
|
|
60
|
+
if (typeof parsed?.opencodeBinary !== 'string') {
|
|
61
|
+
return '';
|
|
62
|
+
}
|
|
63
|
+
const trimmed = parsed.opencodeBinary.trim();
|
|
64
|
+
if (!trimmed) {
|
|
65
|
+
return '';
|
|
66
|
+
}
|
|
67
|
+
try {
|
|
68
|
+
if (fs.statSync(trimmed).isDirectory()) {
|
|
69
|
+
return path.join(trimmed, process.platform === 'win32' ? 'opencode.exe' : 'opencode');
|
|
70
|
+
}
|
|
71
|
+
} catch {
|
|
72
|
+
}
|
|
73
|
+
return trimmed;
|
|
74
|
+
} catch {
|
|
75
|
+
return '';
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function resolveOpenCode2BinaryForStartup() {
|
|
80
|
+
const explicit = typeof process.env.OPENCODE_BINARY === 'string' ? process.env.OPENCODE_BINARY.trim() : '';
|
|
81
|
+
if (explicit) {
|
|
82
|
+
const resolved = assertOpenCode2Binary(explicit);
|
|
83
|
+
if (resolved) {
|
|
84
|
+
return resolved;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const configured = readConfiguredOpenCodeBinary();
|
|
89
|
+
if (configured) {
|
|
90
|
+
const resolved = assertOpenCode2Binary(configured);
|
|
91
|
+
if (resolved) {
|
|
92
|
+
return resolved;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
for (const name of ['opencode', 'opencode2']) {
|
|
97
|
+
const resolvedFromPath = searchPathFor(name);
|
|
98
|
+
if (!resolvedFromPath) continue;
|
|
99
|
+
try {
|
|
100
|
+
const resolved = assertOpenCode2Binary(resolvedFromPath);
|
|
101
|
+
if (resolved) return resolved;
|
|
102
|
+
} catch (error) {
|
|
103
|
+
if (error?.code === 'OPENCODE_BINARY_INVALID') continue;
|
|
104
|
+
throw error;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
109
|
+
|
|
13
110
|
function getStartupServicePaths() {
|
|
14
111
|
if (process.platform === 'darwin') {
|
|
15
112
|
return {
|
|
@@ -82,9 +179,11 @@ function collectStartupEnv(options = {}) {
|
|
|
82
179
|
);
|
|
83
180
|
|
|
84
181
|
if (options.envSnapshot !== false) {
|
|
85
|
-
const opencodeBinary =
|
|
182
|
+
const opencodeBinary = resolveOpenCode2BinaryForStartup();
|
|
86
183
|
if (typeof opencodeBinary === 'string' && opencodeBinary.trim().length > 0) {
|
|
87
184
|
env.OPENCODE_BINARY = opencodeBinary.trim();
|
|
185
|
+
} else {
|
|
186
|
+
delete env.OPENCODE_BINARY;
|
|
88
187
|
}
|
|
89
188
|
}
|
|
90
189
|
const uiPassword = hasUiPasswordConfigured(options.uiPassword) ? options.uiPassword : undefined;
|
|
@@ -367,4 +466,8 @@ export {
|
|
|
367
466
|
getStartupStatus,
|
|
368
467
|
enableStartupService,
|
|
369
468
|
disableStartupService,
|
|
469
|
+
collectStartupEnv,
|
|
470
|
+
assertOpenCode2Binary,
|
|
471
|
+
readConfiguredOpenCodeBinary,
|
|
472
|
+
resolveOpenCode2BinaryForStartup,
|
|
370
473
|
};
|