opencode-drive 0.1.1 → 0.1.3
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 +90 -61
- package/package.json +2 -2
- package/src/cli/api.ts +5 -0
- package/src/cli/commands.ts +117 -134
- package/src/cli/control.ts +43 -0
- package/src/cli/describe.ts +4 -5
- package/src/cli/index.ts +85 -101
- package/src/cli/instance.ts +220 -98
- package/src/cli/mock-backend.ts +25 -0
- package/src/cli/registry.ts +41 -83
- package/src/cli/restart.ts +7 -0
- package/src/cli/script.ts +66 -0
- package/src/cli/send.ts +19 -39
- package/src/cli/start.ts +100 -33
- package/src/cli/stop.ts +4 -28
- package/src/cli/types.ts +7 -30
- package/src/client/backend.ts +94 -37
- package/src/client/client.ts +15 -31
- package/src/client/index.ts +0 -3
- package/src/client/protocol.ts +133 -44
- package/src/client/protocol.types.ts +62 -0
- package/src/experimental/campaign.ts +2 -3
- package/src/experimental/driver.ts +29 -18
- package/src/experimental/flow-driver.ts +130 -45
- package/src/experimental/flows/properties.ts +19 -13
- package/src/experimental/flows/types.ts +0 -1
- package/src/experimental/hello-driver.ts +11 -4
- package/src/experimental/reproduce-stale-running-visible.ts +1 -1
- package/src/experimental/stale-running-driver.ts +45 -13
- package/src/index.ts +2 -0
- 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,25 @@
|
|
|
1
|
+
import { connectBackendSimulation } from "../client/index.js"
|
|
2
|
+
|
|
3
|
+
const response = "This is a sample response from opencode-drive."
|
|
4
|
+
|
|
5
|
+
export async function connectMockBackend(endpoint: string) {
|
|
6
|
+
const backend = await connectBackendSimulation({ url: endpoint })
|
|
7
|
+
let closing = false
|
|
8
|
+
await backend.attach((request) => {
|
|
9
|
+
void backend
|
|
10
|
+
.chunk(request.id, [{ type: "textDelta", text: response }])
|
|
11
|
+
.then(() => backend.finish(request.id))
|
|
12
|
+
.catch((error) => {
|
|
13
|
+
if (!closing)
|
|
14
|
+
console.error(
|
|
15
|
+
`error: ${error instanceof Error ? error.message : String(error)}`,
|
|
16
|
+
)
|
|
17
|
+
})
|
|
18
|
+
})
|
|
19
|
+
return {
|
|
20
|
+
close() {
|
|
21
|
+
closing = true
|
|
22
|
+
backend.close()
|
|
23
|
+
},
|
|
24
|
+
}
|
|
25
|
+
}
|
package/src/cli/registry.ts
CHANGED
|
@@ -1,92 +1,54 @@
|
|
|
1
|
+
import { mkdir, rm } from "node:fs/promises"
|
|
1
2
|
import { homedir } from "node:os"
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
import { join } from "node:path"
|
|
4
|
+
|
|
5
|
+
export interface InstanceManifest {
|
|
6
|
+
readonly version: 1
|
|
7
|
+
readonly name: string
|
|
8
|
+
readonly pid: number
|
|
9
|
+
readonly artifacts: string
|
|
10
|
+
readonly visible: boolean
|
|
11
|
+
readonly endpoints: { readonly ui: string; readonly backend: string }
|
|
12
|
+
readonly control: string
|
|
13
|
+
}
|
|
5
14
|
|
|
6
15
|
export function registryDirectory() {
|
|
7
|
-
|
|
8
|
-
return join(process.env.XDG_STATE_HOME ?? join(homedir(), ".local", "state"), "opencode-drive", "instances")
|
|
16
|
+
return process.env.DRIVE_REGISTRY_DIR ?? join(homedir(), ".local", "state", "opencode-drive", "instances")
|
|
9
17
|
}
|
|
10
18
|
|
|
11
19
|
export function manifestPath(name: string) {
|
|
12
20
|
return join(registryDirectory(), `${validateName(name)}.json`)
|
|
13
21
|
}
|
|
14
22
|
|
|
15
|
-
export
|
|
16
|
-
|
|
17
|
-
const file = manifestPath(manifest.name)
|
|
18
|
-
const lock = `${file}.lock`
|
|
19
|
-
const existing = await readManifest(file)
|
|
20
|
-
if (existing && processAlive(existing.pid)) throw new Error(`drive instance "${manifest.name}" is already running`)
|
|
21
|
-
if (existing) await rm(file, { force: true })
|
|
22
|
-
const handle = await open(lock, "wx").catch(() => undefined)
|
|
23
|
-
if (!handle) throw new Error(`drive instance "${manifest.name}" is already starting`)
|
|
24
|
-
try {
|
|
25
|
-
const current = await readManifest(file)
|
|
26
|
-
if (current && processAlive(current.pid)) throw new Error(`drive instance "${manifest.name}" is already running`)
|
|
27
|
-
const temporary = `${file}.${process.pid}.tmp`
|
|
28
|
-
await Bun.write(temporary, `${JSON.stringify(manifest, undefined, 2)}\n`)
|
|
29
|
-
await rename(temporary, file)
|
|
30
|
-
return file
|
|
31
|
-
} finally {
|
|
32
|
-
await handle.close()
|
|
33
|
-
await rm(lock, { force: true })
|
|
34
|
-
}
|
|
23
|
+
export function controlPath(name: string) {
|
|
24
|
+
return join(registryDirectory(), `${validateName(name)}.sock`)
|
|
35
25
|
}
|
|
36
26
|
|
|
37
|
-
export async function
|
|
38
|
-
|
|
39
|
-
const
|
|
40
|
-
if (
|
|
41
|
-
await rm(
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
export async function transferInstance(manifest: InstanceManifest, pid: number) {
|
|
45
|
-
const file = manifestPath(manifest.name)
|
|
46
|
-
const current = await readManifest(file)
|
|
47
|
-
if (current?.pid !== manifest.pid) throw new Error(`drive instance "${manifest.name}" changed ownership`)
|
|
48
|
-
const temporary = `${file}.${process.pid}.tmp`
|
|
49
|
-
await Bun.write(temporary, `${JSON.stringify({ ...manifest, pid }, undefined, 2)}\n`)
|
|
50
|
-
await rename(temporary, file)
|
|
27
|
+
export async function register(manifest: InstanceManifest) {
|
|
28
|
+
await mkdir(registryDirectory(), { recursive: true })
|
|
29
|
+
const existing = await read(manifest.name).catch(() => undefined)
|
|
30
|
+
if (existing && alive(existing.pid)) throw new Error(`drive instance "${manifest.name}" is already running`)
|
|
31
|
+
await rm(manifest.control, { force: true })
|
|
32
|
+
await Bun.write(manifestPath(manifest.name), `${JSON.stringify(manifest, undefined, 2)}\n`)
|
|
51
33
|
}
|
|
52
34
|
|
|
53
|
-
export async function resolveInstance(name
|
|
54
|
-
const
|
|
55
|
-
if (
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
return instance
|
|
35
|
+
export async function resolveInstance(name = "default") {
|
|
36
|
+
const manifest = await read(name)
|
|
37
|
+
if (!alive(manifest.pid)) {
|
|
38
|
+
await unregister(name)
|
|
39
|
+
throw new Error(`drive instance "${name}" is not running`)
|
|
59
40
|
}
|
|
60
|
-
|
|
61
|
-
if (instances.length === 0) throw new Error("no drive instances are running")
|
|
62
|
-
throw new Error(`multiple drive instances are running; pass --name (${instances.map((item) => item.name).join(", ")})`)
|
|
41
|
+
return manifest
|
|
63
42
|
}
|
|
64
43
|
|
|
65
|
-
export async function
|
|
66
|
-
await
|
|
67
|
-
const directory = registryDirectory()
|
|
68
|
-
const files = (await readdir(directory)).filter((file) => file.endsWith(".json"))
|
|
69
|
-
const entries = await Promise.all(files.map((file) => readManifest(join(directory, file))))
|
|
70
|
-
const active = entries.filter((entry): entry is InstanceManifest => entry !== undefined && processAlive(entry.pid))
|
|
71
|
-
const activeNames = new Set(active.map((entry) => entry.name))
|
|
72
|
-
await Promise.all(
|
|
73
|
-
files
|
|
74
|
-
.filter((file) => !activeNames.has(basename(file, ".json")))
|
|
75
|
-
.map((file) => rm(join(directory, file), { force: true })),
|
|
76
|
-
)
|
|
77
|
-
return active.sort((a, b) => a.name.localeCompare(b.name))
|
|
44
|
+
export async function unregister(name: string) {
|
|
45
|
+
await Promise.all([rm(manifestPath(name), { force: true }), rm(controlPath(name), { force: true })])
|
|
78
46
|
}
|
|
79
47
|
|
|
80
|
-
async function
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
if (!isManifest(value)) return undefined
|
|
85
|
-
return value
|
|
86
|
-
} catch {
|
|
87
|
-
await rm(file, { force: true })
|
|
88
|
-
return undefined
|
|
89
|
-
}
|
|
48
|
+
async function read(name: string): Promise<InstanceManifest> {
|
|
49
|
+
const value: unknown = await Bun.file(manifestPath(name)).json().catch(() => undefined)
|
|
50
|
+
if (!isManifest(value)) throw new Error(`drive instance "${name}" was not found`)
|
|
51
|
+
return value
|
|
90
52
|
}
|
|
91
53
|
|
|
92
54
|
function isManifest(value: unknown): value is InstanceManifest {
|
|
@@ -94,16 +56,19 @@ function isManifest(value: unknown): value is InstanceManifest {
|
|
|
94
56
|
if (!("version" in value) || value.version !== 1) return false
|
|
95
57
|
if (!("name" in value) || typeof value.name !== "string") return false
|
|
96
58
|
if (!("pid" in value) || typeof value.pid !== "number") return false
|
|
97
|
-
if (!("startedAt" in value) || typeof value.startedAt !== "string") return false
|
|
98
|
-
if (!("mode" in value) || (value.mode !== "simulated" && value.mode !== "real")) return false
|
|
99
|
-
if (!("headless" in value) || typeof value.headless !== "boolean") return false
|
|
100
|
-
if (!("cwd" in value) || typeof value.cwd !== "string") return false
|
|
101
59
|
if (!("artifacts" in value) || typeof value.artifacts !== "string") return false
|
|
60
|
+
if (!("visible" in value) || typeof value.visible !== "boolean") return false
|
|
61
|
+
if (!("control" in value) || typeof value.control !== "string") return false
|
|
102
62
|
if (!("endpoints" in value) || typeof value.endpoints !== "object" || value.endpoints === null) return false
|
|
103
63
|
return "ui" in value.endpoints && typeof value.endpoints.ui === "string" && "backend" in value.endpoints && typeof value.endpoints.backend === "string"
|
|
104
64
|
}
|
|
105
65
|
|
|
106
|
-
function
|
|
66
|
+
function validateName(name: string) {
|
|
67
|
+
if (!/^[a-zA-Z0-9][a-zA-Z0-9._-]{0,63}$/.test(name)) throw new Error(`invalid instance name "${name}"`)
|
|
68
|
+
return name
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function alive(pid: number) {
|
|
107
72
|
try {
|
|
108
73
|
process.kill(pid, 0)
|
|
109
74
|
return true
|
|
@@ -111,10 +76,3 @@ function processAlive(pid: number) {
|
|
|
111
76
|
return false
|
|
112
77
|
}
|
|
113
78
|
}
|
|
114
|
-
|
|
115
|
-
function validateName(name: string) {
|
|
116
|
-
if (!/^[a-zA-Z0-9][a-zA-Z0-9._-]{0,63}$/.test(name)) {
|
|
117
|
-
throw new Error("instance names must contain 1-64 letters, numbers, dots, underscores, or dashes")
|
|
118
|
-
}
|
|
119
|
-
return name
|
|
120
|
-
}
|
|
@@ -0,0 +1,66 @@
|
|
|
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 Promise.race([
|
|
48
|
+
script({ ui, backend, artifacts, signal }),
|
|
49
|
+
new Promise<never>((_resolve, reject) => {
|
|
50
|
+
signal.addEventListener(
|
|
51
|
+
"abort",
|
|
52
|
+
() => reject(signal.reason ?? new Error("script restarted")),
|
|
53
|
+
{ once: true },
|
|
54
|
+
)
|
|
55
|
+
}),
|
|
56
|
+
])
|
|
57
|
+
} finally {
|
|
58
|
+
signal.removeEventListener("abort", abort)
|
|
59
|
+
ui.close()
|
|
60
|
+
backend.close()
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function isDriveScript(value: unknown): value is DriveScript {
|
|
65
|
+
return typeof value === "function"
|
|
66
|
+
}
|
package/src/cli/send.ts
CHANGED
|
@@ -1,48 +1,28 @@
|
|
|
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
|
-
if (
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
console.log(JSON.stringify(result.results[0]?.result, undefined, 2))
|
|
21
|
-
return
|
|
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((await resolveInstance(options.name)).endpoints.ui, options.commands)
|
|
9
|
+
if (
|
|
10
|
+
options.commands.length === 1 &&
|
|
11
|
+
["ui.screenshot", "ui.end-record"].includes(
|
|
12
|
+
options.commands[0]?.operation ?? "",
|
|
13
|
+
)
|
|
14
|
+
) {
|
|
15
|
+
console.log(result.results[0]?.result)
|
|
24
16
|
return
|
|
25
17
|
}
|
|
26
|
-
if (
|
|
27
|
-
|
|
18
|
+
if (
|
|
19
|
+
options.commands.length === 1 &&
|
|
20
|
+
["ui.state", "ui.start-record"].includes(
|
|
21
|
+
options.commands[0]?.operation ?? "",
|
|
22
|
+
)
|
|
23
|
+
) {
|
|
24
|
+
console.log(JSON.stringify(result.results[0]?.result, undefined, 2))
|
|
28
25
|
return
|
|
29
26
|
}
|
|
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
|
-
}
|
|
27
|
+
console.log("success")
|
|
48
28
|
}
|
package/src/cli/start.ts
CHANGED
|
@@ -1,56 +1,123 @@
|
|
|
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"
|
|
2
|
+
import { connectMockBackend } from "./mock-backend.js"
|
|
3
|
+
import { runScript } from "./script.js"
|
|
4
|
+
import { listenControl } from "./control.js"
|
|
6
5
|
import type { StartOptions } from "./types.js"
|
|
7
6
|
|
|
8
7
|
export async function start(options: StartOptions) {
|
|
9
|
-
if (options.campaign) return runCampaign(options)
|
|
10
8
|
const instance = await launchInstance({
|
|
11
|
-
name: options.name,
|
|
12
9
|
command: options.command,
|
|
13
10
|
dev: options.dev,
|
|
14
11
|
state: options.state,
|
|
12
|
+
scripted: options.script !== undefined,
|
|
15
13
|
visible: options.visible,
|
|
16
14
|
})
|
|
17
|
-
console.error(`opencode-drive: ${instance.manifest.name}`)
|
|
18
|
-
console.error(`opencode-drive: artifacts ${instance.manifest.artifacts}`)
|
|
19
|
-
console.error(`opencode-drive: send commands with opencode-drive send --name ${instance.manifest.name}`)
|
|
20
|
-
if (options.detach) {
|
|
21
|
-
await instance.waitForDrive("both")
|
|
22
|
-
await instance.detach()
|
|
23
|
-
return
|
|
24
|
-
}
|
|
25
15
|
const interrupt = () => void instance.stop()
|
|
16
|
+
let completed = false
|
|
17
|
+
let current: ReturnType<typeof run> | undefined
|
|
18
|
+
let restarting: Promise<void> | undefined
|
|
26
19
|
process.once("SIGINT", interrupt)
|
|
27
20
|
process.once("SIGTERM", interrupt)
|
|
21
|
+
const closeControl = options.visible
|
|
22
|
+
? await listenControl(() => {
|
|
23
|
+
if (restarting) return restarting
|
|
24
|
+
restarting = (async () => {
|
|
25
|
+
const previous = current
|
|
26
|
+
previous?.abort.abort(new Error("script restarted"))
|
|
27
|
+
await previous?.promise.catch(() => undefined)
|
|
28
|
+
await instance.restart()
|
|
29
|
+
current = run(options, instance)
|
|
30
|
+
await current.ready
|
|
31
|
+
})().finally(() => {
|
|
32
|
+
restarting = undefined
|
|
33
|
+
})
|
|
34
|
+
return restarting
|
|
35
|
+
})
|
|
36
|
+
: undefined
|
|
28
37
|
try {
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
const
|
|
32
|
-
|
|
33
|
-
if (options.commands.length === 1 && ["render", "end-record"].includes(options.commands[0]?.operation ?? "")) {
|
|
34
|
-
console.log(result.results[0]?.result)
|
|
35
|
-
return
|
|
36
|
-
}
|
|
37
|
-
if (options.commands.length === 1 && ["llm.pending", "state", "start-record"].includes(options.commands[0]?.operation ?? "")) {
|
|
38
|
-
console.log(JSON.stringify(result.results[0]?.result, undefined, 2))
|
|
39
|
-
return
|
|
40
|
-
}
|
|
41
|
-
console.log("success")
|
|
38
|
+
current = run(options, instance)
|
|
39
|
+
if (options.visible) {
|
|
40
|
+
const status = await instance.wait()
|
|
41
|
+
if (status !== 0) process.exitCode = status
|
|
42
42
|
return
|
|
43
43
|
}
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
await
|
|
47
|
-
|
|
44
|
+
while (true) {
|
|
45
|
+
const active: NonNullable<typeof current> = current
|
|
46
|
+
await active.promise
|
|
47
|
+
if (active !== current) continue
|
|
48
|
+
completed = true
|
|
49
|
+
break
|
|
48
50
|
}
|
|
49
|
-
const status = await instance.child.exited
|
|
50
|
-
if (status !== 0) process.exitCode = status
|
|
51
51
|
} finally {
|
|
52
52
|
process.off("SIGINT", interrupt)
|
|
53
53
|
process.off("SIGTERM", interrupt)
|
|
54
|
+
current?.abort.abort(new Error("opencode-drive stopped"))
|
|
55
|
+
await closeControl?.()
|
|
54
56
|
await instance.stop()
|
|
57
|
+
if (options.script && !options.visible)
|
|
58
|
+
report(instance, completed ? "completed" : undefined)
|
|
55
59
|
}
|
|
56
60
|
}
|
|
61
|
+
|
|
62
|
+
function run(
|
|
63
|
+
options: StartOptions,
|
|
64
|
+
instance: Awaited<ReturnType<typeof launchInstance>>,
|
|
65
|
+
) {
|
|
66
|
+
const abort = new AbortController()
|
|
67
|
+
const child = instance.child
|
|
68
|
+
let ready!: () => void
|
|
69
|
+
const readiness = new Promise<void>((resolve) => {
|
|
70
|
+
ready = resolve
|
|
71
|
+
})
|
|
72
|
+
return {
|
|
73
|
+
abort,
|
|
74
|
+
ready: readiness,
|
|
75
|
+
promise: (async () => {
|
|
76
|
+
await instance.waitForDrive("both")
|
|
77
|
+
if (options.script) {
|
|
78
|
+
const script = runScript(
|
|
79
|
+
options.script,
|
|
80
|
+
instance.artifacts,
|
|
81
|
+
instance.endpoints,
|
|
82
|
+
abort.signal,
|
|
83
|
+
)
|
|
84
|
+
ready()
|
|
85
|
+
await script
|
|
86
|
+
if (options.visible) {
|
|
87
|
+
await Promise.race([
|
|
88
|
+
child.exited,
|
|
89
|
+
new Promise<void>((resolve) => {
|
|
90
|
+
abort.signal.addEventListener("abort", () => resolve(), {
|
|
91
|
+
once: true,
|
|
92
|
+
})
|
|
93
|
+
}),
|
|
94
|
+
])
|
|
95
|
+
}
|
|
96
|
+
return
|
|
97
|
+
}
|
|
98
|
+
const mock = await connectMockBackend(instance.endpoints.backend)
|
|
99
|
+
ready()
|
|
100
|
+
abort.signal.addEventListener("abort", () => mock.close(), { once: true })
|
|
101
|
+
if (!options.visible) report(instance)
|
|
102
|
+
const status = await Promise.race([
|
|
103
|
+
child.exited,
|
|
104
|
+
new Promise<number>((resolve) => {
|
|
105
|
+
abort.signal.addEventListener("abort", () => resolve(0), {
|
|
106
|
+
once: true,
|
|
107
|
+
})
|
|
108
|
+
}),
|
|
109
|
+
])
|
|
110
|
+
mock.close()
|
|
111
|
+
if (status !== 0 && !abort.signal.aborted) process.exitCode = status
|
|
112
|
+
})(),
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function report(
|
|
117
|
+
instance: Awaited<ReturnType<typeof launchInstance>>,
|
|
118
|
+
status?: string,
|
|
119
|
+
) {
|
|
120
|
+
if (status) console.error(`opencode-drive: ${status}`)
|
|
121
|
+
console.error(`opencode-drive: artifacts ${instance.artifacts}`)
|
|
122
|
+
console.error(`opencode-drive: logs ${instance.logs}`)
|
|
123
|
+
}
|
package/src/cli/stop.ts
CHANGED
|
@@ -1,31 +1,7 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { request } from "./control.js"
|
|
2
|
+
import { resolveInstance } from "./registry.js"
|
|
2
3
|
|
|
3
|
-
export async function stop(name
|
|
4
|
-
|
|
5
|
-
if (!manifest.headless) throw new Error(`drive instance "${manifest.name}" is visible`)
|
|
6
|
-
process.kill(manifest.pid, "SIGTERM")
|
|
7
|
-
await Promise.race([waitForExit(manifest.pid), Bun.sleep(1_000)])
|
|
8
|
-
if (alive(manifest.pid)) process.kill(manifest.pid, "SIGKILL")
|
|
9
|
-
await waitForExit(manifest.pid)
|
|
10
|
-
await unregisterInstance(manifest.name, manifest.pid)
|
|
4
|
+
export async function stop(name: string) {
|
|
5
|
+
await request((await resolveInstance(name)).control, "stop")
|
|
11
6
|
console.log("success")
|
|
12
7
|
}
|
|
13
|
-
|
|
14
|
-
function waitForExit(pid: number) {
|
|
15
|
-
return new Promise<void>((resolve) => {
|
|
16
|
-
const check = () => {
|
|
17
|
-
if (!alive(pid)) return resolve()
|
|
18
|
-
setTimeout(check, 25)
|
|
19
|
-
}
|
|
20
|
-
check()
|
|
21
|
-
})
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
function alive(pid: number) {
|
|
25
|
-
try {
|
|
26
|
-
process.kill(pid, 0)
|
|
27
|
-
return true
|
|
28
|
-
} catch {
|
|
29
|
-
return false
|
|
30
|
-
}
|
|
31
|
-
}
|
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
|