@toist/aja 0.7.1 → 0.8.0
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 +49 -1
- package/README.md +62 -0
- package/package.json +6 -4
- package/src/cache-db.ts +1 -9
- package/src/cli.ts +17 -11
- package/src/client.ts +76 -0
- package/src/data-db.ts +1 -13
- package/src/index.ts +35 -2
- package/src/instance-metadata.ts +48 -0
- package/src/kinds/index.ts +23 -61
- package/src/lock.ts +27 -53
- package/src/migrate.ts +3 -3
- package/src/pipeline-store.ts +31 -0
- package/src/resources-fs.ts +43 -0
- package/src/resources.ts +27 -190
- package/src/run-events.ts +42 -0
- package/src/runtime-db.ts +11 -30
- package/src/server.ts +506 -496
- package/src/sqlite-runtime.ts +135 -0
- package/src/startRunner.ts +56 -70
- package/src/stores/sqlite.ts +243 -0
- package/src/stores/types.ts +18 -0
- package/src/config.ts +0 -129
- package/src/db-handles.ts +0 -70
- package/src/hitl.ts +0 -257
- package/src/instance.ts +0 -64
- package/src/kinds/control.ts +0 -26
- package/src/kinds/data.ts +0 -30
- package/src/kinds/db.ts +0 -92
- package/src/kinds/hitl.ts +0 -56
- package/src/kinds/http.ts +0 -134
- package/src/kinds/runs.ts +0 -130
- package/src/kinds/transform.ts +0 -123
- package/src/kinds/types.ts +0 -16
- package/src/pipeline.ts +0 -605
- package/src/runs.ts +0 -53
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,54 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to `@toist/aja` are recorded here.
|
|
4
4
|
|
|
5
|
+
## 0.8.0 — 2026-05-08
|
|
6
|
+
|
|
7
|
+
Major refactor: `@toist/aja` is now the SQLite host package, with the
|
|
8
|
+
execution kernel extracted to `@toist/core`.
|
|
9
|
+
|
|
10
|
+
**New:**
|
|
11
|
+
|
|
12
|
+
- `createSqliteRuntime({ rootDir, ... })` — durable runtime constructor.
|
|
13
|
+
Owns lock acquisition, migrations, db handle opening, store wiring,
|
|
14
|
+
registry projection, resource resolution. Returns a `ToistRuntime`
|
|
15
|
+
with `close()`.
|
|
16
|
+
- `mountApi(runtime, options): Hono` factory replaces module-level
|
|
17
|
+
state in `server.ts`. `startRunner` builds a runtime then calls
|
|
18
|
+
`mountApi`.
|
|
19
|
+
- `loadFilesystemResources(rootDir)` — reads `toist.yml`, performs
|
|
20
|
+
`${env:VAR}` and `${env:VAR:-default}` substitution, fails closed
|
|
21
|
+
at config-load time on missing required vars.
|
|
22
|
+
- `kindAllowlist` field in `instance.json` filters the registry
|
|
23
|
+
projection at construction time. Glob support (`agent.*`).
|
|
24
|
+
- `StartRunnerOptions.storage: "sqlite" | "memory"` flag.
|
|
25
|
+
- `StartRunnerOptions.runtime?: ToistRuntime` late-bound injection.
|
|
26
|
+
- `HostedRuntime` interface (extends `ToistRuntime` with optional
|
|
27
|
+
`close`, `adminDb`, `pipelinesDir`).
|
|
28
|
+
- `createFilesystemPipelineStore({ rootDir, registry, watch })` for
|
|
29
|
+
YAML pipeline loading + watching.
|
|
30
|
+
- `POST /api/runs` async ad-hoc spec-run endpoint.
|
|
31
|
+
- `GET /api/runs/:id/events` SSE event stream.
|
|
32
|
+
- `client.pipelines.runSpec(spec, payload)` and `client.runs.events(runId)`.
|
|
33
|
+
- `/manifest` response carries `version` + `schema` URL for stable
|
|
34
|
+
downstream consumption.
|
|
35
|
+
|
|
36
|
+
**Removed:**
|
|
37
|
+
|
|
38
|
+
- `db-handles.ts`, `config.ts` (process-global `setRootDir` etc.),
|
|
39
|
+
`runtime.ts` (`createProcessRuntime`). Replaced by explicit
|
|
40
|
+
`createSqliteRuntime`.
|
|
41
|
+
- Local `pipeline.ts`, `hitl.ts`, `runs.ts`, `kinds/{control,data,db,hitl,
|
|
42
|
+
http,runs,transform,types}.ts` — all moved to `@toist/core`.
|
|
43
|
+
`@toist/aja` re-exports the kernel surface.
|
|
44
|
+
- Filesystem `resources/*.yaml` loader (replaced by `toist.yml`).
|
|
45
|
+
|
|
46
|
+
**Renamed:**
|
|
47
|
+
|
|
48
|
+
- `instance.ts` → `instance-metadata.ts` (frees the name for a possible
|
|
49
|
+
future `@toist/instance` package).
|
|
50
|
+
|
|
51
|
+
**New dependency:** `@toist/core@0.8.0`.
|
|
52
|
+
|
|
5
53
|
## 0.7.1 — 2026-05-06
|
|
6
54
|
|
|
7
55
|
Fix @toist/aja cross-package dependency versions not being bumped by release script
|
|
@@ -77,7 +125,7 @@ monorepo per Phase F W2 and shipped to the Gitea npm registry per W3.
|
|
|
77
125
|
|
|
78
126
|
- Public API: `startRunner(options): Promise<RunnerHandle>`,
|
|
79
127
|
`register(...kinds)`, `getKind(id)`, `manifest()`. See instance-spec at
|
|
80
|
-
https://toist.in for the full contract.
|
|
128
|
+
https://toist.in/docs for the full contract.
|
|
81
129
|
- HTTP server (Hono on Bun) — mounts API at `/api/*` and serves the
|
|
82
130
|
bundled UI from `@toist/ui`'s `distDir` at `/`.
|
|
83
131
|
- Pipeline dispatcher with HITL-aware suspendable executor (per-node
|
package/README.md
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# `@toist/aja`
|
|
2
|
+
|
|
3
|
+
Durable Toist host surface: SQLite runtime, HTTP runner, UI mount, filesystem helpers, and the typed runner client.
|
|
4
|
+
|
|
5
|
+
```ts
|
|
6
|
+
import { startRunner } from "@toist/aja"
|
|
7
|
+
|
|
8
|
+
await startRunner({
|
|
9
|
+
rootDir: import.meta.dir,
|
|
10
|
+
port: 3000,
|
|
11
|
+
})
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
## Main exports
|
|
15
|
+
|
|
16
|
+
- `startRunner(options)`
|
|
17
|
+
- `createSqliteRuntime(options)`
|
|
18
|
+
- `createFilesystemPipelineStore({ rootDir, watch? })`
|
|
19
|
+
- `loadFilesystemResources(rootDir)`
|
|
20
|
+
- `createRunnerClient({ baseUrl })`
|
|
21
|
+
- convenience re-exports from `@toist/core`
|
|
22
|
+
|
|
23
|
+
## Client quickstart
|
|
24
|
+
|
|
25
|
+
```ts
|
|
26
|
+
import { createRunnerClient } from "@toist/aja"
|
|
27
|
+
|
|
28
|
+
const client = createRunnerClient({ baseUrl: "http://localhost:3000" })
|
|
29
|
+
const started = await client.pipelines.runSpec(spec, { name: "world" })
|
|
30
|
+
for await (const event of client.runs.events(started.id)) {
|
|
31
|
+
console.log(event.type)
|
|
32
|
+
}
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Runtime layout
|
|
36
|
+
|
|
37
|
+
With `startRunner({ rootDir, port })`, the runner:
|
|
38
|
+
|
|
39
|
+
- loads pipelines from `<rootDir>/pipelines`
|
|
40
|
+
- stores runtime state under `<rootDir>/data`
|
|
41
|
+
- reads resources from `<rootDir>/toist.yml`
|
|
42
|
+
- serves UI + API from one Bun process
|
|
43
|
+
|
|
44
|
+
## `/manifest`
|
|
45
|
+
|
|
46
|
+
`GET /api/manifest` returns stable JSON for tooling:
|
|
47
|
+
|
|
48
|
+
```json
|
|
49
|
+
{
|
|
50
|
+
"version": 1,
|
|
51
|
+
"schema": "https://toist.in/schemas/manifest-v1.json",
|
|
52
|
+
"kinds": []
|
|
53
|
+
}
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## Resources
|
|
57
|
+
|
|
58
|
+
`loadFilesystemResources(rootDir)` reads `toist.yml` and resolves `${env:VAR}` and `${env:VAR:-fallback}` substitutions.
|
|
59
|
+
|
|
60
|
+
## When to use `@toist/aja`
|
|
61
|
+
|
|
62
|
+
Use it when you want a durable single-tenant runner with HTTP APIs. For in-process execution without the server, import `@toist/core` directly.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@toist/aja",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./src/index.ts",
|
|
6
6
|
"types": "./src/index.ts",
|
|
@@ -14,15 +14,17 @@
|
|
|
14
14
|
"files": [
|
|
15
15
|
"src/",
|
|
16
16
|
"migrations/",
|
|
17
|
-
"CHANGELOG.md"
|
|
17
|
+
"CHANGELOG.md",
|
|
18
|
+
"README.md"
|
|
18
19
|
],
|
|
19
20
|
"scripts": {
|
|
20
21
|
"dev": "bun --watch src/server.ts",
|
|
21
22
|
"smoke:client": "bun test/client-smoke.ts"
|
|
22
23
|
},
|
|
23
24
|
"dependencies": {
|
|
24
|
-
"@toist/
|
|
25
|
-
"@toist/
|
|
25
|
+
"@toist/core": "0.8.0",
|
|
26
|
+
"@toist/spec": "0.8.0",
|
|
27
|
+
"@toist/ui": "0.8.0",
|
|
26
28
|
"hono": "^4.7.7",
|
|
27
29
|
"proper-lockfile": "^4.1.2",
|
|
28
30
|
"yaml": "^2.8.4"
|
package/src/cache-db.ts
CHANGED
|
@@ -1,17 +1,9 @@
|
|
|
1
1
|
// 2121
|
|
2
|
-
// Cache database — separate file from runtime.db so its drop-recreate
|
|
3
|
-
// semantics (TTL makes content valueless) do not entangle runtime durability.
|
|
4
|
-
//
|
|
5
|
-
// Schema is owned and bootstrapped by makeCache() in cache.ts. No migrations
|
|
6
|
-
// here: cache table can be dropped or recreated freely.
|
|
7
|
-
|
|
8
2
|
import { Database } from "bun:sqlite"
|
|
9
3
|
import { mkdirSync } from "node:fs"
|
|
10
4
|
import { dirname } from "node:path"
|
|
11
|
-
import { cacheDbPath } from "./config.ts"
|
|
12
5
|
|
|
13
|
-
export function openCacheDb(): Database {
|
|
14
|
-
const path = cacheDbPath()
|
|
6
|
+
export function openCacheDb(path: string): Database {
|
|
15
7
|
mkdirSync(dirname(path), { recursive: true })
|
|
16
8
|
return new Database(path, { create: true })
|
|
17
9
|
}
|
package/src/cli.ts
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
import { existsSync, readFileSync } from "node:fs"
|
|
10
10
|
import { dirname, isAbsolute, resolve } from "node:path"
|
|
11
11
|
import YAML from "yaml"
|
|
12
|
-
import { register, startRunner } from "./index.ts"
|
|
12
|
+
import { createSqliteRuntime, register, startRunner } from "./index.ts"
|
|
13
13
|
|
|
14
14
|
// ---------------------------------------------------------------------------
|
|
15
15
|
// Config schema
|
|
@@ -22,8 +22,8 @@ interface ToistConfig {
|
|
|
22
22
|
root?: string
|
|
23
23
|
/** Pipeline YAML directory. Default: <root>/pipelines */
|
|
24
24
|
pipelines?: string
|
|
25
|
-
/** Resource
|
|
26
|
-
resources?: string
|
|
25
|
+
/** Resource path override (legacy). Default: <root>/resources */
|
|
26
|
+
resources?: string | Record<string, Record<string, unknown>>
|
|
27
27
|
/** Data directory (SQLite files). Default: <root>/data */
|
|
28
28
|
data?: string
|
|
29
29
|
/** TypeScript kind files to import and register before starting. */
|
|
@@ -59,7 +59,7 @@ if (args.includes("--help") || args.includes("-h")) {
|
|
|
59
59
|
console.log("Environment:")
|
|
60
60
|
console.log(" TOIST_PORT / PORT Override port from config")
|
|
61
61
|
console.log("")
|
|
62
|
-
console.log("Full docs: https://toist.in")
|
|
62
|
+
console.log("Full docs: https://toist.in/docs")
|
|
63
63
|
process.exit(0)
|
|
64
64
|
}
|
|
65
65
|
|
|
@@ -113,7 +113,7 @@ const cfg = (YAML.parse(raw) ?? {}) as ToistConfig
|
|
|
113
113
|
const root = cfg.root ? resolve(configDir, cfg.root) : configDir
|
|
114
114
|
|
|
115
115
|
const pipelineDir = resolvePath(configDir, root, cfg.pipelines, "pipelines")
|
|
116
|
-
const resourceDir = resolvePath(configDir, root, cfg.resources, "resources")
|
|
116
|
+
const resourceDir = resolvePath(configDir, root, typeof cfg.resources === "string" ? cfg.resources : undefined, "resources")
|
|
117
117
|
const dataDir = resolvePath(configDir, root, cfg.data, "data")
|
|
118
118
|
const port = Number(process.env.TOIST_PORT ?? process.env.PORT ?? cfg.port ?? 3000)
|
|
119
119
|
|
|
@@ -135,13 +135,19 @@ if (cfg.kinds && cfg.kinds.length > 0) {
|
|
|
135
135
|
}
|
|
136
136
|
}
|
|
137
137
|
|
|
138
|
+
void cfg.disableMcp
|
|
139
|
+
|
|
140
|
+
const runtime = await createSqliteRuntime({
|
|
141
|
+
rootDir: root,
|
|
142
|
+
pipelinesDir: pipelineDir,
|
|
143
|
+
resourcesDir: resourceDir,
|
|
144
|
+
dataDir,
|
|
145
|
+
})
|
|
146
|
+
|
|
138
147
|
await startRunner({
|
|
139
148
|
port,
|
|
140
|
-
rootDir:
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
dataDir,
|
|
144
|
-
disableUi: cfg.disableUi ?? false,
|
|
149
|
+
rootDir: root,
|
|
150
|
+
runtime,
|
|
151
|
+
disableUi: cfg.disableUi ?? false,
|
|
145
152
|
disableWatch: cfg.disableWatch ?? false,
|
|
146
|
-
disableMcp: cfg.disableMcp ?? false,
|
|
147
153
|
})
|
package/src/client.ts
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
// that want to drive the runner over its HTTP API without hard-coding paths.
|
|
6
6
|
|
|
7
7
|
import type { NodeKindManifest, ValidateResult } from "@toist/spec"
|
|
8
|
+
import type { ToistEvent } from "@toist/core"
|
|
8
9
|
|
|
9
10
|
export interface RunnerClientOptions {
|
|
10
11
|
/** Runner API base URL. Accepts either the API root
|
|
@@ -74,6 +75,12 @@ export type PipelineRunResponse =
|
|
|
74
75
|
| { id: number; pipeline: string; status: "suspended"; suspendedAt: string; task: PipelineTaskRef; steps: StepResult[] }
|
|
75
76
|
| { id: number; pipeline: string; status: "error"; error: string }
|
|
76
77
|
|
|
78
|
+
export interface AsyncRunStartResponse {
|
|
79
|
+
id: number
|
|
80
|
+
pipeline: string
|
|
81
|
+
status: "running"
|
|
82
|
+
}
|
|
83
|
+
|
|
77
84
|
export interface RunListItem {
|
|
78
85
|
id: number
|
|
79
86
|
pipeline: string
|
|
@@ -161,12 +168,14 @@ export interface RunnerClient {
|
|
|
161
168
|
create(yaml: string): Promise<PipelineSaveResult>
|
|
162
169
|
update(id: string, yaml: string): Promise<PipelineSaveResult>
|
|
163
170
|
run(id: string, payload?: Record<string, unknown>): Promise<PipelineRunResponse>
|
|
171
|
+
runSpec(spec: unknown, payload?: Record<string, unknown>): Promise<AsyncRunStartResponse>
|
|
164
172
|
}
|
|
165
173
|
runs: {
|
|
166
174
|
list(opts?: { pipeline?: string; limit?: number }): Promise<RunListItem[]>
|
|
167
175
|
get(id: number): Promise<RunListItem>
|
|
168
176
|
nodes(id: number): Promise<RunNodeOutput[]>
|
|
169
177
|
logs(id: number): Promise<RunLogLine[]>
|
|
178
|
+
events(id: number): AsyncIterable<ToistEvent>
|
|
170
179
|
}
|
|
171
180
|
tasks: {
|
|
172
181
|
list(opts?: { status?: string; runId?: number; assignee?: string; limit?: number }): Promise<TaskListItem[]>
|
|
@@ -234,6 +243,10 @@ export function createRunnerClient(options: RunnerClientOptions): RunnerClient {
|
|
|
234
243
|
method: "POST",
|
|
235
244
|
body: json(payload),
|
|
236
245
|
}),
|
|
246
|
+
runSpec: (spec, payload = {}) => request<AsyncRunStartResponse>("/runs", {
|
|
247
|
+
method: "POST",
|
|
248
|
+
body: json({ spec, payload }),
|
|
249
|
+
}),
|
|
237
250
|
},
|
|
238
251
|
|
|
239
252
|
runs: {
|
|
@@ -241,6 +254,7 @@ export function createRunnerClient(options: RunnerClientOptions): RunnerClient {
|
|
|
241
254
|
get: (id) => request<RunListItem>(`/runs/${id}`),
|
|
242
255
|
nodes: (id) => request<RunNodeOutput[]>(`/runs/${id}/nodes`),
|
|
243
256
|
logs: (id) => request<RunLogLine[]>(`/runs/${id}/logs`),
|
|
257
|
+
events: (id) => streamEvents(f, `${base}/runs/${id}/events`, options.headers),
|
|
244
258
|
},
|
|
245
259
|
|
|
246
260
|
tasks: {
|
|
@@ -303,3 +317,65 @@ async function readJsonOrText(response: Response): Promise<unknown> {
|
|
|
303
317
|
try { return JSON.parse(text) }
|
|
304
318
|
catch { return text }
|
|
305
319
|
}
|
|
320
|
+
|
|
321
|
+
function streamEvents(
|
|
322
|
+
f: typeof fetch,
|
|
323
|
+
url: string,
|
|
324
|
+
extraHeaders?: HeadersInit,
|
|
325
|
+
): AsyncIterable<ToistEvent> {
|
|
326
|
+
return {
|
|
327
|
+
[Symbol.asyncIterator]() {
|
|
328
|
+
let reader: ReadableStreamDefaultReader<Uint8Array> | null = null
|
|
329
|
+
let buffer = ""
|
|
330
|
+
const decoder = new TextDecoder()
|
|
331
|
+
const queue: ToistEvent[] = []
|
|
332
|
+
let finished = false
|
|
333
|
+
|
|
334
|
+
async function ensureReader() {
|
|
335
|
+
if (reader) return reader
|
|
336
|
+
const headers = new Headers(extraHeaders)
|
|
337
|
+
headers.set("accept", "text/event-stream")
|
|
338
|
+
const response = await f(url, { headers })
|
|
339
|
+
if (!response.ok) {
|
|
340
|
+
const body = await readJsonOrText(response)
|
|
341
|
+
throw new RunnerHttpError(response.status, response.statusText, body)
|
|
342
|
+
}
|
|
343
|
+
if (!response.body) throw new Error("runner returned no SSE body")
|
|
344
|
+
reader = response.body.getReader()
|
|
345
|
+
return reader
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
async function pump(): Promise<void> {
|
|
349
|
+
const r = await ensureReader()
|
|
350
|
+
while (queue.length === 0 && !finished) {
|
|
351
|
+
const { value, done } = await r.read()
|
|
352
|
+
if (done) {
|
|
353
|
+
finished = true
|
|
354
|
+
return
|
|
355
|
+
}
|
|
356
|
+
buffer += decoder.decode(value, { stream: true })
|
|
357
|
+
let boundary = buffer.indexOf("\n\n")
|
|
358
|
+
while (boundary !== -1) {
|
|
359
|
+
const frame = buffer.slice(0, boundary)
|
|
360
|
+
buffer = buffer.slice(boundary + 2)
|
|
361
|
+
const data = frame
|
|
362
|
+
.split("\n")
|
|
363
|
+
.filter((line) => line.startsWith("data:"))
|
|
364
|
+
.map((line) => line.slice(5).trim())
|
|
365
|
+
.join("\n")
|
|
366
|
+
if (data) queue.push(JSON.parse(data) as ToistEvent)
|
|
367
|
+
boundary = buffer.indexOf("\n\n")
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
return {
|
|
373
|
+
async next(): Promise<IteratorResult<ToistEvent>> {
|
|
374
|
+
await pump()
|
|
375
|
+
if (queue.length > 0) return { value: queue.shift()!, done: false }
|
|
376
|
+
return { value: undefined as never, done: true }
|
|
377
|
+
},
|
|
378
|
+
}
|
|
379
|
+
},
|
|
380
|
+
}
|
|
381
|
+
}
|
package/src/data-db.ts
CHANGED
|
@@ -1,21 +1,9 @@
|
|
|
1
1
|
// 2121
|
|
2
|
-
// Domain data store. The instance owns the schema entirely — every table here
|
|
3
|
-
// is created by a pipeline (via db.insert) or by a domain migration the project
|
|
4
|
-
// chooses to add. Platform never writes to this DB directly.
|
|
5
|
-
//
|
|
6
|
-
// Kinds receive this as `ctx.db`. Pipelines write to it (db.insert), read
|
|
7
|
-
// from it (db.query), or any custom domain kind interacts with it as needed.
|
|
8
|
-
//
|
|
9
|
-
// Location: <dataDir>/data.db (per context/instance-spec.md §3 — the host's
|
|
10
|
-
// schema-on-write domain DB lives in data/ alongside runtime.db and cache.db).
|
|
11
|
-
|
|
12
2
|
import { Database } from "bun:sqlite"
|
|
13
3
|
import { mkdirSync } from "node:fs"
|
|
14
4
|
import { dirname } from "node:path"
|
|
15
|
-
import { dataDbPath } from "./config.ts"
|
|
16
5
|
|
|
17
|
-
export function openDataDb(): Database {
|
|
18
|
-
const path = dataDbPath()
|
|
6
|
+
export function openDataDb(path: string): Database {
|
|
19
7
|
mkdirSync(dirname(path), { recursive: true })
|
|
20
8
|
return new Database(path, { create: true })
|
|
21
9
|
}
|
package/src/index.ts
CHANGED
|
@@ -9,6 +9,9 @@
|
|
|
9
9
|
|
|
10
10
|
// Lifecycle entry — what hosts call to start an instance.
|
|
11
11
|
export { startRunner, type StartRunnerOptions, type RunnerHandle } from "./startRunner.ts"
|
|
12
|
+
export { createSqliteRuntime, type CreateSqliteRuntimeOptions } from "./sqlite-runtime.ts"
|
|
13
|
+
export { loadFilesystemResources } from "./resources-fs.ts"
|
|
14
|
+
export { createFilesystemPipelineStore, type FilesystemPipelineStore } from "./pipeline-store.ts"
|
|
12
15
|
|
|
13
16
|
// Typed HTTP client — what external hosts/tools use to drive an instance.
|
|
14
17
|
export {
|
|
@@ -22,6 +25,7 @@ export {
|
|
|
22
25
|
type StepResult,
|
|
23
26
|
type PipelineTaskRef,
|
|
24
27
|
type PipelineRunResponse,
|
|
28
|
+
type AsyncRunStartResponse,
|
|
25
29
|
type RunListItem,
|
|
26
30
|
type RunNodeOutput,
|
|
27
31
|
type RunLogLine,
|
|
@@ -31,15 +35,44 @@ export {
|
|
|
31
35
|
type ResourceRecord,
|
|
32
36
|
} from "./client.ts"
|
|
33
37
|
|
|
34
|
-
//
|
|
35
|
-
export {
|
|
38
|
+
// Kernel convenience re-exports.
|
|
39
|
+
export {
|
|
40
|
+
builtinKinds,
|
|
41
|
+
createRegistry,
|
|
42
|
+
createMemoryRuntime,
|
|
43
|
+
createEventStream,
|
|
44
|
+
runSpec,
|
|
45
|
+
register,
|
|
46
|
+
getKind,
|
|
47
|
+
manifest,
|
|
48
|
+
} from "@toist/core"
|
|
36
49
|
|
|
37
50
|
// Type re-exports from @toist/spec, so a host that imports only
|
|
38
51
|
// @toist/aja doesn't need a second package on its dependency list when
|
|
39
52
|
// it only writes kinds (rather than authoring pipeline-format tooling).
|
|
53
|
+
export type {
|
|
54
|
+
ToistRuntime,
|
|
55
|
+
ResumableRuntime,
|
|
56
|
+
RunLedger,
|
|
57
|
+
TaskStore,
|
|
58
|
+
LogStore,
|
|
59
|
+
NodeOutputStore,
|
|
60
|
+
ResourceResolver,
|
|
61
|
+
KindRegistry,
|
|
62
|
+
RunCtx,
|
|
63
|
+
RunOutcome,
|
|
64
|
+
RunOptions,
|
|
65
|
+
ToistEvent,
|
|
66
|
+
EventSink,
|
|
67
|
+
RunSpecOptions,
|
|
68
|
+
RunSpecResult,
|
|
69
|
+
} from "@toist/core"
|
|
70
|
+
|
|
40
71
|
export type {
|
|
41
72
|
NodeKind,
|
|
42
73
|
NodeKindManifest,
|
|
74
|
+
DataHandle,
|
|
75
|
+
DataStatement,
|
|
43
76
|
ParamDef,
|
|
44
77
|
PortDef,
|
|
45
78
|
ExecContext,
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
// 2121
|
|
2
|
+
// Per-instance metadata: what kind of platform-instance this is (which
|
|
3
|
+
// customer, what tier, what teasers to surface), distinct from the runtime
|
|
4
|
+
// state in runtime.db.
|
|
5
|
+
|
|
6
|
+
import { existsSync, readFileSync } from "node:fs"
|
|
7
|
+
import { join } from "node:path"
|
|
8
|
+
|
|
9
|
+
export interface Teaser {
|
|
10
|
+
id: string
|
|
11
|
+
title: string
|
|
12
|
+
description?: string
|
|
13
|
+
category?: string
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface Instance {
|
|
17
|
+
platformVersion?: string
|
|
18
|
+
instanceName?: string
|
|
19
|
+
tier?: string
|
|
20
|
+
kindAllowlist?: string[]
|
|
21
|
+
teasers: Teaser[]
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const DEFAULTS: Instance = { teasers: [] }
|
|
25
|
+
|
|
26
|
+
// Re-read on every call. instance.json is tiny and changes rarely; the cost
|
|
27
|
+
// is negligible and avoids stale-cache surprises when the file is edited.
|
|
28
|
+
export function loadInstance(rootDir: string): Instance {
|
|
29
|
+
const path = join(rootDir, "instance.json")
|
|
30
|
+
if (!existsSync(path)) return DEFAULTS
|
|
31
|
+
|
|
32
|
+
try {
|
|
33
|
+
const raw = readFileSync(path, "utf8")
|
|
34
|
+
const parsed = JSON.parse(raw) as Partial<Instance>
|
|
35
|
+
return {
|
|
36
|
+
platformVersion: parsed.platformVersion,
|
|
37
|
+
instanceName: parsed.instanceName,
|
|
38
|
+
tier: parsed.tier,
|
|
39
|
+
kindAllowlist: Array.isArray(parsed.kindAllowlist)
|
|
40
|
+
? parsed.kindAllowlist.filter((value): value is string => typeof value === "string")
|
|
41
|
+
: undefined,
|
|
42
|
+
teasers: Array.isArray(parsed.teasers) ? parsed.teasers : [],
|
|
43
|
+
}
|
|
44
|
+
} catch (err) {
|
|
45
|
+
console.warn(`[instance] failed to read ${path}:`, (err as Error).message)
|
|
46
|
+
return DEFAULTS
|
|
47
|
+
}
|
|
48
|
+
}
|
package/src/kinds/index.ts
CHANGED
|
@@ -1,66 +1,28 @@
|
|
|
1
1
|
// 2121
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
import { trigger, sink } from "./control.ts"
|
|
8
|
-
import { humanInput } from "./hitl.ts"
|
|
9
|
-
import { runsLastOutput, runsNodeOutput, pipelineRun } from "./runs.ts"
|
|
2
|
+
// Aja-side kind registry shim.
|
|
3
|
+
//
|
|
4
|
+
// Builtins and registry implementation now live in @toist/core. Aja keeps
|
|
5
|
+
// this module so existing local imports and custom kind registration patterns
|
|
6
|
+
// continue to work during the package carve-out.
|
|
10
7
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
transformSort,
|
|
19
|
-
transformAggregate,
|
|
20
|
-
dbInsert,
|
|
21
|
-
dbQuery,
|
|
22
|
-
httpFetch,
|
|
23
|
-
httpPost,
|
|
24
|
-
humanInput,
|
|
25
|
-
runsLastOutput,
|
|
26
|
-
runsNodeOutput,
|
|
27
|
-
pipelineRun,
|
|
28
|
-
]
|
|
8
|
+
export {
|
|
9
|
+
builtinKinds,
|
|
10
|
+
createRegistry,
|
|
11
|
+
register,
|
|
12
|
+
getKind,
|
|
13
|
+
manifest,
|
|
14
|
+
} from "@toist/core"
|
|
29
15
|
|
|
30
|
-
export
|
|
16
|
+
export type {
|
|
17
|
+
NodeKind,
|
|
18
|
+
NodeKindManifest,
|
|
19
|
+
DataHandle,
|
|
20
|
+
DataStatement,
|
|
21
|
+
ParamDef,
|
|
22
|
+
PortDef,
|
|
23
|
+
ExecContext,
|
|
24
|
+
PlatformCtx,
|
|
25
|
+
} from "@toist/spec"
|
|
31
26
|
|
|
32
|
-
|
|
33
|
-
* Register one or more custom domain kinds. Domain projects edit
|
|
34
|
-
* `runner/src/kinds/custom.ts` (created by `platform init`) and call
|
|
35
|
-
* `register(...)` there with their own kinds. Builtin kinds and domain kinds
|
|
36
|
-
* live in the same registry — `getKind(id)` resolves both transparently.
|
|
37
|
-
*
|
|
38
|
-
* Re-registering an existing id replaces the previous entry; useful for hot
|
|
39
|
-
* iteration but warn loudly so accidental shadows are visible.
|
|
40
|
-
*/
|
|
41
|
-
export function register(...kinds: NodeKind<any, any>[]): void {
|
|
42
|
-
for (const k of kinds) {
|
|
43
|
-
if (registry.has(k.id) && !builtins.find((b) => b.id === k.id)) {
|
|
44
|
-
console.warn(`[kinds] re-registering "${k.id}" — previous registration replaced`)
|
|
45
|
-
}
|
|
46
|
-
registry.set(k.id, k)
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
export function getKind(id: string): NodeKind<any, any> | undefined {
|
|
51
|
-
return registry.get(id)
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
export function manifest(): NodeKindManifest[] {
|
|
55
|
-
return [...registry.values()].map(({ run: _e, ...rest }) => rest)
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
export type { NodeKind, NodeKindManifest, ParamDef, PortDef, ExecContext, PlatformCtx } from "./types.ts"
|
|
59
|
-
|
|
60
|
-
// Side-effect import: domain kinds register themselves when this module loads.
|
|
61
|
-
// Dynamic import + top-level await defers custom.ts evaluation until AFTER
|
|
62
|
-
// `registry` and `register` are initialised — a static `import "./custom.ts"`
|
|
63
|
-
// gets hoisted ahead of the const init, putting `registry` in the temporal
|
|
64
|
-
// dead zone when custom.ts calls register(). The dynamic form runs at
|
|
65
|
-
// statement position, after init, breaking the cycle.
|
|
27
|
+
// Preserve the old side-effect registration hook for domain kinds.
|
|
66
28
|
await import("./custom.ts")
|
package/src/lock.ts
CHANGED
|
@@ -1,64 +1,38 @@
|
|
|
1
1
|
// 2121
|
|
2
|
-
// Single-leader runner lock. Prevents two runner processes from racing on
|
|
3
|
-
// migrations, scheduled cron-wheel firings, or HITL resume dispatch.
|
|
4
|
-
//
|
|
5
|
-
// The lock guards the data/ directory using a sentinel lockfile at
|
|
6
|
-
// data/.lock. proper-lockfile creates a holder directory that includes mtime;
|
|
7
|
-
// stale locks (from a crashed previous process) are detected after `stale` ms
|
|
8
|
-
// and overtaken automatically.
|
|
9
|
-
//
|
|
10
|
-
// Watch-mode reload tolerance: bun --watch SIGTERMs the old process before
|
|
11
|
-
// spawning the new one. The cleanup handlers below release the lock on
|
|
12
|
-
// SIGTERM/SIGINT/exit. Async lock acquisition with retries handles the brief
|
|
13
|
-
// window where the old process has not yet released.
|
|
14
|
-
|
|
15
2
|
import lockfile from "proper-lockfile"
|
|
16
3
|
import { mkdirSync } from "node:fs"
|
|
17
|
-
import { dataDir, lockfilePath } from "./config.ts"
|
|
18
4
|
|
|
19
|
-
|
|
5
|
+
export interface AcquireRunnerLockOptions {
|
|
6
|
+
dir: string
|
|
7
|
+
lockfilePath: string
|
|
8
|
+
}
|
|
20
9
|
|
|
21
|
-
export async function acquireRunnerLock(): Promise<void
|
|
22
|
-
|
|
23
|
-
const lockPath = lockfilePath()
|
|
24
|
-
mkdirSync(dir, { recursive: true })
|
|
10
|
+
export async function acquireRunnerLock(opts: AcquireRunnerLockOptions): Promise<() => Promise<void>> {
|
|
11
|
+
mkdirSync(opts.dir, { recursive: true })
|
|
25
12
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
})
|
|
33
|
-
} catch (err) {
|
|
34
|
-
const msg = err instanceof Error ? err.message : String(err)
|
|
35
|
-
console.error(`[runner] failed to acquire ${lockPath}: ${msg}`)
|
|
36
|
-
console.error(`[runner] another runner appears live. Stop it first; only one runner per project is allowed.`)
|
|
37
|
-
process.exit(1)
|
|
38
|
-
}
|
|
13
|
+
let release = await lockfile.lock(opts.dir, {
|
|
14
|
+
lockfilePath: opts.lockfilePath,
|
|
15
|
+
realpath: false,
|
|
16
|
+
stale: 10_000,
|
|
17
|
+
retries: { retries: 5, factor: 2, minTimeout: 100, maxTimeout: 1000 },
|
|
18
|
+
})
|
|
39
19
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
20
|
+
let released = false
|
|
21
|
+
const cleanup = async () => {
|
|
22
|
+
if (released) return
|
|
23
|
+
released = true
|
|
24
|
+
const current = release
|
|
25
|
+
release = async () => {}
|
|
26
|
+
try {
|
|
27
|
+
await current()
|
|
28
|
+
} catch {
|
|
29
|
+
// best-effort cleanup on process exit / stop
|
|
30
|
+
}
|
|
45
31
|
}
|
|
46
32
|
|
|
47
|
-
process.on("exit",
|
|
48
|
-
process.on("SIGTERM", () => { cleanup()
|
|
49
|
-
process.on("SIGINT",
|
|
50
|
-
}
|
|
33
|
+
process.on("exit", () => { void cleanup() })
|
|
34
|
+
process.on("SIGTERM", () => { void cleanup() })
|
|
35
|
+
process.on("SIGINT", () => { void cleanup() })
|
|
51
36
|
|
|
52
|
-
|
|
53
|
-
* `stop()` path. Idempotent: no-op if the lock is not held. The signal
|
|
54
|
-
* cleanup handlers installed by acquireRunnerLock remain in place. */
|
|
55
|
-
export async function releaseRunnerLock(): Promise<void> {
|
|
56
|
-
if (!release) return
|
|
57
|
-
const r = release
|
|
58
|
-
release = null
|
|
59
|
-
try {
|
|
60
|
-
await r()
|
|
61
|
-
} catch {
|
|
62
|
-
// best-effort — surfacing this would mask the actual stop reason
|
|
63
|
-
}
|
|
37
|
+
return cleanup
|
|
64
38
|
}
|