@kinski/opencode-web-gateway 0.0.0-stage → 1.0.0
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/LICENSE +21 -0
- package/README.md +167 -2
- package/bin/opencode-web-gateway.js +235 -0
- package/dist/index.js +1245 -0
- package/examples/opencode.v1.json +14 -0
- package/examples/opencode.v2.jsonc +14 -0
- package/examples/web-gateway.config.json +7 -0
- package/package.json +37 -4
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 opencode-web-gateway contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,168 @@
|
|
|
1
|
-
#
|
|
1
|
+
# opencode-web-gateway
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
浏览器出网桥:让 opencode 的原生 **DeepSeek provider** 只访问本机,真正请求 DeepSeek 的 `fetch` 由网页完成。
|
|
4
|
+
|
|
5
|
+
插件在本机 `127.0.0.1:8787` 启动端点;页面主动通过 WebSocket 连接插件,转发原始请求体和响应字节(包括 SSE),不解析、不改写。
|
|
6
|
+
|
|
7
|
+
- **不捆绑 opencode**:使用你已安装的 opencode;本包包含全局 CLI、插件和网页
|
|
8
|
+
- **只影响 DeepSeek 通路**:保留 opencode 原生 `deepseek` provider、原生模型目录和 `/connect`,只覆盖它的 `baseURL`
|
|
9
|
+
- **参数透传**:`temperature`、`top_p`、`tools`、reasoning 等由 opencode 原生 provider 构造,插件按字节转发
|
|
10
|
+
- **Key 原样透传**:只使用 `/connect` 或 `DEEPSEEK_API_KEY` 的凭据;插件不保存、不注入、不覆盖
|
|
11
|
+
- 插件内嵌完整网页,不需要额外前端资源
|
|
12
|
+
- 同一份插件支持 opencode v1(`server()`)和 v2(`setup()`)
|
|
13
|
+
- 默认只监听回环地址;跨源页面需要显式加入 `allowedOrigins`
|
|
14
|
+
|
|
15
|
+
## 安装(推荐:全局 CLI)
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
npm install -g @kinski/opencode-web-gateway
|
|
19
|
+
opencode-web-gateway
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
`npm install -g` 会把 `opencode-web-gateway` 写到 PATH。之后在任何目录运行:
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
opencode-web-gateway
|
|
26
|
+
# 不自动打开浏览器
|
|
27
|
+
opencode-web-gateway --no-open
|
|
28
|
+
|
|
29
|
+
# 直接跑一次
|
|
30
|
+
opencode-web-gateway run -m deepseek/deepseek-flash "hello"
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
它做三件事:
|
|
34
|
+
|
|
35
|
+
1. 调用你系统已安装的 `opencode`,不捆绑 opencode;
|
|
36
|
+
2. 通过额外的 opencode config source 加载本包插件,并只覆盖原生 `deepseek` 的 `baseURL`;
|
|
37
|
+
3. 等本地桥接页就绪后自动打开页面。
|
|
38
|
+
|
|
39
|
+
不会改写你的项目 `opencode.json`、全局 opencode 配置、`/connect` 或 auth 存储。除 Node.js 外不需要 bash、PowerShell 或额外的项目文件。
|
|
40
|
+
|
|
41
|
+
要求:Node.js 20+,并且 `opencode` 已在 PATH(也可用 `OPENCODE_BIN` 指定)。
|
|
42
|
+
|
|
43
|
+
## 可选:项目内手动配置
|
|
44
|
+
|
|
45
|
+
如果不想用全局 CLI,也可以把本包作为普通 opencode 插件加载,并在项目配置里覆盖 DeepSeek `baseURL`:
|
|
46
|
+
|
|
47
|
+
opencode v2 的 `opencode.jsonc`:
|
|
48
|
+
|
|
49
|
+
```jsonc
|
|
50
|
+
{
|
|
51
|
+
"$schema": "https://opencode.ai/config.json",
|
|
52
|
+
"plugin": ["opencode-web-gateway"]
|
|
53
|
+
}
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
opencode v1 的 `opencode.json`:
|
|
57
|
+
|
|
58
|
+
```json
|
|
59
|
+
{
|
|
60
|
+
"$schema": "https://opencode.ai/config.json",
|
|
61
|
+
"plugin": ["opencode-web-gateway"]
|
|
62
|
+
}
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## 只覆盖原生 DeepSeek 的 baseURL
|
|
66
|
+
|
|
67
|
+
不要创建自定义 provider。把原生 `deepseek` 的端点改到本地即可:
|
|
68
|
+
|
|
69
|
+
opencode v1:
|
|
70
|
+
|
|
71
|
+
```json
|
|
72
|
+
{
|
|
73
|
+
"provider": {
|
|
74
|
+
"deepseek": {
|
|
75
|
+
"options": {
|
|
76
|
+
"baseURL": "http://127.0.0.1:8787/v1"
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
opencode v2:
|
|
84
|
+
|
|
85
|
+
```jsonc
|
|
86
|
+
{
|
|
87
|
+
"providers": {
|
|
88
|
+
"deepseek": {
|
|
89
|
+
"settings": {
|
|
90
|
+
"baseURL": "http://127.0.0.1:8787/v1"
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
完整示例见 [`examples/opencode.v1.json`](examples/opencode.v1.json) 和 [`examples/opencode.v2.jsonc`](examples/opencode.v2.jsonc)。
|
|
98
|
+
|
|
99
|
+
原生模型目录会保留(当前为 `deepseek/deepseek-flash`、`deepseek/deepseek-v4-pro`),provider ID 仍是 `deepseek`,所以 `/connect`、模型 capability、工具调用和参数映射都不变。
|
|
100
|
+
|
|
101
|
+
## 连接与 Key
|
|
102
|
+
|
|
103
|
+
推荐直接使用 opencode 原生流程:
|
|
104
|
+
|
|
105
|
+
```text
|
|
106
|
+
/connect -> DeepSeek -> 粘贴 API Key
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
或在启动前设置 `DEEPSEEK_API_KEY`。插件将 opencode 发来的 `Authorization` 原样转发给页面,不保存、不注入、不覆盖。
|
|
110
|
+
|
|
111
|
+
`web-gateway.config.json` 只用于上游、端口、跨源页面等传输配置:
|
|
112
|
+
|
|
113
|
+
```json
|
|
114
|
+
{
|
|
115
|
+
"upstream": "https://api.deepseek.com",
|
|
116
|
+
"host": "127.0.0.1",
|
|
117
|
+
"port": 8787,
|
|
118
|
+
"allowedOrigins": [],
|
|
119
|
+
"waitForBridgeMs": 15000
|
|
120
|
+
}
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
## 使用
|
|
124
|
+
|
|
125
|
+
1. 启动 opencode,`/connect` 连接 DeepSeek(或设置 `DEEPSEEK_API_KEY`)。
|
|
126
|
+
2. 打开 <http://127.0.0.1:8787/>,保持标签页打开。
|
|
127
|
+
3. 页面显示“已连接”后,在 opencode 中执行 `/models`,选择原生 DeepSeek 模型(如 `deepseek/deepseek-flash`)。
|
|
128
|
+
4. 如果请求时页面还没打开,请求会等待 `waitForBridgeMs`(默认 15 秒),然后返回 503。
|
|
129
|
+
|
|
130
|
+
## 托管页面(pages.dev)
|
|
131
|
+
|
|
132
|
+
页面也可以部署到 Cloudflare Pages 等静态托管。页面会默认连接 `ws://127.0.0.1:8787/bridge`。
|
|
133
|
+
|
|
134
|
+
跨源页面默认会被插件拒绝。部署后,在本机 `web-gateway.config.json` 加入页面来源:
|
|
135
|
+
|
|
136
|
+
```json
|
|
137
|
+
{
|
|
138
|
+
"allowedOrigins": ["https://kinski-opencode-web-gateway.pages.dev"]
|
|
139
|
+
}
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
然后刷新 opencode/插件,再打开托管页面。HTTPS 页面连接本机回环 `ws://127.0.0.1` 已在 Chrome 验证;如果浏览器拦截混合内容,直接使用插件自带的 <http://127.0.0.1:8787/> 即可。
|
|
143
|
+
|
|
144
|
+
## 配置项
|
|
145
|
+
|
|
146
|
+
| 字段 | 默认 | 说明 |
|
|
147
|
+
| --- | --- | --- |
|
|
148
|
+
| `upstream` | `https://api.deepseek.com` | 浏览器实际 fetch 的上游 |
|
|
149
|
+
| `host` | `127.0.0.1` | 监听地址;`0.0.0.0` 可供局域网页面连接 |
|
|
150
|
+
| `port` | `8787` | 本地端口,修改后同步修改 provider 的 `baseURL` |
|
|
151
|
+
| `allowedOrigins` | `[]` | 允许跨源 WebSocket 的页面来源列表(精确 origin) |
|
|
152
|
+
| `token` | `""` | 可选的页面访问令牌 |
|
|
153
|
+
| `waitForBridgeMs` | `15000` | 没有页面时请求等待时间 |
|
|
154
|
+
|
|
155
|
+
## 安全
|
|
156
|
+
|
|
157
|
+
- 使用 `/connect` 时,Key 在 opencode 的 auth 存储中;插件不改写它,只在转发瞬间把 `Authorization` 交给页面。
|
|
158
|
+
- 页面 HTML、bootstrap、localStorage、日志都不保存 Key。
|
|
159
|
+
- 默认监听 `127.0.0.1`;跨源页面必须显式加入 `allowedOrigins`。
|
|
160
|
+
- 可使用 `token` 增加一道页面访问校验。
|
|
161
|
+
- 页面只转发,不解析 SSE;上游必须允许浏览器 CORS(DeepSeek 已支持)。
|
|
162
|
+
|
|
163
|
+
## 兼容性
|
|
164
|
+
|
|
165
|
+
- 已测试:opencode **1.19.5**(v1 API)与 **2.2.0**(v2 API),包含原生 `/connect` 凭据透传。
|
|
166
|
+
- 1.18.x 的 Bun 运行时对 `node:http` upgrade socket 写入不刷新,不适用于本 WebSocket 桥。
|
|
167
|
+
|
|
168
|
+
MIT License
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Global CLI: run the system opencode with this package loaded as a plugin.
|
|
3
|
+
// No shell scripts and no changes to the opencode config on disk.
|
|
4
|
+
|
|
5
|
+
import { spawn, spawnSync } from "node:child_process"
|
|
6
|
+
import { existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs"
|
|
7
|
+
import { tmpdir } from "node:os"
|
|
8
|
+
import path from "node:path"
|
|
9
|
+
import { fileURLToPath } from "node:url"
|
|
10
|
+
|
|
11
|
+
const here = path.dirname(fileURLToPath(import.meta.url))
|
|
12
|
+
const packageRoot = path.resolve(here, "..")
|
|
13
|
+
const packageJson = JSON.parse(readFileSync(path.join(packageRoot, "package.json"), "utf8"))
|
|
14
|
+
|
|
15
|
+
const help = `Usage: opencode-web-gateway [--no-open] [opencode args...]
|
|
16
|
+
|
|
17
|
+
Runs the system opencode with the opencode-web-gateway plugin loaded and
|
|
18
|
+
DeepSeek's baseURL pointed at the local bridge. Project opencode config is
|
|
19
|
+
preserved; the gateway is added through an extra config source.
|
|
20
|
+
|
|
21
|
+
Options:
|
|
22
|
+
--no-open Do not open the bridge page in a browser
|
|
23
|
+
-h, --help Show this help
|
|
24
|
+
-v, --version Print the CLI version
|
|
25
|
+
|
|
26
|
+
Everything after the options is passed to opencode, for example:
|
|
27
|
+
opencode-web-gateway run -m deepseek/deepseek-flash "hello"
|
|
28
|
+
`
|
|
29
|
+
|
|
30
|
+
let args = process.argv.slice(2)
|
|
31
|
+
let noOpen = false
|
|
32
|
+
|
|
33
|
+
if (args[0] === "--help" || args[0] === "-h") {
|
|
34
|
+
process.stdout.write(help)
|
|
35
|
+
process.exit(0)
|
|
36
|
+
}
|
|
37
|
+
if (args[0] === "--version" || args[0] === "-v") {
|
|
38
|
+
console.log(packageJson.version)
|
|
39
|
+
process.exit(0)
|
|
40
|
+
}
|
|
41
|
+
while (args[0] === "--no-open") {
|
|
42
|
+
noOpen = true
|
|
43
|
+
args = args.slice(1)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const DEFAULT_CONFIG = {
|
|
47
|
+
upstream: "https://api.deepseek.com",
|
|
48
|
+
host: "127.0.0.1",
|
|
49
|
+
port: 8787,
|
|
50
|
+
allowedOrigins: [],
|
|
51
|
+
waitForBridgeMs: 15000,
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function fail(message) {
|
|
55
|
+
console.error(message)
|
|
56
|
+
process.exitCode = 1
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function readConfig() {
|
|
60
|
+
const candidates = [
|
|
61
|
+
process.env.OPENCODE_WEB_GATEWAY_CONFIG,
|
|
62
|
+
path.join(process.cwd(), "web-gateway.config.json"),
|
|
63
|
+
path.join(process.cwd(), ".opencode", "web-gateway.config.json"),
|
|
64
|
+
].filter(Boolean)
|
|
65
|
+
|
|
66
|
+
for (const candidate of candidates) {
|
|
67
|
+
if (!existsSync(candidate)) continue
|
|
68
|
+
try {
|
|
69
|
+
const value = JSON.parse(readFileSync(candidate, "utf8"))
|
|
70
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) continue
|
|
71
|
+
return { path: path.resolve(candidate), value }
|
|
72
|
+
} catch {
|
|
73
|
+
console.warn(`warning: ignoring invalid config ${candidate}`)
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return null
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function displayHost(host) {
|
|
80
|
+
if (host === "0.0.0.0") return "127.0.0.1"
|
|
81
|
+
if (host === "::" || host === "0:0:0:0:0:0:0:0") return "[::1]"
|
|
82
|
+
if (host.includes(":") && !host.startsWith("[")) return `[${host}]`
|
|
83
|
+
return host
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function openBrowser(url) {
|
|
87
|
+
try {
|
|
88
|
+
if (process.platform === "win32") {
|
|
89
|
+
spawn(process.env.ComSpec || "cmd.exe", ["/d", "/s", "/c", `start "" "${url}"`], {
|
|
90
|
+
stdio: "ignore",
|
|
91
|
+
windowsHide: true,
|
|
92
|
+
}).unref()
|
|
93
|
+
} else if (process.platform === "darwin") {
|
|
94
|
+
spawn("open", [url], { stdio: "ignore", detached: true }).unref()
|
|
95
|
+
} else {
|
|
96
|
+
spawn("xdg-open", [url], { stdio: "ignore", detached: true }).unref()
|
|
97
|
+
}
|
|
98
|
+
} catch {
|
|
99
|
+
// The URL is printed above; opening a browser is best-effort.
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
async function openWhenReady(url, child) {
|
|
104
|
+
const deadline = Date.now() + 10_000
|
|
105
|
+
while (Date.now() < deadline) {
|
|
106
|
+
if (child.exitCode !== null) return
|
|
107
|
+
try {
|
|
108
|
+
const response = await fetch(`${url}healthz`, { signal: AbortSignal.timeout(500) })
|
|
109
|
+
if (response.ok) {
|
|
110
|
+
openBrowser(url)
|
|
111
|
+
return
|
|
112
|
+
}
|
|
113
|
+
} catch {
|
|
114
|
+
// server not up yet
|
|
115
|
+
}
|
|
116
|
+
await new Promise((resolve) => setTimeout(resolve, 250))
|
|
117
|
+
}
|
|
118
|
+
openBrowser(url)
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const opencode = process.env.OPENCODE_BIN || (process.platform === "win32" ? "opencode.exe" : "opencode")
|
|
122
|
+
const probe = spawnSync(opencode, ["--version"], {
|
|
123
|
+
encoding: "utf8",
|
|
124
|
+
windowsHide: true,
|
|
125
|
+
})
|
|
126
|
+
if (probe.error || probe.status !== 0) {
|
|
127
|
+
fail(
|
|
128
|
+
`opencode was not found (${opencode}).\n` +
|
|
129
|
+
"Install opencode with its normal installer, or set OPENCODE_BIN to the executable.",
|
|
130
|
+
)
|
|
131
|
+
process.exit()
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const opencodeVersionText = `${probe.stdout || ""}\n${probe.stderr || ""}`
|
|
135
|
+
const opencodeVersion = (opencodeVersionText.match(/\d+\.\d+\.\d+/) || ["unknown"])[0]
|
|
136
|
+
const opencodeMajor = Number(opencodeVersion.split(".")[0]) || 0
|
|
137
|
+
|
|
138
|
+
const found = readConfig()
|
|
139
|
+
const config = { ...DEFAULT_CONFIG, ...(found ? found.value : {}) }
|
|
140
|
+
const port = Number(config.port) || DEFAULT_CONFIG.port
|
|
141
|
+
const host = String(config.host || DEFAULT_CONFIG.host)
|
|
142
|
+
const pageUrl = `http://${displayHost(host)}:${port}/`
|
|
143
|
+
|
|
144
|
+
const temp = mkdtempSync(path.join(tmpdir(), "opencode-web-gateway-"))
|
|
145
|
+
const cleanup = () => {
|
|
146
|
+
try {
|
|
147
|
+
rmSync(temp, { recursive: true, force: true })
|
|
148
|
+
} catch {
|
|
149
|
+
// best effort
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
process.on("exit", cleanup)
|
|
153
|
+
|
|
154
|
+
let configPath = found ? found.path : ""
|
|
155
|
+
if (!configPath) {
|
|
156
|
+
configPath = path.join(temp, "web-gateway.config.json")
|
|
157
|
+
writeFileSync(configPath, `${JSON.stringify(config, null, 2)}\n`, "utf8")
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const baseURL = `http://${displayHost(host)}:${port}/v1`
|
|
161
|
+
const pluginPath = packageRoot.replace(/\\/g, "/")
|
|
162
|
+
const overlay = {
|
|
163
|
+
plugin: [pluginPath],
|
|
164
|
+
provider: {
|
|
165
|
+
deepseek: {
|
|
166
|
+
options: { baseURL },
|
|
167
|
+
},
|
|
168
|
+
},
|
|
169
|
+
providers: {
|
|
170
|
+
deepseek: {
|
|
171
|
+
settings: { baseURL },
|
|
172
|
+
},
|
|
173
|
+
},
|
|
174
|
+
}
|
|
175
|
+
const overlayPath = path.join(temp, "opencode.json")
|
|
176
|
+
writeFileSync(overlayPath, `${JSON.stringify(overlay, null, 2)}\n`, "utf8")
|
|
177
|
+
|
|
178
|
+
const env = {
|
|
179
|
+
...process.env,
|
|
180
|
+
OPENCODE_WEB_GATEWAY_CONFIG: configPath,
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
if (!process.env.OPENCODE_CONFIG) {
|
|
184
|
+
env.OPENCODE_CONFIG = overlayPath
|
|
185
|
+
} else if (!process.env.OPENCODE_CONFIG_CONTENT) {
|
|
186
|
+
// Keep the caller's OPENCODE_CONFIG and add the bridge overlay as a document.
|
|
187
|
+
env.OPENCODE_CONFIG_CONTENT = JSON.stringify(overlay)
|
|
188
|
+
} else {
|
|
189
|
+
fail(
|
|
190
|
+
"Both OPENCODE_CONFIG and OPENCODE_CONFIG_CONTENT are set in this environment.\n" +
|
|
191
|
+
"opencode-web-gateway needs one config slot for its overlay; unset one of them.",
|
|
192
|
+
)
|
|
193
|
+
process.exit()
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const finalArgs = [...args]
|
|
197
|
+
const has = (flag) => finalArgs.includes(flag)
|
|
198
|
+
if (opencodeMajor >= 2 && !has("--standalone") && !has("--server") && !has("--version") && !has("-v") && !has("--help") && !has("-h")) {
|
|
199
|
+
finalArgs.unshift("--standalone")
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const firstCommand = args.find((arg) => !arg.startsWith("-"))
|
|
203
|
+
const wantsPage = !noOpen && (!firstCommand || firstCommand === "run" || firstCommand === "serve")
|
|
204
|
+
|
|
205
|
+
console.log(`opencode-web-gateway ${packageJson.version}`)
|
|
206
|
+
console.log(` opencode ${opencodeVersion}`)
|
|
207
|
+
console.log(` page ${pageUrl}`)
|
|
208
|
+
console.log(` config ${configPath}`)
|
|
209
|
+
if (noOpen) console.log(" browser disabled (--no-open)")
|
|
210
|
+
|
|
211
|
+
let child
|
|
212
|
+
try {
|
|
213
|
+
child = spawn(opencode, finalArgs, {
|
|
214
|
+
cwd: process.cwd(),
|
|
215
|
+
env,
|
|
216
|
+
stdio: "inherit",
|
|
217
|
+
windowsHide: false,
|
|
218
|
+
})
|
|
219
|
+
} catch (error) {
|
|
220
|
+
fail(`could not start opencode: ${error instanceof Error ? error.message : String(error)}`)
|
|
221
|
+
process.exit()
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
child.on("error", (error) => {
|
|
225
|
+
fail(`could not start opencode: ${error.message}`)
|
|
226
|
+
cleanup()
|
|
227
|
+
})
|
|
228
|
+
child.on("exit", (code, signal) => {
|
|
229
|
+
cleanup()
|
|
230
|
+
process.exitCode = code ?? (signal ? 1 : 0)
|
|
231
|
+
})
|
|
232
|
+
|
|
233
|
+
if (wantsPage) {
|
|
234
|
+
void openWhenReady(pageUrl, child)
|
|
235
|
+
}
|