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/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 { send } from "./send.js"
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 name = Flag.string("name").pipe(Flag.optional, Flag.withDescription("Instance name"))
16
- const driver = Flag.string("driver").pipe(Flag.optional, Flag.withDescription("TypeScript driver module"))
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("start", {
19
- name,
20
- driver,
21
- campaign: Flag.string("campaign").pipe(Flag.optional, Flag.withDescription("Campaign module")),
22
- seed: Flag.integer("seed").pipe(Flag.withDefault(Date.now() % 1_000_000)),
23
- caseIndex: Flag.integer("case").pipe(
24
- Flag.filter((value) => value >= 0, () => "Case must be non-negative"),
25
- Flag.optional,
26
- ),
27
- count: Flag.integer("count").pipe(
28
- Flag.filter((value) => value > 0, () => "Count must be greater than zero"),
29
- Flag.optional,
30
- ),
31
- concurrency: Flag.integer("concurrency").pipe(
32
- Flag.filter((value) => value > 0, () => "Concurrency must be greater than zero"),
33
- Flag.withDefault(1),
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
- visible: Flag.boolean("visible").pipe(Flag.withDescription("Show OpenCode in the terminal")),
36
- detach: Flag.boolean("detach").pipe(Flag.withDescription("Keep OpenCode running in the background (default)")),
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
- { command: "opencode-drive start --name demo --visible", description: "Launch a visible simulated instance" },
44
- { command: "opencode-drive start --name demo --detach", description: "Launch a background simulated instance" },
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 sendCommand = Command.make("send", { name, driver }, (config) =>
49
- execute(() => send(toSendOptions(config, extracted.commands, extracted.app)))).pipe(
50
- Command.withDescription("Connect to an existing drive-enabled OpenCode instance"),
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 describeCommand = Command.make("describe", { name }, (config) =>
60
- execute(() => describe(Option.getOrUndefined(config.name)))).pipe(
61
- Command.withDescription("Describe a registered OpenCode instance"),
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)))).pipe(
66
- Command.withDescription("Stop a registered headless OpenCode instance"),
67
- )
105
+ execute(() => stop(Option.getOrUndefined(config.name))),
106
+ ).pipe(Command.withDescription("Stop a named OpenCode instance"))
68
107
 
69
- const restartCommand = Command.make("restart", { name }, (config) =>
70
- execute(() => restart(Option.getOrUndefined(config.name)))).pipe(
71
- Command.withDescription("Restart a registered OpenCode client"),
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 apiCommand = Command.make("api", {}, () => execute(api)).pipe(
75
- Command.withDescription("Print the OpenCode drive protocol"),
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([startCommand, sendCommand, describeCommand, stopCommand, restartCommand, apiCommand]),
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 name: Option.Option<string>
91
- readonly driver: Option.Option<string>
92
- readonly campaign: Option.Option<string>
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
- const driver = Option.getOrUndefined(config.driver)
107
- const campaign = Option.getOrUndefined(config.campaign)
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: Option.getOrUndefined(config.name),
112
- driver,
113
- campaign,
114
- seed: config.seed,
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
- config: { readonly name: Option.Option<string>; readonly driver: Option.Option<string> },
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
- const options = {
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) => Effect.sync(() => {
162
- console.error(`error: ${error instanceof Error ? error.message : String(error)}`)
163
- process.exitCode = 1
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(`opencode-drive: ${error instanceof Error ? error.message : String(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
  }