opencode-drive 0.1.2 → 0.1.4
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 +19 -80
- package/package.json +1 -1
- package/src/cli/commands.ts +104 -68
- package/src/cli/control.ts +143 -0
- package/src/cli/index.ts +149 -106
- package/src/cli/instance.ts +222 -168
- package/src/cli/list.ts +10 -0
- package/src/cli/logs.ts +9 -0
- package/src/cli/mock-backend.ts +37 -0
- package/src/cli/registry.ts +224 -71
- package/src/cli/response-generator.ts +334 -0
- package/src/cli/responses.ts +20 -0
- package/src/cli/restart.ts +2 -65
- package/src/cli/script.ts +77 -0
- package/src/cli/send.ts +22 -39
- package/src/cli/start.ts +202 -65
- package/src/cli/stop.ts +19 -26
- package/src/cli/types.ts +7 -30
- package/src/client/backend.ts +95 -33
- package/src/client/client.ts +86 -29
- package/src/client/protocol.ts +116 -29
- package/src/client/protocol.types.ts +17 -23
- package/src/experimental/driver.ts +29 -18
- package/src/experimental/flow-driver.ts +130 -43
- package/src/experimental/flows/properties.ts +19 -13
- package/src/experimental/hello-driver.ts +11 -4
- package/src/experimental/stale-running-driver.ts +45 -13
- package/src/index.ts +2 -0
- package/src/cli/describe.ts +0 -14
- package/src/cli/driver-runner.ts +0 -39
- package/src/cli/driver.ts +0 -28
- package/src/experimental/cli-campaign.ts +0 -179
package/src/cli/instance.ts
CHANGED
|
@@ -1,173 +1,178 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { mkdir, rm } from "node:fs/promises"
|
|
2
2
|
import { tmpdir } from "node:os"
|
|
3
3
|
import { join, resolve } from "node:path"
|
|
4
|
-
import { registerInstance, registryDirectory, transferInstance, unregisterInstance } from "./registry.js"
|
|
5
|
-
import type { InstanceManifest } from "./types.js"
|
|
6
4
|
|
|
7
5
|
export interface LaunchOptions {
|
|
8
|
-
readonly name
|
|
6
|
+
readonly name: string
|
|
9
7
|
readonly command?: ReadonlyArray<string>
|
|
10
8
|
readonly dev?: string
|
|
11
9
|
readonly state?: string
|
|
10
|
+
readonly scripted?: boolean
|
|
12
11
|
readonly visible?: boolean
|
|
13
12
|
readonly env?: Readonly<Record<string, string>>
|
|
14
13
|
}
|
|
15
14
|
|
|
16
|
-
export async function launchInstance(options: LaunchOptions
|
|
17
|
-
const
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
const
|
|
15
|
+
export async function launchInstance(options: LaunchOptions) {
|
|
16
|
+
const artifacts = resolve(
|
|
17
|
+
join(tmpdir(), "opencode-drive", `run-${crypto.randomUUID().slice(0, 6)}`),
|
|
18
|
+
)
|
|
19
|
+
const logs = join(artifacts, "logs")
|
|
21
20
|
const endpoints = {
|
|
22
|
-
ui: `ws://127.0.0.1:${
|
|
23
|
-
backend: `ws://127.0.0.1:${
|
|
24
|
-
}
|
|
25
|
-
const manifest: InstanceManifest = {
|
|
26
|
-
version: 1,
|
|
27
|
-
name,
|
|
28
|
-
pid: process.pid,
|
|
29
|
-
startedAt: new Date().toISOString(),
|
|
30
|
-
mode: "simulated",
|
|
31
|
-
headless: options.visible !== true,
|
|
32
|
-
cwd,
|
|
33
|
-
artifacts,
|
|
34
|
-
endpoints,
|
|
21
|
+
ui: `ws://127.0.0.1:${await freePort()}`,
|
|
22
|
+
backend: `ws://127.0.0.1:${await freePort()}`,
|
|
35
23
|
}
|
|
24
|
+
const drive = join(artifacts, "drive")
|
|
36
25
|
await Promise.all([
|
|
37
|
-
mkdir(
|
|
38
|
-
mkdir(
|
|
26
|
+
mkdir(logs, { recursive: true }),
|
|
27
|
+
mkdir(drive, { recursive: true }),
|
|
39
28
|
mkdir(join(artifacts, "home", ".cache"), { recursive: true }),
|
|
40
29
|
mkdir(join(artifacts, "home", ".config"), { recursive: true }),
|
|
41
30
|
mkdir(join(artifacts, "home", ".local", "share"), { recursive: true }),
|
|
42
31
|
mkdir(join(artifacts, "home", ".local", "state"), { recursive: true }),
|
|
43
32
|
])
|
|
44
|
-
|
|
33
|
+
await Bun.write(
|
|
34
|
+
join(drive, `${options.name}.json`),
|
|
35
|
+
`${JSON.stringify({ endpoints }, undefined, 2)}\n`,
|
|
36
|
+
)
|
|
37
|
+
const state = options.state
|
|
38
|
+
? resolve(options.state)
|
|
39
|
+
: join(artifacts, "state")
|
|
45
40
|
if (!options.state) {
|
|
46
41
|
await rm(state, { recursive: true, force: true })
|
|
47
42
|
await Promise.all([
|
|
48
43
|
mkdir(join(state, "files", ".git"), { recursive: true }),
|
|
49
44
|
mkdir(join(state, "files", ".opencode"), { recursive: true }),
|
|
45
|
+
mkdir(join(state, "files", "src"), { recursive: true }),
|
|
50
46
|
])
|
|
51
|
-
await
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
{
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
"
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
47
|
+
await Promise.all([
|
|
48
|
+
Bun.write(
|
|
49
|
+
join(state, "files", ".opencode", "opencode.jsonc"),
|
|
50
|
+
`${JSON.stringify(
|
|
51
|
+
{
|
|
52
|
+
model: "simulation/gpt-sim-model",
|
|
53
|
+
permissions: [{ action: "*", resource: "*", effect: "allow" }],
|
|
54
|
+
providers: {
|
|
55
|
+
simulation: {
|
|
56
|
+
name: "Simulation",
|
|
57
|
+
package: "aisdk:@ai-sdk/openai-compatible",
|
|
58
|
+
settings: { baseURL: "https://api.openai.com/v1" },
|
|
59
|
+
request: { body: { apiKey: "sim-key" } },
|
|
60
|
+
models: {
|
|
61
|
+
"gpt-sim-model": {
|
|
62
|
+
name: "Simulated Model",
|
|
63
|
+
capabilities: {
|
|
64
|
+
tools: true,
|
|
65
|
+
input: ["text"],
|
|
66
|
+
output: ["text"],
|
|
67
|
+
},
|
|
68
|
+
limit: { context: 128000, output: 16000 },
|
|
68
69
|
},
|
|
69
|
-
capabilities: { tools: true, input: ["text"], output: ["text"] },
|
|
70
|
-
limit: { context: 128000, output: 16000 },
|
|
71
70
|
},
|
|
72
71
|
},
|
|
73
72
|
},
|
|
74
73
|
},
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
)
|
|
79
|
-
|
|
74
|
+
undefined,
|
|
75
|
+
2,
|
|
76
|
+
)}\n`,
|
|
77
|
+
),
|
|
78
|
+
Bun.write(
|
|
79
|
+
join(state, "files", "src", "garden.js"),
|
|
80
|
+
'export function greet(name) {\n return `Hello, ${name}.`\n}\n',
|
|
81
|
+
),
|
|
82
|
+
])
|
|
80
83
|
}
|
|
81
|
-
await registerInstance(manifest)
|
|
82
84
|
const environment = cleanEnv({
|
|
83
85
|
...process.env,
|
|
84
86
|
...options.env,
|
|
85
|
-
DRIVE_REGISTRY_DIR: registryDirectory(),
|
|
86
87
|
OPENCODE_SIMULATE: "1",
|
|
87
88
|
OPENCODE_SIMULATE_STATE: state,
|
|
88
|
-
|
|
89
|
+
DRIVE_REGISTRY_DIR: drive,
|
|
90
|
+
OPENCODE_DRIVE: options.name,
|
|
89
91
|
OPENCODE_DRIVE_RENDERER: options.visible ? "visible" : "headless",
|
|
90
|
-
OPENCODE_CONFIG_DIR: join(
|
|
92
|
+
OPENCODE_CONFIG_DIR: join(artifacts, ".opencode"),
|
|
91
93
|
OPENCODE_DB: ":memory:",
|
|
94
|
+
OPENCODE_LOG_LEVEL: !options.visible
|
|
95
|
+
? "INFO"
|
|
96
|
+
: process.env.OPENCODE_LOG_LEVEL,
|
|
92
97
|
OPENCODE_TEST_HOME: artifacts,
|
|
93
98
|
XDG_CACHE_HOME: join(artifacts, "home", ".cache"),
|
|
94
99
|
XDG_CONFIG_HOME: join(artifacts, "home", ".config"),
|
|
95
|
-
XDG_DATA_HOME:
|
|
100
|
+
XDG_DATA_HOME: logs,
|
|
96
101
|
XDG_STATE_HOME: join(artifacts, "home", ".local", "state"),
|
|
97
102
|
})
|
|
98
103
|
const command = options.dev
|
|
99
|
-
? await prepareDev(
|
|
104
|
+
? await prepareDev(artifacts, options.dev)
|
|
100
105
|
: options.command?.length
|
|
101
106
|
? [...options.command]
|
|
102
107
|
: ["opencode2"]
|
|
103
|
-
const
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
108
|
+
const spawn = () =>
|
|
109
|
+
Bun.spawn(command, {
|
|
110
|
+
cwd: artifacts,
|
|
111
|
+
env: environment,
|
|
112
|
+
stdin: options.visible ? "inherit" : "ignore",
|
|
113
|
+
stdout: !options.visible
|
|
114
|
+
? Bun.file(join(logs, "opencode.stdout.log"))
|
|
115
|
+
: "inherit",
|
|
116
|
+
stderr: !options.visible
|
|
117
|
+
? Bun.file(join(logs, "opencode.stderr.log"))
|
|
118
|
+
: "inherit",
|
|
119
|
+
})
|
|
120
|
+
let child = spawn()
|
|
121
|
+
let stopping: Promise<void> | undefined
|
|
122
|
+
let restarting: Promise<void> | undefined
|
|
113
123
|
return {
|
|
114
|
-
|
|
124
|
+
artifacts,
|
|
125
|
+
logs,
|
|
126
|
+
endpoints,
|
|
115
127
|
get child() {
|
|
116
128
|
return child
|
|
117
129
|
},
|
|
118
|
-
async waitForDrive(
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
130
|
+
async waitForDrive(
|
|
131
|
+
requirement: "ui" | "backend" | "both" = "both",
|
|
132
|
+
timeout = 60_000,
|
|
133
|
+
) {
|
|
134
|
+
const urls =
|
|
135
|
+
requirement === "both"
|
|
136
|
+
? [endpoints.ui, endpoints.backend]
|
|
137
|
+
: [endpoints[requirement]]
|
|
138
|
+
await Promise.all(
|
|
139
|
+
urls.map((url) => waitForWebSocket(url, child.exited, timeout)),
|
|
140
|
+
)
|
|
128
141
|
},
|
|
129
|
-
restart() {
|
|
130
|
-
if (
|
|
131
|
-
|
|
142
|
+
async restart() {
|
|
143
|
+
if (restarting) return restarting
|
|
144
|
+
restarting = (async () => {
|
|
132
145
|
await terminate(child)
|
|
133
|
-
child = spawn(
|
|
146
|
+
child = spawn()
|
|
134
147
|
await Promise.all([
|
|
135
148
|
waitForWebSocket(endpoints.ui, child.exited, 60_000),
|
|
136
149
|
waitForWebSocket(endpoints.backend, child.exited, 60_000),
|
|
137
150
|
])
|
|
138
151
|
})().finally(() => {
|
|
139
|
-
|
|
152
|
+
restarting = undefined
|
|
140
153
|
})
|
|
141
|
-
return
|
|
154
|
+
return restarting
|
|
142
155
|
},
|
|
143
156
|
async wait() {
|
|
144
157
|
while (true) {
|
|
145
158
|
const current = child
|
|
146
159
|
const status = await current.exited
|
|
147
|
-
if (
|
|
148
|
-
await
|
|
160
|
+
if (restarting) {
|
|
161
|
+
await restarting
|
|
149
162
|
continue
|
|
150
163
|
}
|
|
151
|
-
if (
|
|
164
|
+
if (current !== child) continue
|
|
152
165
|
return status
|
|
153
166
|
}
|
|
154
167
|
},
|
|
155
|
-
stop(
|
|
156
|
-
if (
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
if (child.exitCode === null) child.kill("SIGKILL")
|
|
162
|
-
} else if (child.exitCode === null) {
|
|
163
|
-
child.kill("SIGTERM")
|
|
164
|
-
await Promise.race([child.exited, Bun.sleep(1_000)])
|
|
165
|
-
}
|
|
166
|
-
if (child.exitCode === null) child.kill("SIGKILL")
|
|
167
|
-
await child.exited
|
|
168
|
-
await unregisterInstance(name, process.pid)
|
|
168
|
+
stop() {
|
|
169
|
+
if (stopping) return stopping
|
|
170
|
+
stopping = (async () => {
|
|
171
|
+
if (restarting) await restarting.catch(() => undefined)
|
|
172
|
+
await terminate(child)
|
|
173
|
+
await stopService(join(artifacts, "home", ".local", "state"))
|
|
169
174
|
})()
|
|
170
|
-
return
|
|
175
|
+
return stopping
|
|
171
176
|
},
|
|
172
177
|
}
|
|
173
178
|
}
|
|
@@ -180,67 +185,48 @@ async function terminate(child: Bun.Subprocess) {
|
|
|
180
185
|
await child.exited
|
|
181
186
|
}
|
|
182
187
|
|
|
183
|
-
export async function restartInstance(manifest: InstanceManifest) {
|
|
184
|
-
const value: unknown = await Bun.file(join(manifest.artifacts, "launch.json")).json()
|
|
185
|
-
if (!isLaunchInfo(value)) throw new Error(`drive instance "${manifest.name}" has no valid restart metadata`)
|
|
186
|
-
const child = spawn(value.command, value.environment, manifest)
|
|
187
|
-
await transferInstance(manifest, child.pid)
|
|
188
|
-
try {
|
|
189
|
-
await Promise.all([
|
|
190
|
-
waitForWebSocket(manifest.endpoints.ui, child.exited, 60_000),
|
|
191
|
-
waitForWebSocket(manifest.endpoints.backend, child.exited, 60_000),
|
|
192
|
-
])
|
|
193
|
-
child.unref()
|
|
194
|
-
return child.pid
|
|
195
|
-
} catch (error) {
|
|
196
|
-
if (child.exitCode === null) child.kill("SIGKILL")
|
|
197
|
-
await child.exited
|
|
198
|
-
throw error
|
|
199
|
-
}
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
function spawn(command: ReadonlyArray<string>, environment: Readonly<Record<string, string>>, manifest: InstanceManifest) {
|
|
203
|
-
return Bun.spawn([...command], {
|
|
204
|
-
cwd: manifest.cwd,
|
|
205
|
-
env: environment,
|
|
206
|
-
stdin: manifest.headless ? "ignore" : "inherit",
|
|
207
|
-
stdout: manifest.headless ? Bun.file(join(manifest.artifacts, "opencode.stdout.log")) : "inherit",
|
|
208
|
-
stderr: manifest.headless ? Bun.file(join(manifest.artifacts, "opencode.stderr.log")) : "inherit",
|
|
209
|
-
})
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
function isLaunchInfo(value: unknown): value is {
|
|
213
|
-
readonly command: ReadonlyArray<string>
|
|
214
|
-
readonly environment: Readonly<Record<string, string>>
|
|
215
|
-
} {
|
|
216
|
-
if (typeof value !== "object" || value === null) return false
|
|
217
|
-
if (!("command" in value) || !Array.isArray(value.command) || !value.command.every((item) => typeof item === "string")) return false
|
|
218
|
-
if (!("environment" in value) || typeof value.environment !== "object" || value.environment === null || Array.isArray(value.environment)) return false
|
|
219
|
-
return Object.values(value.environment).every((item) => typeof item === "string")
|
|
220
|
-
}
|
|
221
|
-
|
|
222
188
|
async function prepareDev(cwd: string, directory: string) {
|
|
223
189
|
const root = resolve(directory)
|
|
224
190
|
const entrypoint = join(root, "packages", "cli", "src", "index.ts")
|
|
225
|
-
if (!(await Bun.file(entrypoint).exists()))
|
|
226
|
-
|
|
191
|
+
if (!(await Bun.file(entrypoint).exists()))
|
|
192
|
+
throw new Error(`OpenCode development entrypoint not found: ${entrypoint}`)
|
|
193
|
+
const solidPackage = join(
|
|
194
|
+
root,
|
|
195
|
+
"packages",
|
|
196
|
+
"tui",
|
|
197
|
+
"node_modules",
|
|
198
|
+
"@opentui",
|
|
199
|
+
"solid",
|
|
200
|
+
"package.json",
|
|
201
|
+
)
|
|
227
202
|
if (!(await Bun.file(solidPackage).exists())) {
|
|
228
|
-
throw new Error(
|
|
203
|
+
throw new Error(
|
|
204
|
+
`OpenCode development dependency not found: ${solidPackage}; run bun install in ${root}`,
|
|
205
|
+
)
|
|
229
206
|
}
|
|
230
207
|
const info: unknown = await Bun.file(solidPackage).json()
|
|
231
|
-
if (!isPackageInfo(info))
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
208
|
+
if (!isPackageInfo(info))
|
|
209
|
+
throw new Error(`Invalid @opentui/solid package metadata: ${solidPackage}`)
|
|
210
|
+
await Bun.write(
|
|
211
|
+
join(cwd, "package.json"),
|
|
212
|
+
`${JSON.stringify(
|
|
213
|
+
{
|
|
214
|
+
private: true,
|
|
215
|
+
dependencies: { "@opentui/solid": info.version },
|
|
216
|
+
},
|
|
217
|
+
undefined,
|
|
218
|
+
2,
|
|
219
|
+
)}\n`,
|
|
220
|
+
)
|
|
236
221
|
const install = Bun.spawn([process.execPath, "install"], {
|
|
237
222
|
cwd,
|
|
238
223
|
stdin: "ignore",
|
|
239
|
-
stdout: "
|
|
240
|
-
stderr: "
|
|
224
|
+
stdout: "ignore",
|
|
225
|
+
stderr: "ignore",
|
|
241
226
|
})
|
|
242
227
|
const status = await install.exited
|
|
243
|
-
if (status !== 0)
|
|
228
|
+
if (status !== 0)
|
|
229
|
+
throw new Error(`bun install failed in ${cwd} with status ${status}`)
|
|
244
230
|
return [
|
|
245
231
|
process.execPath,
|
|
246
232
|
"--conditions=browser",
|
|
@@ -249,27 +235,89 @@ async function prepareDev(cwd: string, directory: string) {
|
|
|
249
235
|
]
|
|
250
236
|
}
|
|
251
237
|
|
|
252
|
-
function isPackageInfo(value: unknown): value is { readonly version: string } {
|
|
253
|
-
return typeof value === "object" && value !== null && "version" in value && typeof value.version === "string"
|
|
254
|
-
}
|
|
255
|
-
|
|
256
238
|
async function freePort() {
|
|
257
|
-
const server = Bun.serve({
|
|
239
|
+
const server = Bun.serve({
|
|
240
|
+
hostname: "127.0.0.1",
|
|
241
|
+
port: 0,
|
|
242
|
+
fetch: () => new Response(),
|
|
243
|
+
})
|
|
258
244
|
const port = server.port
|
|
259
245
|
await server.stop(true)
|
|
260
246
|
return port
|
|
261
247
|
}
|
|
262
248
|
|
|
263
|
-
async function
|
|
249
|
+
async function stopService(state: string) {
|
|
250
|
+
const files = [
|
|
251
|
+
join(state, "opencode", "service-local.json"),
|
|
252
|
+
join(state, "opencode", "service.json"),
|
|
253
|
+
]
|
|
254
|
+
const info = await Promise.all(
|
|
255
|
+
files.map((file) =>
|
|
256
|
+
Bun.file(file)
|
|
257
|
+
.json()
|
|
258
|
+
.catch(() => undefined),
|
|
259
|
+
),
|
|
260
|
+
)
|
|
261
|
+
await Promise.all(
|
|
262
|
+
info.map(async (value) => {
|
|
263
|
+
if (!isServiceInfo(value)) return
|
|
264
|
+
try {
|
|
265
|
+
process.kill(value.pid, "SIGTERM")
|
|
266
|
+
} catch {
|
|
267
|
+
return
|
|
268
|
+
}
|
|
269
|
+
const deadline = Date.now() + 1_000
|
|
270
|
+
while (Date.now() < deadline && alive(value.pid)) await Bun.sleep(25)
|
|
271
|
+
if (alive(value.pid)) process.kill(value.pid, "SIGKILL")
|
|
272
|
+
}),
|
|
273
|
+
)
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
function isServiceInfo(value: unknown): value is { readonly pid: number } {
|
|
277
|
+
return (
|
|
278
|
+
typeof value === "object" &&
|
|
279
|
+
value !== null &&
|
|
280
|
+
"pid" in value &&
|
|
281
|
+
typeof value.pid === "number"
|
|
282
|
+
)
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function alive(pid: number) {
|
|
286
|
+
try {
|
|
287
|
+
process.kill(pid, 0)
|
|
288
|
+
return true
|
|
289
|
+
} catch {
|
|
290
|
+
return false
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
function isPackageInfo(value: unknown): value is { readonly version: string } {
|
|
295
|
+
return (
|
|
296
|
+
typeof value === "object" &&
|
|
297
|
+
value !== null &&
|
|
298
|
+
"version" in value &&
|
|
299
|
+
typeof value.version === "string"
|
|
300
|
+
)
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
async function waitForWebSocket(
|
|
304
|
+
url: string,
|
|
305
|
+
exited: Promise<number>,
|
|
306
|
+
timeout: number,
|
|
307
|
+
) {
|
|
264
308
|
const deadline = Date.now() + timeout
|
|
265
309
|
while (Date.now() < deadline) {
|
|
266
310
|
const connected = await Promise.race([
|
|
267
|
-
open(url)
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
311
|
+
open(url)
|
|
312
|
+
.then((socket) => {
|
|
313
|
+
socket.terminate()
|
|
314
|
+
return true
|
|
315
|
+
})
|
|
316
|
+
.catch(() => false),
|
|
271
317
|
exited.then((code) => {
|
|
272
|
-
throw new Error(
|
|
318
|
+
throw new Error(
|
|
319
|
+
`OpenCode exited with status ${code} before ${url} became ready`,
|
|
320
|
+
)
|
|
273
321
|
}),
|
|
274
322
|
])
|
|
275
323
|
if (connected) return
|
|
@@ -281,15 +329,21 @@ async function waitForWebSocket(url: string, exited: Promise<number>, timeout: n
|
|
|
281
329
|
function open(url: string) {
|
|
282
330
|
return new Promise<WebSocket>((resolveSocket, reject) => {
|
|
283
331
|
const socket = new WebSocket(url)
|
|
284
|
-
socket.addEventListener("open", () => resolveSocket(socket), {
|
|
285
|
-
|
|
332
|
+
socket.addEventListener("open", () => resolveSocket(socket), {
|
|
333
|
+
once: true,
|
|
334
|
+
})
|
|
335
|
+
socket.addEventListener(
|
|
336
|
+
"error",
|
|
337
|
+
() => reject(new Error(`cannot connect to ${url}`)),
|
|
338
|
+
{ once: true },
|
|
339
|
+
)
|
|
286
340
|
})
|
|
287
341
|
}
|
|
288
342
|
|
|
289
|
-
function randomSuffix() {
|
|
290
|
-
return crypto.randomUUID().slice(0, 6)
|
|
291
|
-
}
|
|
292
|
-
|
|
293
343
|
function cleanEnv(env: Readonly<Record<string, string | undefined>>) {
|
|
294
|
-
return Object.fromEntries(
|
|
344
|
+
return Object.fromEntries(
|
|
345
|
+
Object.entries(env).filter(
|
|
346
|
+
(entry): entry is [string, string] => entry[1] !== undefined,
|
|
347
|
+
),
|
|
348
|
+
)
|
|
295
349
|
}
|
package/src/cli/list.ts
ADDED
package/src/cli/logs.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { join } from "node:path"
|
|
2
|
+
import { resolveInstance } from "./registry.js"
|
|
3
|
+
|
|
4
|
+
export async function logs(name?: string) {
|
|
5
|
+
const manifest = await resolveInstance(name)
|
|
6
|
+
console.log(
|
|
7
|
+
join(manifest.artifacts, "logs", "opencode", "log", "opencode*.log"),
|
|
8
|
+
)
|
|
9
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { connectBackendSimulation } from "../client/index.js"
|
|
2
|
+
import { generateResponse } from "./response-generator.js"
|
|
3
|
+
import type { createResponseSettings } from "./response-generator.js"
|
|
4
|
+
|
|
5
|
+
export async function connectMockBackend(
|
|
6
|
+
endpoint: string,
|
|
7
|
+
responses: ReturnType<typeof createResponseSettings>,
|
|
8
|
+
) {
|
|
9
|
+
const backend = await connectBackendSimulation({ url: endpoint })
|
|
10
|
+
await backend.attach(async (request) => {
|
|
11
|
+
const response = generateResponse(responses.current(), request)
|
|
12
|
+
for (const item of response.items) {
|
|
13
|
+
if (item.type !== "textDelta" && item.type !== "reasoningDelta") {
|
|
14
|
+
await backend.chunk(request.id, [item])
|
|
15
|
+
continue
|
|
16
|
+
}
|
|
17
|
+
for (const text of splitText(item.text)) {
|
|
18
|
+
await backend.chunk(request.id, [{ ...item, text }])
|
|
19
|
+
await Bun.sleep(45 + Math.floor(Math.random() * 35))
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
await backend.finish(request.id, response.finish)
|
|
23
|
+
})
|
|
24
|
+
return {
|
|
25
|
+
close() {
|
|
26
|
+
backend.close()
|
|
27
|
+
},
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function splitText(text: string) {
|
|
32
|
+
const words = text.match(/\S+\s*/g) ?? [text]
|
|
33
|
+
return Array.from(
|
|
34
|
+
{ length: Math.ceil(words.length / 3) },
|
|
35
|
+
(_, index) => words.slice(index * 3, index * 3 + 3).join(""),
|
|
36
|
+
)
|
|
37
|
+
}
|