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/index.ts
CHANGED
|
@@ -2,82 +2,152 @@
|
|
|
2
2
|
import { NodeRuntime, NodeServices } from "@effect/platform-node"
|
|
3
3
|
import { Effect, Option } from "effect"
|
|
4
4
|
import { Command, Flag } from "effect/unstable/cli"
|
|
5
|
-
import {
|
|
6
|
-
import { describe } from "./describe.js"
|
|
5
|
+
import { api } from "./api.js"
|
|
7
6
|
import { extractCommands } from "./parse.js"
|
|
7
|
+
import { list } from "./list.js"
|
|
8
|
+
import { logs } from "./logs.js"
|
|
9
|
+
import { restart } from "./restart.js"
|
|
10
|
+
import { responses } from "./responses.js"
|
|
11
|
+
import { send } from "./send.js"
|
|
8
12
|
import { start } from "./start.js"
|
|
9
13
|
import { stop } from "./stop.js"
|
|
10
|
-
import { api } from "./api.js"
|
|
11
|
-
import { restart } from "./restart.js"
|
|
12
14
|
import type { DriveCommand, SendOptions, StartOptions } from "./types.js"
|
|
13
15
|
|
|
14
16
|
const extracted = extract()
|
|
15
|
-
const
|
|
16
|
-
|
|
17
|
+
const startName = Flag.string("name").pipe(
|
|
18
|
+
Flag.withDefault("default"),
|
|
19
|
+
Flag.withDescription("Instance name"),
|
|
20
|
+
)
|
|
21
|
+
const name = Flag.string("name").pipe(
|
|
22
|
+
Flag.optional,
|
|
23
|
+
Flag.withDescription("Instance name (inferred when exactly one is running)"),
|
|
24
|
+
)
|
|
17
25
|
|
|
18
|
-
const startCommand = Command.make(
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
Flag.
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
Flag.
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
Flag.
|
|
33
|
-
|
|
26
|
+
const startCommand = Command.make(
|
|
27
|
+
"start",
|
|
28
|
+
{
|
|
29
|
+
name: startName,
|
|
30
|
+
daemon: Flag.boolean("daemon").pipe(
|
|
31
|
+
Flag.withDescription("Run as detached instance owner"),
|
|
32
|
+
),
|
|
33
|
+
script: Flag.string("script").pipe(
|
|
34
|
+
Flag.optional,
|
|
35
|
+
Flag.withDescription("JavaScript or TypeScript automation module"),
|
|
36
|
+
),
|
|
37
|
+
visible: Flag.boolean("visible").pipe(
|
|
38
|
+
Flag.withDescription("Show OpenCode in the terminal"),
|
|
39
|
+
),
|
|
40
|
+
dev: Flag.string("dev").pipe(
|
|
41
|
+
Flag.optional,
|
|
42
|
+
Flag.withDescription("Path to an OpenCode development checkout"),
|
|
43
|
+
),
|
|
44
|
+
state: Flag.string("state").pipe(
|
|
45
|
+
Flag.optional,
|
|
46
|
+
Flag.withDescription("Simulation snapshot containing files/"),
|
|
47
|
+
),
|
|
48
|
+
},
|
|
49
|
+
(config) =>
|
|
50
|
+
execute(() =>
|
|
51
|
+
start(toStartOptions(config, extracted.commands, extracted.app)),
|
|
52
|
+
),
|
|
53
|
+
).pipe(
|
|
54
|
+
Command.withDescription("Launch a local simulated OpenCode instance"),
|
|
55
|
+
Command.withExamples([
|
|
56
|
+
{
|
|
57
|
+
command: "opencode-drive start",
|
|
58
|
+
description: "Launch headless OpenCode on the default ports",
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
command: "opencode-drive start --visible",
|
|
62
|
+
description: "Launch visible OpenCode on the default ports",
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
command: "opencode-drive start --script ./drive.ts",
|
|
66
|
+
description: "Launch headless OpenCode and run a script",
|
|
67
|
+
},
|
|
68
|
+
]),
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
const sendCommand = Command.make("send", { name }, (config) =>
|
|
72
|
+
execute(() =>
|
|
73
|
+
send(
|
|
74
|
+
toSendOptions(
|
|
75
|
+
Option.getOrUndefined(config.name),
|
|
76
|
+
extracted.commands,
|
|
77
|
+
extracted.app,
|
|
78
|
+
),
|
|
79
|
+
),
|
|
34
80
|
),
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
dev: Flag.string("dev").pipe(Flag.optional, Flag.withDescription("Path to an OpenCode development checkout")),
|
|
38
|
-
state: Flag.string("state").pipe(Flag.optional, Flag.withDescription("Simulation snapshot containing files/")),
|
|
39
|
-
anchor: Flag.string("anchor").pipe(Flag.optional),
|
|
40
|
-
}, (config) => execute(() => start(toStartOptions(config, extracted.commands, extracted.app)))).pipe(
|
|
41
|
-
Command.withDescription("Launch and own a local simulated OpenCode instance"),
|
|
81
|
+
).pipe(
|
|
82
|
+
Command.withDescription("Send UI commands to OpenCode on the default port"),
|
|
42
83
|
Command.withExamples([
|
|
43
|
-
{
|
|
44
|
-
|
|
84
|
+
{
|
|
85
|
+
command:
|
|
86
|
+
'opencode-drive send --command.ui.type \'{"text":"hello"}\' --command.ui.state',
|
|
87
|
+
description: "Execute an ordered UI command batch",
|
|
88
|
+
},
|
|
45
89
|
]),
|
|
46
90
|
)
|
|
47
91
|
|
|
48
|
-
const
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
Command.withExamples([
|
|
52
|
-
{
|
|
53
|
-
command: "opencode-drive send --name demo --command.ui.type '{\"text\":\"hello\"}' --command.ui.state",
|
|
54
|
-
description: "Execute an ordered command batch",
|
|
55
|
-
},
|
|
56
|
-
]),
|
|
57
|
-
)
|
|
92
|
+
const apiCommand = Command.make("api", {}, () => execute(api)).pipe(
|
|
93
|
+
Command.withDescription("Print the OpenCode drive UI protocol"),
|
|
94
|
+
)
|
|
58
95
|
|
|
59
|
-
const
|
|
60
|
-
execute(() =>
|
|
61
|
-
|
|
62
|
-
|
|
96
|
+
const restartCommand = Command.make("restart", { name }, (config) =>
|
|
97
|
+
execute(() => restart(Option.getOrUndefined(config.name))),
|
|
98
|
+
).pipe(
|
|
99
|
+
Command.withDescription(
|
|
100
|
+
"Restart a named OpenCode instance and rerun its script",
|
|
101
|
+
),
|
|
102
|
+
)
|
|
63
103
|
|
|
64
104
|
const stopCommand = Command.make("stop", { name }, (config) =>
|
|
65
|
-
execute(() => stop(Option.getOrUndefined(config.name)))
|
|
66
|
-
|
|
67
|
-
)
|
|
105
|
+
execute(() => stop(Option.getOrUndefined(config.name))),
|
|
106
|
+
).pipe(Command.withDescription("Stop a named OpenCode instance"))
|
|
68
107
|
|
|
69
|
-
const
|
|
70
|
-
execute(() =>
|
|
71
|
-
|
|
72
|
-
)
|
|
108
|
+
const logsCommand = Command.make("logs", { name }, (config) =>
|
|
109
|
+
execute(() => logs(Option.getOrUndefined(config.name))),
|
|
110
|
+
).pipe(Command.withDescription("List log files for a named OpenCode instance"))
|
|
73
111
|
|
|
74
|
-
const
|
|
75
|
-
Command.withDescription("
|
|
112
|
+
const listCommand = Command.make("list", {}, () => execute(list)).pipe(
|
|
113
|
+
Command.withDescription("List active OpenCode instances"),
|
|
76
114
|
)
|
|
77
115
|
|
|
116
|
+
const responsesCommand = Command.make(
|
|
117
|
+
"responses",
|
|
118
|
+
{
|
|
119
|
+
name,
|
|
120
|
+
types: Flag.string("types").pipe(
|
|
121
|
+
Flag.optional,
|
|
122
|
+
Flag.withDescription("Comma-delimited response types"),
|
|
123
|
+
),
|
|
124
|
+
tools: Flag.string("tools").pipe(
|
|
125
|
+
Flag.optional,
|
|
126
|
+
Flag.withDescription("Comma-delimited tool names, or * for all tools"),
|
|
127
|
+
),
|
|
128
|
+
},
|
|
129
|
+
(config) =>
|
|
130
|
+
execute(() =>
|
|
131
|
+
responses({
|
|
132
|
+
name: Option.getOrUndefined(config.name),
|
|
133
|
+
types: Option.getOrUndefined(config.types),
|
|
134
|
+
tools: Option.getOrUndefined(config.tools),
|
|
135
|
+
}),
|
|
136
|
+
),
|
|
137
|
+
).pipe(Command.withDescription("Configure simulated LLM response generation"))
|
|
138
|
+
|
|
78
139
|
const root = Command.make("opencode-drive").pipe(
|
|
79
140
|
Command.withDescription("Drive real and simulated OpenCode instances"),
|
|
80
|
-
Command.withSubcommands([
|
|
141
|
+
Command.withSubcommands([
|
|
142
|
+
startCommand,
|
|
143
|
+
sendCommand,
|
|
144
|
+
listCommand,
|
|
145
|
+
responsesCommand,
|
|
146
|
+
logsCommand,
|
|
147
|
+
restartCommand,
|
|
148
|
+
stopCommand,
|
|
149
|
+
apiCommand,
|
|
150
|
+
]),
|
|
81
151
|
)
|
|
82
152
|
|
|
83
153
|
Command.runWith(root, { version: "0.1.0" })(extracted.args).pipe(
|
|
@@ -87,81 +157,52 @@ Command.runWith(root, { version: "0.1.0" })(extracted.args).pipe(
|
|
|
87
157
|
|
|
88
158
|
function toStartOptions(
|
|
89
159
|
config: {
|
|
90
|
-
readonly
|
|
91
|
-
readonly
|
|
92
|
-
readonly
|
|
93
|
-
readonly seed: number
|
|
94
|
-
readonly caseIndex: Option.Option<number>
|
|
95
|
-
readonly count: Option.Option<number>
|
|
96
|
-
readonly concurrency: number
|
|
160
|
+
readonly script: Option.Option<string>
|
|
161
|
+
readonly name: string
|
|
162
|
+
readonly daemon: boolean
|
|
97
163
|
readonly visible: boolean
|
|
98
|
-
readonly detach: boolean
|
|
99
164
|
readonly dev: Option.Option<string>
|
|
100
165
|
readonly state: Option.Option<string>
|
|
101
|
-
readonly anchor: Option.Option<string>
|
|
102
166
|
},
|
|
103
167
|
commands: ReadonlyArray<DriveCommand>,
|
|
104
168
|
app: ReadonlyArray<string>,
|
|
105
169
|
): StartOptions {
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
const visible = config.visible
|
|
170
|
+
if (commands.length > 0)
|
|
171
|
+
throw new Error("start does not accept command flags; use send or --script")
|
|
109
172
|
const options = {
|
|
110
173
|
kind: "start" as const,
|
|
111
|
-
name:
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
caseIndex: Option.getOrUndefined(config.caseIndex),
|
|
116
|
-
count: Option.getOrUndefined(config.count),
|
|
117
|
-
concurrency: config.concurrency,
|
|
118
|
-
visible,
|
|
119
|
-
detach: !visible && (config.detach || (driver === undefined && campaign === undefined && commands.length === 0)),
|
|
174
|
+
name: config.name,
|
|
175
|
+
daemon: config.daemon,
|
|
176
|
+
script: Option.getOrUndefined(config.script),
|
|
177
|
+
visible: config.visible,
|
|
120
178
|
dev: Option.getOrUndefined(config.dev),
|
|
121
179
|
state: Option.getOrUndefined(config.state),
|
|
122
|
-
anchor: Option.getOrUndefined(config.anchor),
|
|
123
180
|
command: app,
|
|
124
|
-
commands,
|
|
125
|
-
}
|
|
126
|
-
assertExecutionMode(options.driver, options.campaign, commands)
|
|
127
|
-
if (options.dev !== undefined && app.length > 0) throw new Error("--dev cannot be combined with a command after --")
|
|
128
|
-
if (options.caseIndex !== undefined && options.campaign === undefined) throw new Error("--case requires --campaign")
|
|
129
|
-
if (options.visible && options.campaign !== undefined && options.caseIndex === undefined) {
|
|
130
|
-
throw new Error("visible campaign runs require --case")
|
|
131
|
-
}
|
|
132
|
-
if (config.detach && (options.driver !== undefined || options.campaign !== undefined || commands.length > 0)) {
|
|
133
|
-
throw new Error("--detach cannot be combined with --driver, --campaign, or command flags")
|
|
134
181
|
}
|
|
182
|
+
if (options.dev !== undefined && app.length > 0)
|
|
183
|
+
throw new Error("--dev cannot be combined with a command after --")
|
|
135
184
|
return options
|
|
136
185
|
}
|
|
137
186
|
|
|
138
187
|
function toSendOptions(
|
|
139
|
-
|
|
188
|
+
name: string | undefined,
|
|
140
189
|
commands: ReadonlyArray<DriveCommand>,
|
|
141
190
|
app: ReadonlyArray<string>,
|
|
142
191
|
): SendOptions {
|
|
143
192
|
if (app.length > 0) throw new Error("send does not accept a command after --")
|
|
144
|
-
|
|
145
|
-
kind: "send" as const,
|
|
146
|
-
name: Option.getOrUndefined(config.name),
|
|
147
|
-
driver: Option.getOrUndefined(config.driver),
|
|
148
|
-
commands,
|
|
149
|
-
}
|
|
150
|
-
assertExecutionMode(options.driver, undefined, commands)
|
|
151
|
-
return options
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
function assertExecutionMode(driver: string | undefined, campaign: string | undefined, commands: ReadonlyArray<DriveCommand>) {
|
|
155
|
-
const count = Number(driver !== undefined) + Number(campaign !== undefined) + Number(commands.length > 0)
|
|
156
|
-
if (count > 1) throw new Error("command flags, --driver, and --campaign are mutually exclusive")
|
|
193
|
+
return { kind: "send", name, commands }
|
|
157
194
|
}
|
|
158
195
|
|
|
159
196
|
function execute(task: () => Promise<void>) {
|
|
160
197
|
return Effect.tryPromise({ try: task, catch: (error) => error }).pipe(
|
|
161
|
-
Effect.catch((error) =>
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
198
|
+
Effect.catch((error) =>
|
|
199
|
+
Effect.sync(() => {
|
|
200
|
+
console.error(
|
|
201
|
+
`error: ${error instanceof Error ? error.message : String(error)}`,
|
|
202
|
+
)
|
|
203
|
+
process.exitCode = 1
|
|
204
|
+
}),
|
|
205
|
+
),
|
|
165
206
|
)
|
|
166
207
|
}
|
|
167
208
|
|
|
@@ -169,7 +210,9 @@ function extract() {
|
|
|
169
210
|
try {
|
|
170
211
|
return extractCommands(process.argv.slice(2))
|
|
171
212
|
} catch (error) {
|
|
172
|
-
console.error(
|
|
213
|
+
console.error(
|
|
214
|
+
`opencode-drive: ${error instanceof Error ? error.message : String(error)}`,
|
|
215
|
+
)
|
|
173
216
|
return process.exit(1)
|
|
174
217
|
}
|
|
175
218
|
}
|