opencode-drive 0.1.1 → 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.
@@ -1,18 +1,38 @@
1
- import { connectBackendSimulation, connectSimulation, type OpenedExchange } from "../client/index.js"
1
+ import {
2
+ connectBackendSimulation,
3
+ connectSimulation,
4
+ type OpenedExchange,
5
+ } from "../client/index.js"
2
6
 
3
7
  const uiUrl = process.env.OPENCODE_SIMULATION_UI_WS
4
8
  const backendUrl = process.env.OPENCODE_SIMULATION_BACKEND_WS
5
9
 
6
10
  const ui = await connectSimulation(uiUrl ? { url: uiUrl } : undefined)
7
- const backend = await connectBackendSimulation(backendUrl ? { url: backendUrl } : undefined)
11
+ const backend = await connectBackendSimulation(
12
+ backendUrl ? { url: backendUrl } : undefined,
13
+ )
8
14
 
9
15
  console.log("ui:", ui.url)
10
16
  console.log("backend:", backend.url)
11
17
 
18
+ let completed!: () => void
19
+ const responseCompleted = new Promise<void>((resolve) => {
20
+ completed = resolve
21
+ })
22
+
12
23
  await backend.attach(async (request: OpenedExchange) => {
13
- console.log("llm.request:", JSON.stringify({ id: request.id, model: (request.body as { model?: string }).model }))
14
- await backend.chunk(request.id, [{ type: "textDelta", text: "Hello from opencode-probe." }])
24
+ console.log(
25
+ "llm.request:",
26
+ JSON.stringify({
27
+ id: request.id,
28
+ model: (request.body as { model?: string }).model,
29
+ }),
30
+ )
31
+ await backend.chunk(request.id, [
32
+ { type: "textDelta", text: "Hello from opencode-probe." },
33
+ ])
15
34
  await backend.finish(request.id, "stop")
35
+ completed()
16
36
  })
17
37
 
