@toist/in 0.7.1 → 0.8.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/src/index.ts CHANGED
@@ -1,37 +1,2 @@
1
1
  #!/usr/bin/env bun
2
- // 2121 toist
3
- // @toist/in — interactive setup wizard for Toist.
4
- //
5
- // Usage:
6
- // bunx @toist/in run wizard → writes toist.yml + creates dirs
7
- // bunx @toist/in upgrade upgrade @toist/* deps in current directory
8
-
9
- import { wizard } from "./wizard.ts"
10
- import { upgrade } from "./upgrade.ts"
11
-
12
- const args = process.argv.slice(2)
13
- const arg = args[0]
14
-
15
- if (arg === "--help" || arg === "-h") {
16
- console.log("bunx @toist/in run setup wizard → writes toist.yml")
17
- console.log("bunx @toist/in upgrade upgrade @toist/* deps")
18
- console.log("")
19
- console.log("To start the runner after setup:")
20
- console.log(" bunx @toist/aja")
21
- console.log("")
22
- console.log("Full docs: https://toist.in")
23
- process.exit(0)
24
- }
25
-
26
- if (!arg) {
27
- await wizard()
28
- } else if (arg === "upgrade") {
29
- await upgrade()
30
- } else {
31
- console.error(`Unknown command: ${arg}`)
32
- console.error("")
33
- console.error("Usage:")
34
- console.error(" bunx @toist/in run setup wizard")
35
- console.error(" bunx @toist/in upgrade upgrade @toist/* deps")
36
- process.exit(1)
37
- }
2
+ import "./cli.ts"
package/src/runtime.ts ADDED
@@ -0,0 +1,75 @@
1
+ import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs"
2
+ import { join, resolve, dirname } from "node:path"
3
+ import { homedir } from "node:os"
4
+ import { createMemoryRuntime, runSpec, type ResumableRuntime, type RunSpecResult, type ToistRuntime } from "@toist/core"
5
+ import { createSqliteRuntime } from "@toist/aja"
6
+
7
+ interface RuntimeWithClose extends ToistRuntime {
8
+ close?: () => Promise<void>
9
+ }
10
+
11
+ export interface StoredRunDescriptor {
12
+ spec: unknown
13
+ payload: Record<string, unknown>
14
+ }
15
+
16
+ export function getCliStateDir(): string {
17
+ const xdg = process.env.XDG_STATE_HOME
18
+ if (xdg) return resolve(xdg, "toist")
19
+
20
+ const home = process.env.HOME || homedir()
21
+ if (home) return resolve(home, ".toist")
22
+
23
+ return resolve(process.cwd(), ".toist")
24
+ }
25
+
26
+ export async function getCliRuntime(options: { ephemeral?: boolean } = {}): Promise<RuntimeWithClose> {
27
+ if (options.ephemeral) return createMemoryRuntime()
28
+
29
+ const rootDir = getCliStateDir()
30
+ mkdirSync(rootDir, { recursive: true })
31
+ return await createSqliteRuntime({ rootDir, dataDir: rootDir })
32
+ }
33
+
34
+ export function getRunDescriptorPath(runId: number): string {
35
+ return join(getCliStateDir(), "runs", `${runId}.json`)
36
+ }
37
+
38
+ export function storeRunDescriptor(runId: number, descriptor: StoredRunDescriptor): void {
39
+ const path = getRunDescriptorPath(runId)
40
+ mkdirSync(dirname(path), { recursive: true })
41
+ writeFileSync(path, JSON.stringify(descriptor, null, 2))
42
+ }
43
+
44
+ export function loadRunDescriptor(runId: number): StoredRunDescriptor | null {
45
+ const path = getRunDescriptorPath(runId)
46
+ if (!existsSync(path)) return null
47
+ return JSON.parse(readFileSync(path, "utf8")) as StoredRunDescriptor
48
+ }
49
+
50
+ export function deleteRunDescriptor(runId: number): void {
51
+ const path = getRunDescriptorPath(runId)
52
+ if (existsSync(path)) rmSync(path, { force: true })
53
+ }
54
+
55
+ export function isResumableRuntime(runtime: ToistRuntime): runtime is ResumableRuntime {
56
+ return typeof (runtime as ResumableRuntime).resumeRun === "function"
57
+ }
58
+
59
+ export async function resumeLocalStoredRun(
60
+ runtime: ToistRuntime,
61
+ runId: number,
62
+ descriptor: StoredRunDescriptor,
63
+ response: { taskId: number; token: string; response: unknown; respondedBy?: string },
64
+ ): Promise<RunSpecResult> {
65
+ await runtime.tasks.answer(response.taskId, response.token, response.response, response.respondedBy ?? "cli")
66
+ const resumeOutputs = await runtime.outputs.list(runId)
67
+ await runtime.runs.markRunning(runId, { trigger: "resumed", clearCurrentNode: true })
68
+ return await runSpec(descriptor.spec, descriptor.payload, {
69
+ runtime,
70
+ resumeRunId: runId,
71
+ resumeOutputs,
72
+ trigger: "resumed",
73
+ events: true,
74
+ })
75
+ }
package/src/wizard.ts CHANGED
@@ -1,144 +1,91 @@
1
1
  // 2121 toist
