@toist/in 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/src/index.ts CHANGED
@@ -1,38 +1,2 @@
1
1
  #!/usr/bin/env bun
2
- // 2121 toist
3
- // @toist/in — scaffold a new toist instance, or upgrade an existing one.
4
- //
5
- // Usage:
6
- // bunx @toist/in <path> scaffold a new instance at <path>
7
- // bunx @toist/in upgrade @toist/* deps in the current directory
8
-
9
- import { existsSync } from "node:fs"
10
- import { resolve } from "node:path"
11
- import { scaffold } from "./scaffold.ts"
12
- import { upgrade } from "./upgrade.ts"
13
-
14
- const args = process.argv.slice(2)
15
- const arg = args[0]
16
-
17
- if (arg === "--help" || arg === "-h") {
18
- console.log("bunx @toist/in <path> scaffold a new toist instance at <path>")
19
- console.log("bunx @toist/in upgrade @toist/* deps in the current directory")
20
- console.log("")
21
- console.log("Full docs: https://toist.in")
22
- process.exit(0)
23
- }
24
-
25
- if (!arg) {
26
- // No path given — upgrade if we're inside a toist instance, else error.
27
- const pkgPath = resolve(process.cwd(), "package.json")
28
- if (!existsSync(pkgPath)) {
29
- console.error("No package.json found in the current directory.")
30
- console.error("")
31
- console.error("To scaffold a new instance: bunx @toist/in <path>")
32
- console.error("To upgrade an existing one: run from the instance directory")
33
- process.exit(1)
34
- }
35
- await upgrade()
36
- } else {
37
- await scaffold(arg)
38
- }
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/upgrade.ts CHANGED
@@ -5,17 +5,31 @@
5
5
 
6
6
  import { readFile } from "node:fs/promises"
7
7
  import { existsSync } from "node:fs"
8
- import { resolve } from "node:path"
8
+ import { basename, dirname, resolve } from "node:path"
9
9
  import { spawnSync } from "node:child_process"
10
10
 
11
+ function resolvePackageDir(cwd: string): string | null {
12
+ const localPkg = resolve(cwd, "package.json")
13
+ if (existsSync(localPkg)) return cwd
14
+
15
+ if (basename(cwd) === ".toist") {
16
+ const parent = dirname(cwd)
17
+ if (existsSync(resolve(parent, "package.json"))) return parent
18
+ }
19
+
20
+ return null
21
+ }
22
+
11
23
  export async function upgrade(cwd = process.cwd()): Promise<void> {
12
- const pkgPath = resolve(cwd, "package.json")
13
- if (!existsSync(pkgPath)) {
24
+ const packageDir = resolvePackageDir(cwd)
25
+ if (!packageDir) {
14
26
  console.error(`No package.json in ${cwd}.`)
15
- console.error("Run from your toist instance directory.")
27
+ console.error("Run from your host repo root or .toist/ directory.")
16
28
  process.exit(1)
17
29
  }
18
30
 
31
+ const pkgPath = resolve(packageDir, "package.json")
32
+
19
33
  const pkg = JSON.parse(await readFile(pkgPath, "utf8")) as {
20
34
  dependencies?: Record<string, string>
21
35
  devDependencies?: Record<string, string>
@@ -39,7 +53,7 @@ export async function upgrade(cwd = process.cwd()): Promise<void> {
39
53
  console.log(`Upgrading: ${targets.join(", ")}\n`)
40
54
  const result = spawnSync("bun", ["update", "--latest", ...targets], {
41
55
  stdio: "inherit",
42
- cwd,
56
+ cwd: packageDir,
43
57
  })
44
58
  process.exit(result.status ?? 0)
45
59
  }
package/src/wizard.ts ADDED
@@ -0,0 +1,91 @@
1
+ // 2121 toist
2
+
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"
6
+
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
12
+
13
+ type TemplateId = typeof TEMPLATES[number]["id"]
14
+
15
+ function templatesDir(): string {
16
+ return resolve(import.meta.dir, "..", "templates")
17
+ }
18
+
19
+ function suggestTemplate(dest: string): TemplateId {
20
+ if (basename(dest) === ".toist") return "embedded"
21
+ return "default"
22
+ }
23
+
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"
28
+ }
29
+
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
+ }
44
+
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
+ }
52
+
53
+ async function chooseTemplate(defaultTemplate: TemplateId): Promise<TemplateId> {
54
+ const rl = createInterface({ input: process.stdin, output: process.stdout })
55
+ try {
56
+ console.log("")
57
+ console.log("How will you run pipelines?")
58
+ for (const [index, template] of TEMPLATES.entries()) {
59
+ console.log(` ${index + 1}) ${template.label}`)
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()
67
+ }
68
+ }
69
+
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)
75
+
76
+ if (!existsSync(templateDir)) {
77
+ console.error(`Template not found: ${templateId}`)
78
+ process.exit(1)
79
+ }
80
+
81
+ if (existsSync(dest)) {
82
+ console.error(`Destination already exists: ${dest}`)
83
+ process.exit(1)
84
+ }
85
+
86
+ copyTemplate(templateDir, dest)
87
+ rewritePackageName(dest)
88
+
89
+ const relative = dest.startsWith(cwd) ? `./${dest.slice(cwd.length).replace(/^\/+/, "") || basename(dest)}` : dest
90
+ console.log(`✓ created ${relative} (${templateId} template)`)
91
+ }
@@ -1,6 +1,10 @@
1
1
  # my-toist-instance
