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.
@@ -1,179 +0,0 @@
1
- import { mkdir } from "node:fs/promises"
2
- import { tmpdir } from "node:os"
3
- import { basename, join, resolve } from "node:path"
4
- import { pathToFileURL } from "node:url"
5
- import type { DefinedCampaign } from "./campaign-api.js"
6
- import { connectDrive } from "./drive.js"
7
- import { launchInstance } from "../cli/instance.js"
8
- import type { StartOptions } from "../cli/types.js"
9
-
10
- interface CaseResult {
11
- readonly index: number
12
- readonly seed: number
13
- readonly status: "passed" | "failed"
14
- readonly durationMs: number
15
- readonly artifacts: string
16
- readonly error?: string
17
- readonly result?: unknown
18
- }
19
-
20
- export async function runCampaign(options: StartOptions) {
21
- const file = resolve(options.campaign!)
22
- const campaign = await loadCampaign(file)
23
- const root = resolve(process.env.DRIVE_CAMPAIGN_ROOT ?? join(tmpdir(), `opencode-drive-campaign-${Date.now()}`))
24
- const indexes = options.caseIndex === undefined
25
- ? Array.from({ length: options.count ?? campaign.count ?? 1 }, (_, index) => index)
26
- : [options.caseIndex]
27
- if (indexes.length === 0) throw new Error("campaign count must be greater than zero")
28
- await mkdir(root, { recursive: true })
29
- const results: CaseResult[] = []
30
- const cursor = { value: 0 }
31
- const controller = new AbortController()
32
- const active = new Set<Awaited<ReturnType<typeof launchInstance>>>()
33
- const interrupt = () => {
34
- controller.abort()
35
- void Promise.all([...active].map((instance) => instance.stop(true)))
36
- }
37
- process.once("SIGINT", interrupt)
38
- process.once("SIGTERM", interrupt)
39
- try {
40
- const workers = Array.from(
41
- { length: Math.min(options.visible ? 1 : options.concurrency, indexes.length) },
42
- async () => {
43
- while (!controller.signal.aborted && cursor.value < indexes.length) {
44
- const index = indexes[cursor.value++]!
45
- const result = await runCase(campaign, file, root, index, options, controller.signal, active)
46
- results.push(result)
47
- console.log(`[${results.length}/${indexes.length}] case=${index} seed=${result.seed} ${result.status}`)
48
- }
49
- },
50
- )
51
- await Promise.all(workers)
52
- } finally {
53
- process.off("SIGINT", interrupt)
54
- process.off("SIGTERM", interrupt)
55
- await Promise.all([...active].map((instance) => instance.stop(true)))
56
- }
57
- if (controller.signal.aborted) throw new Error("campaign interrupted")
58
- results.sort((a, b) => a.index - b.index)
59
- const summary = {
60
- campaign: file,
61
- seed: options.seed,
62
- count: results.length,
63
- passed: results.filter((result) => result.status === "passed").length,
64
- failed: results.filter((result) => result.status === "failed").length,
65
- durationMs: results.reduce((total, result) => total + result.durationMs, 0),
66
- results,
67
- }
68
- await Bun.write(resolve(root, "summary.json"), `${JSON.stringify(summary, undefined, 2)}\n`)
69
- if (summary.failed > 0) throw new Error(`${summary.failed} campaign case${summary.failed === 1 ? "" : "s"} failed; see ${root}`)
70
- console.log(JSON.stringify(summary, undefined, 2))
71
- }
72
-
73
- async function runCase(
74
- campaign: DefinedCampaign,
75
- campaignFile: string,
76
- root: string,
77
- index: number,
78
- options: StartOptions,
79
- signal: AbortSignal,
80
- active: Set<Awaited<ReturnType<typeof launchInstance>>>,
81
- ): Promise<CaseResult> {
82
- const seed = options.seed + index
83
- const artifacts = resolve(root, `case-${String(index).padStart(6, "0")}-${seed}`)
84
- const started = Date.now()
85
- await mkdir(artifacts, { recursive: true })
86
- const generated = { index, seed, artifacts }
87
- try {
88
- const flow = await campaign.generate(generated)
89
- await Bun.write(resolve(artifacts, "flow.json"), `${JSON.stringify(flow, undefined, 2)}\n`)
90
- const prepared = await campaign.prepare?.({ ...generated, flow })
91
- const instance = await launchInstance({
92
- name: `campaign-${process.pid}-${index}`,
93
- command: options.command.length > 0 ? options.command : prepared?.command,
94
- dev: options.dev,
95
- state: prepared?.state ?? options.state,
96
- visible: options.visible,
97
- env: prepared?.env,
98
- })
99
- active.add(instance)
100
- try {
101
- await instance.waitForDrive("both")
102
- const session = await connectDrive(instance.manifest.endpoints)
103
- try {
104
- const result = await campaign.run({
105
- ...generated,
106
- flow,
107
- name: instance.manifest.name,
108
- ui: session.ui,
109
- backend: session.backend,
110
- signal,
111
- })
112
- const value: CaseResult = {
113
- index,
114
- seed,
115
- status: "passed",
116
- durationMs: Date.now() - started,
117
- artifacts,
118
- result,
119
- }
120
- await Bun.write(resolve(artifacts, "result.json"), `${JSON.stringify(value, undefined, 2)}\n`)
121
- return value
122
- } finally {
123
- session.close()
124
- }
125
- } finally {
126
- active.delete(instance)
127
- await instance.stop(true)
128
- }
129
- } catch (error) {
130
- const message = error instanceof Error ? error.message : String(error)
131
- const result: CaseResult = {
132
- index,
133
- seed,
134
- status: "failed",
135
- durationMs: Date.now() - started,
136
- artifacts,
137
- error: message,
138
- }
139
- await Bun.write(resolve(artifacts, "failure.json"), `${JSON.stringify({
140
- ...result,
141
- replay: replayCommand(campaignFile, index, options),
142
- }, undefined, 2)}\n`)
143
- return result
144
- }
145
- }
146
-
147
- function replayCommand(campaignFile: string, index: number, options: StartOptions) {
148
- const args = [
149
- "opencode-drive",
150
- "start",
151
- "--campaign",
152
- campaignFile,
153
- "--seed",
154
- String(options.seed),
155
- "--case",
156
- String(index),
157
- "--visible",
158
- ...(options.state ? ["--state", options.state] : []),
159
- ...(options.anchor ? ["--anchor", options.anchor] : []),
160
- ...(options.command.length > 0 ? ["--", ...options.command] : []),
161
- ]
162
- return args.map((arg) => JSON.stringify(arg)).join(" ")
163
- }
164
-
165
- async function loadCampaign(file: string): Promise<DefinedCampaign> {
166
- const module: { readonly default?: unknown } = await import(`${pathToFileURL(file).href}?drive=${Date.now()}`)
167
- const value = module.default
168
- if (!isCampaign(value)) {
169
- throw new Error(`${basename(file)} must default-export defineCampaign(...)`)
170
- }
171
- return value
172
- }
173
-
174
- function isCampaign(value: unknown): value is DefinedCampaign {
175
- if (typeof value !== "object" || value === null) return false
176
- if (!("kind" in value) || value.kind !== "opencode-drive/campaign") return false
177
- if (!("generate" in value) || typeof value.generate !== "function") return false
178
- return "run" in value && typeof value.run === "function"
179
- }