@toist/aja 0.6.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 +57 -1
- package/README.md +62 -0
- package/package.json +11 -5
- package/src/cache-db.ts +1 -9
- package/src/cli.ts +153 -0
- 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,62 @@
|
|
|
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
|
+
|
|
53
|
+
## 0.7.1 — 2026-05-06
|
|
54
|
+
|
|
55
|
+
Fix @toist/aja cross-package dependency versions not being bumped by release script
|
|
56
|
+
|
|
57
|
+
## 0.7.0 — 2026-05-06
|
|
58
|
+
|
|
59
|
+
Add @toist/aja CLI (bunx @toist/aja --config toist.yml) and @toist/in interactive setup wizard (bunx @toist/in). Establishes aja=runner, in=adoption tool split. No forced directory layout — user chooses paths via wizard, config drives the runner.
|
|
60
|
+
|
|
5
61
|
## Unreleased
|
|
6
62
|
|
|
7
63
|
## 0.6.1 — 2026-05-05
|
|
@@ -69,7 +125,7 @@ monorepo per Phase F W2 and shipped to the Gitea npm registry per W3.
|
|
|
69
125
|
|
|
70
126
|
- Public API: `startRunner(options): Promise<RunnerHandle>`,
|
|
71
127
|
`register(...kinds)`, `getKind(id)`, `manifest()`. See instance-spec at
|
|
72
|
-
https://toist.in for the full contract.
|
|
128
|
+
https://toist.in/docs for the full contract.
|
|
73
129
|
- HTTP server (Hono on Bun) — mounts API at `/api/*` and serves the
|
|
74
130
|
bundled UI from `@toist/ui`'s `distDir` at `/`.
|
|
75
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,9 +1,12 @@
|
|
|
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",
|
|
7
|
+
"bin": {
|
|
8
|
+
"toist-aja": "./src/cli.ts"
|
|
9
|
+
},
|
|
7
10
|
"exports": {
|
|
8
11
|
".": "./src/index.ts",
|
|
9
12
|
"./client": "./src/client.ts"
|
|
@@ -11,17 +14,20 @@
|
|
|
11
14
|
"files": [
|
|
12
15
|
"src/",
|
|
13
16
|
"migrations/",
|
|
14
|
-
"CHANGELOG.md"
|
|
17
|
+
"CHANGELOG.md",
|
|
18
|
+
"README.md"
|
|
15
19
|
],
|
|
16
20
|
"scripts": {
|
|
17
21
|
"dev": "bun --watch src/server.ts",
|
|
18
22
|
"smoke:client": "bun test/client-smoke.ts"
|
|
19
23
|
},
|
|
20
24
|
"dependencies": {
|
|
21
|
-
"@toist/
|
|
22
|
-
"@toist/
|
|
25
|
+
"@toist/core": "0.8.0",
|
|
26
|
+
"@toist/spec": "0.8.0",
|
|
27
|
+
"@toist/ui": "0.8.0",
|
|
23
28
|
"hono": "^4.7.7",
|
|
24
|
-
"proper-lockfile": "^4.1.2"
|
|
29
|
+
"proper-lockfile": "^4.1.2",
|
|
30
|
+
"yaml": "^2.8.4"
|
|
25
31
|
},
|
|
26
32
|
"devDependencies": {
|
|
27
33
|
"@types/proper-lockfile": "^4.1.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
ADDED
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
// 2121 toist
|
|
3
|
+
// @toist/aja CLI — start a Toist runner from a config file.
|
|
4
|
+
//
|
|
5
|
+
// Usage:
|
|
6
|
+
// bunx @toist/aja auto-discover toist.yml in cwd
|
|
7
|
+
// bunx @toist/aja --config <path> explicit config file
|
|
8
|
+
|
|
9
|
+
import { existsSync, readFileSync } from "node:fs"
|
|
10
|
+
import { dirname, isAbsolute, resolve } from "node:path"
|
|
11
|
+
import YAML from "yaml"
|
|
12
|
+
import { createSqliteRuntime, register, startRunner } from "./index.ts"
|
|
13
|
+
|
|
14
|
+
// ---------------------------------------------------------------------------
|
|
15
|
+
// Config schema
|
|
16
|
+
// ---------------------------------------------------------------------------
|
|
17
|
+
|
|
18
|
+
interface ToistConfig {
|
|
19
|
+
/** TCP port. Env PORT or TOIST_PORT takes precedence. */
|
|
20
|
+
port?: number
|
|
21
|
+
/** Root directory for default path resolution. Defaults to config file dir. */
|
|
22
|
+
root?: string
|
|
23
|
+
/** Pipeline YAML directory. Default: <root>/pipelines */
|
|
24
|
+
pipelines?: string
|
|
25
|
+
/** Resource path override (legacy). Default: <root>/resources */
|
|
26
|
+
resources?: string | Record<string, Record<string, unknown>>
|
|
27
|
+
/** Data directory (SQLite files). Default: <root>/data */
|
|
28
|
+
data?: string
|
|
29
|
+
/** TypeScript kind files to import and register before starting. */
|
|
30
|
+
kinds?: string[]
|
|
31
|
+
/** Disable the bundled UI. */
|
|
32
|
+
disableUi?: boolean
|
|
33
|
+
/** Disable filesystem watch on pipelines/resources. */
|
|
34
|
+
disableWatch?: boolean
|
|
35
|
+
/** Disable the MCP server. */
|
|
36
|
+
disableMcp?: boolean
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// ---------------------------------------------------------------------------
|
|
40
|
+
// CLI args
|
|
41
|
+
// ---------------------------------------------------------------------------
|
|
42
|
+
|
|
43
|
+
const args = process.argv.slice(2)
|
|
44
|
+
|
|
45
|
+
if (args.includes("--help") || args.includes("-h")) {
|
|
46
|
+
console.log("Usage:")
|
|
47
|
+
console.log(" bunx @toist/aja auto-discover toist.yml in cwd")
|
|
48
|
+
console.log(" bunx @toist/aja --config <path> explicit config file")
|
|
49
|
+
console.log("")
|
|
50
|
+
console.log("Config file format (YAML):")
|
|
51
|
+
console.log(" port: 3000")
|
|
52
|
+
console.log(" root: . # base for all relative paths")
|
|
53
|
+
console.log(" pipelines: ./pipelines # override pipeline dir")
|
|
54
|
+
console.log(" resources: ./resources # override resource dir")
|
|
55
|
+
console.log(" data: ./data # override data dir")
|
|
56
|
+
console.log(" kinds: # TypeScript kind files to register")
|
|
57
|
+
console.log(" - ./src/kinds/index.ts")
|
|
58
|
+
console.log("")
|
|
59
|
+
console.log("Environment:")
|
|
60
|
+
console.log(" TOIST_PORT / PORT Override port from config")
|
|
61
|
+
console.log("")
|
|
62
|
+
console.log("Full docs: https://toist.in/docs")
|
|
63
|
+
process.exit(0)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const configIdx = args.indexOf("--config")
|
|
67
|
+
const configArg = configIdx !== -1 ? args[configIdx + 1] : null
|
|
68
|
+
|
|
69
|
+
// ---------------------------------------------------------------------------
|
|
70
|
+
// Config discovery
|
|
71
|
+
// ---------------------------------------------------------------------------
|
|
72
|
+
|
|
73
|
+
const CONFIG_NAMES = ["toist.yml", "toist.yaml"]
|
|
74
|
+
|
|
75
|
+
function findConfig(): string {
|
|
76
|
+
if (configArg) {
|
|
77
|
+
const p = resolve(process.cwd(), configArg)
|
|
78
|
+
if (!existsSync(p)) {
|
|
79
|
+
console.error(`Config file not found: ${p}`)
|
|
80
|
+
process.exit(1)
|
|
81
|
+
}
|
|
82
|
+
return p
|
|
83
|
+
}
|
|
84
|
+
for (const name of CONFIG_NAMES) {
|
|
85
|
+
const p = resolve(process.cwd(), name)
|
|
86
|
+
if (existsSync(p)) return p
|
|
87
|
+
}
|
|
88
|
+
console.error("No toist.yml found in current directory.")
|
|
89
|
+
console.error("")
|
|
90
|
+
console.error("Create one, or use: bunx @toist/aja --config <path>")
|
|
91
|
+
console.error("To scaffold a new Toist setup: bunx @toist/in")
|
|
92
|
+
process.exit(1)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// ---------------------------------------------------------------------------
|
|
96
|
+
// Path resolution — all paths relative to config file directory
|
|
97
|
+
// ---------------------------------------------------------------------------
|
|
98
|
+
|
|
99
|
+
function resolvePath(configDir: string, root: string, p: string | undefined, defaultSub: string): string {
|
|
100
|
+
if (p) return isAbsolute(p) ? p : resolve(configDir, p)
|
|
101
|
+
return resolve(configDir, root, defaultSub)
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// ---------------------------------------------------------------------------
|
|
105
|
+
// Main
|
|
106
|
+
// ---------------------------------------------------------------------------
|
|
107
|
+
|
|
108
|
+
const configPath = findConfig()
|
|
109
|
+
const configDir = dirname(configPath)
|
|
110
|
+
const raw = readFileSync(configPath, "utf8")
|
|
111
|
+
const cfg = (YAML.parse(raw) ?? {}) as ToistConfig
|
|
112
|
+
|
|
113
|
+
const root = cfg.root ? resolve(configDir, cfg.root) : configDir
|
|
114
|
+
|
|
115
|
+
const pipelineDir = resolvePath(configDir, root, cfg.pipelines, "pipelines")
|
|
116
|
+
const resourceDir = resolvePath(configDir, root, typeof cfg.resources === "string" ? cfg.resources : undefined, "resources")
|
|
117
|
+
const dataDir = resolvePath(configDir, root, cfg.data, "data")
|
|
118
|
+
const port = Number(process.env.TOIST_PORT ?? process.env.PORT ?? cfg.port ?? 3000)
|
|
119
|
+
|
|
120
|
+
// Register kinds from config — dynamic imports resolved relative to config dir.
|
|
121
|
+
if (cfg.kinds && cfg.kinds.length > 0) {
|
|
122
|
+
for (const kindPath of cfg.kinds) {
|
|
123
|
+
const abs = isAbsolute(kindPath) ? kindPath : resolve(configDir, kindPath)
|
|
124
|
+
if (!existsSync(abs)) {
|
|
125
|
+
console.error(`Kind file not found: ${abs}`)
|
|
126
|
+
process.exit(1)
|
|
127
|
+
}
|
|
128
|
+
const mod = await import(abs)
|
|
129
|
+
const kinds = Object.values(mod).filter(Boolean)
|
|
130
|
+
if (kinds.length === 0) {
|
|
131
|
+
console.warn(`[toist] warning: no exports found in kind file ${abs}`)
|
|
132
|
+
} else {
|
|
133
|
+
register(...(kinds as Parameters<typeof register>))
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
void cfg.disableMcp
|
|
139
|
+
|
|
140
|
+
const runtime = await createSqliteRuntime({
|
|
141
|
+
rootDir: root,
|
|
142
|
+
pipelinesDir: pipelineDir,
|
|
143
|
+
resourcesDir: resourceDir,
|
|
144
|
+
dataDir,
|
|
145
|
+
})
|
|
146
|
+
|
|
147
|
+
await startRunner({
|
|
148
|
+
port,
|
|
149
|
+
rootDir: root,
|
|
150
|
+
runtime,
|
|
151
|
+
disableUi: cfg.disableUi ?? false,
|
|
152
|
+
disableWatch: cfg.disableWatch ?? false,
|
|
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
|
+
}
|