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/README.md +57 -38
- package/package.json +1 -1
- package/src/cli/commands.ts +101 -68
- package/src/cli/control.ts +43 -0
- package/src/cli/describe.ts +4 -5
- package/src/cli/index.ts +83 -110
- package/src/cli/instance.ts +193 -148
- package/src/cli/mock-backend.ts +25 -0
- package/src/cli/registry.ts +41 -83
- package/src/cli/restart.ts +3 -66
- package/src/cli/script.ts +66 -0
- package/src/cli/send.ts +19 -39
- package/src/cli/start.ts +93 -68
- package/src/cli/stop.ts +4 -28
- package/src/cli/types.ts +7 -30
- package/src/client/backend.ts +94 -32
- package/src/client/protocol.ts +115 -29
- package/src/client/protocol.types.ts +17 -23
- package/src/experimental/driver.ts +29 -18
- package/src/experimental/flow-driver.ts +130 -43
- package/src/experimental/flows/properties.ts +19 -13
- package/src/experimental/hello-driver.ts +11 -4
- package/src/experimental/stale-running-driver.ts +45 -13
- package/src/index.ts +2 -0
- package/src/cli/driver-runner.ts +0 -39
- package/src/cli/driver.ts +0 -28
- package/src/experimental/cli-campaign.ts +0 -179
|
@@ -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
|
-
}
|