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
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { requestResponses } from "./control.js"
|
|
2
|
+
import { resolveInstance } from "./registry.js"
|
|
3
|
+
|
|
4
|
+
export async function responses(options: {
|
|
5
|
+
readonly name?: string
|
|
6
|
+
readonly types?: string
|
|
7
|
+
readonly tools?: string
|
|
8
|
+
}) {
|
|
9
|
+
const manifest = await resolveInstance(options.name)
|
|
10
|
+
const configuration = await requestResponses(manifest.control, {
|
|
11
|
+
...(options.types === undefined ? {} : { types: split(options.types) }),
|
|
12
|
+
...(options.tools === undefined ? {} : { tools: split(options.tools) }),
|
|
13
|
+
})
|
|
14
|
+
console.log(`Types: ${configuration.types.join(",")}`)
|
|
15
|
+
console.log(`Tools: ${configuration.tools.join(",")}`)
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function split(value: string) {
|
|
19
|
+
return value.split(",").map((item) => item.trim())
|
|
20
|
+
}
|
package/src/cli/restart.ts
CHANGED
|
@@ -1,70 +1,7 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { join } from "node:path"
|
|
3
|
-
import { restartInstance } from "./instance.js"
|
|
1
|
+
import { request } from "./control.js"
|
|
4
2
|
import { resolveInstance } from "./registry.js"
|
|
5
3
|
|
|
6
4
|
export async function restart(name?: string) {
|
|
7
|
-
|
|
8
|
-
if (!manifest.headless) {
|
|
9
|
-
await restartVisible(manifest)
|
|
10
|
-
console.log("success")
|
|
11
|
-
return
|
|
12
|
-
}
|
|
13
|
-
process.kill(manifest.pid, "SIGTERM")
|
|
14
|
-
await Promise.race([waitForExit(manifest.pid), Bun.sleep(1_000)])
|
|
15
|
-
if (alive(manifest.pid)) process.kill(manifest.pid, "SIGKILL")
|
|
16
|
-
await waitForExit(manifest.pid)
|
|
17
|
-
await restartInstance(manifest)
|
|
5
|
+
await request((await resolveInstance(name)).control, "restart")
|
|
18
6
|
console.log("success")
|
|
19
7
|
}
|
|
20
|
-
|
|
21
|
-
async function restartVisible(manifest: Awaited<ReturnType<typeof resolveInstance>>) {
|
|
22
|
-
if (!(await Bun.file(join(manifest.artifacts, "launch.json")).exists())) {
|
|
23
|
-
throw new Error(`drive instance "${manifest.name}" was started by a version that does not support restart`)
|
|
24
|
-
}
|
|
25
|
-
const token = randomUUID()
|
|
26
|
-
const request = join(manifest.artifacts, "restart-request.json")
|
|
27
|
-
const response = join(manifest.artifacts, "restart-response.json")
|
|
28
|
-
await Bun.file(response).delete().catch(() => undefined)
|
|
29
|
-
await Bun.write(request, `${JSON.stringify({ token })}\n`)
|
|
30
|
-
const deadline = Date.now() + 60_000
|
|
31
|
-
while (Date.now() < deadline) {
|
|
32
|
-
const value = await Bun.file(response).json().catch(() => undefined)
|
|
33
|
-
if (isRestartResponse(value) && value.token === token) {
|
|
34
|
-
if (!value.success) throw new Error(value.error ?? "visible restart failed")
|
|
35
|
-
return
|
|
36
|
-
}
|
|
37
|
-
if (!alive(manifest.pid)) throw new Error(`drive instance "${manifest.name}" exited while restarting`)
|
|
38
|
-
await Bun.sleep(25)
|
|
39
|
-
}
|
|
40
|
-
throw new Error(`timed out restarting drive instance "${manifest.name}"`)
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
function isRestartResponse(value: unknown): value is {
|
|
44
|
-
readonly token: string
|
|
45
|
-
readonly success: boolean
|
|
46
|
-
readonly error?: string
|
|
47
|
-
} {
|
|
48
|
-
if (typeof value !== "object" || value === null) return false
|
|
49
|
-
if (!("token" in value) || typeof value.token !== "string") return false
|
|
50
|
-
return "success" in value && typeof value.success === "boolean"
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
function waitForExit(pid: number) {
|
|
54
|
-
return new Promise<void>((resolve) => {
|
|
55
|
-
const check = () => {
|
|
56
|
-
if (!alive(pid)) return resolve()
|
|
57
|
-
setTimeout(check, 25)
|
|
58
|
-
}
|
|
59
|
-
check()
|
|
60
|
-
})
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
function alive(pid: number) {
|
|
64
|
-
try {
|
|
65
|
-
process.kill(pid, 0)
|
|
66
|
-
return true
|
|
67
|
-
} catch {
|
|
68
|
-
return false
|
|
69
|
-
}
|
|
70
|
-
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { resolve } from "node:path"
|
|
2
|
+
import { pathToFileURL } from "node:url"
|
|
3
|
+
import { connectBackendSimulation, connectSimulation } from "../client/index.js"
|
|
4
|
+
import type {
|
|
5
|
+
BackendSimulationClient,
|
|
6
|
+
SimulationClient,
|
|
7
|
+
} from "../client/index.js"
|
|
8
|
+
|
|
9
|
+
export interface ScriptContext {
|
|
10
|
+
readonly ui: SimulationClient
|
|
11
|
+
readonly backend: BackendSimulationClient
|
|
12
|
+
readonly artifacts: string
|
|
13
|
+
readonly signal: AbortSignal
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export type DriveScript = (context: ScriptContext) => void | Promise<void>
|
|
17
|
+
|
|
18
|
+
export function defineScript(script: DriveScript) {
|
|
19
|
+
return script
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export async function runScript(
|
|
23
|
+
file: string,
|
|
24
|
+
artifacts: string,
|
|
25
|
+
endpoints: { readonly ui: string; readonly backend: string },
|
|
26
|
+
signal: AbortSignal,
|
|
27
|
+
) {
|
|
28
|
+
const module: { readonly default?: unknown } = await import(
|
|
29
|
+
pathToFileURL(resolve(file)).href
|
|
30
|
+
)
|
|
31
|
+
const script = module.default
|
|
32
|
+
if (!isDriveScript(script))
|
|
33
|
+
throw new Error("script must default-export a function")
|
|
34
|
+
const ui = await connectSimulation({ url: endpoints.ui })
|
|
35
|
+
const backend = await connectBackendSimulation({
|
|
36
|
+
url: endpoints.backend,
|
|
37
|
+
}).catch((error) => {
|
|
38
|
+
ui.close()
|
|
39
|
+
throw error
|
|
40
|
+
})
|
|
41
|
+
const abort = () => {
|
|
42
|
+
ui.close()
|
|
43
|
+
backend.close()
|
|
44
|
+
}
|
|
45
|
+
signal.addEventListener("abort", abort, { once: true })
|
|
46
|
+
try {
|
|
47
|
+
await waitForEditor(ui, signal)
|
|
48
|
+
await Promise.race([
|
|
49
|
+
script({ ui, backend, artifacts, signal }),
|
|
50
|
+
new Promise<never>((_resolve, reject) => {
|
|
51
|
+
signal.addEventListener(
|
|
52
|
+
"abort",
|
|
53
|
+
() => reject(signal.reason ?? new Error("script restarted")),
|
|
54
|
+
{ once: true },
|
|
55
|
+
)
|
|
56
|
+
}),
|
|
57
|
+
])
|
|
58
|
+
} finally {
|
|
59
|
+
signal.removeEventListener("abort", abort)
|
|
60
|
+
ui.close()
|
|
61
|
+
backend.close()
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async function waitForEditor(ui: SimulationClient, signal: AbortSignal) {
|
|
66
|
+
const deadline = Date.now() + 30_000
|
|
67
|
+
while (Date.now() < deadline) {
|
|
68
|
+
signal.throwIfAborted()
|
|
69
|
+
if ((await ui.state()).focused.editor) return
|
|
70
|
+
await Bun.sleep(50)
|
|
71
|
+
}
|
|
72
|
+
throw new Error("timed out waiting for the prompt editor")
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function isDriveScript(value: unknown): value is DriveScript {
|
|
76
|
+
return typeof value === "function"
|
|
77
|
+
}
|
package/src/cli/send.ts
CHANGED
|
@@ -1,48 +1,31 @@
|
|
|
1
|
-
import { tmpdir } from "node:os"
|
|
2
|
-
import { join, resolve } from "node:path"
|
|
3
|
-
import { defaultBackendPort, defaultPort } from "../client/index.js"
|
|
4
1
|
import { executeCommands } from "./commands.js"
|
|
5
|
-
import { runDriver } from "./driver.js"
|
|
6
|
-
import { resolveInstance } from "./registry.js"
|
|
7
2
|
import type { SendOptions } from "./types.js"
|
|
3
|
+
import { resolveInstance } from "./registry.js"
|
|
8
4
|
|
|
9
5
|
export async function send(options: SendOptions) {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
console.log("success")
|
|
6
|
+
if (options.commands.length === 0)
|
|
7
|
+
throw new Error("send requires at least one --command.ui.* flag")
|
|
8
|
+
const result = await executeCommands(
|
|
9
|
+
(await resolveInstance(options.name)).endpoints.ui,
|
|
10
|
+
options.commands,
|
|
11
|
+
)
|
|
12
|
+
if (
|
|
13
|
+
options.commands.length === 1 &&
|
|
14
|
+
["ui.screenshot", "ui.end-record"].includes(
|
|
15
|
+
options.commands[0]?.operation ?? "",
|
|
16
|
+
)
|
|
17
|
+
) {
|
|
18
|
+
console.log(result.results[0]?.result)
|
|
24
19
|
return
|
|
25
20
|
}
|
|
26
|
-
if (
|
|
27
|
-
|
|
21
|
+
if (
|
|
22
|
+
options.commands.length === 1 &&
|
|
23
|
+
["ui.state", "ui.start-record"].includes(
|
|
24
|
+
options.commands[0]?.operation ?? "",
|
|
25
|
+
)
|
|
26
|
+
) {
|
|
27
|
+
console.log(JSON.stringify(result.results[0]?.result, undefined, 2))
|
|
28
28
|
return
|
|
29
29
|
}
|
|
30
|
-
console.log(
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
function defaultManifest() {
|
|
34
|
-
return {
|
|
35
|
-
version: 1 as const,
|
|
36
|
-
name: "default",
|
|
37
|
-
pid: 0,
|
|
38
|
-
startedAt: new Date().toISOString(),
|
|
39
|
-
mode: "real" as const,
|
|
40
|
-
headless: false,
|
|
41
|
-
cwd: process.cwd(),
|
|
42
|
-
artifacts: join(tmpdir(), "opencode-drive", "default"),
|
|
43
|
-
endpoints: {
|
|
44
|
-
ui: `ws://127.0.0.1:${defaultPort}`,
|
|
45
|
-
backend: `ws://127.0.0.1:${defaultBackendPort}`,
|
|
46
|
-
},
|
|
47
|
-
}
|
|
30
|
+
console.log("success")
|
|
48
31
|
}
|
package/src/cli/start.ts
CHANGED
|
@@ -1,98 +1,235 @@
|
|
|
1
|
-
import { resolve } from "node:path"
|
|
2
|
-
import { executeCommands } from "./commands.js"
|
|
3
|
-
import { runCampaign } from "../experimental/cli-campaign.js"
|
|
4
|
-
import { runDriver } from "./driver.js"
|
|
5
1
|
import { launchInstance } from "./instance.js"
|
|
6
|
-
import
|
|
2
|
+
import { mkdir, rm } from "node:fs/promises"
|
|
7
3
|
import { join } from "node:path"
|
|
4
|
+
import { connectMockBackend } from "./mock-backend.js"
|
|
5
|
+
import { createResponseSettings } from "./response-generator.js"
|
|
6
|
+
import { runScript } from "./script.js"
|
|
7
|
+
import { listenControl } from "./control.js"
|
|
8
|
+
import {
|
|
9
|
+
controlPath,
|
|
10
|
+
markReady,
|
|
11
|
+
markStarting,
|
|
12
|
+
register,
|
|
13
|
+
registryDirectory,
|
|
14
|
+
resolveInstance,
|
|
15
|
+
unregister,
|
|
16
|
+
} from "./registry.js"
|
|
17
|
+
import type { StartOptions } from "./types.js"
|
|
8
18
|
|
|
9
19
|
export async function start(options: StartOptions) {
|
|
10
|
-
if (options.
|
|
20
|
+
if (!options.visible && !options.script && !options.daemon)
|
|
21
|
+
return startDetached(options)
|
|
22
|
+
const responses = createResponseSettings()
|
|
11
23
|
const instance = await launchInstance({
|
|
12
24
|
name: options.name,
|
|
13
25
|
command: options.command,
|
|
14
26
|
dev: options.dev,
|
|
15
27
|
state: options.state,
|
|
28
|
+
scripted: options.script !== undefined,
|
|
16
29
|
visible: options.visible,
|
|
17
30
|
})
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
31
|
+
await register({
|
|
32
|
+
version: 1,
|
|
33
|
+
name: options.name,
|
|
34
|
+
pid: process.pid,
|
|
35
|
+
startedAt: new Date().toISOString(),
|
|
36
|
+
cwd: process.cwd(),
|
|
37
|
+
artifacts: instance.artifacts,
|
|
38
|
+
visible: options.visible,
|
|
39
|
+
status: "starting",
|
|
40
|
+
endpoints: instance.endpoints,
|
|
41
|
+
control: controlPath(options.name),
|
|
42
|
+
}).catch(async (error) => {
|
|
43
|
+
await instance.stop()
|
|
44
|
+
throw error
|
|
45
|
+
})
|
|
26
46
|
const interrupt = () => void instance.stop()
|
|
27
|
-
|
|
47
|
+
let completed = false
|
|
48
|
+
let current: ReturnType<typeof run> | undefined
|
|
49
|
+
let restarting: Promise<void> | undefined
|
|
50
|
+
let stopping = false
|
|
28
51
|
process.once("SIGINT", interrupt)
|
|
29
52
|
process.once("SIGTERM", interrupt)
|
|
53
|
+
let closeControl: (() => Promise<void>) | undefined
|
|
30
54
|
try {
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
55
|
+
closeControl = await listenControl(controlPath(options.name), {
|
|
56
|
+
restart: () => {
|
|
57
|
+
if (restarting) return restarting
|
|
58
|
+
restarting = (async () => {
|
|
59
|
+
await markStarting(options.name, process.pid)
|
|
60
|
+
const previous = current
|
|
61
|
+
previous?.abort.abort(new Error("script restarted"))
|
|
62
|
+
await previous?.promise.catch(() => undefined)
|
|
63
|
+
await instance.restart()
|
|
64
|
+
current = run(options, instance, responses)
|
|
65
|
+
await current.ready
|
|
66
|
+
await markReady(options.name, process.pid)
|
|
67
|
+
})().finally(() => {
|
|
68
|
+
restarting = undefined
|
|
69
|
+
})
|
|
70
|
+
return restarting
|
|
71
|
+
},
|
|
72
|
+
stop: async () => {
|
|
73
|
+
stopping = true
|
|
74
|
+
current?.abort.abort(new Error("opencode-drive stopped"))
|
|
75
|
+
await instance.stop()
|
|
76
|
+
},
|
|
77
|
+
responses: async (input) => {
|
|
78
|
+
if (options.script)
|
|
79
|
+
throw new Error("responses are unavailable when --script owns the simulation backend")
|
|
80
|
+
return responses.update(input)
|
|
81
|
+
},
|
|
82
|
+
})
|
|
83
|
+
current = run(options, instance, responses)
|
|
84
|
+
await current.ready
|
|
85
|
+
await markReady(options.name, process.pid)
|
|
86
|
+
if (options.visible) {
|
|
87
|
+
const status = await instance.wait()
|
|
88
|
+
if (status !== 0 && !stopping) process.exitCode = status
|
|
44
89
|
return
|
|
45
90
|
}
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
await
|
|
49
|
-
|
|
91
|
+
while (true) {
|
|
92
|
+
const active: NonNullable<typeof current> = current
|
|
93
|
+
await active.promise
|
|
94
|
+
if (stopping) break
|
|
95
|
+
if (restarting) {
|
|
96
|
+
await restarting
|
|
97
|
+
continue
|
|
98
|
+
}
|
|
99
|
+
if (active !== current) continue
|
|
100
|
+
completed = true
|
|
101
|
+
break
|
|
50
102
|
}
|
|
51
|
-
const status = await instance.wait()
|
|
52
|
-
if (status !== 0) process.exitCode = status
|
|
53
103
|
} finally {
|
|
54
104
|
process.off("SIGINT", interrupt)
|
|
55
105
|
process.off("SIGTERM", interrupt)
|
|
56
|
-
|
|
106
|
+
current?.abort.abort(new Error("opencode-drive stopped"))
|
|
107
|
+
await closeControl?.()
|
|
57
108
|
await instance.stop()
|
|
109
|
+
await unregister(options.name, process.pid)
|
|
110
|
+
if (options.script && !options.visible)
|
|
111
|
+
report(instance, completed ? "completed" : undefined)
|
|
58
112
|
}
|
|
59
113
|
}
|
|
60
114
|
|
|
61
|
-
function
|
|
62
|
-
const
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
const
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
115
|
+
async function startDetached(options: StartOptions) {
|
|
116
|
+
const existing = await resolveInstance(options.name, { ready: false }).catch(() => undefined)
|
|
117
|
+
if (existing)
|
|
118
|
+
throw new Error(`drive instance "${options.name}" is already running`)
|
|
119
|
+
const ownerLog = join(registryDirectory(), `${options.name}.log`)
|
|
120
|
+
await mkdir(registryDirectory(), { recursive: true })
|
|
121
|
+
await rm(ownerLog, { force: true })
|
|
122
|
+
const child = Bun.spawn(
|
|
123
|
+
[
|
|
124
|
+
process.execPath,
|
|
125
|
+
process.argv[1]!,
|
|
126
|
+
"start",
|
|
127
|
+
"--daemon",
|
|
128
|
+
"--name",
|
|
129
|
+
options.name,
|
|
130
|
+
...(options.script ? ["--script", options.script] : []),
|
|
131
|
+
...(options.dev ? ["--dev", options.dev] : []),
|
|
132
|
+
...(options.state ? ["--state", options.state] : []),
|
|
133
|
+
...(options.command.length ? ["--", ...options.command] : []),
|
|
134
|
+
],
|
|
135
|
+
{
|
|
136
|
+
cwd: process.cwd(),
|
|
137
|
+
env: process.env,
|
|
138
|
+
stdin: "ignore",
|
|
139
|
+
stdout: "ignore",
|
|
140
|
+
stderr: Bun.file(ownerLog),
|
|
141
|
+
},
|
|
142
|
+
)
|
|
143
|
+
child.unref()
|
|
144
|
+
const deadline = Date.now() + 60_000
|
|
145
|
+
while (Date.now() < deadline) {
|
|
146
|
+
const manifest = await resolveInstance(options.name).catch(() => undefined)
|
|
147
|
+
if (manifest?.pid === child.pid) {
|
|
148
|
+
report({
|
|
149
|
+
artifacts: manifest.artifacts,
|
|
150
|
+
logs: `${manifest.artifacts}/logs`,
|
|
151
|
+
})
|
|
152
|
+
return
|
|
153
|
+
}
|
|
154
|
+
if (child.exitCode !== null)
|
|
155
|
+
throw new Error(
|
|
156
|
+
`detached instance exited with status ${child.exitCode}; see ${ownerLog}`,
|
|
157
|
+
)
|
|
158
|
+
await Bun.sleep(50)
|
|
159
|
+
}
|
|
160
|
+
await terminateOwner(child)
|
|
161
|
+
throw new Error(
|
|
162
|
+
`timed out starting drive instance "${options.name}"; see ${ownerLog}`,
|
|
163
|
+
)
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
async function terminateOwner(child: Bun.Subprocess) {
|
|
167
|
+
if (child.exitCode !== null) return
|
|
168
|
+
child.kill("SIGTERM")
|
|
169
|
+
const deadline = Date.now() + 1_000
|
|
170
|
+
while (child.exitCode === null && Date.now() < deadline) await Bun.sleep(25)
|
|
171
|
+
if (child.exitCode === null) child.kill("SIGKILL")
|
|
172
|
+
await child.exited
|
|
73
173
|
}
|
|
74
174
|
|
|
75
|
-
|
|
175
|
+
function run(
|
|
176
|
+
options: StartOptions,
|
|
76
177
|
instance: Awaited<ReturnType<typeof launchInstance>>,
|
|
77
|
-
|
|
78
|
-
response: string,
|
|
79
|
-
state: { token: string | undefined },
|
|
178
|
+
responses: ReturnType<typeof createResponseSettings>,
|
|
80
179
|
) {
|
|
81
|
-
const
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
180
|
+
const abort = new AbortController()
|
|
181
|
+
const child = instance.child
|
|
182
|
+
let ready!: () => void
|
|
183
|
+
const readiness = new Promise<void>((resolve) => {
|
|
184
|
+
ready = resolve
|
|
185
|
+
})
|
|
186
|
+
return {
|
|
187
|
+
abort,
|
|
188
|
+
ready: readiness,
|
|
189
|
+
promise: (async () => {
|
|
190
|
+
await instance.waitForDrive("both")
|
|
191
|
+
if (options.script) {
|
|
192
|
+
const script = runScript(
|
|
193
|
+
options.script,
|
|
194
|
+
instance.artifacts,
|
|
195
|
+
instance.endpoints,
|
|
196
|
+
abort.signal,
|
|
197
|
+
)
|
|
198
|
+
ready()
|
|
199
|
+
await script
|
|
200
|
+
if (options.visible) {
|
|
201
|
+
await Promise.race([
|
|
202
|
+
child.exited,
|
|
203
|
+
new Promise<void>((resolve) =>
|
|
204
|
+
abort.signal.addEventListener("abort", () => resolve(), {
|
|
205
|
+
once: true,
|
|
206
|
+
}),
|
|
207
|
+
),
|
|
208
|
+
])
|
|
209
|
+
}
|
|
210
|
+
return
|
|
211
|
+
}
|
|
212
|
+
const mock = await connectMockBackend(instance.endpoints.backend, responses)
|
|
213
|
+
ready()
|
|
214
|
+
abort.signal.addEventListener("abort", () => mock.close(), { once: true })
|
|
215
|
+
const status = await Promise.race([
|
|
216
|
+
child.exited,
|
|
217
|
+
new Promise<number>((resolve) =>
|
|
218
|
+
abort.signal.addEventListener("abort", () => resolve(0), {
|
|
219
|
+
once: true,
|
|
220
|
+
}),
|
|
221
|
+
),
|
|
222
|
+
])
|
|
223
|
+
mock.close()
|
|
224
|
+
if (status !== 0 && !abort.signal.aborted) process.exitCode = status
|
|
225
|
+
})(),
|
|
93
226
|
}
|
|
94
227
|
}
|
|
95
228
|
|
|
96
|
-
function
|
|
97
|
-
|
|
229
|
+
function report(
|
|
230
|
+
instance: { readonly artifacts: string; readonly logs: string },
|
|
231
|
+
status?: string,
|
|
232
|
+
) {
|
|
233
|
+
if (status) console.error(`opencode-drive: ${status}`)
|
|
234
|
+
console.error(`opencode-drive: artifacts ${instance.artifacts}`)
|
|
98
235
|
}
|
package/src/cli/stop.ts
CHANGED
|
@@ -1,31 +1,24 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { request } from "./control.js"
|
|
2
|
+
import { manifestPath, resolveInstance } from "./registry.js"
|
|
2
3
|
|
|
3
4
|
export async function stop(name?: string) {
|
|
4
|
-
const manifest = await resolveInstance(name
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
5
|
+
const manifest = await resolveInstance(name)
|
|
6
|
+
await request(manifest.control, "stop")
|
|
7
|
+
const deadline = Date.now() + 10_000
|
|
8
|
+
while (Date.now() < deadline) {
|
|
9
|
+
const current: unknown = await Bun.file(manifestPath(manifest.name))
|
|
10
|
+
.json()
|
|
11
|
+
.catch(() => undefined)
|
|
12
|
+
if (
|
|
13
|
+
typeof current !== "object" ||
|
|
14
|
+
current === null ||
|
|
15
|
+
!("pid" in current) ||
|
|
16
|
+
current.pid !== manifest.pid
|
|
17
|
+
) {
|
|
18
|
+
console.log("success")
|
|
19
|
+
return
|
|
19
20
|
}
|
|
20
|
-
|
|
21
|
-
})
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
function alive(pid: number) {
|
|
25
|
-
try {
|
|
26
|
-
process.kill(pid, 0)
|
|
27
|
-
return true
|
|
28
|
-
} catch {
|
|
29
|
-
return false
|
|
21
|
+
await Bun.sleep(25)
|
|
30
22
|
}
|
|
23
|
+
throw new Error(`timed out stopping drive instance "${manifest.name}"`)
|
|
31
24
|
}
|
package/src/cli/types.ts
CHANGED
|
@@ -1,46 +1,23 @@
|
|
|
1
|
-
export interface InstanceManifest {
|
|
2
|
-
readonly version: 1
|
|
3
|
-
readonly name: string
|
|
4
|
-
readonly pid: number
|
|
5
|
-
readonly startedAt: string
|
|
6
|
-
readonly mode: "simulated" | "real"
|
|
7
|
-
readonly headless: boolean
|
|
8
|
-
readonly cwd: string
|
|
9
|
-
readonly artifacts: string
|
|
10
|
-
readonly endpoints: {
|
|
11
|
-
readonly ui: string
|
|
12
|
-
readonly backend: string
|
|
13
|
-
}
|
|
14
|
-
}
|
|
15
|
-
|
|
16
1
|
export interface DriveCommand {
|
|
17
2
|
readonly operation: string
|
|
18
3
|
readonly value?: string
|
|
19
4
|
}
|
|
20
5
|
|
|
21
|
-
export interface
|
|
22
|
-
readonly name?: string
|
|
23
|
-
readonly driver?: string
|
|
24
|
-
readonly commands: ReadonlyArray<DriveCommand>
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
export interface StartOptions extends CommonOptions {
|
|
6
|
+
export interface StartOptions {
|
|
28
7
|
readonly kind: "start"
|
|
29
|
-
readonly
|
|
30
|
-
readonly
|
|
31
|
-
readonly
|
|
32
|
-
readonly caseIndex?: number
|
|
33
|
-
readonly count?: number
|
|
34
|
-
readonly concurrency: number
|
|
8
|
+
readonly name: string
|
|
9
|
+
readonly daemon: boolean
|
|
10
|
+
readonly script?: string
|
|
35
11
|
readonly visible: boolean
|
|
36
12
|
readonly dev?: string
|
|
37
13
|
readonly state?: string
|
|
38
|
-
readonly anchor?: string
|
|
39
14
|
readonly command: ReadonlyArray<string>
|
|
40
15
|
}
|
|
41
16
|
|
|
42
|
-
export interface SendOptions
|
|
17
|
+
export interface SendOptions {
|
|
43
18
|
readonly kind: "send"
|
|
19
|
+
readonly name?: string
|
|
20
|
+
readonly commands: ReadonlyArray<DriveCommand>
|
|
44
21
|
}
|
|
45
22
|
|
|
46
23
|
export type CliOptions = StartOptions | SendOptions
|