opencode-drive 0.1.8 → 0.1.9

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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://json.schemastore.org/package.json",
3
3
  "name": "opencode-drive",
4
- "version": "0.1.8",
4
+ "version": "0.1.9",
5
5
  "description": "Drive real and simulated OpenCode instances",
6
6
  "license": "MIT",
7
7
  "type": "module",
package/src/cli/index.ts CHANGED
@@ -45,10 +45,6 @@ const startCommand = Command.make(
45
45
  Flag.optional,
46
46
  Flag.withDescription("Path to an OpenCode development checkout"),
47
47
  ),
48
- state: Flag.string("state").pipe(
49
- Flag.optional,
50
- Flag.withDescription("Simulation snapshot containing files/"),
51
- ),
52
48
  },
53
49
  (config) =>
54
50
  execute(() =>
@@ -173,7 +169,6 @@ function toStartOptions(
173
169
  readonly visible: boolean
174
170
  readonly record: boolean
175
171
  readonly dev: Option.Option<string>
176
- readonly state: Option.Option<string>
177
172
  },
178
173
  commands: ReadonlyArray<DriveCommand>,
179
174
  app: ReadonlyArray<string>,
@@ -188,7 +183,6 @@ function toStartOptions(
188
183
  visible: config.visible,
189
184
  record: config.record,
190
185
  dev: Option.getOrUndefined(config.dev),
191
- state: Option.getOrUndefined(config.state),
192
186
  command: app,
193
187
  }
194
188
  if (options.dev !== undefined && app.length > 0)
@@ -1,17 +1,18 @@
1
- import { mkdir, rm } from "node:fs/promises"
1
+ import { mkdir } from "node:fs/promises"
2
2
  import { tmpdir } from "node:os"
3
3
  import { join, resolve } from "node:path"
4
4
  import { ensureMediaDirectory } from "./media.js"
5
+ import type { DriveScriptSetup } from "./script.js"
5
6
 
6
7
  export interface LaunchOptions {
7
8
  readonly name: string
8
9
  readonly command?: ReadonlyArray<string>
9
10
  readonly dev?: string
10
- readonly state?: string
11
11
  readonly scripted?: boolean
12
12
  readonly visible?: boolean
13
13
  readonly record?: boolean
14
14
  readonly env?: Readonly<Record<string, string>>
15
+ readonly setup?: DriveScriptSetup
15
16
  }
16
17
 
17
18
  export async function launchInstance(options: LaunchOptions) {
@@ -47,62 +48,57 @@ export async function launchInstance(options: LaunchOptions) {
47
48
  )}\n`,
48
49
  )
49
50
  await writeDriveManifest()
50
- const state = options.state
51
- ? resolve(options.state)
52
- : join(artifacts, "state")
53
- if (!options.state) {
54
- await rm(state, { recursive: true, force: true })
55
- await Promise.all([
56
- mkdir(join(state, "files", ".git"), { recursive: true }),
57
- mkdir(join(state, "files", ".opencode"), { recursive: true }),
58
- mkdir(join(state, "files", "src"), { recursive: true }),
59
- ])
60
- await Promise.all([
61
- Bun.write(
62
- join(state, "files", ".opencode", "opencode.jsonc"),
63
- `${JSON.stringify(
64
- {
65
- model: "simulation/gpt-sim-model",
66
- permissions: [{ action: "*", resource: "*", effect: "allow" }],
67
- providers: {
68
- simulation: {
69
- name: "Simulation",
70
- package: "aisdk:@ai-sdk/openai-compatible",
71
- settings: { baseURL: "https://api.openai.com/v1" },
72
- request: { body: { apiKey: "sim-key" } },
73
- models: {
74
- "gpt-sim-model": {
75
- name: "Simulated Model",
76
- capabilities: {
77
- tools: true,
78
- input: ["text"],
79
- output: ["text"],
80
- },
81
- limit: { context: 128000, output: 16000 },
51
+ const files = join(artifacts, "files")
52
+ await Promise.all([
53
+ mkdir(join(files, ".git"), { recursive: true }),
54
+ mkdir(join(files, ".opencode"), { recursive: true }),
55
+ mkdir(join(files, "src"), { recursive: true }),
56
+ ])
57
+ await Promise.all([
58
+ Bun.write(
59
+ join(files, ".opencode", "opencode.jsonc"),
60
+ `${JSON.stringify(
61
+ {
62
+ model: "simulation/gpt-sim-model",
63
+ permissions: [{ action: "*", resource: "*", effect: "allow" }],
64
+ providers: {
65
+ simulation: {
66
+ name: "Simulation",
67
+ package: "aisdk:@ai-sdk/openai-compatible",
68
+ settings: { baseURL: "https://api.openai.com/v1" },
69
+ request: { body: { apiKey: "sim-key" } },
70
+ models: {
71
+ "gpt-sim-model": {
72
+ name: "Simulated Model",
73
+ capabilities: {
74
+ tools: true,
75
+ input: ["text"],
76
+ output: ["text"],
82
77
  },
78
+ limit: { context: 128000, output: 16000 },
83
79
  },
84
80
  },
85
81
  },
86
82
  },
87
- undefined,
88
- 2,
89
- )}\n`,
90
- ),
91
- Bun.write(
92
- join(state, "files", "src", "garden.js"),
93
- 'export function greet(name) {\n return `Hello, ${name}.`\n}\n',
94
- ),
95
- ])
96
- }
83
+ },
84
+ undefined,
85
+ 2,
86
+ )}\n`,
87
+ ),
88
+ Bun.write(
89
+ join(files, "src", "garden.js"),
90
+ 'export function greet(name) {\n return `Hello, ${name}.`\n}\n',
91
+ ),
92
+ ])
93
+ await options.setup?.({ directory: files })
97
94
  const environment = cleanEnv({
98
95
  ...process.env,
99
96
  ...options.env,
100
97
  OPENCODE_SIMULATE: "1",
101
- OPENCODE_SIMULATE_STATE: state,
102
98
  DRIVE_REGISTRY_DIR: drive,
103
99
  OPENCODE_DRIVE: options.name,
104
100
  OPENCODE_DRIVE_RENDERER: options.visible ? "visible" : "headless",
105
- OPENCODE_CONFIG_DIR: join(artifacts, ".opencode"),
101
+ OPENCODE_CONFIG_DIR: join(files, ".opencode"),
106
102
  OPENCODE_DB: ":memory:",
107
103
  OPENCODE_LOG_LEVEL: !options.visible
108
104
  ? "INFO"
@@ -114,13 +110,13 @@ export async function launchInstance(options: LaunchOptions) {
114
110
  XDG_STATE_HOME: join(artifacts, "home", ".local", "state"),
115
111
  })
116
112
  const command = options.dev
117
- ? await prepareDev(artifacts, options.dev)
113
+ ? await prepareDev(files, options.dev)
118
114
  : options.command?.length
119
115
  ? [...options.command]
120
116
  : ["opencode2"]
121
117
  const spawn = () =>
122
118
  Bun.spawn(command, {
123
- cwd: artifacts,
119
+ cwd: files,
124
120
  env: environment,
125
121
  stdin: options.visible ? "inherit" : "ignore",
126
122
  stdout: !options.visible
package/src/cli/script.ts CHANGED
@@ -15,23 +15,43 @@ export interface ScriptContext {
15
15
 
16
16
  export type DriveScript = (context: ScriptContext) => void | Promise<void>
17
17
 
18
+ export interface ScriptSetupContext {
19
+ readonly directory: string
20
+ }
21
+
22
+ export type DriveScriptSetup = (
23
+ context: ScriptSetupContext,
24
+ ) => void | Promise<void>
25
+
26
+ export interface LoadedDriveScript {
27
+ readonly run: DriveScript
28
+ readonly setup?: DriveScriptSetup
29
+ }
30
+
18
31
  export function defineScript(script: DriveScript) {
19
32
  return script
20
33
  }
21
34
 
35
+ export async function loadScript(file: string): Promise<LoadedDriveScript> {
36
+ const module: { readonly default?: unknown; readonly setup?: unknown } =
37
+ await import(pathToFileURL(resolve(file)).href)
38
+ if (!isDriveScript(module.default))
39
+ throw new Error("script must default-export a function")
40
+ if (module.setup !== undefined && !isDriveScriptSetup(module.setup))
41
+ throw new Error("script setup export must be a function")
42
+ return {
43
+ run: module.default,
44
+ ...(module.setup === undefined ? {} : { setup: module.setup }),
45
+ }
46
+ }
47
+
22
48
  export async function runScript(
23
- file: string,
49
+ script: DriveScript,
24
50
  artifacts: string,
25
51
  endpoints: { readonly ui: string; readonly backend: string },
26
52
  signal: AbortSignal,
27
53
  onScreenshot?: (path: string) => void,
28
54
  ) {
29
- const module: { readonly default?: unknown } = await import(
30
- pathToFileURL(resolve(file)).href
31
- )
32
- const script = module.default
33
- if (!isDriveScript(script))
34
- throw new Error("script must default-export a function")
35
55
  const ui = await connectSimulation({ url: endpoints.ui, onScreenshot })
36
56
  const backend = await connectBackendSimulation({
37
57
  url: endpoints.backend,
@@ -76,3 +96,7 @@ async function waitForEditor(ui: SimulationClient, signal: AbortSignal) {
76
96
  function isDriveScript(value: unknown): value is DriveScript {
77
97
  return typeof value === "function"
78
98
  }
99
+
100
+ function isDriveScriptSetup(value: unknown): value is DriveScriptSetup {
101
+ return typeof value === "function"
102
+ }
package/src/cli/start.ts CHANGED
@@ -5,7 +5,8 @@ import { connectSimulation } from "../client/index.js"
5
5
  import { exportRecording } from "../recording/index.js"
6
6
  import { connectMockBackend } from "./mock-backend.js"
7
7
  import { createResponseSettings } from "./response-generator.js"
8
- import { runScript } from "./script.js"
8
+ import { loadScript, runScript } from "./script.js"
9
+ import type { DriveScript } from "./script.js"
9
10
  import { listenControl } from "./control.js"
10
11
  import {
11
12
  controlPath,
@@ -21,15 +22,16 @@ import type { StartOptions } from "./types.js"
21
22
  export async function start(options: StartOptions) {
22
23
  if (!options.visible && !options.script && !options.daemon)
23
24
  return startDetached(options)
25
+ const script = options.script ? await loadScript(options.script) : undefined
24
26
  const responses = createResponseSettings()
25
27
  const instance = await launchInstance({
26
28
  name: options.name,
27
29
  command: options.command,
28
30
  dev: options.dev,
29
- state: options.state,
30
31
  scripted: options.script !== undefined,
31
32
  visible: options.visible,
32
33
  record: options.record,
34
+ setup: script?.setup,
33
35
  })
34
36
  await register({
35
37
  version: 1,
@@ -102,7 +104,9 @@ export async function start(options: StartOptions) {
102
104
  driveReady = false
103
105
  await instance.restart()
104
106
  recording = undefined
105
- current = run(options, instance, responses, (path) => screenshots.push(path))
107
+ current = run(options, instance, responses, script?.run, (path) =>
108
+ screenshots.push(path),
109
+ )
106
110
  await current.ready
107
111
  driveReady = true
108
112
  await markReady(options.name, process.pid)
@@ -119,7 +123,9 @@ export async function start(options: StartOptions) {
119
123
  return responses.update(input)
120
124
  },
121
125
  })
122
- current = run(options, instance, responses, (path) => screenshots.push(path))
126
+ current = run(options, instance, responses, script?.run, (path) =>
127
+ screenshots.push(path),
128
+ )
123
129
  await current.ready
124
130
  driveReady = true
125
131
  await markReady(options.name, process.pid)
@@ -213,7 +219,6 @@ async function startDetached(options: StartOptions) {
213
219
  options.name,
214
220
  ...(options.script ? ["--script", options.script] : []),
215
221
  ...(options.dev ? ["--dev", options.dev] : []),
216
- ...(options.state ? ["--state", options.state] : []),
217
222
  ...(options.record ? ["--record"] : []),
218
223
  ...(options.command.length ? ["--", ...options.command] : []),
219
224
  ],
@@ -261,6 +266,7 @@ function run(
261
266
  options: StartOptions,
262
267
  instance: Awaited<ReturnType<typeof launchInstance>>,
263
268
  responses: ReturnType<typeof createResponseSettings>,
269
+ driveScript: DriveScript | undefined,
264
270
  onScreenshot: (path: string) => void,
265
271
  ) {
266
272
  const abort = new AbortController()
@@ -274,9 +280,9 @@ function run(
274
280
  ready: readiness,
275
281
  promise: (async () => {
276
282
  await instance.waitForDrive("both")
277
- if (options.script) {
283
+ if (driveScript) {
278
284
  const script = runScript(
279
- options.script,
285
+ driveScript,
280
286
  instance.artifacts,
281
287
  instance.endpoints,
282
288
  abort.signal,
package/src/cli/types.ts CHANGED
@@ -11,7 +11,6 @@ export interface StartOptions {
11
11
  readonly visible: boolean
12
12
  readonly record: boolean
13
13
  readonly dev?: string
14
- readonly state?: string
15
14
  readonly command: ReadonlyArray<string>
16
15
  }
17
16
 
@@ -19,7 +19,7 @@ export default defineScript(async ({ artifacts, backend, ui }) => {
19
19
  index: 0,
20
20
  id: "call_read",
21
21
  name: "read",
22
- input: { filePath: join(artifacts, "state", "files", "src", "garden.js") },
22
+ input: { filePath: join(artifacts, "files", "src", "garden.js") },
23
23
  },
24
24
  ])
25
25
  await backend.finish(request.id, "tool-calls")
package/src/index.ts CHANGED
@@ -1,3 +1,8 @@
1
1
  export * from "./client/index.js"
2
2
  export { defineScript } from "./cli/script.js"
3
- export type { DriveScript, ScriptContext } from "./cli/script.js"
3
+ export type {
4
+ DriveScript,
5
+ DriveScriptSetup,
6
+ ScriptContext,
7
+ ScriptSetupContext,
8
+ } from "./cli/script.js"