@toist/aja 0.6.0 → 0.6.1
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/CHANGELOG.md +16 -0
- package/package.json +7 -5
- package/src/client.ts +305 -0
- package/src/index.ts +21 -0
- package/src/pipeline.ts +4 -0
- package/src/server.ts +16 -2
- package/src/startRunner.ts +8 -3
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,22 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to `@toist/aja` are recorded here.
|
|
4
4
|
|
|
5
|
+
## Unreleased
|
|
6
|
+
|
|
7
|
+
## 0.6.1 — 2026-05-05
|
|
8
|
+
|
|
9
|
+
Fix published @toist/aja dependency resolution for embedded runners and include the typed runner client/smoke-test surface.
|
|
10
|
+
|
|
11
|
+
- Added `createRunnerClient()` typed HTTP adapter, exported from
|
|
12
|
+
`@toist/aja` and `@toist/aja/client`, for driving runner instances
|
|
13
|
+
without hard-coding API paths.
|
|
14
|
+
- Added `smoke:client` script that starts an embedded runner, drives it via
|
|
15
|
+
the client, and verifies persisted node outputs.
|
|
16
|
+
- Changed published package dependencies on `@toist/spec` and `@toist/ui`
|
|
17
|
+
from workspace protocol to concrete `0.6.1` versions so external embeds
|
|
18
|
+
install transitive runtime packages correctly.
|
|
19
|
+
- Documented the v1 embedded-instance limit: one runner per process.
|
|
20
|
+
|
|
5
21
|
## 0.6.0 — 2026-05-05
|
|
6
22
|
|
|
7
23
|
Lockstep version bump alongside `@toist/in@0.6.0`. No functional changes
|
package/package.json
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@toist/aja",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./src/index.ts",
|
|
6
6
|
"types": "./src/index.ts",
|
|
7
7
|
"exports": {
|
|
8
|
-
".": "./src/index.ts"
|
|
8
|
+
".": "./src/index.ts",
|
|
9
|
+
"./client": "./src/client.ts"
|
|
9
10
|
},
|
|
10
11
|
"files": [
|
|
11
12
|
"src/",
|
|
@@ -13,11 +14,12 @@
|
|
|
13
14
|
"CHANGELOG.md"
|
|
14
15
|
],
|
|
15
16
|
"scripts": {
|
|
16
|
-
"dev": "bun --watch src/server.ts"
|
|
17
|
+
"dev": "bun --watch src/server.ts",
|
|
18
|
+
"smoke:client": "bun test/client-smoke.ts"
|
|
17
19
|
},
|
|
18
20
|
"dependencies": {
|
|
19
|
-
"@toist/spec": "0.6.
|
|
20
|
-
"@toist/ui": "0.6.
|
|
21
|
+
"@toist/spec": "0.6.1",
|
|
22
|
+
"@toist/ui": "0.6.1",
|
|
21
23
|
"hono": "^4.7.7",
|
|
22
24
|
"proper-lockfile": "^4.1.2"
|
|
23
25
|
},
|
package/src/client.ts
ADDED
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
// 2121
|
|
2
|
+
// Typed HTTP client for a running @toist/aja instance.
|
|
3
|
+
//
|
|
4
|
+
// This is the public de-facto adapter for hosts (Pi, tests, external tools)
|
|
5
|
+
// that want to drive the runner over its HTTP API without hard-coding paths.
|
|
6
|
+
|
|
7
|
+
import type { NodeKindManifest, ValidateResult } from "@toist/spec"
|
|
8
|
+
|
|
9
|
+
export interface RunnerClientOptions {
|
|
10
|
+
/** Runner API base URL. Accepts either the API root
|
|
11
|
+
* (`http://localhost:2132/api`) or the runner root
|
|
12
|
+
* (`http://localhost:2132`); the client normalizes to `/api`. */
|
|
13
|
+
baseUrl: string
|
|
14
|
+
/** Optional fetch implementation for tests or non-Bun runtimes. */
|
|
15
|
+
fetch?: typeof fetch
|
|
16
|
+
/** Extra headers sent with every request (e.g. auth). */
|
|
17
|
+
headers?: HeadersInit
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export class RunnerHttpError extends Error {
|
|
21
|
+
constructor(
|
|
22
|
+
public readonly status: number,
|
|
23
|
+
public readonly statusText: string,
|
|
24
|
+
public readonly body: unknown,
|
|
25
|
+
) {
|
|
26
|
+
super(`Runner HTTP ${status} ${statusText}`)
|
|
27
|
+
this.name = "RunnerHttpError"
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface PipelineSummary {
|
|
32
|
+
id: string
|
|
33
|
+
label: string
|
|
34
|
+
description?: string
|
|
35
|
+
nodeCount: number
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface PipelineSource {
|
|
39
|
+
id: string
|
|
40
|
+
yaml: string
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface PipelineSaveResult {
|
|
44
|
+
id?: string
|
|
45
|
+
ok?: boolean
|
|
46
|
+
errors?: string[]
|
|
47
|
+
error?: string
|
|
48
|
+
[key: string]: unknown
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export interface StepResult {
|
|
52
|
+
id: string
|
|
53
|
+
kind: string
|
|
54
|
+
label: string
|
|
55
|
+
status: "complete" | "failed"
|
|
56
|
+
params?: Record<string, unknown>
|
|
57
|
+
input?: Record<string, unknown>
|
|
58
|
+
output?: unknown
|
|
59
|
+
error?: string
|
|
60
|
+
duration_ms: number
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export interface PipelineTaskRef {
|
|
64
|
+
id: number
|
|
65
|
+
kind: string
|
|
66
|
+
prompt: string
|
|
67
|
+
schema: unknown
|
|
68
|
+
assignee: string | null
|
|
69
|
+
responseToken: string
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export type PipelineRunResponse =
|
|
73
|
+
| { id: number; pipeline: string; status: "done"; result: unknown; steps: StepResult[] }
|
|
74
|
+
| { id: number; pipeline: string; status: "suspended"; suspendedAt: string; task: PipelineTaskRef; steps: StepResult[] }
|
|
75
|
+
| { id: number; pipeline: string; status: "error"; error: string }
|
|
76
|
+
|
|
77
|
+
export interface RunListItem {
|
|
78
|
+
id: number
|
|
79
|
+
pipeline: string
|
|
80
|
+
status: string
|
|
81
|
+
payload: string | null
|
|
82
|
+
result: string | null
|
|
83
|
+
steps: string | null
|
|
84
|
+
error: string | null
|
|
85
|
+
started_at: string
|
|
86
|
+
finished_at?: string | null
|
|
87
|
+
current_node?: string | null
|
|
88
|
+
updated_at?: string | null
|
|
89
|
+
trigger?: string
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export interface RunNodeOutput {
|
|
93
|
+
nodeId: string
|
|
94
|
+
output: unknown
|
|
95
|
+
startedAt: string
|
|
96
|
+
finishedAt: string
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export interface RunLogLine {
|
|
100
|
+
id: number
|
|
101
|
+
ts: string
|
|
102
|
+
level: "info" | "warn" | "error" | string
|
|
103
|
+
msg: string | null
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export interface TaskListItem {
|
|
107
|
+
id: number
|
|
108
|
+
run_id: number
|
|
109
|
+
node_id: string
|
|
110
|
+
kind: string
|
|
111
|
+
prompt: string | null
|
|
112
|
+
assignee: string | null
|
|
113
|
+
status: "open" | "answered" | "expired" | "cancelled"
|
|
114
|
+
created_at: string
|
|
115
|
+
responded_at: string | null
|
|
116
|
+
pipeline: string | null
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export interface TaskFull {
|
|
120
|
+
id: number
|
|
121
|
+
runId: number
|
|
122
|
+
pipeline: string | null
|
|
123
|
+
nodeId: string
|
|
124
|
+
kind: string
|
|
125
|
+
prompt: string
|
|
126
|
+
schema: unknown
|
|
127
|
+
assignee: string | null
|
|
128
|
+
responseToken: string
|
|
129
|
+
status: "open" | "answered" | "expired" | "cancelled"
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export interface ResourceTypeSummary {
|
|
133
|
+
name: string
|
|
134
|
+
description?: string
|
|
135
|
+
schema: Record<string, unknown>
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export interface ResourceRecord {
|
|
139
|
+
id: number
|
|
140
|
+
name: string
|
|
141
|
+
type: string
|
|
142
|
+
fields: Record<string, unknown>
|
|
143
|
+
created_at: string
|
|
144
|
+
updated_at: string
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export interface RunnerClient {
|
|
148
|
+
instance: {
|
|
149
|
+
root(): Promise<unknown>
|
|
150
|
+
get(): Promise<unknown>
|
|
151
|
+
}
|
|
152
|
+
kinds: {
|
|
153
|
+
list(): Promise<{ kinds: NodeKindManifest[] }>
|
|
154
|
+
invoke(id: string, body?: { params?: Record<string, unknown>; input?: Record<string, unknown>; confirm?: boolean }): Promise<unknown>
|
|
155
|
+
}
|
|
156
|
+
pipelines: {
|
|
157
|
+
list(): Promise<PipelineSummary[]>
|
|
158
|
+
get(id: string): Promise<unknown>
|
|
159
|
+
source(id: string): Promise<PipelineSource>
|
|
160
|
+
validate(input: { yaml: string } | { spec: unknown }): Promise<ValidateResult>
|
|
161
|
+
create(yaml: string): Promise<PipelineSaveResult>
|
|
162
|
+
update(id: string, yaml: string): Promise<PipelineSaveResult>
|
|
163
|
+
run(id: string, payload?: Record<string, unknown>): Promise<PipelineRunResponse>
|
|
164
|
+
}
|
|
165
|
+
runs: {
|
|
166
|
+
list(opts?: { pipeline?: string; limit?: number }): Promise<RunListItem[]>
|
|
167
|
+
get(id: number): Promise<RunListItem>
|
|
168
|
+
nodes(id: number): Promise<RunNodeOutput[]>
|
|
169
|
+
logs(id: number): Promise<RunLogLine[]>
|
|
170
|
+
}
|
|
171
|
+
tasks: {
|
|
172
|
+
list(opts?: { status?: string; runId?: number; assignee?: string; limit?: number }): Promise<TaskListItem[]>
|
|
173
|
+
get(id: number): Promise<TaskFull>
|
|
174
|
+
respond(runId: number, taskId: number, body: { token: string; response: unknown; respondedBy?: string }): Promise<PipelineRunResponse>
|
|
175
|
+
}
|
|
176
|
+
resources: {
|
|
177
|
+
types(): Promise<ResourceTypeSummary[]>
|
|
178
|
+
list(): Promise<ResourceRecord[]>
|
|
179
|
+
get(name: string): Promise<ResourceRecord>
|
|
180
|
+
upsert(name: string, type: string, fields: Record<string, unknown>): Promise<ResourceRecord>
|
|
181
|
+
put(name: string, body: { type?: string; fields?: Record<string, unknown> }): Promise<ResourceRecord>
|
|
182
|
+
delete(name: string): Promise<void>
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
export function createRunnerClient(options: RunnerClientOptions): RunnerClient {
|
|
187
|
+
const base = normalizeBaseUrl(options.baseUrl)
|
|
188
|
+
const f = options.fetch ?? fetch
|
|
189
|
+
|
|
190
|
+
async function request<T>(path: string, init: RequestInit = {}, opts: { allowErrorJson?: boolean } = {}): Promise<T> {
|
|
191
|
+
const headers = new Headers(options.headers)
|
|
192
|
+
if (init.body !== undefined && !headers.has("Content-Type")) headers.set("Content-Type", "application/json")
|
|
193
|
+
if (init.headers) new Headers(init.headers).forEach((v, k) => headers.set(k, v))
|
|
194
|
+
|
|
195
|
+
const r = await f(`${base}${path}`, { ...init, headers })
|
|
196
|
+
const body = await readJsonOrText(r)
|
|
197
|
+
if (!r.ok && !opts.allowErrorJson) throw new RunnerHttpError(r.status, r.statusText, body)
|
|
198
|
+
return body as T
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const json = (value: unknown) => JSON.stringify(value ?? {})
|
|
202
|
+
|
|
203
|
+
return {
|
|
204
|
+
instance: {
|
|
205
|
+
root: () => request<unknown>("/"),
|
|
206
|
+
get: () => request<unknown>("/instance"),
|
|
207
|
+
},
|
|
208
|
+
|
|
209
|
+
kinds: {
|
|
210
|
+
list: () => request<{ kinds: NodeKindManifest[] }>("/manifest"),
|
|
211
|
+
invoke: (id, body = {}) => request<unknown>(`/kinds/${enc(id)}/invoke`, {
|
|
212
|
+
method: "POST",
|
|
213
|
+
body: json(body),
|
|
214
|
+
}),
|
|
215
|
+
},
|
|
216
|
+
|
|
217
|
+
pipelines: {
|
|
218
|
+
list: () => request<PipelineSummary[]>("/pipelines"),
|
|
219
|
+
get: (id) => request<unknown>(`/pipelines/${enc(id)}`),
|
|
220
|
+
source: (id) => request<PipelineSource>(`/pipelines/${enc(id)}/source`),
|
|
221
|
+
validate: (input) => request<ValidateResult>("/pipelines/validate", {
|
|
222
|
+
method: "POST",
|
|
223
|
+
body: json(input),
|
|
224
|
+
}, { allowErrorJson: true }),
|
|
225
|
+
create: (yaml) => request<PipelineSaveResult>("/pipelines", {
|
|
226
|
+
method: "POST",
|
|
227
|
+
body: json({ yaml }),
|
|
228
|
+
}),
|
|
229
|
+
update: (id, yaml) => request<PipelineSaveResult>(`/pipelines/${enc(id)}`, {
|
|
230
|
+
method: "PUT",
|
|
231
|
+
body: json({ yaml }),
|
|
232
|
+
}),
|
|
233
|
+
run: (id, payload = {}) => request<PipelineRunResponse>(`/pipelines/${enc(id)}/run`, {
|
|
234
|
+
method: "POST",
|
|
235
|
+
body: json(payload),
|
|
236
|
+
}),
|
|
237
|
+
},
|
|
238
|
+
|
|
239
|
+
runs: {
|
|
240
|
+
list: (opts = {}) => request<RunListItem[]>(`/runs${query({ pipeline: opts.pipeline, limit: opts.limit })}`),
|
|
241
|
+
get: (id) => request<RunListItem>(`/runs/${id}`),
|
|
242
|
+
nodes: (id) => request<RunNodeOutput[]>(`/runs/${id}/nodes`),
|
|
243
|
+
logs: (id) => request<RunLogLine[]>(`/runs/${id}/logs`),
|
|
244
|
+
},
|
|
245
|
+
|
|
246
|
+
tasks: {
|
|
247
|
+
list: (opts = {}) => request<TaskListItem[]>(`/tasks${query({
|
|
248
|
+
status: opts.status,
|
|
249
|
+
run_id: opts.runId,
|
|
250
|
+
assignee: opts.assignee,
|
|
251
|
+
limit: opts.limit,
|
|
252
|
+
})}`),
|
|
253
|
+
get: (id) => request<TaskFull>(`/tasks/${id}`),
|
|
254
|
+
respond: (runId, taskId, body) => request<PipelineRunResponse>(`/runs/${runId}/tasks/${taskId}/respond`, {
|
|
255
|
+
method: "POST",
|
|
256
|
+
body: json(body),
|
|
257
|
+
}),
|
|
258
|
+
},
|
|
259
|
+
|
|
260
|
+
resources: {
|
|
261
|
+
types: () => request<ResourceTypeSummary[]>("/resource-types"),
|
|
262
|
+
list: () => request<ResourceRecord[]>("/resources"),
|
|
263
|
+
get: (name) => request<ResourceRecord>(`/resources/${enc(name)}`),
|
|
264
|
+
upsert: (name, type, fields) => request<ResourceRecord>("/resources", {
|
|
265
|
+
method: "POST",
|
|
266
|
+
body: json({ name, type, fields }),
|
|
267
|
+
}),
|
|
268
|
+
put: (name, body) => request<ResourceRecord>(`/resources/${enc(name)}`, {
|
|
269
|
+
method: "PUT",
|
|
270
|
+
body: json(body),
|
|
271
|
+
}),
|
|
272
|
+
delete: async (name) => {
|
|
273
|
+
await request<unknown>(`/resources/${enc(name)}`, { method: "DELETE" })
|
|
274
|
+
},
|
|
275
|
+
},
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
function normalizeBaseUrl(input: string): string {
|
|
280
|
+
const trimmed = input.replace(/\/+$/, "")
|
|
281
|
+
return trimmed.endsWith("/api") ? trimmed : `${trimmed}/api`
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function enc(value: string): string {
|
|
285
|
+
return encodeURIComponent(value)
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
function query(values: Record<string, string | number | undefined>): string {
|
|
289
|
+
const q = new URLSearchParams()
|
|
290
|
+
for (const [k, v] of Object.entries(values)) {
|
|
291
|
+
if (v !== undefined) q.set(k, String(v))
|
|
292
|
+
}
|
|
293
|
+
const s = q.toString()
|
|
294
|
+
return s ? `?${s}` : ""
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
async function readJsonOrText(response: Response): Promise<unknown> {
|
|
298
|
+
if (response.status === 204) return null
|
|
299
|
+
const text = await response.text()
|
|
300
|
+
if (!text) return null
|
|
301
|
+
const ct = response.headers.get("content-type") ?? ""
|
|
302
|
+
if (ct.includes("application/json")) return JSON.parse(text)
|
|
303
|
+
try { return JSON.parse(text) }
|
|
304
|
+
catch { return text }
|
|
305
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -10,6 +10,27 @@
|
|
|
10
10
|
// Lifecycle entry — what hosts call to start an instance.
|
|
11
11
|
export { startRunner, type StartRunnerOptions, type RunnerHandle } from "./startRunner.ts"
|
|
12
12
|
|
|
13
|
+
// Typed HTTP client — what external hosts/tools use to drive an instance.
|
|
14
|
+
export {
|
|
15
|
+
createRunnerClient,
|
|
16
|
+
RunnerHttpError,
|
|
17
|
+
type RunnerClient,
|
|
18
|
+
type RunnerClientOptions,
|
|
19
|
+
type PipelineSummary,
|
|
20
|
+
type PipelineSource,
|
|
21
|
+
type PipelineSaveResult,
|
|
22
|
+
type StepResult,
|
|
23
|
+
type PipelineTaskRef,
|
|
24
|
+
type PipelineRunResponse,
|
|
25
|
+
type RunListItem,
|
|
26
|
+
type RunNodeOutput,
|
|
27
|
+
type RunLogLine,
|
|
28
|
+
type TaskListItem,
|
|
29
|
+
type TaskFull,
|
|
30
|
+
type ResourceTypeSummary,
|
|
31
|
+
type ResourceRecord,
|
|
32
|
+
} from "./client.ts"
|
|
33
|
+
|
|
13
34
|
// Kind registration — must be called before startRunner.
|
|
14
35
|
export { register, getKind, manifest } from "./kinds/index.ts"
|
|
15
36
|
|
package/src/pipeline.ts
CHANGED
|
@@ -305,6 +305,10 @@ export async function runPipeline(
|
|
|
305
305
|
if (o.result.kind === "complete") {
|
|
306
306
|
results[winner.id] = o.result.output
|
|
307
307
|
done.add(winner.id)
|
|
308
|
+
// Persist every completed node output, not only pre-suspend checkpoints.
|
|
309
|
+
// This keeps /runs/:id/nodes and runs.nodeOutput useful for normal done
|
|
310
|
+
// runs, while preserving the same resume-skip storage semantics.
|
|
311
|
+
persistNodeOutputs(runtimeDb(), baseCtx.runId, { [winner.id]: o.result.output })
|
|
308
312
|
steps.push({
|
|
309
313
|
id: winner.id, kind: node.kind, label, status: "complete",
|
|
310
314
|
params: o.params, input: o.input, output: o.result.output,
|
package/src/server.ts
CHANGED
|
@@ -351,7 +351,7 @@ apiApp.get("/tasks", (c) => {
|
|
|
351
351
|
const limit = Number(c.req.query("limit") ?? 200)
|
|
352
352
|
|
|
353
353
|
const where: string[] = []
|
|
354
|
-
const args:
|
|
354
|
+
const args: Array<string | number> = []
|
|
355
355
|
if (status !== "all") { where.push("t.status = ?"); args.push(status) }
|
|
356
356
|
if (assignee) { where.push("t.assignee = ?"); args.push(assignee) }
|
|
357
357
|
if (runId) { where.push("t.run_id = ?"); args.push(Number(runId)) }
|
|
@@ -464,6 +464,19 @@ apiApp.get("/runs/:id", (c) => {
|
|
|
464
464
|
return c.json(row)
|
|
465
465
|
})
|
|
466
466
|
|
|
467
|
+
apiApp.get("/runs/:id/nodes", (c) => {
|
|
468
|
+
const runId = Number(c.req.param("id"))
|
|
469
|
+
const rows = runtimeDb().prepare(
|
|
470
|
+
"SELECT node_id, output_json, started_at, finished_at FROM node_outputs WHERE run_id = ? ORDER BY started_at ASC",
|
|
471
|
+
).all(runId) as Array<{ node_id: string; output_json: string | null; started_at: string; finished_at: string }>
|
|
472
|
+
return c.json(rows.map((r) => ({
|
|
473
|
+
nodeId: r.node_id,
|
|
474
|
+
output: r.output_json != null ? JSON.parse(r.output_json) as unknown : null,
|
|
475
|
+
startedAt: r.started_at,
|
|
476
|
+
finishedAt: r.finished_at,
|
|
477
|
+
})))
|
|
478
|
+
})
|
|
479
|
+
|
|
467
480
|
apiApp.get("/runs/:id/logs", (c) => {
|
|
468
481
|
const runId = Number(c.req.param("id"))
|
|
469
482
|
const rows = runtimeDb().prepare(
|
|
@@ -473,7 +486,8 @@ apiApp.get("/runs/:id/logs", (c) => {
|
|
|
473
486
|
})
|
|
474
487
|
|
|
475
488
|
// ─── lifecycle ───────────────────────────────────────────────────────────────
|
|
476
|
-
setInterval(() => cache.prune(), 5 * 60 * 1000)
|
|
489
|
+
const cachePruneTimer = setInterval(() => cache.prune(), 5 * 60 * 1000)
|
|
490
|
+
cachePruneTimer.unref?.()
|
|
477
491
|
|
|
478
492
|
const PORT = Number(process.env.PORT ?? 2132)
|
|
479
493
|
|
package/src/startRunner.ts
CHANGED
|
@@ -61,6 +61,10 @@ export interface RunnerHandle {
|
|
|
61
61
|
}
|
|
62
62
|
|
|
63
63
|
export async function startRunner(options: StartRunnerOptions): Promise<RunnerHandle> {
|
|
64
|
+
// v1 embedded limit: one runner per process. config/db/registry/server
|
|
65
|
+
// modules keep process-global state; hosts that need multiple isolated
|
|
66
|
+
// runners should spawn separate processes until createRunnerInstance-style
|
|
67
|
+
// state encapsulation lands.
|
|
64
68
|
setRootDir(options.rootDir)
|
|
65
69
|
setDisableUi(options.disableUi ?? false)
|
|
66
70
|
setDisableWatch(options.disableWatch ?? false)
|
|
@@ -74,11 +78,12 @@ export async function startRunner(options: StartRunnerOptions): Promise<RunnerHa
|
|
|
74
78
|
const mod = await import("./server.ts")
|
|
75
79
|
const fetch = mod.fetch as (req: Request) => Response | Promise<Response>
|
|
76
80
|
|
|
77
|
-
const server: Server = Bun.serve({ port: options.port, fetch })
|
|
78
|
-
|
|
81
|
+
const server: Server<undefined> = Bun.serve({ port: options.port, fetch })
|
|
82
|
+
const port = server.port ?? options.port
|
|
83
|
+
console.log(`[runner] http://localhost:${port}`)
|
|
79
84
|
|
|
80
85
|
return {
|
|
81
|
-
port
|
|
86
|
+
port,
|
|
82
87
|
async stop() {
|
|
83
88
|
server.stop()
|
|
84
89
|
await closeDbs()
|