opencode-drive 0.1.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.
Files changed (45) hide show
  1. package/README.md +90 -0
  2. package/bin/opencode-drive +2 -0
  3. package/package.json +45 -0
  4. package/src/cli/commands.ts +175 -0
  5. package/src/cli/describe.ts +14 -0
  6. package/src/cli/driver-runner.ts +39 -0
  7. package/src/cli/driver.ts +28 -0
  8. package/src/cli/index.ts +158 -0
  9. package/src/cli/instance.ts +218 -0
  10. package/src/cli/parse.ts +24 -0
  11. package/src/cli/registry.ts +120 -0
  12. package/src/cli/send.ts +48 -0
  13. package/src/cli/start.ts +56 -0
  14. package/src/cli/types.ts +46 -0
  15. package/src/client/backend.ts +184 -0
  16. package/src/client/client.ts +252 -0
  17. package/src/client/index.ts +17 -0
  18. package/src/client/protocol.ts +186 -0
  19. package/src/experimental/campaign-api.ts +34 -0
  20. package/src/experimental/campaign.ts +144 -0
  21. package/src/experimental/cli-campaign.ts +179 -0
  22. package/src/experimental/drive.ts +37 -0
  23. package/src/experimental/driver.ts +41 -0
  24. package/src/experimental/flow-driver.ts +189 -0
  25. package/src/experimental/flows/generate.ts +278 -0
  26. package/src/experimental/flows/index.ts +6 -0
  27. package/src/experimental/flows/properties.ts +51 -0
  28. package/src/experimental/flows/types.ts +47 -0
  29. package/src/experimental/flows/weights.ts +198 -0
  30. package/src/experimental/generators/config.ts +240 -0
  31. package/src/experimental/generators/filesystem.ts +95 -0
  32. package/src/experimental/generators/generate.ts +32 -0
  33. package/src/experimental/generators/index.ts +10 -0
  34. package/src/experimental/generators/initial-state.ts +37 -0
  35. package/src/experimental/generators/random.ts +35 -0
  36. package/src/experimental/hello-driver.ts +42 -0
  37. package/src/experimental/index.ts +3 -0
  38. package/src/experimental/model/derive.ts +119 -0
  39. package/src/experimental/model/index.ts +2 -0
  40. package/src/experimental/model/model.ts +42 -0
  41. package/src/experimental/opencode-simulation.ts +1 -0
  42. package/src/experimental/reproduce-stale-running-visible.ts +60 -0
  43. package/src/experimental/reproduce-stale-running.ts +26 -0
  44. package/src/experimental/stale-running-driver.ts +74 -0
  45. package/src/index.ts +1 -0