18
38
  for (let attempt = 0; attempt < 60; attempt++) {
@@ -22,20 +42,11 @@ for (let attempt = 0; attempt < 60; attempt++) {
22
42
  if (attempt === 59) throw new Error("prompt editor did not become ready")
23
43
  }
24
44
 
25
- await ui.typeText(process.argv.slice(2).join(" ") || "Hello from opencode-probe")
45
+ await ui.typeText(
46
+ process.argv.slice(2).join(" ") || "Hello from opencode-probe",
47
+ )
26
48
  await ui.pressEnter()
27
-
28
- for (let attempt = 0; attempt < 30; attempt++) {
29
- await new Promise((resolve) => setTimeout(resolve, 500))
30
- if ((await backend.pendingExchanges()).exchanges.length === 0) {
31
- console.log("response complete")
32
- ui.close()
33
- backend.close()
34
- process.exit(0)
35
- }
36
- }
37
-
38
- console.log(await ui.state())
49
+ await responseCompleted
50
+ console.log("response complete")
39
51
  ui.close()
40
52
  backend.close()
41
- throw new Error("assistant reply did not render")
@@ -1,16 +1,31 @@
1
- import { connectBackendSimulation, connectSimulation, type OpenedExchange } from "../client/index.js"
2
- import { flowProperties, type FlowPropertyContext, type FlowResult, type FlowScenario, type TurnOutcome } from "./flows/index.js"
1
+ import {
2
+ connectBackendSimulation,
3
+ connectSimulation,
4
+ type OpenedExchange,
5
+ } from "../client/index.js"
6
+ import {
7
+ flowProperties,
8
+ type FlowPropertyContext,
9
+ type FlowResult,
10
+ type FlowScenario,
11
+ type TurnOutcome,
12
+ } from "./flows/index.js"
3
13
 
4
14
  const scenarioPath = process.argv[2]
5
15
  const resultPath = process.argv[3]
6
- if (scenarioPath === undefined) throw new Error("usage: flow-driver SCENARIO [RESULT]")
16
+ if (scenarioPath === undefined)
17
+ throw new Error("usage: flow-driver SCENARIO [RESULT]")
7
18
 
8
19
  const scenario: FlowScenario = await Bun.file(scenarioPath).json()
9
20
  const stepDelay = Number(process.env.OPENCODE_PROBE_STEP_DELAY ?? "0")
10
21
  const chunkDelay = Number(process.env.OPENCODE_PROBE_CHUNK_DELAY ?? "30")
11
22
  const started = Date.now()
12
- const ui = await connectSimulation({ url: requiredEnv("OPENCODE_SIMULATION_UI_WS") })
13
- const backend = await connectBackendSimulation({ url: requiredEnv("OPENCODE_SIMULATION_BACKEND_WS") })
23
+ const ui = await connectSimulation({
24
+ url: requiredEnv("OPENCODE_SIMULATION_UI_WS"),
25
+ })
26
+ const backend = await connectBackendSimulation({
27
+ url: requiredEnv("OPENCODE_SIMULATION_BACKEND_WS"),
28
+ })
14
29
  const responses = scenario.turns.flatMap((turn) => turn.responses)
15
30
  let assistantExchanges = 0
16
31
  let responseCursor = 0
@@ -25,19 +40,27 @@ await backend.attach(async (request: OpenedExchange) => {
25
40
  try {
26
41
  if (isTitleRequest(request)) {
27
42
  titleExchanges++
28
- await backend.chunk(request.id, [{ type: "textDelta", text: scenario.name }])
43
+ await backend.chunk(request.id, [
44
+ { type: "textDelta", text: scenario.name },
45
+ ])
29
46
  await backend.finish(request.id, "stop")
30
47
  return
31
48
  }
32
49
  if (isSubagentRequest(request)) {
33
50
  subagentExchanges++
34
- await backend.chunk(request.id, [{ type: "textDelta", text: "The nested simulation fixture is consistent." }])
51
+ await backend.chunk(request.id, [
52
+ {
53
+ type: "textDelta",
54
+ text: "The nested simulation fixture is consistent.",
55
+ },
56
+ ])
35
57
  await backend.finish(request.id, "stop")
36
58
  return
37
59
  }
38
60
  const response = responses[responseCursor++]
39
61
  assistantExchanges++
40
- if (response === undefined) throw new Error(`unexpected assistant exchange ${assistantExchanges}`)
62
+ if (response === undefined)
63
+ throw new Error(`unexpected assistant exchange ${assistantExchanges}`)
41
64
  active.add(request.id)
42
65
  for (const chunk of response.chunks) {
43
66
  await backend.chunk(request.id, chunk)
@@ -46,9 +69,11 @@ await backend.attach(async (request: OpenedExchange) => {
46
69
  }
47
70
  if (chunkDelay > 0) await Bun.sleep(Math.max(chunkDelay * 3, 100))
48
71
  if (response.terminal === "disconnect") await backend.disconnect(request.id)
49
- else if (response.terminal !== "invalid-provider-event") await backend.finish(request.id, response.finish)
72
+ else if (response.terminal !== "invalid-provider-event")
73
+ await backend.finish(request.id, response.finish)
50
74
  } catch (error) {
51
- if (!interrupted.has(request.id)) failure = error instanceof Error ? error : new Error(String(error))
75
+ if (!interrupted.has(request.id))
76
+ failure = error instanceof Error ? error : new Error(String(error))
52
77
  } finally {
53
78
  active.delete(request.id)
54
79
  }
@@ -59,7 +84,10 @@ try {
59
84
  let expectedExchanges = 0
60
85
  for (const turn of scenario.turns) {
61
86
  if (failure !== undefined) throw failure
62
- await waitFor("prompt editor before submit", async () => (await ui.state()).focused.editor)
87
+ await waitFor(
88
+ "prompt editor before submit",
89
+ async () => (await ui.state()).focused.editor,
90
+ )
63
91
  expectedExchanges += turn.responses.length
64
92
  const beforeChunks = chunksSent
65
93
  await ui.typeText(turn.prompt)
@@ -67,26 +95,38 @@ try {
67
95
  await runProperties("afterSubmit", { turn, ui, backend, waitFor })
68
96
  if (turn.interaction === "double-submit") await ui.pressEnter()
69
97
  let outcome: TurnOutcome = "completed"
70
- const hasTools = turn.responses.some((response) => (response.toolNames?.length ?? 0) > 0)
71
- const settleTools = turn.interaction === "interrupt" || !hasTools
72
- ? undefined
73
- : settleBlockingUi(expectedExchanges)
98
+ const hasTools = turn.responses.some(
99
+ (response) => (response.toolNames?.length ?? 0) > 0,
100
+ )
101
+ const settleTools =
102
+ turn.interaction === "interrupt" || !hasTools
103
+ ? undefined
104
+ : settleBlockingUi(expectedExchanges)
74
105
  if (turn.interaction === "steer") {
75
- await waitFor("active stream", async () => active.size > 0 && chunksSent > beforeChunks)
106
+ await waitFor(
107
+ "active stream",
108
+ async () => active.size > 0 && chunksSent > beforeChunks,
109
+ )
76
110
  await ui.typeText(turn.steerPrompt ?? "Also inspect the active boundary.")
77
111
  await ui.pressEnter()
78
112
  }
79
113
  if (turn.interaction === "interrupt") {
80
- await waitFor("interruptible stream", async () => active.size > 0 && chunksSent > beforeChunks)
114
+ await waitFor(
115
+ "interruptible stream",
116
+ async () => active.size > 0 && chunksSent > beforeChunks,
117
+ )
81
118
  for (const id of active) interrupted.add(id)
82
119
  await ui.pressKey("escape")
83
120
  await ui.pressKey("escape")
84
- await waitFor("interrupted provider drain", async () => active.size === 0 && (await backend.pendingExchanges()).exchanges.length === 0)
121
+ await waitFor("interrupted provider drain", async () => active.size === 0)
85
122
  responseCursor = expectedExchanges
86
123
  outcome = "interrupted"
87
124
  } else if (turn.interaction === "provider-drop") {
88
- await waitFor("dropped provider exchange", async () => responseCursor >= expectedExchanges)
89
- await waitFor("dropped provider drain", async () => active.size === 0 && (await backend.pendingExchanges()).exchanges.length === 0)
125
+ await waitFor(
126
+ "dropped provider exchange",
127
+ async () => responseCursor >= expectedExchanges,
128
+ )
129
+ await waitFor("dropped provider drain", async () => active.size === 0)
90
130
  outcome = "provider-error"
91
131
  } else {
92
132
  await settleTools
@@ -96,11 +136,16 @@ try {
96
136
  return responseCursor >= expectedExchanges
97
137
  })
98
138
  }
99
- await runProperties("afterTerminal", { turn, ui, backend, outcome, waitFor })
139
+ await runProperties("afterTerminal", {
140
+ turn,
141
+ ui,
142
+ backend,
143
+ outcome,
144
+ waitFor,
145
+ })
100
146
  if (stepDelay > 0) await Bun.sleep(stepDelay)
101
147
  }
102
- await waitFor("provider idle", async () => (await backend.pendingExchanges()).exchanges.length === 0)
103
- const trace = await ui.traceExport()
148
+ await waitFor("provider idle", async () => active.size === 0)
104
149
  const result: FlowResult = {
105
150
  seed: scenario.seed,
106
151
  name: scenario.name,
@@ -108,22 +153,29 @@ try {
108
153
  assistantExchanges,
109
154
  subagentExchanges,
110
155
  titleExchanges,
111
- traceRecords: trace.records.length,
112
156
  durationMs: Date.now() - started,
113
157
  finalState: await ui.state(),
114
158
  }
115
- if (resultPath !== undefined) await Bun.write(resultPath, `${JSON.stringify(result, undefined, 2)}\n`)
159
+ if (resultPath !== undefined)
160
+ await Bun.write(resultPath, `${JSON.stringify(result, undefined, 2)}\n`)
116
161
  console.log(JSON.stringify({ ...result, finalState: undefined }))
117
162
  } catch (error) {
118
163
  if (resultPath !== undefined) {
119
- await Bun.write(`${resultPath}.failure.json`, `${JSON.stringify({
120
- error: error instanceof Error ? error.message : String(error),
121
- assistantExchanges,
122
- subagentExchanges,
123
- titleExchanges,
124
- pendingExchanges: (await backend.pendingExchanges()).exchanges,
125
- state: await ui.state(),
126
- }, undefined, 2)}\n`)
164
+ await Bun.write(
165
+ `${resultPath}.failure.json`,
166
+ `${JSON.stringify(
167
+ {
168
+ error: error instanceof Error ? error.message : String(error),
169
+ assistantExchanges,
170
+ subagentExchanges,
171
+ titleExchanges,
172
+ activeExchanges: [...active],
173
+ state: await ui.state(),
174
+ },
175
+ undefined,
176
+ 2,
177
+ )}\n`,
178
+ )
127
179
  }
128
180
  throw error
129
181
  } finally {
@@ -138,25 +190,51 @@ function requiredEnv(name: string) {
138
190
  }
139
191
 
140
192
  function isTitleRequest(request: OpenedExchange) {
141
- if (typeof request.body !== "object" || request.body === null || !("messages" in request.body)) return false
193
+ if (
194
+ typeof request.body !== "object" ||
195
+ request.body === null ||
196
+ !("messages" in request.body)
197
+ )
198
+ return false
142
199
  const messages = request.body.messages
143
200
  if (!Array.isArray(messages)) return false
144
201
  const first = messages[0]
145
- if (typeof first !== "object" || first === null || !("content" in first)) return false
146
- return typeof first.content === "string" && first.content.includes("You are a title generator")
202
+ if (typeof first !== "object" || first === null || !("content" in first))
203
+ return false
204
+ return (
205
+ typeof first.content === "string" &&
206
+ first.content.includes("You are a title generator")
207
+ )
147
208
  }
148
209
 
149
210
  function isSubagentRequest(request: OpenedExchange) {
150
- if (typeof request.body !== "object" || request.body === null || !("messages" in request.body)) return false
211
+ if (
212
+ typeof request.body !== "object" ||
213
+ request.body === null ||
214
+ !("messages" in request.body)
215
+ )
216
+ return false
151
217
  const messages = request.body.messages
152
218
  if (!Array.isArray(messages)) return false
153
219
  return messages.some((message) => {
154
- if (typeof message !== "object" || message === null || !("content" in message)) return false
155
- return typeof message.content === "string" && message.content.includes("Inspect the simulation fixture.")
220
+ if (
221
+ typeof message !== "object" ||
222
+ message === null ||
223
+ !("content" in message)
224
+ )
225
+ return false
226
+ return (
227
+ typeof message.content === "string" &&
228
+ message.content.includes("Inspect the simulation fixture.")
229
+ )
156
230
  })
157
231
  }
158
232
 
159
- async function waitFor(label: string, check: () => Promise<boolean>, timeout = 30_000) {
233
+ async function waitFor(
234
+ label: string,
235
+ check: () => Promise<boolean>,
236
+ timeout = 30_000,
237
+ ) {
160
238
  const deadline = Date.now() + timeout
161
239
  while (Date.now() < deadline) {
162
240
  if (await check()) return
@@ -165,16 +243,22 @@ async function waitFor(label: string, check: () => Promise<boolean>, timeout = 3
165
243
  throw new Error(`timed out waiting for ${label}`)
166
244
  }
167
245
 
168
- async function runProperties(stage: "afterSubmit" | "afterTerminal", context: FlowPropertyContext) {
246
+ async function runProperties(
247
+ stage: "afterSubmit" | "afterTerminal",
248
+ context: FlowPropertyContext,
249
+ ) {
169
250
  for (const property of flowProperties) {
170
251
  const check = property[stage]
171
252
  if (check === undefined) continue
172
253
  try {
173
254
  await check(context)
174
255
  } catch (error) {
175
- throw new Error(`property ${property.name} failed for ${context.turn.marker}: ${error instanceof Error ? error.message : String(error)}`, {
176
- cause: error,
177
- })
256
+ throw new Error(
257
+ `property ${property.name} failed for ${context.turn.marker}: ${error instanceof Error ? error.message : String(error)}`,
258
+ {
259
+ cause: error,
260
+ },
261
+ )
178
262
  }
179
263
  }
180
264
  }
@@ -185,5 +269,6 @@ async function settleBlockingUi(expectedExchanges: number) {
185
269
  await ui.pressEnter()
186
270
  await Bun.sleep(25)
187
271
  }
188
- if (responseCursor < expectedExchanges) throw new Error("timed out settling blocking tool UI")
272
+ if (responseCursor < expectedExchanges)
273
+ throw new Error("timed out settling blocking tool UI")
189
274
  }
@@ -1,4 +1,8 @@
1
- import type { BackendSimulationClient, SimulationClient, UiState } from "../../client/index.js"
1
+ import type {
2
+ BackendSimulationClient,
3
+ SimulationClient,
4
+ UiState,
5
+ } from "../../client/index.js"
2
6
  import type { FlowTurn } from "./types.js"
3
7
 
4
8
  export type TurnOutcome = "completed" | "interrupted" | "provider-error"
@@ -8,7 +12,10 @@ export interface FlowPropertyContext {
8
12
  readonly ui: SimulationClient
9
13
  readonly backend: BackendSimulationClient
10
14
  readonly outcome?: TurnOutcome
11
- readonly waitFor: (label: string, check: () => Promise<boolean>) => Promise<void>
15
+ readonly waitFor: (
16
+ label: string,
17
+ check: () => Promise<boolean>,
18
+ ) => Promise<void>
12
19
  }
13
20
 
14
21
  export interface FlowProperty {
@@ -23,17 +30,16 @@ export const flowProperties: ReadonlyArray<FlowProperty> = [
23
30
  defineProperty({
24
31
  name: "submitted-turn-shows-running",
25
32
  afterSubmit: (context) => {
26
- if (context.turn.responses.some((response) => response.terminal === "invalid-provider-event" || response.terminal === "disconnect")) return Promise.resolve()
27
- return context.waitFor("submitted turn to show running", async () => isRunning(await context.ui.state()))
28
- },
29
- }),
30
- defineProperty({
31
- name: "turn-reaches-terminal-outcome",
32
- afterTerminal: async (context) => {
33
- if (context.outcome === undefined) throw new Error("turn has no terminal outcome")
34
- await context.waitFor(
35
- "terminal turn to have no provider exchange",
36
- async () => (await context.backend.pendingExchanges()).exchanges.length === 0,
33
+ if (
34
+ context.turn.responses.some(
35
+ (response) =>
36
+ response.terminal === "invalid-provider-event" ||
37
+ response.terminal === "disconnect",
38
+ )
39
+ )
40
+ return Promise.resolve()
41
+ return context.waitFor("submitted turn to show running", async () =>
42
+ isRunning(await context.ui.state()),
37
43
  )
38
44
  },
39
45
  }),
@@ -41,7 +41,6 @@ export interface FlowResult {
41
41
  readonly assistantExchanges: number
42
42
  readonly subagentExchanges: number
43
43
  readonly titleExchanges: number
44
- readonly traceRecords: number
45
44
  readonly durationMs: number
46
45
  readonly finalState: UiState
47
46
  }
@@ -1,7 +1,15 @@
1
- import { connectBackendSimulation, connectSimulation, type OpenedExchange } from "../client/index.js"
1
+ import {
2
+ connectBackendSimulation,
3
+ connectSimulation,
4
+ type OpenedExchange,
5
+ } from "../client/index.js"
2
6
 
3
- const ui = await connectSimulation({ url: requiredEnv("OPENCODE_SIMULATION_UI_WS") })
4
- const backend = await connectBackendSimulation({ url: requiredEnv("OPENCODE_SIMULATION_BACKEND_WS") })
7
+ const ui = await connectSimulation({
8
+ url: requiredEnv("OPENCODE_SIMULATION_UI_WS"),
9
+ })
10
+ const backend = await connectBackendSimulation({
11
+ url: requiredEnv("OPENCODE_SIMULATION_BACKEND_WS"),
12
+ })
5
13
  let completed!: () => void
6
14
  const responseCompleted = new Promise<void>((resolve) => {
7
15
  completed = resolve
@@ -18,7 +26,6 @@ try {
18
26
  await ui.typeText("Say hello")
19
27
  await ui.pressEnter()
20
28
  await responseCompleted
21
- await waitFor(async () => (await backend.pendingExchanges()).exchanges.length === 0)
22
29
  await ui.state()
23
30
  await Bun.sleep(Number(process.env.OPENCODE_PROBE_HOLD_MS ?? "10000"))
24
31
  } finally {
@@ -38,7 +38,7 @@ const child = Bun.spawn([
38
38
  "--driver", `bun ${path.resolve("src/experimental/stale-running-driver.ts")}`,
39
39
  "--",
40
40
  "bun", "run", "--conditions=browser", "--preload=@opentui/solid/preload",
41
- "/root/projects/opencode-latest/packages/cli/src/index.ts", "--standalone",
41
+ "/root/projects/opencode-latest/packages/cli/src/index.ts",
42
42
  ], {
43
43
  cwd: path.resolve("."),
44
44
  env: {
@@ -1,20 +1,35 @@
1
- import { connectBackendSimulation, connectSimulation, type OpenedExchange } from "../client/index.js"
1
+ import {
2
+ connectBackendSimulation,
3
+ connectSimulation,
4
+ type OpenedExchange,
5
+ } from "../client/index.js"
2
6
  import { isRunning } from "./flows/index.js"
3
7
 
4
- const ui = await connectSimulation({ url: requiredEnv("OPENCODE_SIMULATION_UI_WS") })
5
- const backend = await connectBackendSimulation({ url: requiredEnv("OPENCODE_SIMULATION_BACKEND_WS") })
8
+ const ui = await connectSimulation({
9
+ url: requiredEnv("OPENCODE_SIMULATION_UI_WS"),
10
+ })
11
+ const backend = await connectBackendSimulation({
12
+ url: requiredEnv("OPENCODE_SIMULATION_BACKEND_WS"),
13
+ })
6
14
  const requestOpened = deferred()
7
15
  const releaseResponse = deferred()
8
16
  const responseFinished = deferred()
9
17
 
10
18
  await backend.attach(async (request: OpenedExchange) => {
11
19
  if (isTitleRequest(request)) {
12
- await backend.chunk(request.id, [{ type: "textDelta", text: "Stale running reproduction" }])
20
+ await backend.chunk(request.id, [
21
+ { type: "textDelta", text: "Stale running reproduction" },
22
+ ])
13
23
  await backend.finish(request.id, "stop")
14
24
  return
15
25
  }
16
26
  requestOpened.resolve()
17
- await backend.chunk(request.id, [{ type: "textDelta", text: "The provider turn is finishing while events are disconnected." }])
27
+ await backend.chunk(request.id, [
28
+ {
29
+ type: "textDelta",
30
+ text: "The provider turn is finishing while events are disconnected.",
31
+ },
32
+ ])
18
33
  await releaseResponse.promise
19
34
  await backend.finish(request.id, "stop")
20
35
  responseFinished.resolve()
@@ -22,19 +37,23 @@ await backend.attach(async (request: OpenedExchange) => {
22
37
 
23
38
  try {
24
39
  await waitFor("prompt editor", async () => (await ui.state()).focused.editor)
25
- await ui.typeText("Reproduce stale running status across an event-stream reconnect")
40
+ await ui.typeText(
41
+ "Reproduce stale running status across an event-stream reconnect",
42
+ )
26
43
  await ui.pressEnter()
27
44
  await requestOpened.promise
28
45
  await waitFor("running TUI", async () => isRunning(await ui.state()))
29
46
 
30
47
  releaseResponse.resolve()
31
48
  await responseFinished.promise
32
- await waitFor("provider drain", async () => (await backend.pendingExchanges()).exchanges.length === 0)
33
49
  await Bun.sleep(1_000)
34
50
 
35
51
  const state = await ui.state()
36
- if (!isRunning(state)) throw new Error("stale running status was not reproduced")
37
- console.log("REPRODUCED: backend provider work is idle while the TUI still displays running.")
52
+ if (!isRunning(state))
53
+ throw new Error("stale running status was not reproduced")
54
+ console.log(
55
+ "REPRODUCED: backend provider work is idle while the TUI still displays running.",
56
+ )
38
57
  await Bun.sleep(Number(process.env.OPENCODE_PROBE_HOLD_MS ?? "10000"))
39
58
  } finally {
40
59
  ui.close()
@@ -56,15 +75,28 @@ function requiredEnv(name: string) {
56
75
  }
57
76
 
58
77
  function isTitleRequest(request: OpenedExchange) {
59
- if (typeof request.body !== "object" || request.body === null || !("messages" in request.body)) return false
78
+ if (
79
+ typeof request.body !== "object" ||
80
+ request.body === null ||
81
+ !("messages" in request.body)
82
+ )
83
+ return false
60
84
  const messages = request.body.messages
61
85
  if (!Array.isArray(messages)) return false
62
86
  const first = messages[0]
63
- if (typeof first !== "object" || first === null || !("content" in first)) return false
64
- return typeof first.content === "string" && first.content.includes("You are a title generator")
87
+ if (typeof first !== "object" || first === null || !("content" in first))
88
+ return false
89
+ return (
90
+ typeof first.content === "string" &&
91
+ first.content.includes("You are a title generator")
92
+ )
65
93
  }
66
94
 
67
- async function waitFor(label: string, check: () => Promise<boolean>, timeout = 30_000) {
95
+ async function waitFor(
96
+ label: string,
97
+ check: () => Promise<boolean>,
98
+ timeout = 30_000,
99
+ ) {
68
100
  const deadline = Date.now() + timeout
69
101
  while (Date.now() < deadline) {
70
102
  if (await check()) return
package/src/index.ts CHANGED
@@ -1 +1,3 @@
1
1
  export * from "./client/index.js"
2
+ export { defineScript } from "./cli/script.js"
3
+ export type { DriveScript, ScriptContext } from "./cli/script.js"
@@ -1,39 +0,0 @@
1
- import { resolve } from "node:path"
2
- import { pathToFileURL } from "node:url"
3
- import { connectDrive } from "../experimental/drive.js"
4
- import type { Driver } from "../experimental/drive.js"
5
-
6
- const driver = process.argv[2]
7
- if (!driver) throw new Error("driver file is required")
8
- const name = requiredArgument(3, "instance name")
9
- const ui = requiredArgument(4, "frontend WebSocket URL")
10
- const backend = requiredArgument(5, "backend WebSocket URL")
11
- const artifacts = requiredArgument(6, "artifacts directory")
12
-
13
- const module: { readonly default?: unknown } = await import(pathToFileURL(resolve(driver)).href)
14
- if (!isDriver(module.default)) throw new Error("driver must default-export defineDriver(...)")
15
- const controller = new AbortController()
16
- process.once("SIGINT", () => controller.abort())
17
- process.once("SIGTERM", () => controller.abort())
18
- const session = await connectDrive({ ui, backend })
19
- try {
20
- await module.default({
21
- name,
22
- ui: session.ui,
23
- backend: session.backend,
24
- artifacts,
25
- signal: controller.signal,
26
- })
27
- } finally {
28
- session.close()
29
- }
30
-
31
- function isDriver(value: unknown): value is Driver {
32
- return typeof value === "function"
33
- }
34
-
35
- function requiredArgument(index: number, name: string) {
36
- const value = process.argv[index]
37
- if (!value) throw new Error(`${name} is required`)
38
- return value
39
- }
package/src/cli/driver.ts DELETED
@@ -1,28 +0,0 @@
1
- import { mkdir } from "node:fs/promises"
2
- import { resolve } from "node:path"
3
- import type { InstanceManifest } from "./types.js"
4
-
5
- export async function runDriver(driver: string, manifest: InstanceManifest) {
6
- await mkdir(manifest.artifacts, { recursive: true })
7
- const child = Bun.spawn([
8
- process.execPath,
9
- resolve(import.meta.dir, "driver-runner.ts"),
10
- resolve(driver),
11
- manifest.name,
12
- manifest.endpoints.ui,
13
- manifest.endpoints.backend,
14
- manifest.artifacts,
15
- ], {
16
- cwd: process.cwd(),
17
- env: cleanEnv(process.env),
18
- stdin: "inherit",
19
- stdout: "inherit",
20
- stderr: "inherit",
21
- })
22
- const status = await child.exited
23
- if (status !== 0) throw new Error(`driver exited with status ${status}`)
24
- }
25
-
26
- function cleanEnv(env: Readonly<Record<string, string | undefined>>) {
27
- return Object.fromEntries(Object.entries(env).filter((entry): entry is [string, string] => entry[1] !== undefined))
28
- }