@toist/aja 0.5.0 → 0.7.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 +29 -0
- package/package.json +12 -6
- package/src/cli.ts +147 -0
- 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,35 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to `@toist/aja` are recorded here.
|
|
4
4
|
|
|
5
|
+
## 0.7.1 — 2026-05-06
|
|
6
|
+
|
|
7
|
+
Fix @toist/aja cross-package dependency versions not being bumped by release script
|
|
8
|
+
|
|
9
|
+
## 0.7.0 — 2026-05-06
|
|
10
|
+
|
|
11
|
+
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.
|
|
12
|
+
|
|
13
|
+
## Unreleased
|
|
14
|
+
|
|
15
|
+
## 0.6.1 — 2026-05-05
|
|
16
|
+
|
|
17
|
+
Fix published @toist/aja dependency resolution for embedded runners and include the typed runner client/smoke-test surface.
|
|
18
|
+
|
|
19
|
+
- Added `createRunnerClient()` typed HTTP adapter, exported from
|
|
20
|
+
`@toist/aja` and `@toist/aja/client`, for driving runner instances
|
|
21
|
+
without hard-coding API paths.
|
|
22
|
+
- Added `smoke:client` script that starts an embedded runner, drives it via
|
|
23
|
+
the client, and verifies persisted node outputs.
|
|
24
|
+
- Changed published package dependencies on `@toist/spec` and `@toist/ui`
|
|
25
|
+
from workspace protocol to concrete `0.6.1` versions so external embeds
|
|
26
|
+
install transitive runtime packages correctly.
|
|
27
|
+
- Documented the v1 embedded-instance limit: one runner per process.
|
|
28
|
+
|
|
29
|
+
## 0.6.0 — 2026-05-05
|
|
30
|
+
|
|
31
|
+
Lockstep version bump alongside `@toist/in@0.6.0`. No functional changes
|
|
32
|
+
in this package.
|
|
33
|
+
|
|
5
34
|
## 0.5.0 — 2026-05-05
|
|
6
35
|
|
|
7
36
|
Lockstep version bump. No functional changes in this package.
|
package/package.json
CHANGED
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@toist/aja",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.1",
|
|
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
|
-
".": "./src/index.ts"
|
|
11
|
+
".": "./src/index.ts",
|
|
12
|
+
"./client": "./src/client.ts"
|
|
9
13
|
},
|
|
10
14
|
"files": [
|
|
11
15
|
"src/",
|
|
@@ -13,13 +17,15 @@
|
|
|
13
17
|
"CHANGELOG.md"
|
|
14
18
|
],
|
|
15
19
|
"scripts": {
|
|
16
|
-
"dev": "bun --watch src/server.ts"
|
|
20
|
+
"dev": "bun --watch src/server.ts",
|
|
21
|
+
"smoke:client": "bun test/client-smoke.ts"
|
|
17
22
|
},
|
|
18
23
|
"dependencies": {
|
|
19
|
-
"@toist/spec": "0.
|
|
20
|
-
"@toist/ui": "0.
|
|
24
|
+
"@toist/spec": "0.7.1",
|
|
25
|
+
"@toist/ui": "0.7.1",
|
|
21
26
|
"hono": "^4.7.7",
|
|
22
|
-
"proper-lockfile": "^4.1.2"
|
|
27
|
+
"proper-lockfile": "^4.1.2",
|
|
28
|
+
"yaml": "^2.8.4"
|
|
23
29
|
},
|
|
24
30
|
"devDependencies": {
|
|
25
31
|
"@types/proper-lockfile": "^4.1.4"
|
package/src/cli.ts
ADDED
|
@@ -0,0 +1,147 @@
|
|
|
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 { 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 YAML directory. Default: <root>/resources */
|
|
26
|
+
resources?: string
|
|
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")
|
|
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, cfg.resources, "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
|
+
await startRunner({
|
|
139
|
+
port,
|
|
140
|
+
rootDir: root,
|
|
141
|
+
pipelineDir,
|
|
142
|
+
resourceDir,
|
|
143
|
+
dataDir,
|
|
144
|
+
disableUi: cfg.disableUi ?? false,
|
|
145
|
+
disableWatch: cfg.disableWatch ?? false,
|
|
146
|
+
disableMcp: cfg.disableMcp ?? false,
|
|
147
|
+
})
|
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()
|