opencode-drive 0.1.2 → 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 +57 -38
- package/package.json +1 -1
- package/src/cli/commands.ts +101 -68
- package/src/cli/control.ts +43 -0
- package/src/cli/describe.ts +4 -5
- package/src/cli/index.ts +83 -110
- package/src/cli/instance.ts +193 -148
- package/src/cli/mock-backend.ts +25 -0
- package/src/cli/registry.ts +41 -83
- package/src/cli/restart.ts +3 -66
- package/src/cli/script.ts +66 -0
- package/src/cli/send.ts +19 -39
- package/src/cli/start.ts +93 -68
- package/src/cli/stop.ts +4 -28
- package/src/cli/types.ts +7 -30
- package/src/client/backend.ts +94 -32
- package/src/client/protocol.ts +115 -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/driver-runner.ts +0 -39
- package/src/cli/driver.ts +0 -28
- package/src/experimental/cli-campaign.ts +0 -179
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
|
-
export async function restart(name
|
|
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)
|
|
4
|
+
export async function restart(name: string) {
|
|
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,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,98 +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
|
-
import { join } from "node:path"
|
|
8
6
|
|
|
9
7
|
export async function start(options: StartOptions) {
|
|
10
|
-
if (options.campaign) return runCampaign(options)
|
|
11
8
|
const instance = await launchInstance({
|
|
12
|
-
name: options.name,
|
|
13
9
|
command: options.command,
|
|
14
10
|
dev: options.dev,
|
|
15
11
|
state: options.state,
|
|
12
|
+
scripted: options.script !== undefined,
|
|
16
13
|
visible: options.visible,
|
|
17
14
|
})
|
|
18
|
-
console.error(`opencode-drive: ${instance.manifest.name}`)
|
|
19
|
-
console.error(`opencode-drive: artifacts ${instance.manifest.artifacts}`)
|
|
20
|
-
console.error(`opencode-drive: send commands with opencode-drive send --name ${instance.manifest.name}`)
|
|
21
|
-
if (options.detach) {
|
|
22
|
-
await instance.waitForDrive("both")
|
|
23
|
-
await instance.detach()
|
|
24
|
-
return
|
|
25
|
-
}
|
|
26
15
|
const interrupt = () => void instance.stop()
|
|
27
|
-
|
|
16
|
+
let completed = false
|
|
17
|
+
let current: ReturnType<typeof run> | undefined
|
|
18
|
+
let restarting: Promise<void> | undefined
|
|
28
19
|
process.once("SIGINT", interrupt)
|
|
29
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
|
|
30
37
|
try {
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
const
|
|
34
|
-
|
|
35
|
-
if (options.commands.length === 1 && ["ui.screenshot", "ui.end-record"].includes(options.commands[0]?.operation ?? "")) {
|
|
36
|
-
console.log(result.results[0]?.result)
|
|
37
|
-
return
|
|
38
|
-
}
|
|
39
|
-
if (options.commands.length === 1 && ["llm.pending", "ui.state", "ui.start-record"].includes(options.commands[0]?.operation ?? "")) {
|
|
40
|
-
console.log(JSON.stringify(result.results[0]?.result, undefined, 2))
|
|
41
|
-
return
|
|
42
|
-
}
|
|
43
|
-
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
|
|
44
42
|
return
|
|
45
43
|
}
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
await
|
|
49
|
-
|
|
44
|
+
while (true) {
|
|
45
|
+
const active: NonNullable<typeof current> = current
|
|
46
|
+
await active.promise
|
|
47
|
+
if (active !== current) continue
|
|
48
|
+
completed = true
|
|
49
|
+
break
|
|
50
50
|
}
|
|
51
|
-
const status = await instance.wait()
|
|
52
|
-
if (status !== 0) process.exitCode = status
|
|
53
51
|
} finally {
|
|
54
52
|
process.off("SIGINT", interrupt)
|
|
55
53
|
process.off("SIGTERM", interrupt)
|
|
56
|
-
|
|
54
|
+
current?.abort.abort(new Error("opencode-drive stopped"))
|
|
55
|
+
await closeControl?.()
|
|
57
56
|
await instance.stop()
|
|
57
|
+
if (options.script && !options.visible)
|
|
58
|
+
report(instance, completed ? "completed" : undefined)
|
|
58
59
|
}
|
|
59
60
|
}
|
|
60
61
|
|
|
61
|
-
function
|
|
62
|
-
|
|
63
|
-
const response = join(instance.manifest.artifacts, "restart-response.json")
|
|
64
|
-
const state = { token: undefined as string | undefined, busy: false }
|
|
65
|
-
const timer = setInterval(() => {
|
|
66
|
-
if (state.busy) return
|
|
67
|
-
state.busy = true
|
|
68
|
-
void handleVisibleRestart(instance, request, response, state).finally(() => {
|
|
69
|
-
state.busy = false
|
|
70
|
-
})
|
|
71
|
-
}, 50)
|
|
72
|
-
return () => clearInterval(timer)
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
async function handleVisibleRestart(
|
|
62
|
+
function run(
|
|
63
|
+
options: StartOptions,
|
|
76
64
|
instance: Awaited<ReturnType<typeof launchInstance>>,
|
|
77
|
-
request: string,
|
|
78
|
-
response: string,
|
|
79
|
-
state: { token: string | undefined },
|
|
80
65
|
) {
|
|
81
|
-
const
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
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
|
+
})(),
|
|
93
113
|
}
|
|
94
114
|
}
|
|
95
115
|
|
|
96
|
-
function
|
|
97
|
-
|
|
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}`)
|
|
98
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
|