@@ -0,0 +1,218 @@
1
+ import { mkdir, rm } from "node:fs/promises"
2
+ import { tmpdir } from "node:os"
3
+ import { join, resolve } from "node:path"
4
+ import { registerInstance, registryDirectory, transferInstance, unregisterInstance } from "./registry.js"
5
+ import type { InstanceManifest } from "./types.js"
6
+
7
+ export interface LaunchOptions {
8
+ readonly name?: string
9
+ readonly command?: ReadonlyArray<string>
10
+ readonly dev?: string
11
+ readonly state?: string
12
+ readonly visible?: boolean
13
+ readonly env?: Readonly<Record<string, string>>
14
+ }
15
+
16
+ export async function launchInstance(options: LaunchOptions = {}) {
17
+ const name = options.name ?? "default"
18
+ const artifacts = resolve(join(tmpdir(), "opencode-drive", `${name}-${randomSuffix()}`))
19
+ const cwd = artifacts
20
+ const [uiPort, backendPort] = await Promise.all([freePort(), freePort()])
21
+ const endpoints = {
22
+ ui: `ws://127.0.0.1:${uiPort}`,
23
+ backend: `ws://127.0.0.1:${backendPort}`,
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,
35
+ }
36
+ await Promise.all([
37
+ mkdir(artifacts, { recursive: true }),
38
+ mkdir(cwd, { recursive: true }),
39
+ mkdir(join(artifacts, "home", ".cache"), { recursive: true }),
40
+ mkdir(join(artifacts, "home", ".config"), { recursive: true }),
41
+ mkdir(join(artifacts, "home", ".local", "share"), { recursive: true }),
42
+ mkdir(join(artifacts, "home", ".local", "state"), { recursive: true }),
43
+ ])
44
+ const state = options.state ? resolve(options.state) : join(artifacts, "state")
45
+ if (!options.state) {
46
+ await rm(state, { recursive: true, force: true })
47
+ await Promise.all([
48
+ mkdir(join(state, "files", ".git"), { recursive: true }),
49
+ mkdir(join(state, "files", ".opencode"), { recursive: true }),
50
+ ])
51
+ await Bun.write(
52
+ join(state, "files", ".opencode", "opencode.jsonc"),
53
+ `${JSON.stringify(
54
+ {
55
+ model: "simulation/sim-model",
56
+ permissions: [{ action: "*", resource: "*", effect: "allow" }],
57
+ providers: {
58
+ simulation: {
59
+ name: "Simulation",
60
+ request: { body: { apiKey: "sim-key" } },
61
+ models: {
62
+ "sim-model": {
63
+ name: "Simulated Model",
64
+ api: {
65
+ type: "aisdk",
66
+ package: "@ai-sdk/openai-compatible",
67
+ url: "https://api.openai.com/v1",
68
+ },
69
+ capabilities: { tools: true, input: ["text"], output: ["text"] },
70
+ limit: { context: 128000, output: 16000 },
71
+ },
72
+ },
73
+ },
74
+ },
75
+ },
76
+ undefined,
77
+ 2,
78
+ )}\n`,
79
+ )
80
+ }
81
+ await registerInstance(manifest)
82
+ const environment = cleanEnv({
83
+ ...process.env,
84
+ ...options.env,
85
+ DRIVE_REGISTRY_DIR: registryDirectory(),
86
+ OPENCODE_SIMULATE: "1",
87
+ OPENCODE_SIMULATE_STATE: state,
88
+ OPENCODE_DRIVE: name,
89
+ OPENCODE_DRIVE_RENDERER: options.visible ? "visible" : "headless",
90
+ OPENCODE_CONFIG_DIR: join(cwd, ".opencode"),
91
+ OPENCODE_DB: ":memory:",
92
+ XDG_CACHE_HOME: join(artifacts, "home", ".cache"),
93
+ XDG_CONFIG_HOME: join(artifacts, "home", ".config"),
94
+ XDG_DATA_HOME: join(artifacts, "home", ".local", "share"),
95
+ XDG_STATE_HOME: join(artifacts, "home", ".local", "state"),
96
+ })
97
+ const command = options.dev
98
+ ? await prepareDev(cwd, options.dev)
99
+ : options.command?.length
100
+ ? [...options.command]
101
+ : ["opencode2", "--standalone"]
102
+ const visible = options.visible === true
103
+ const child = Bun.spawn(command, {
104
+ cwd,
105
+ env: environment,
106
+ stdin: visible ? "inherit" : "ignore",
107
+ stdout: visible ? "inherit" : Bun.file(join(artifacts, "opencode.stdout.log")),
108
+ stderr: visible ? "inherit" : Bun.file(join(artifacts, "opencode.stderr.log")),
109
+ })
110
+ const lifecycle = { detached: false, stopping: undefined as Promise<void> | undefined }
111
+ return {
112
+ manifest,
113
+ child,
114
+ async waitForDrive(requirement: "ui" | "backend" | "both" = "both", timeout = 60_000) {
115
+ const urls = requirement === "both"
116
+ ? [endpoints.ui, endpoints.backend]
117
+ : [requirement === "ui" ? endpoints.ui : endpoints.backend]
118
+ await Promise.all(urls.map((url) => waitForWebSocket(url, child.exited, timeout)))
119
+ },
120
+ async detach() {
121
+ await transferInstance(manifest, child.pid)
122
+ child.unref()
123
+ lifecycle.detached = true
124
+ },
125
+ stop(force = false) {
126
+ if (lifecycle.detached) return Promise.resolve()
127
+ if (lifecycle.stopping) return lifecycle.stopping
128
+ lifecycle.stopping = (async () => {
129
+ if (force) {
130
+ if (child.exitCode === null) child.kill("SIGKILL")
131
+ } else if (child.exitCode === null) {
132
+ child.kill("SIGTERM")
133
+ await Promise.race([child.exited, Bun.sleep(1_000)])
134
+ }
135
+ if (child.exitCode === null) child.kill("SIGKILL")
136
+ await child.exited
137
+ await unregisterInstance(name, process.pid)
138
+ })()
139
+ return lifecycle.stopping
140
+ },
141
+ }
142
+ }
143
+
144
+ async function prepareDev(cwd: string, directory: string) {
145
+ const root = resolve(directory)
146
+ const entrypoint = join(root, "packages", "cli", "src", "index.ts")
147
+ if (!(await Bun.file(entrypoint).exists())) throw new Error(`OpenCode development entrypoint not found: ${entrypoint}`)
148
+ const solidPackage = join(root, "packages", "tui", "node_modules", "@opentui", "solid", "package.json")
149
+ if (!(await Bun.file(solidPackage).exists())) {
150
+ throw new Error(`OpenCode development dependency not found: ${solidPackage}; run bun install in ${root}`)
151
+ }
152
+ const info: unknown = await Bun.file(solidPackage).json()
153
+ if (!isPackageInfo(info)) throw new Error(`Invalid @opentui/solid package metadata: ${solidPackage}`)
154
+ await Bun.write(join(cwd, "package.json"), `${JSON.stringify({
155
+ private: true,
156
+ dependencies: { "@opentui/solid": info.version },
157
+ }, undefined, 2)}\n`)
158
+ const install = Bun.spawn([process.execPath, "install"], {
159
+ cwd,
160
+ stdin: "ignore",
161
+ stdout: "inherit",
162
+ stderr: "inherit",
163
+ })
164
+ const status = await install.exited
165
+ if (status !== 0) throw new Error(`bun install failed in ${cwd} with status ${status}`)
166
+ return [
167
+ process.execPath,
168
+ "--conditions=browser",
169
+ "--preload=@opentui/solid/preload",
170
+ entrypoint,
171
+ "--standalone",
172
+ ]
173
+ }
174
+
175
+ function isPackageInfo(value: unknown): value is { readonly version: string } {
176
+ return typeof value === "object" && value !== null && "version" in value && typeof value.version === "string"
177
+ }
178
+
179
+ async function freePort() {
180
+ const server = Bun.serve({ port: 0, hostname: "127.0.0.1", fetch: () => new Response() })
181
+ const port = server.port
182
+ await server.stop(true)
183
+ return port
184
+ }
185
+
186
+ async function waitForWebSocket(url: string, exited: Promise<number>, timeout: number) {
187
+ const deadline = Date.now() + timeout
188
+ while (Date.now() < deadline) {
189
+ const connected = await Promise.race([
190
+ open(url).then((socket) => {
191
+ socket.close()
192
+ return true
193
+ }).catch(() => false),
194
+ exited.then((code) => {
195
+ throw new Error(`OpenCode exited with status ${code} before ${url} became ready`)
196
+ }),
197
+ ])
198
+ if (connected) return
199
+ await Bun.sleep(50)
200
+ }
201
+ throw new Error(`timed out waiting for drive endpoint ${url}`)
202
+ }
203
+
204
+ function open(url: string) {
205
+ return new Promise<WebSocket>((resolveSocket, reject) => {
206
+ const socket = new WebSocket(url)
207
+ socket.addEventListener("open", () => resolveSocket(socket), { once: true })
208
+ socket.addEventListener("error", () => reject(new Error(`cannot connect to ${url}`)), { once: true })
209
+ })
210
+ }
211
+
212
+ function randomSuffix() {
213
+ return crypto.randomUUID().slice(0, 6)
214
+ }
215
+
216
+ function cleanEnv(env: Readonly<Record<string, string | undefined>>) {
217
+ return Object.fromEntries(Object.entries(env).filter((entry): entry is [string, string] => entry[1] !== undefined))
218
+ }
@@ -0,0 +1,24 @@
1
+ import { commandAcceptsValue } from "./commands.js"
2
+ import type { DriveCommand } from "./types.js"
3
+
4
+ export function extractCommands(args: ReadonlyArray<string>) {
5
+ const commands: DriveCommand[] = []
6
+ const remaining: string[] = []
7
+ const separator = args.indexOf("--")
8
+ const cli = separator === -1 ? args : args.slice(0, separator)
9
+ const app = separator === -1 ? [] : args.slice(separator + 1)
10
+
11
+ for (let index = 0; index < cli.length; index++) {
12
+ const flag = cli[index]!
13
+ if (!flag.startsWith("--command.")) {
14
+ remaining.push(flag)
15
+ continue
16
+ }
17
+ const operation = flag.slice("--command.".length)
18
+ const acceptsValue = commandAcceptsValue(operation)
19
+ const value = acceptsValue ? cli[++index] : undefined
20
+ if (acceptsValue && (value === undefined || value.startsWith("--"))) throw new Error(`${flag} requires a value`)
21
+ commands.push({ operation, ...(value === undefined ? {} : { value }) })
22
+ }
23
+ return { args: remaining, app, commands }
24
+ }
@@ -0,0 +1,120 @@
1
+ import { homedir } from "node:os"
2
+ import { basename, join } from "node:path"
3
+ import { mkdir, open, readdir, rename, rm } from "node:fs/promises"
4
+ import type { InstanceManifest } from "./types.js"
5
+
6
+ export function registryDirectory() {
7
+ if (process.env.DRIVE_REGISTRY_DIR) return process.env.DRIVE_REGISTRY_DIR
8
+ return join(process.env.XDG_STATE_HOME ?? join(homedir(), ".local", "state"), "opencode-drive", "instances")
9
+ }
10
+
11
+ export function manifestPath(name: string) {
12
+ return join(registryDirectory(), `${validateName(name)}.json`)
13
+ }
14
+
15
+ export async function registerInstance(manifest: InstanceManifest) {
16
+ await mkdir(registryDirectory(), { recursive: true })
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
+ }
35
+ }
36
+
37
+ export async function unregisterInstance(name: string, pid: number) {
38
+ const file = manifestPath(name)
39
+ const manifest = await readManifest(file)
40
+ if (manifest?.pid !== pid) return
41
+ await rm(file, { force: true })
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)
51
+ }
52
+
53
+ export async function resolveInstance(name?: string) {
54
+ const instances = await listInstances()
55
+ if (name) {
56
+ const instance = instances.find((item) => item.name === name)
57
+ if (!instance) throw new Error(`drive instance "${name}" was not found`)
58
+ return instance
59
+ }
60
+ if (instances.length === 1) return instances[0]!
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(", ")})`)
63
+ }
64
+
65
+ export async function listInstances() {
66
+ await mkdir(registryDirectory(), { recursive: true })
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))
78
+ }
79
+
80
+ async function readManifest(file: string): Promise<InstanceManifest | undefined> {
81
+ if (!(await Bun.file(file).exists())) return undefined
82
+ try {
83
+ const value = await Bun.file(file).json()
84
+ if (!isManifest(value)) return undefined
85
+ return value
86
+ } catch {
87
+ await rm(file, { force: true })
88
+ return undefined
89
+ }
90
+ }
91
+
92
+ function isManifest(value: unknown): value is InstanceManifest {
93
+ if (typeof value !== "object" || value === null) return false
94
+ if (!("version" in value) || value.version !== 1) return false
95
+ if (!("name" in value) || typeof value.name !== "string") return false
96
+ 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
+ if (!("artifacts" in value) || typeof value.artifacts !== "string") return false
102
+ if (!("endpoints" in value) || typeof value.endpoints !== "object" || value.endpoints === null) return false
103
+ return "ui" in value.endpoints && typeof value.endpoints.ui === "string" && "backend" in value.endpoints && typeof value.endpoints.backend === "string"
104
+ }
105
+
106
+ function processAlive(pid: number) {
107
+ try {
108
+ process.kill(pid, 0)
109
+ return true
110
+ } catch {
111
+ return false
112
+ }
113
+ }
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,48 @@
1
+ import { tmpdir } from "node:os"
2
+ import { join, resolve } from "node:path"
3
+ import { defaultBackendPort, defaultPort } from "../client/index.js"
4
+ import { executeCommands } from "./commands.js"
5
+ import { runDriver } from "./driver.js"
6
+ import { resolveInstance } from "./registry.js"
7
+ import type { SendOptions } from "./types.js"
8
+
9
+ export async function send(options: SendOptions) {
10
+ const manifest = options.name
11
+ ? await resolveInstance(options.name)
12
+ : await resolveInstance("default").catch(() => defaultManifest())
13
+ if (options.commands.length > 0) {
14
+ const result = await executeCommands(manifest, options.commands)
15
+ if (options.commands.length === 1 && ["render", "end-record"].includes(options.commands[0]?.operation ?? "")) {
16
+ console.log(result.results[0]?.result)
17
+ return
18
+ }
19
+ if (options.commands.length === 1 && ["llm.pending", "state", "start-record"].includes(options.commands[0]?.operation ?? "")) {
20
+ console.log(JSON.stringify(result.results[0]?.result, undefined, 2))
21
+ return
22
+ }
23
+ console.log("success")
24
+ return
25
+ }
26
+ if (options.driver) {
27
+ await runDriver(resolve(options.driver), manifest)
28
+ return
29
+ }
30
+ console.log(JSON.stringify(manifest, undefined, 2))
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
+ }
48
+ }
@@ -0,0 +1,56 @@
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
+ import { launchInstance } from "./instance.js"
6
+ import type { StartOptions } from "./types.js"
7
+
8
+ export async function start(options: StartOptions) {
9
+ if (options.campaign) return runCampaign(options)
10
+ const instance = await launchInstance({
11
+ name: options.name,
12
+ command: options.command,
13
+ dev: options.dev,
14
+ state: options.state,
15
+ visible: options.visible,
16
+ })
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
+ const interrupt = () => void instance.stop()
26
+ process.once("SIGINT", interrupt)
27
+ process.once("SIGTERM", interrupt)
28
+ try {
29
+ if (options.commands.length > 0) {
30
+ await instance.waitForDrive("both")
31
+ const result = await executeCommands(instance.manifest, options.commands)
32
+ await instance.stop()
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")
42
+ return
43
+ }
44
+ if (options.driver) {
45
+ await instance.waitForDrive("both")
46
+ await runDriver(resolve(options.driver), instance.manifest)
47
+ return
48
+ }
49
+ const status = await instance.child.exited
50
+ if (status !== 0) process.exitCode = status
51
+ } finally {
52
+ process.off("SIGINT", interrupt)
53
+ process.off("SIGTERM", interrupt)
54
+ await instance.stop()
55
+ }
56
+ }
@@ -0,0 +1,46 @@
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
+ export interface DriveCommand {
17
+ readonly operation: string
18
+ readonly value?: string
19
+ }
20
+
21
+ export interface CommonOptions {
22
+ readonly name?: string
23
+ readonly driver?: string
24
+ readonly commands: ReadonlyArray<DriveCommand>
25
+ }
26
+
27
+ export interface StartOptions extends CommonOptions {
28
+ readonly kind: "start"
29
+ readonly detach: boolean
30
+ readonly campaign?: string
31
+ readonly seed: number
32
+ readonly caseIndex?: number
33
+ readonly count?: number
34
+ readonly concurrency: number
35
+ readonly visible: boolean
36
+ readonly dev?: string
37
+ readonly state?: string
38
+ readonly anchor?: string
39
+ readonly command: ReadonlyArray<string>
40
+ }
41
+
42
+ export interface SendOptions extends CommonOptions {
43
+ readonly kind: "send"
44
+ }
45
+
46
+ export type CliOptions = StartOptions | SendOptions