2
2
 
3
- Generated with [`bunx @toist/in`](https://toist.in).
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/`.
4
8
 
5
9
  ## First run
6
10
 
@@ -12,20 +16,20 @@ Open `http://localhost:3000` to use the UI.
12
16
  ## Layout
13
17
 
14
18
  - `start.ts` — bootstrap; calls `startRunner` from `@toist/aja`
15
- - `kinds/custom.ts` — register your domain-specific kinds here
19
+ - `kinds/custom.ts` — register your domain-specific kinds here (or import them from normal workspace code)
16
20
  - `pipelines/*.yaml` — pipeline definitions (per pipeline-spec)
17
- - `resources/*.yaml` — resource definitions (per resource-spec)
21
+ - `toist.yml` — resource config for this Toist home
18
22
  - `data/` — runtime SQLite databases (gitignored)
19
23
 
20
24
  ## Upgrading
21
25
 
22
26
  ```sh
23
- bunx @toist/in
27
+ bunx @toist/in upgrade
24
28
  ```
25
29
 
26
- Run without arguments from the instance directory. Auto-detects every
27
- `@toist/*` dep in `package.json` and bumps them to the latest version.
30
+ Run from the instance directory or host repo root. Bumps every
31
+ `@toist/*` dep in `package.json` to the latest version.
28
32
 
29
33
  ## More
30
34
 
31
- Full quickstart and docs: https://toist.in
35
+ Full quickstart and docs: https://toist.in/docs
@@ -7,7 +7,7 @@
7
7
  "dev": "bun --watch start.ts"
8
8
  },
9
9
  "dependencies": {
10
- "@toist/aja": "^0.6.1",
11
- "@toist/spec": "^0.6.1"
10
+ "@toist/aja": "^0.8.0",
11
+ "@toist/spec": "^0.8.0"
12
12
  }
13
13
  }
@@ -1,7 +1,7 @@
1
1
  // 2121 toist — register your domain-specific kinds here.
2
2
  //
3
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 for
4
+ // reference shapes, and pipeline-spec / kind-spec at https://toist.in/docs for
5
5
  // the full contract.
6
6
  //
7
7
  // Example:
@@ -1,5 +1,5 @@
1
1
  // 2121 toist instance bootstrap.
2
- // See https://toist.in for the full quickstart.
2
+ // See https://toist.in/docs for the full quickstart.
3
3
 
4
4
  import { startRunner } from "@toist/aja"
5
5
 
@@ -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))
package/src/scaffold.ts DELETED
@@ -1,67 +0,0 @@
1
- // 2121 toist
2
- // Scaffold logic for @toist/in. Recursively copies templates/default/* into
3
- // the target directory, with two `_`-prefixed filenames renamed at copy time:
4
- //
5
- // - `_gitignore` -> `.gitignore`
6
- // - `_package.json` -> `package.json`
7
- //
8
- // The underscore prefix exists because npm strips files like `.gitignore` and
9
- // (in some configurations) `package.json` from publishable subdirs. Prefixing
10
- // dodges those rules; the runtime rename produces the real names in the host.
11
- //
12
- // Templates live in `<package>/templates/default/`, relative to this file's
13
- // runtime location. Works whether the package is consumed as a workspace
14
- // link (in this repo) or from node_modules (in a host repo).
15
-
16
- import { mkdir, copyFile, readdir, stat } from "node:fs/promises"
17
- import { existsSync } from "node:fs"
18
- import { dirname, join, resolve } from "node:path"
19
- import { fileURLToPath } from "node:url"
20
-
21
- const __dir = dirname(fileURLToPath(import.meta.url))
22
- const TEMPLATE_DIR = resolve(__dir, "..", "templates", "default")
23
-
24
- const RENAMES: Record<string, string> = {
25
- _gitignore: ".gitignore",
26
- "_package.json": "package.json",
27
- }
28
-
29
- export async function scaffold(target: string): Promise<void> {
30
- const dest = resolve(process.cwd(), target)
31
-
32
- if (existsSync(dest)) {
33
- const entries = await readdir(dest)
34
- if (entries.length > 0) {
35
- console.error(`Target directory '${dest}' is not empty. Aborting.`)
36
- process.exit(1)
37
- }
38
- } else {
39
- await mkdir(dest, { recursive: true })
40
- }
41
-
42
- await copyTree(TEMPLATE_DIR, dest)
43
-
44
- console.log(`\nScaffolded toist instance at ${dest}\n`)
45
- console.log("Next:")
46
- console.log(` cd ${target}`)
47
- console.log(` bun install`)
48
- console.log(` bun start.ts`)
49
- console.log("")
50
- console.log(`Full docs: https://toist.in`)
51
- console.log("")
52
- }
53
-
54
- async function copyTree(src: string, dest: string): Promise<void> {
55
- for (const entry of await readdir(src)) {
56
- const srcPath = join(src, entry)
57
- const destName = RENAMES[entry] ?? entry
58
- const destPath = join(dest, destName)
59
- const info = await stat(srcPath)
60
- if (info.isDirectory()) {
61
- await mkdir(destPath, { recursive: true })
62
- await copyTree(srcPath, destPath)
63
- } else {
64
- await copyFile(srcPath, destPath)
65
- }
66
- }
67
- }