2
- // Interactive setup wizard for @toist/in.
3
- // Asks a few questions, writes toist.yml, creates directories.
4
2
 
5
- import { existsSync, mkdirSync, writeFileSync } from "node:fs"
6
- import { createInterface } from "node:readline"
7
- import { basename, resolve } from "node:path"
3
+ import { copyFileSync, existsSync, mkdirSync, readdirSync, readFileSync, statSync, writeFileSync } from "node:fs"
4
+ import { basename, dirname, join, resolve } from "node:path"
5
+ import { createInterface } from "node:readline/promises"
8
6
 
9
- // Line-by-line reader that works with both TTY and piped stdin.
10
- function makeReader() {
11
- const iface = createInterface({ input: process.stdin, output: process.stdout, terminal: false })
12
- const lines: string[] = []
13
- let resolve: ((v: string) => void) | null = null
7
+ const TEMPLATES = [
8
+ { id: "default", label: "Durable runner (default)" },
9
+ { id: "embedded", label: "Embedded `.toist/` runner" },
10
+ { id: "embedded-memory", label: "Memory-only (library use)" },
11
+ ] as const
14
12
 
15
- iface.on("line", (line) => {
16
- if (resolve) {
17
- const r = resolve
18
- resolve = null
19
- r(line)
20
- } else {
21
- lines.push(line)
22
- }
23
- })
24
-
25
- iface.on("close", () => {
26
- if (resolve) resolve("")
27
- })
13
+ type TemplateId = typeof TEMPLATES[number]["id"]
28
14
 
29
- return {
30
- read(): Promise<string> {
31
- if (lines.length > 0) return Promise.resolve(lines.shift()!)
32
- return new Promise((r) => { resolve = r })
33
- },
34
- close() { iface.close() },
35
- }
15
+ function templatesDir(): string {
16
+ return resolve(import.meta.dir, "..", "templates")
36
17
  }
37
18
 
38
- async function prompt(reader: ReturnType<typeof makeReader>, label: string, def: string): Promise<string> {
39
- process.stdout.write(`${label} [${def}] `)
40
- const v = (await reader.read()).trim()
41
- return v || def
19
+ function suggestTemplate(dest: string): TemplateId {
20
+ if (basename(dest) === ".toist") return "embedded"
21
+ return "default"
42
22
  }
43
23
 
44
- async function yn(reader: ReturnType<typeof makeReader>, label: string, def = false): Promise<boolean> {
45
- const hint = def ? "Y/n" : "y/N"
46
- process.stdout.write(`${label} [${hint}] `)
47
- const v = (await reader.read()).trim().toLowerCase()
48
- if (!v) return def
49
- return v === "y" || v === "yes"
24
+ function packageNameFor(dest: string): string {
25
+ const base = basename(dest)
26
+ if (base === ".toist") return `${basename(dirname(dest)) || "toist"}-toist`
27
+ return base || "toist"
50
28
  }
51
29
 
52
- export async function wizard(cwd = process.cwd()): Promise<void> {
53
- const reader = makeReader()
54
-
55
- console.log("")
56
- console.log("Welcome to Toist. Let's set up your project.")
57
- console.log("")
58
-
59
- const defaultName = basename(cwd)
60
- const name = await prompt(reader, "Instance name? ", defaultName)
61
- const pipelines = await prompt(reader, "Pipelines dir? ", "pipelines")
62
- const resources = await prompt(reader, "Resources dir? ", "resources")
63
- const data = await prompt(reader, "Data dir? ", "data")
64
- const port = await prompt(reader, "Port? ", "3000")
30
+ function copyTemplate(src: string, dest: string) {
31
+ mkdirSync(dest, { recursive: true })
32
+ for (const entry of readdirSync(src)) {
33
+ const srcPath = join(src, entry)
34
+ const outName = entry === "_gitignore" ? ".gitignore" : entry === "_package.json" ? "package.json" : entry
35
+ const destPath = join(dest, outName)
36
+ const info = statSync(srcPath)
37
+ if (info.isDirectory()) {
38
+ copyTemplate(srcPath, destPath)
39
+ } else {
40
+ copyFileSync(srcPath, destPath)
41
+ }
42
+ }
43
+ }
65
44
 
66
- console.log("")
67
- const hasKinds = await yn(reader, "Register custom TypeScript kinds?")
45
+ function rewritePackageName(dest: string) {
46
+ const path = join(dest, "package.json")
47
+ if (!existsSync(path)) return
48
+ const pkg = JSON.parse(readFileSync(path, "utf8")) as { name?: string }
49
+ pkg.name = packageNameFor(dest)
50
+ writeFileSync(path, JSON.stringify(pkg, null, 2) + "\n")
51
+ }
68
52
 
69
- const kinds: string[] = []
70
- if (hasKinds) {
53
+ async function chooseTemplate(defaultTemplate: TemplateId): Promise<TemplateId> {
54
+ const rl = createInterface({ input: process.stdin, output: process.stdout })
55
+ try {
71
56
  console.log("")
72
- let more = true
73
- while (more) {
74
- const kindFile = await prompt(reader, " Kind file path? ", "src/kinds/index.ts")
75
- kinds.push(kindFile)
76
- more = await yn(reader, " Add another? ")
57
+ console.log("How will you run pipelines?")
58
+ for (const [index, template] of TEMPLATES.entries()) {
59
+ console.log(` ${index + 1}) ${template.label}`)
77
60
  }
61
+ const defaultIndex = TEMPLATES.findIndex((template) => template.id === defaultTemplate) + 1
62
+ const answer = (await rl.question(`Choice [${defaultIndex}]: `)).trim()
63
+ const picked = Number(answer || defaultIndex)
64
+ return TEMPLATES[picked - 1]?.id ?? defaultTemplate
65
+ } finally {
66
+ rl.close()
78
67
  }
68
+ }
79
69
 
80
- reader.close()
81
-
82
- // ---------------------------------------------------------------------------
83
- // Write toist.yml
84
- // ---------------------------------------------------------------------------
85
-
86
- const kindsBlock = kinds.length > 0
87
- ? `\nkinds:\n${kinds.map((k) => ` - ${k}`).join("\n")}`
88
- : ""
89
-
90
- const configContent = [
91
- `# toist.yml — generated by bunx @toist/in`,
92
- `# Run with: bunx @toist/aja`,
93
- ``,
94
- `instance: ${name}`,
95
- `port: ${port}`,
96
- `pipelines: ./${pipelines}`,
97
- `resources: ./${resources}`,
98
- `data: ./${data}`,
99
- ...(kinds.length > 0 ? [``, `kinds:`, ...kinds.map((k) => ` - ${k}`)] : []),
100
- ``,
101
- ].join("\n")
102
-
103
- const configPath = resolve(cwd, "toist.yml")
104
- const alreadyExists = existsSync(configPath)
105
-
106
- console.log("")
70
+ export async function wizard(cwd = process.cwd(), destArg?: string): Promise<void> {
71
+ const destName = destArg ?? "toist"
72
+ const templateId = await chooseTemplate(suggestTemplate(destName))
73
+ const templateDir = join(templatesDir(), templateId)
74
+ const dest = resolve(cwd, destName)
107
75
 
108
- if (alreadyExists) {
109
- console.log("toist.yml already exists — skipping (remove it to re-run wizard).")
110
- } else {
111
- writeFileSync(configPath, configContent, "utf8")
112
- console.log("Wrote toist.yml")
76
+ if (!existsSync(templateDir)) {
77
+ console.error(`Template not found: ${templateId}`)
78
+ process.exit(1)
113
79
  }
114
80
 
115
- // ---------------------------------------------------------------------------
116
- // Create directories
117
- // ---------------------------------------------------------------------------
118
-
119
- for (const dir of [pipelines, resources, data]) {
120
- const abs = resolve(cwd, dir)
121
- if (!existsSync(abs)) {
122
- mkdirSync(abs, { recursive: true })
123
- // Keep data/ out of git, commit the rest
124
- if (dir === data) {
125
- writeFileSync(resolve(abs, ".gitignore"), "*.db\n*.db-shm\n*.db-wal\n.lock/\n")
126
- } else {
127
- writeFileSync(resolve(abs, ".gitkeep"), "")
128
- }
129
- console.log(`Created ${dir}/`)
130
- }
81
+ if (existsSync(dest)) {
82
+ console.error(`Destination already exists: ${dest}`)
83
+ process.exit(1)
131
84
  }
132
85
 
133
- // ---------------------------------------------------------------------------
134
- // Done
135
- // ---------------------------------------------------------------------------
86
+ copyTemplate(templateDir, dest)
87
+ rewritePackageName(dest)
136
88
 
137
- console.log("")
138
- console.log("Done. Start with:")
139
- console.log(" bunx @toist/aja")
140
- console.log("")
141
- console.log("Or add a script to package.json:")
142
- console.log(' "toist": "bunx @toist/aja"')
143
- console.log("")
89
+ const relative = dest.startsWith(cwd) ? `./${dest.slice(cwd.length).replace(/^\/+/, "") || basename(dest)}` : dest
90
+ console.log(`✓ created ${relative} (${templateId} template)`)
144
91
  }
@@ -0,0 +1,35 @@
1
+ # my-toist-instance
2
+
3
+ Generated with [`bunx @toist/in`](https://toist.in/docs).
4
+
5
+ This directory is a Toist home. It can be used either as a dedicated
6
+ instance directory or as an embedded repo-owned home such as
7
+ `<repo>/.toist/`.
8
+
9
+ ## First run
10
+
11
+ 1. `bun install`
12
+ 2. `bun start.ts` — runner boots on `http://localhost:3000`.
13
+
14
+ Open `http://localhost:3000` to use the UI.
15
+
16
+ ## Layout
17
+
18
+ - `start.ts` — bootstrap; calls `startRunner` from `@toist/aja`
19
+ - `kinds/custom.ts` — register your domain-specific kinds here (or import them from normal workspace code)
20
+ - `pipelines/*.yaml` — pipeline definitions (per pipeline-spec)
21
+ - `toist.yml` — resource config for this Toist home
22
+ - `data/` — runtime SQLite databases (gitignored)
23
+
24
+ ## Upgrading
25
+
26
+ ```sh
27
+ bunx @toist/in upgrade
28
+ ```
29
+
30
+ Run from the instance directory or host repo root. Bumps every
31
+ `@toist/*` dep in `package.json` to the latest version.
32
+
33
+ ## More
34
+
35
+ Full quickstart and docs: https://toist.in/docs
@@ -0,0 +1,7 @@
1
+ node_modules/
2
+ bun.lock
3
+ data/*.db
4
+ data/*.db-shm
5
+ data/*.db-wal
6
+ data/.lock/
7
+ .platform/
@@ -0,0 +1,13 @@
1
+ {
2
+ "name": "my-toist-instance",
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "scripts": {
6
+ "start": "bun start.ts",
7
+ "dev": "bun --watch start.ts"
8
+ },
9
+ "dependencies": {
10
+ "@toist/aja": "^0.8.1",
11
+ "@toist/spec": "^0.8.1"
12
+ }
13
+ }
File without changes
@@ -0,0 +1,23 @@
1
+ // 2121 toist — register your domain-specific kinds here.
2
+ //
3
+ // Each export should be a NodeKind<P, I>. See the @toist/aja built-ins for
4
+ // reference shapes, and pipeline-spec / kind-spec at https://toist.in/docs for
5
+ // the full contract.
6
+ //
7
+ // Example:
8
+ //
9
+ // import type { NodeKind } from "@toist/spec"
10
+ //
11
+ // export const greet: NodeKind<{ name: string }, { msg: string }> = {
12
+ // id: "demo.greet",
13
+ // params: { name: { type: "string", required: true } },
14
+ // async execute(ctx) {
15
+ // return { msg: `Hello, ${ctx.params.name}!` }
16
+ // },
17
+ // }
18
+ //
19
+ // Then in start.ts:
20
+ // import * as customKinds from "./kinds/custom.ts"
21
+ // register(...Object.values(customKinds))
22
+
23
+ export {}
File without changes
File without changes
@@ -0,0 +1,14 @@
1
+ // 2121 toist instance bootstrap.
2
+ // See https://toist.in/docs for the full quickstart.
3
+
4
+ import { startRunner } from "@toist/aja"
5
+
6
+ // Uncomment to register domain-specific kinds:
7
+ // import { register } from "@toist/aja"
8
+ // import * as customKinds from "./kinds/custom.ts"
9
+ // register(...Object.values(customKinds))
10
+
11
+ await startRunner({
12
+ port: Number(process.env.PORT ?? 3000),
13
+ rootDir: import.meta.dir,
14
+ })
@@ -0,0 +1,2 @@
1
+ # toist.yml — resource config for this Toist home
2
+ resources: {}
@@ -0,0 +1,58 @@
1
+ # my-repo/.toist
2
+
3
+ Generated with [`bunx @toist/in .toist`](https://toist.in/docs).
4
+
5
+ This directory is the embedded Toist home for an existing repo.
6
+ `@toist/aja` runs pipelines; `.toist/` is the repo-owned local home for
7
+ Toist config, pipeline definitions, and runtime state.
8
+
9
+ ## First run
10
+
11
+ 1. From the host repo root: `bun add @toist/aja @toist/spec`
12
+ 2. Optionally, add a script to your root `package.json`:
13
+ ```json
14
+ "scripts": { "toist": "bun .toist/start.ts" }
15
+ ```
16
+ 3. Start the embedded instance: `bun .toist/start.ts`
17
+
18
+ Open `http://localhost:3000` to use the UI.
19
+
20
+ ## Layout
21
+
22
+ ```
23
+ .toist/
24
+ start.ts — embedded bootstrap; calls startRunner from @toist/aja
25
+ instance.json — instance name and tier (optional)
26
+ pipelines/ — pipeline YAML files owned by this repo
27
+ toist.yml — resource config for this embedded home
28
+ data/ — runtime SQLite databases (gitignored)
29
+ ```
30
+
31
+ Custom TypeScript kinds stay in your host repo's normal source tree or
32
+ workspace packages. Import and register them from `.toist/start.ts`:
33
+
34
+ ```ts
35
+ import { register } from "@toist/aja"
36
+ import * as myKinds from "../src/toist-kinds.ts"
37
+ register(...Object.values(myKinds))
38
+ ```
39
+
40
+ ## Environment
41
+
42
+ | Variable | Default | Notes |
43
+ |---|---|---|
44
+ | `TOIST_PORT` | `3000` | Port the runner listens on |
45
+ | `PORT` | `3000` | Fallback if `TOIST_PORT` is unset |
46
+
47
+ ## Upgrading
48
+
49
+ ```sh
50
+ bunx @toist/in upgrade
51
+ ```
52
+
53
+ Run from `.toist/` or from the host repo root. Finds `package.json`
54
+ and bumps every `@toist/*` dependency to the latest version.
55
+
56
+ ## More
57
+
58
+ Full quickstart and docs: https://toist.in/docs
@@ -0,0 +1,5 @@
1
+ data/*.db
2
+ data/*.db-shm
3
+ data/*.db-wal
4
+ data/.lock/
5
+ .platform/
File without changes
@@ -0,0 +1,4 @@
1
+ {
2
+ "instanceName": "my-project",
3
+ "tier": "local"
4
+ }
File without changes
File without changes
@@ -0,0 +1,24 @@
1
+ // 2121 toist — embedded bootstrap for .toist/
2
+ // See https://toist.in/docs for the full quickstart.
3
+ //
4
+ // rootDir is set to this directory (.toist/).
5
+ // The runner resolves all paths relative to it:
6
+ // .toist/pipelines/ — pipeline YAML files
7
+ // .toist/toist.yml — resource config
8
+ // .toist/data/ — runtime SQLite databases (gitignored)
9
+ //
10
+ // Custom kinds belong in your host repo's normal source tree.
11
+ // Import and register them here before calling startRunner.
12
+ //
13
+ // Examples:
14
+ // import * as myKinds from "../src/toist-kinds.ts"
15
+ // import * as myKinds from "../packages/my-kinds/src/index.ts"
16
+ // import { register } from "@toist/aja"
17
+ // register(...Object.values(myKinds))
18
+
19
+ import { startRunner } from "@toist/aja"
20
+
21
+ await startRunner({
22
+ port: Number(process.env.TOIST_PORT ?? process.env.PORT ?? 3000),
23
+ rootDir: import.meta.dir, // = <repo>/.toist/
24
+ })
@@ -0,0 +1,2 @@
1
+ # toist.yml — resource config for this embedded Toist home
2
+ resources: {}
@@ -0,0 +1,12 @@
1
+ # memory-only Toist host
2
+
3
+ Generated with `bunx @toist/in` using the memory-only template.
4
+
5
+ This template uses `runSpec()` and `createMemoryRuntime()` directly. There is no HTTP runner, no UI, and no durable SQLite state.
6
+
7
+ ## First run
8
+
9
+ 1. Add `@toist/core` and `@toist/spec` to your host repo.
10
+ 2. Run `bun start.ts`.
11
+
12
+ The example reads `./pipelines/hello.yml` and prints the result JSON.
@@ -0,0 +1 @@
1
+ node_modules
@@ -0,0 +1,12 @@
1
+ apiVersion: "2121.fi/v1"
2
+ id: hello
3
+ nodes:
4
+ - id: greet
5
+ kind: data.json
6
+ params:
7
+ value: { expr: '"hello " + ctx.params.name' }
8
+ - id: out
9
+ kind: sink
10
+ dependsOn: [greet]
11
+ input:
12
+ value: { expr: "ctx.results.greet" }
@@ -0,0 +1,16 @@
1
+ import { readFileSync } from "node:fs"
2
+ import { join } from "node:path"
3
+ import { parseYaml } from "@toist/spec"
4
+ import { createMemoryRuntime, runSpec } from "@toist/core"
5
+
6
+ const spec = parseYaml(readFileSync(join(import.meta.dir, "pipelines", "hello.yml"), "utf8"))
7
+ const runtime = createMemoryRuntime()
8
+ const payload = process.argv[2] ? JSON.parse(process.argv[2]) : { name: "world" }
9
+ const result = await runSpec(spec, payload, { runtime, events: true })
10
+
11
+ for await (const event of result.events ?? []) {
12
+ console.error(`[event] ${event.type}`)
13
+ }
14
+
15
+ const { events: _events, ...printable } = result
16
+ console.log(JSON.stringify(printable, null, 2))