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/src/cli/index.ts CHANGED
@@ -2,82 +2,87 @@
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 { send } from "./send.js"
8
8
  import { start } from "./start.js"
9
- import { stop } from "./stop.js"
10
- import { api } from "./api.js"
11
9
  import { restart } from "./restart.js"
12
10
  import type { DriveCommand, SendOptions, StartOptions } from "./types.js"
13
11
 
14
12
  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
13
 
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),
34
- ),
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"),
14
+ const startCommand = Command.make(
15
+ "start",
16
+ {
17
+ script: Flag.string("script").pipe(
18
+ Flag.optional,
19
+ Flag.withDescription("JavaScript or TypeScript automation module"),
20
+ ),
21
+ visible: Flag.boolean("visible").pipe(
22
+ Flag.withDescription("Show OpenCode in the terminal"),
23
+ ),
24
+ dev: Flag.string("dev").pipe(
25
+ Flag.optional,
26
+ Flag.withDescription("Path to an OpenCode development checkout"),
27
+ ),
28
+ state: Flag.string("state").pipe(
29
+ Flag.optional,
30
+ Flag.withDescription("Simulation snapshot containing files/"),
31
+ ),
32
+ },
33
+ (config) =>
34
+ execute(() =>
35
+ start(toStartOptions(config, extracted.commands, extracted.app)),
36
+ ),
37
+ ).pipe(
38
+ Command.withDescription("Launch a local simulated OpenCode instance"),
42
39
  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" },
40
+ {
41
+ command: "opencode-drive start",
42
+ description: "Launch headless OpenCode on the default ports",
43
+ },
44
+ {
45
+ command: "opencode-drive start --visible",
46
+ description: "Launch visible OpenCode on the default ports",
47
+ },
48
+ {
49
+ command: "opencode-drive start --script ./drive.ts",
50
+ description: "Launch headless OpenCode and run a script",
51
+ },
45
52
  ]),
46
53
  )
47
54
 
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
- )
58
-
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
- )
63
-
64
- 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
- )
68
-
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
- )
55
+ const sendCommand = Command.make("send", {}, () =>
56
+ execute(() => send(toSendOptions(extracted.commands, extracted.app))),
57
+ ).pipe(
58
+ Command.withDescription("Send UI commands to OpenCode on the default port"),
59
+ Command.withExamples([
60
+ {
61
+ command:
62
+ 'opencode-drive send --command.ui.type \'{"text":"hello"}\' --command.ui.state',
63
+ description: "Execute an ordered UI command batch",
64
+ },
65
+ ]),
66
+ )
73
67
 
74
68
  const apiCommand = Command.make("api", {}, () => execute(api)).pipe(
75
- Command.withDescription("Print the OpenCode drive protocol"),
69
+ Command.withDescription("Print the OpenCode drive UI protocol"),
70
+ )
71
+
72
+ const restartCommand = Command.make("restart", {}, () => execute(restart)).pipe(
73
+ Command.withDescription(
74
+ "Restart the active visible OpenCode instance and rerun its script",
75
+ ),
76
76
  )
77
77
 
78
78
  const root = Command.make("opencode-drive").pipe(
79
79
  Command.withDescription("Drive real and simulated OpenCode instances"),
80
- Command.withSubcommands([startCommand, sendCommand, describeCommand, stopCommand, restartCommand, apiCommand]),
80
+ Command.withSubcommands([
81
+ startCommand,
82
+ sendCommand,
83
+ restartCommand,
84
+ apiCommand,
85
+ ]),
81
86
  )
82
87
 
83
88
  Command.runWith(root, { version: "0.1.0" })(extracted.args).pipe(
@@ -87,81 +92,47 @@ Command.runWith(root, { version: "0.1.0" })(extracted.args).pipe(
87
92
 
88
93
  function toStartOptions(
89
94
  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
95
+ readonly script: Option.Option<string>
97
96
  readonly visible: boolean
98
- readonly detach: boolean
99
97
  readonly dev: Option.Option<string>
100
98
  readonly state: Option.Option<string>
101
- readonly anchor: Option.Option<string>
102
99
  },
103
100
  commands: ReadonlyArray<DriveCommand>,
104
101
  app: ReadonlyArray<string>,
105
102
  ): StartOptions {
106
- const driver = Option.getOrUndefined(config.driver)
107
- const campaign = Option.getOrUndefined(config.campaign)
108
- const visible = config.visible
103
+ if (commands.length > 0)
104
+ throw new Error("start does not accept command flags; use send or --script")
109
105
  const options = {
110
106
  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)),
107
+ script: Option.getOrUndefined(config.script),
108
+ visible: config.visible,
120
109
  dev: Option.getOrUndefined(config.dev),
121
110
  state: Option.getOrUndefined(config.state),
122
- anchor: Option.getOrUndefined(config.anchor),
123
111
  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
112
  }
113
+ if (options.dev !== undefined && app.length > 0)
114
+ throw new Error("--dev cannot be combined with a command after --")
135
115
  return options
136
116
  }
137
117
 
138
118
  function toSendOptions(
139
- config: { readonly name: Option.Option<string>; readonly driver: Option.Option<string> },
140
119
  commands: ReadonlyArray<DriveCommand>,
141
120
  app: ReadonlyArray<string>,
142
121
  ): SendOptions {
143
122
  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")
123
+ return { kind: "send", commands }
157
124
  }
158
125
 
159
126
  function execute(task: () => Promise<void>) {
160
127
  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
- })),
128
+ Effect.catch((error) =>
129
+ Effect.sync(() => {
130
+ console.error(
131
+ `error: ${error instanceof Error ? error.message : String(error)}`,
132
+ )
133
+ process.exitCode = 1
134
+ }),
135
+ ),
165
136
  )
166
137
  }
167
138
 
@@ -169,7 +140,9 @@ function extract() {
169
140
  try {
170
141
  return extractCommands(process.argv.slice(2))
171
142
  } catch (error) {
172
- console.error(`opencode-drive: ${error instanceof Error ? error.message : String(error)}`)
143
+ console.error(
144
+ `opencode-drive: ${error instanceof Error ? error.message : String(error)}`,
145
+ )
173
146
  return process.exit(1)
174
147
  }
175
148
  }