tubeless 0.1.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.
Files changed (224) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +106 -0
  3. package/dist/abort.d.ts +12 -0
  4. package/dist/abort.d.ts.map +1 -0
  5. package/dist/abort.js +49 -0
  6. package/dist/abort.js.map +1 -0
  7. package/dist/batch.d.ts +22 -0
  8. package/dist/batch.d.ts.map +1 -0
  9. package/dist/batch.js +73 -0
  10. package/dist/batch.js.map +1 -0
  11. package/dist/checkpoint.d.ts +33 -0
  12. package/dist/checkpoint.d.ts.map +1 -0
  13. package/dist/checkpoint.js +71 -0
  14. package/dist/checkpoint.js.map +1 -0
  15. package/dist/child-execution.d.ts +30 -0
  16. package/dist/child-execution.d.ts.map +1 -0
  17. package/dist/child-execution.js +270 -0
  18. package/dist/child-execution.js.map +1 -0
  19. package/dist/cli-command.d.ts +29 -0
  20. package/dist/cli-command.d.ts.map +1 -0
  21. package/dist/cli-command.js +314 -0
  22. package/dist/cli-command.js.map +1 -0
  23. package/dist/cli-parser.d.ts +28 -0
  24. package/dist/cli-parser.d.ts.map +1 -0
  25. package/dist/cli-parser.js +557 -0
  26. package/dist/cli-parser.js.map +1 -0
  27. package/dist/cli-pipeline-command.d.ts +84 -0
  28. package/dist/cli-pipeline-command.d.ts.map +1 -0
  29. package/dist/cli-pipeline-command.js +185 -0
  30. package/dist/cli-pipeline-command.js.map +1 -0
  31. package/dist/cli-types.d.ts +220 -0
  32. package/dist/cli-types.d.ts.map +1 -0
  33. package/dist/cli-types.js +19 -0
  34. package/dist/cli-types.js.map +1 -0
  35. package/dist/cli.d.ts +6 -0
  36. package/dist/cli.d.ts.map +1 -0
  37. package/dist/cli.js +6 -0
  38. package/dist/cli.js.map +1 -0
  39. package/dist/env.d.ts +2 -0
  40. package/dist/env.d.ts.map +1 -0
  41. package/dist/env.js +8 -0
  42. package/dist/env.js.map +1 -0
  43. package/dist/file-utils.d.ts +4 -0
  44. package/dist/file-utils.d.ts.map +1 -0
  45. package/dist/file-utils.js +35 -0
  46. package/dist/file-utils.js.map +1 -0
  47. package/dist/interactive-reporter.d.ts +38 -0
  48. package/dist/interactive-reporter.d.ts.map +1 -0
  49. package/dist/interactive-reporter.js +308 -0
  50. package/dist/interactive-reporter.js.map +1 -0
  51. package/dist/lifecycle.d.ts +17 -0
  52. package/dist/lifecycle.d.ts.map +1 -0
  53. package/dist/lifecycle.js +77 -0
  54. package/dist/lifecycle.js.map +1 -0
  55. package/dist/live-ticker.d.ts +21 -0
  56. package/dist/live-ticker.d.ts.map +1 -0
  57. package/dist/live-ticker.js +377 -0
  58. package/dist/live-ticker.js.map +1 -0
  59. package/dist/mapped-child-progress.d.ts +109 -0
  60. package/dist/mapped-child-progress.d.ts.map +1 -0
  61. package/dist/mapped-child-progress.js +133 -0
  62. package/dist/mapped-child-progress.js.map +1 -0
  63. package/dist/node.d.ts +5 -0
  64. package/dist/node.d.ts.map +1 -0
  65. package/dist/node.js +5 -0
  66. package/dist/node.js.map +1 -0
  67. package/dist/paths.d.ts +9 -0
  68. package/dist/paths.d.ts.map +1 -0
  69. package/dist/paths.js +19 -0
  70. package/dist/paths.js.map +1 -0
  71. package/dist/pipeline-command-marker.d.ts +5 -0
  72. package/dist/pipeline-command-marker.d.ts.map +1 -0
  73. package/dist/pipeline-command-marker.js +21 -0
  74. package/dist/pipeline-command-marker.js.map +1 -0
  75. package/dist/pipeline-diagnostics.d.ts +4 -0
  76. package/dist/pipeline-diagnostics.d.ts.map +1 -0
  77. package/dist/pipeline-diagnostics.js +17 -0
  78. package/dist/pipeline-diagnostics.js.map +1 -0
  79. package/dist/pipeline-module.d.ts +62 -0
  80. package/dist/pipeline-module.d.ts.map +1 -0
  81. package/dist/pipeline-module.js +157 -0
  82. package/dist/pipeline-module.js.map +1 -0
  83. package/dist/pipeline.d.ts +606 -0
  84. package/dist/pipeline.d.ts.map +1 -0
  85. package/dist/pipeline.js +1444 -0
  86. package/dist/pipeline.js.map +1 -0
  87. package/dist/progress.d.ts +4 -0
  88. package/dist/progress.d.ts.map +1 -0
  89. package/dist/progress.js +8 -0
  90. package/dist/progress.js.map +1 -0
  91. package/dist/prompt-select.d.ts +81 -0
  92. package/dist/prompt-select.d.ts.map +1 -0
  93. package/dist/prompt-select.js +197 -0
  94. package/dist/prompt-select.js.map +1 -0
  95. package/dist/rate-limit.d.ts +7 -0
  96. package/dist/rate-limit.d.ts.map +1 -0
  97. package/dist/rate-limit.js +23 -0
  98. package/dist/rate-limit.js.map +1 -0
  99. package/dist/render.d.ts +21 -0
  100. package/dist/render.d.ts.map +1 -0
  101. package/dist/render.js +60 -0
  102. package/dist/render.js.map +1 -0
  103. package/dist/reporter-entry.d.ts +3 -0
  104. package/dist/reporter-entry.d.ts.map +1 -0
  105. package/dist/reporter-entry.js +3 -0
  106. package/dist/reporter-entry.js.map +1 -0
  107. package/dist/reporter.d.ts +50 -0
  108. package/dist/reporter.d.ts.map +1 -0
  109. package/dist/reporter.js +167 -0
  110. package/dist/reporter.js.map +1 -0
  111. package/dist/retry.d.ts +28 -0
  112. package/dist/retry.d.ts.map +1 -0
  113. package/dist/retry.js +59 -0
  114. package/dist/retry.js.map +1 -0
  115. package/dist/run-store-sqlite.d.ts +12 -0
  116. package/dist/run-store-sqlite.d.ts.map +1 -0
  117. package/dist/run-store-sqlite.js +187 -0
  118. package/dist/run-store-sqlite.js.map +1 -0
  119. package/dist/run-store-ui-http.d.ts +5 -0
  120. package/dist/run-store-ui-http.d.ts.map +1 -0
  121. package/dist/run-store-ui-http.js +19 -0
  122. package/dist/run-store-ui-http.js.map +1 -0
  123. package/dist/run-store-ui-page.d.ts +2 -0
  124. package/dist/run-store-ui-page.d.ts.map +1 -0
  125. package/dist/run-store-ui-page.js +759 -0
  126. package/dist/run-store-ui-page.js.map +1 -0
  127. package/dist/run-store-ui-protocol.d.ts +28 -0
  128. package/dist/run-store-ui-protocol.d.ts.map +1 -0
  129. package/dist/run-store-ui-protocol.js +84 -0
  130. package/dist/run-store-ui-protocol.js.map +1 -0
  131. package/dist/run-store-ui-state.d.ts +21 -0
  132. package/dist/run-store-ui-state.d.ts.map +1 -0
  133. package/dist/run-store-ui-state.js +65 -0
  134. package/dist/run-store-ui-state.js.map +1 -0
  135. package/dist/run-store-ui.d.ts +25 -0
  136. package/dist/run-store-ui.d.ts.map +1 -0
  137. package/dist/run-store-ui.js +217 -0
  138. package/dist/run-store-ui.js.map +1 -0
  139. package/dist/run-store.d.ts +109 -0
  140. package/dist/run-store.d.ts.map +1 -0
  141. package/dist/run-store.js +345 -0
  142. package/dist/run-store.js.map +1 -0
  143. package/dist/testing.d.ts +40 -0
  144. package/dist/testing.d.ts.map +1 -0
  145. package/dist/testing.js +83 -0
  146. package/dist/testing.js.map +1 -0
  147. package/dist/tracing-internal.d.ts +21 -0
  148. package/dist/tracing-internal.d.ts.map +1 -0
  149. package/dist/tracing-internal.js +206 -0
  150. package/dist/tracing-internal.js.map +1 -0
  151. package/dist/tracing-json.d.ts +12 -0
  152. package/dist/tracing-json.d.ts.map +1 -0
  153. package/dist/tracing-json.js +8 -0
  154. package/dist/tracing-json.js.map +1 -0
  155. package/dist/tracing-otel.d.ts +36 -0
  156. package/dist/tracing-otel.d.ts.map +1 -0
  157. package/dist/tracing-otel.js +60 -0
  158. package/dist/tracing-otel.js.map +1 -0
  159. package/dist/tracing.d.ts +48 -0
  160. package/dist/tracing.d.ts.map +1 -0
  161. package/dist/tracing.js +2 -0
  162. package/dist/tracing.js.map +1 -0
  163. package/dist/workbench-bin.d.ts +3 -0
  164. package/dist/workbench-bin.d.ts.map +1 -0
  165. package/dist/workbench-bin.js +8 -0
  166. package/dist/workbench-bin.js.map +1 -0
  167. package/dist/workbench-graph.d.ts +3 -0
  168. package/dist/workbench-graph.d.ts.map +1 -0
  169. package/dist/workbench-graph.js +64 -0
  170. package/dist/workbench-graph.js.map +1 -0
  171. package/dist/workbench-inspect.d.ts +3 -0
  172. package/dist/workbench-inspect.d.ts.map +1 -0
  173. package/dist/workbench-inspect.js +65 -0
  174. package/dist/workbench-inspect.js.map +1 -0
  175. package/dist/workbench-plan.d.ts +3 -0
  176. package/dist/workbench-plan.d.ts.map +1 -0
  177. package/dist/workbench-plan.js +72 -0
  178. package/dist/workbench-plan.js.map +1 -0
  179. package/dist/workbench-run.d.ts +13 -0
  180. package/dist/workbench-run.d.ts.map +1 -0
  181. package/dist/workbench-run.js +115 -0
  182. package/dist/workbench-run.js.map +1 -0
  183. package/dist/workbench-shared.d.ts +67 -0
  184. package/dist/workbench-shared.d.ts.map +1 -0
  185. package/dist/workbench-shared.js +163 -0
  186. package/dist/workbench-shared.js.map +1 -0
  187. package/dist/workbench-studio.d.ts +26 -0
  188. package/dist/workbench-studio.d.ts.map +1 -0
  189. package/dist/workbench-studio.js +57 -0
  190. package/dist/workbench-studio.js.map +1 -0
  191. package/dist/workbench-ui.d.ts +3 -0
  192. package/dist/workbench-ui.d.ts.map +1 -0
  193. package/dist/workbench-ui.js +262 -0
  194. package/dist/workbench-ui.js.map +1 -0
  195. package/dist/workbench.d.ts +5 -0
  196. package/dist/workbench.d.ts.map +1 -0
  197. package/dist/workbench.js +43 -0
  198. package/dist/workbench.js.map +1 -0
  199. package/docs/README.md +31 -0
  200. package/docs/agent-guide.md +168 -0
  201. package/docs/api-reference.md +269 -0
  202. package/docs/api-report.json +298 -0
  203. package/docs/child-pipeline-composition.md +185 -0
  204. package/docs/cli.md +118 -0
  205. package/docs/concepts.md +502 -0
  206. package/docs/getting-started.md +162 -0
  207. package/docs/llms.txt +62 -0
  208. package/docs/recipes.md +51 -0
  209. package/docs/studio.md +86 -0
  210. package/examples/best-effort.ts +42 -0
  211. package/examples/cancellation-and-testing.ts +45 -0
  212. package/examples/child-pipeline.ts +51 -0
  213. package/examples/cli-job.ts +86 -0
  214. package/examples/conditional-step.ts +41 -0
  215. package/examples/fan-out-progress.ts +53 -0
  216. package/examples/local-observability.ts +80 -0
  217. package/examples/publish-with-gates.ts +56 -0
  218. package/examples/rendering.ts +34 -0
  219. package/examples/resumable-enrichment.ts +75 -0
  220. package/examples/tracing.ts +35 -0
  221. package/examples/tsconfig.json +15 -0
  222. package/examples/typed-import.ts +54 -0
  223. package/examples/validated-boundaries.ts +75 -0
  224. package/package.json +156 -0
package/docs/llms.txt ADDED
@@ -0,0 +1,62 @@
1
+ # tubeless
2
+
3
+ > Dependency-free TypeScript primitives for typed, observable data pipelines and their CLI programs.
4
+
5
+ Start:
6
+
7
+ - README: ../README.md
8
+ - First pipeline: ./getting-started.md
9
+ - CLI: ./cli.md
10
+ - Local studio: ./studio.md
11
+ - Choose a pattern: ./recipes.md
12
+ - Execution semantics: ./concepts.md
13
+
14
+ Agent instructions:
15
+
16
+ - Canonical guide: ./agent-guide.md
17
+ - Maintain stable step IDs.
18
+ - Use optional step names only as display overrides; dependencies and selection use IDs.
19
+ - Use pipeline.toMermaid() to generate static graph documentation.
20
+ - Use tubeless inspect with a pipeline module path for a read-only inventory of identity plus the default plan.
21
+ - Use tubeless plan with a pipeline or marked command module to preview target, exact-step, and dry-run selection without executing or supplying domain options. Prefer the marked command when both are exported. Use command.plan() or tubeless plan; do not simulate planning with --plan.
22
+ - Use tubeless graph with a pipeline module path to print Mermaid without a wrapper script.
23
+ - Use tubeless run only with a definePipelineCommand module; put the command's validated application arguments after --.
24
+ - Set dryRun: "skip" on external side effects, or provide a side-effect-free dryRun handler.
25
+ - Use context.log, context.signal, context.sleep, and progress APIs.
26
+ - Use createPipelineTestRuntime from tubeless/testing for deterministic tests with captured logs, statuses, progress, clock, and cancellation.
27
+ - Compile generated agent submissions with bun run eval:agent --; runtime tests, provider invocation, and semantic assessment remain externally controlled.
28
+ - Use definePipelineCommand for pipeline-backed scripts.
29
+ - Omit mapOptions only when validated CLI flags already satisfy same-name pipeline options; map renamed, loaded, or derived values explicitly.
30
+ - Declare public goals with targets: [step] on definePipeline, then select their
31
+ IDs to include required inputs and failure gates. stepIds is an exact filter
32
+ for any step and cannot be combined with targets.
33
+ - Use PipelinePlanStep.selectionReasons for selection explanations; it includes
34
+ target IDs and immediate dependents, so consumers should not rebuild the graph.
35
+ - Branch on PipelineError code/phase/kind, not message text. Original thrown
36
+ codes use sourceCode; failed and dependency-blocked reports expose diagnostic metadata.
37
+ - Use requireOutputs for finalizers that need specific completed step outputs.
38
+ - Use createSteps(optionsSchema), outputSchema, and resultSchema for optional
39
+ dependency-free Standard Schema validation. run supports async schemas;
40
+ synchronous plan does not invoke schemas.
41
+ - Run make check from the package root after package changes.
42
+
43
+ Advanced:
44
+
45
+ - Child pipelines: ./child-pipeline-composition.md
46
+ - Public exports: ./api-reference.md
47
+ - Machine-readable surface: ./api-report.json
48
+ - Agent evaluation cases: ./agent-evaluations.md
49
+
50
+ Executable examples:
51
+
52
+ - ../examples/typed-import.ts
53
+ - ../examples/validated-boundaries.ts
54
+ - ../examples/publish-with-gates.ts
55
+ - ../examples/conditional-step.ts
56
+ - ../examples/best-effort.ts
57
+ - ../examples/child-pipeline.ts
58
+ - ../examples/fan-out-progress.ts
59
+ - ../examples/resumable-enrichment.ts
60
+ - ../examples/cli-job.ts
61
+ - ../examples/cancellation-and-testing.ts
62
+ - ../examples/tracing.ts
@@ -0,0 +1,51 @@
1
+ # Recipe index
2
+
3
+ Every linked TypeScript example is compiled by the package typecheck. Start with
4
+ the smallest example matching the workflow rather than assembling primitives
5
+ from the API inventory.
6
+
7
+ | Intent | Executable recipe | Main primitives |
8
+ | --------------------------------------------- | ------------------------------------------------------------------------ | ------------------------------------------------------------------ |
9
+ | Sequential import or ETL | [`typed-import.ts`](../examples/typed-import.ts) | `createSteps`, `dependsOn`, `requireOutputs`, `targets` |
10
+ | Validate options, outputs, and results | [`validated-boundaries.ts`](../examples/validated-boundaries.ts) | Standard Schema, `outputSchema`, `resultSchema` |
11
+ | Inspect, plan, or graph a pipeline module | [`typed-import.ts`](../examples/typed-import.ts) | `tubeless inspect`, `tubeless plan`, `tubeless graph`, `toMermaid` |
12
+ | Safe write/publish preview | [`publish-with-gates.ts`](../examples/publish-with-gates.ts) | `dryRun`, `optionalDependsOn`, `skipAfterFailureOf` |
13
+ | Deliberately omit unnecessary work | [`conditional-step.ts`](../examples/conditional-step.ts) | `step.skippable`, valued skip, skip-aware output typing |
14
+ | Preserve independent work after failure | [`best-effort.ts`](../examples/best-effort.ts) | `continueOnError`, structured `run` result |
15
+ | Compose one reusable workflow | [`child-pipeline.ts`](../examples/child-pipeline.ts) | `fromPipeline`, `mapOptions`, `mapResult` |
16
+ | Fan out over runtime items | [`fan-out-progress.ts`](../examples/fan-out-progress.ts) | `forEachPipeline`, stable keys, concurrency, progress |
17
+ | Show determinate progress | [`fan-out-progress.ts`](../examples/fan-out-progress.ts) | `reportProgress`, mapped-child progress |
18
+ | Retry and rate-limit remote calls | [`resumable-enrichment.ts`](../examples/resumable-enrichment.ts) | `withRetry`, `RateLimiter`, injected sleep and signal |
19
+ | Resume durable long-running work | [`resumable-enrichment.ts`](../examples/resumable-enrichment.ts) | `dryRun`, `openCheckpoint`, `withCheckpointedBatch` |
20
+ | Expose and run a typed command-line program | [`cli-job.ts`](../examples/cli-job.ts) | `definePipelineCommand`, conditional `mapOptions`, `tubeless run` |
21
+ | Render plans and diagnostics | [`rendering.ts`](../examples/rendering.ts) | `renderPipelinePlan`, `renderPipelineError` |
22
+ | Handle cancellation and deterministic testing | [`cancellation-and-testing.ts`](../examples/cancellation-and-testing.ts) | `createPipelineTestRuntime`, captured status/progress |
23
+ | Export JSON or OpenTelemetry lifecycle events | [`tracing.ts`](../examples/tracing.ts) | trace context, attempt events, JSON exporter |
24
+ | Persist, inspect, and explicitly launch runs | [`local-observability.ts`](../examples/local-observability.ts) | SQLite store, `createPipelineRunProjector`, `definePipelineStudio` |
25
+
26
+ ## Selection rules
27
+
28
+ 1. Use an ordinary step for one unit of domain work.
29
+ 2. Set `dryRun: "skip"` or provide a side-effect-free `dryRun` handler before
30
+ exposing side-effecting work through CLI dry-run support.
31
+ 3. Use `step.skippable` only for a successful policy decision, never to hide an error.
32
+ 4. Use a child pipeline when the child has value independently; use a normal
33
+ helper function when it does not.
34
+ 5. Use `forEachPipeline` when every item needs child-pipeline lifecycle and
35
+ reporting. Use `runConcurrent` for lightweight worker functions.
36
+ 6. Use `definePipelineCommand` for pipeline scripts; use `defineCommand` only
37
+ when the script is not centered on a pipeline. Preview selection with
38
+ `command.plan()` or `tubeless plan`; do not simulate planning with `--plan`.
39
+ 7. Declare public goals with `targets: [step]` on the pipeline, select their IDs
40
+ for goal-oriented execution, and use `stepIds` only for an exact filter. Use
41
+ `requireOutputs` when the final domain result is not meaningful without
42
+ specific step outputs. Read plan `selectionReasons` instead of recreating
43
+ target-closure logic in a CLI or application.
44
+ 8. Use `tubeless/render` when plans or diagnostics cross a human or JSON
45
+ presentation boundary; do not duplicate selection-reason or error formatting.
46
+ Use `tubeless/reporter` for optional TTY run reporters; do not import them
47
+ from `tubeless`.
48
+
49
+ For semantics behind these choices, read [core concepts](./concepts.md).
50
+ For workbench commands, read [the CLI](./cli.md). For the local run UI, read
51
+ [the studio](./studio.md).
package/docs/studio.md ADDED
@@ -0,0 +1,86 @@
1
+ # Local studio
2
+
3
+ Normal pipeline and CLI execution is storage-free. The studio is an optional
4
+ local projection of an append-only SQLite event store. Core does not import it.
5
+
6
+ Record one workbench run by placing `--store` before the command file:
7
+
8
+ ```sh
9
+ bunx tubeless run --store .tubeless/runs.sqlite ./scripts/import.ts -- --source rows.txt
10
+ ```
11
+
12
+ Open the studio only when you want a browser view:
13
+
14
+ ```sh
15
+ bunx tubeless ui --store .tubeless/runs.sqlite
16
+ ```
17
+
18
+ That form is read-only. It does not guess executable modules from recorded
19
+ definitions. Register marked `definePipelineCommand` exports to make a
20
+ **Run pipeline** action available:
21
+
22
+ ```sh
23
+ bunx tubeless ui \
24
+ --store .tubeless/runs.sqlite \
25
+ --command ./scripts/import.ts \
26
+ --command ./scripts/publish.ts
27
+ ```
28
+
29
+ `--export` selects the export when you register exactly one `--command`. Launch
30
+ forms render each command's structured parameter contract: booleans as
31
+ checkboxes, constrained strings as selects, numbers with their bounds, and
32
+ paths or unconstrained values as text fields. Submitted values go through the
33
+ normal typed command parser without a shell. **Preview plan** uses the dry-run
34
+ and step/target values from the same form; it never creates a run.
35
+
36
+ Default bind address is `127.0.0.1`, default port is `4317`, and the default
37
+ store is `.tubeless/runs.sqlite`. Browser-triggered execution requires a
38
+ loopback host (`127.0.0.1`, `::1`, or `localhost`). Non-loopback binding is
39
+ rejected when any command is registered. A non-loopback `--host` without
40
+ commands is allowed and stays read-only: the store is visible to anyone who
41
+ can reach the port, and clear-history is not wired. That bind is a risk you
42
+ enable; see [SECURITY.md](../SECURITY.md).
43
+
44
+ ## Checked-in catalog
45
+
46
+ Declare module references once for a repeatable project catalog:
47
+
48
+ ```ts
49
+ // tubeless.studio.ts
50
+ import { definePipelineStudio } from "tubeless/workbench/studio";
51
+
52
+ export default definePipelineStudio({
53
+ cwd: ".",
54
+ commands: [
55
+ { file: "./scripts/import.ts", export: "ImportCommand", name: "Import rows" },
56
+ { file: "./scripts/publish.ts", export: "PublishCommand" },
57
+ ],
58
+ });
59
+ ```
60
+
61
+ ```sh
62
+ bunx tubeless ui --store .tubeless/runs.sqlite ./tubeless.studio.ts
63
+ ```
64
+
65
+ Command paths and `cwd` are relative to the manifest file. Empty or duplicate
66
+ registrations fail before the studio starts. Presentation-name overrides do not
67
+ change run or pipeline identity.
68
+
69
+ ## What the UI shows
70
+
71
+ The studio combines active and historical runs in one running-first view. Child
72
+ pipeline executions stay beneath their top-level run. Run details include step
73
+ attempts, progress, logs, and structured errors. Nested pipeline steps are
74
+ labeled with the child pipeline and its declared steps; runtime fan-out is
75
+ identified without guessing its item count.
76
+
77
+ Browser plan, launch, and clear-history requests send
78
+ `x-tubeless-studio-plan`, `x-tubeless-studio-launch`, and
79
+ `x-tubeless-studio-clear-history`. Those requests also require a `Host` header
80
+ that matches the bound authority, and plan/launch require `application/json`.
81
+ The header names are part of the local studio protocol. They are same-origin
82
+ guards, not authentication.
83
+
84
+ Programmatic callers can compose the same pieces from
85
+ `tubeless/run-store/sqlite` and `tubeless/run-store/ui`. See
86
+ [`local-observability.ts`](../examples/local-observability.ts).
@@ -0,0 +1,42 @@
1
+ import { createSteps, definePipeline } from "tubeless";
2
+
3
+ interface AuditOptions {
4
+ failLinks?: boolean;
5
+ }
6
+
7
+ const step = createSteps<AuditOptions>();
8
+
9
+ const checkLinks = step("check-links", {
10
+ description: "Validate links independently",
11
+ run: (_inputs, context) => {
12
+ if (context.options.failLinks) throw new Error("broken links found");
13
+ return { checked: 12 };
14
+ },
15
+ });
16
+
17
+ const checkMetadata = step("check-metadata", {
18
+ description: "Validate metadata independently",
19
+ run: () => ({ checked: 8 }),
20
+ });
21
+
22
+ export const BestEffortAuditPipeline = definePipeline({
23
+ id: "best-effort-audit",
24
+ steps: [checkLinks, checkMetadata],
25
+ finalize: (outputs) => ({
26
+ links: outputs["check-links"],
27
+ metadata: outputs["check-metadata"],
28
+ }),
29
+ });
30
+
31
+ export async function runBestEffortExample() {
32
+ const result = await BestEffortAuditPipeline.run({
33
+ failLinks: true,
34
+ continueOnError: true,
35
+ });
36
+
37
+ return {
38
+ errors: result.errors,
39
+ metadataStillCompleted: result.value?.metadata?.checked === 8,
40
+ reports: result.steps,
41
+ };
42
+ }
@@ -0,0 +1,45 @@
1
+ import { createSteps, definePipeline } from "tubeless";
2
+ import { createPipelineTestRuntime } from "tubeless/testing";
3
+
4
+ interface PollOptions {
5
+ attempts: number;
6
+ }
7
+
8
+ const step = createSteps<PollOptions>();
9
+
10
+ const poll = step("poll", {
11
+ description: "Poll cooperatively with an injected, cancellation-aware sleep",
12
+ run: async (_inputs, context) => {
13
+ for (let attempt = 1; attempt <= context.options.attempts; attempt += 1) {
14
+ await context.sleep(100, context.signal);
15
+ context.reportProgress({ completed: attempt, total: context.options.attempts });
16
+ }
17
+ return context.options.attempts;
18
+ },
19
+ });
20
+
21
+ export const PollPipeline = definePipeline({
22
+ id: "poll",
23
+ steps: [poll],
24
+ finalize: (outputs) => outputs.poll ?? 0,
25
+ });
26
+
27
+ export async function runWithTestRuntime() {
28
+ const test = createPipelineTestRuntime({ cwd: "/workspace" });
29
+ const value: number = await test.runOrThrow(PollPipeline, { attempts: 3 });
30
+
31
+ return {
32
+ elapsedMs: test.clock.now(),
33
+ latestProgress: test.latestProgress.get("poll"),
34
+ logs: test.logs,
35
+ statuses: test.statuses,
36
+ value,
37
+ };
38
+ }
39
+
40
+ export async function runCancelledTest() {
41
+ const test = createPipelineTestRuntime();
42
+ test.abort("cancel the test run");
43
+
44
+ return test.run(PollPipeline, { attempts: 3 });
45
+ }
@@ -0,0 +1,51 @@
1
+ import { createSteps, definePipeline } from "tubeless";
2
+
3
+ interface NormalizeOptions {
4
+ rows: readonly string[];
5
+ }
6
+
7
+ const normalizeStep = createSteps<NormalizeOptions>();
8
+
9
+ const normalizeRows = normalizeStep("normalize-rows", {
10
+ run: (_inputs, context) =>
11
+ context.options.rows.map((row) => row.trim().toLowerCase()).filter((row) => row.length > 0),
12
+ });
13
+
14
+ export const NormalizePipeline = definePipeline({
15
+ id: "normalize",
16
+ steps: [normalizeRows],
17
+ targets: [normalizeRows],
18
+ finalize: (outputs) => outputs["normalize-rows"] ?? [],
19
+ });
20
+
21
+ interface ImportOptions {
22
+ lines: readonly string[];
23
+ }
24
+
25
+ const importStep = createSteps<ImportOptions>();
26
+
27
+ const normalizedImport = importStep.fromPipeline("normalized-import", {
28
+ pipeline: NormalizePipeline,
29
+ mapOptions: (_inputs, context) => ({ rows: context.options.lines }),
30
+ mapResult: (rows) => ({ count: rows.length, rows }),
31
+ });
32
+
33
+ export const ChildPipelineImport = definePipeline({
34
+ id: "child-pipeline-import",
35
+ steps: [normalizedImport],
36
+ finalize: (outputs) => outputs["normalized-import"],
37
+ });
38
+
39
+ export async function runChildPipelineExample() {
40
+ return ChildPipelineImport.runOrThrow({
41
+ lines: [" Alpha ", "", "Beta"],
42
+ });
43
+ }
44
+
45
+ if (false) {
46
+ importStep.fromPipeline("invalid-child-selection", {
47
+ pipeline: NormalizePipeline,
48
+ // @ts-expect-error Child run options are checked against its declared target IDs.
49
+ mapOptions: () => ({ rows: [], targets: ["normalise-rows"] }),
50
+ });
51
+ }
@@ -0,0 +1,86 @@
1
+ import * as fs from "fs";
2
+ import { createSteps, definePipeline, requireOutputs } from "tubeless";
3
+ import { definePipelineCommand } from "tubeless/cli";
4
+
5
+ interface ImportOptions {
6
+ lines: readonly string[];
7
+ limit?: number;
8
+ }
9
+
10
+ const step = createSteps<ImportOptions>();
11
+
12
+ const loadRows = step("load-rows", {
13
+ description: "Read raw input records from the caller.",
14
+ run: (_inputs, context) => context.options.lines,
15
+ });
16
+
17
+ const normalizeRows = step("normalize-rows", {
18
+ dependsOn: [loadRows],
19
+ description: "Trim, lowercase, drop blanks, and apply the optional --limit.",
20
+ run: ({ "load-rows": rows }, context) => {
21
+ const normalized = rows.map((row) => row.trim().toLowerCase()).filter((row) => row.length > 0);
22
+ return context.options.limit === undefined
23
+ ? normalized
24
+ : normalized.slice(0, context.options.limit);
25
+ },
26
+ });
27
+
28
+ export const ImportPipeline = definePipeline({
29
+ id: "import",
30
+ steps: [loadRows, normalizeRows],
31
+ targets: [normalizeRows],
32
+ finalize: requireOutputs([normalizeRows], (outputs) => ({
33
+ count: outputs["normalize-rows"].length,
34
+ rows: outputs["normalize-rows"],
35
+ })),
36
+ });
37
+
38
+ /**
39
+ * The kind of entry point a `scripts/*.ts` file would export: flags become a typed,
40
+ * validated `args` object instead of hand-parsed `process.argv`. `--source`'s `path` type
41
+ * resolves relative to `context.cwd` and confirms the file exists before `run` sees it.
42
+ * Pipeline commands also provide exact `--step`, dependency-aware `--target`
43
+ * for the pipeline's declared goals, and `--continue-on-error`, while
44
+ * forwarding the universal `--dry-run` flag into the pipeline automatically.
45
+ * Plan with `command.plan()` or `tubeless plan`, not `--plan`.
46
+ */
47
+ export const ImportCommand = definePipelineCommand(ImportPipeline, {
48
+ description: "Normalize a newline-delimited list of input rows.",
49
+ params: {
50
+ source: {
51
+ type: "path",
52
+ description: "File with one row per line.",
53
+ mustExist: true,
54
+ kind: "file",
55
+ },
56
+ limit: {
57
+ type: "number",
58
+ optional: true,
59
+ integer: true,
60
+ min: 1,
61
+ description: "Only keep the first N normalized rows.",
62
+ },
63
+ },
64
+ mapOptions: (args) => ({
65
+ lines: fs.readFileSync(args.source, "utf8").split("\n"),
66
+ limit: args.limit,
67
+ }),
68
+ summarize: (result) => [`Normalized ${result.count} row(s).`],
69
+ });
70
+
71
+ if (false) {
72
+ const missingLinesParams = { source: { type: "path" } } as const;
73
+ // @ts-expect-error --source does not supply ImportPipeline's required lines option.
74
+ definePipelineCommand(ImportPipeline, { params: missingLinesParams });
75
+
76
+ const renamedOptionalParams = {
77
+ lines: { type: "string", multiple: true },
78
+ max: { type: "number", optional: true },
79
+ } as const;
80
+ // @ts-expect-error --max is not a same-name option on ImportPipeline.
81
+ definePipelineCommand(ImportPipeline, { params: renamedOptionalParams });
82
+ }
83
+
84
+ if (import.meta.main) {
85
+ void ImportCommand.main();
86
+ }
@@ -0,0 +1,41 @@
1
+ import { createSteps, definePipeline } from "tubeless";
2
+
3
+ interface CacheOptions {
4
+ cachedValue?: string;
5
+ refresh: boolean;
6
+ }
7
+
8
+ const step = createSteps<CacheOptions>();
9
+
10
+ const resolveValue = step.skippable("resolve-value", {
11
+ description: "Reuse a cached value unless a refresh is required",
12
+ skip: (_inputs, context) =>
13
+ !context.options.refresh && context.options.cachedValue !== undefined
14
+ ? { reason: "cached value is current", value: context.options.cachedValue }
15
+ : false,
16
+ run: async () => "fresh-value",
17
+ });
18
+
19
+ const formatValue = step("format-value", {
20
+ dependsOn: [resolveValue],
21
+ description: "Format the resolved value for the caller",
22
+ run: ({ "resolve-value": value }) => {
23
+ // A skippable step is always typed as T | undefined, even when this
24
+ // policy skip publishes a value. Keep absence handling explicit.
25
+ if (value === undefined) throw new Error("resolve-value produced no value");
26
+ return value.toUpperCase();
27
+ },
28
+ });
29
+
30
+ export const ConditionalCachePipeline = definePipeline({
31
+ id: "conditional-cache",
32
+ steps: [resolveValue, formatValue],
33
+ finalize: (outputs) => outputs["format-value"] ?? "",
34
+ });
35
+
36
+ export async function runConditionalCacheExample() {
37
+ return ConditionalCachePipeline.runOrThrow({
38
+ cachedValue: "cached-value",
39
+ refresh: false,
40
+ });
41
+ }
@@ -0,0 +1,53 @@
1
+ import { createSteps, definePipeline } from "tubeless";
2
+
3
+ interface ShardOptions {
4
+ records: readonly string[];
5
+ shardId: string;
6
+ }
7
+
8
+ const shardStep = createSteps<ShardOptions>();
9
+
10
+ const processRecords = shardStep("process-records", {
11
+ description: "Process every record in one shard",
12
+ run: async (_inputs, context) => {
13
+ const processed: string[] = [];
14
+ for (const [index, record] of context.options.records.entries()) {
15
+ processed.push(record.toUpperCase());
16
+ context.reportProgress({
17
+ completed: index + 1,
18
+ message: record,
19
+ total: context.options.records.length,
20
+ });
21
+ }
22
+ return { processed, shardId: context.options.shardId };
23
+ },
24
+ });
25
+
26
+ export const ShardPipeline = definePipeline({
27
+ id: "process-shard",
28
+ steps: [processRecords],
29
+ finalize: (outputs) => outputs["process-records"],
30
+ });
31
+
32
+ interface FanOutOptions {
33
+ concurrency: number;
34
+ shards: readonly { id: string; records: readonly string[] }[];
35
+ }
36
+
37
+ const fanOutStep = createSteps<FanOutOptions>();
38
+
39
+ const processShards = fanOutStep.forEachPipeline("process-shards", {
40
+ pipeline: ShardPipeline,
41
+ description: "Process shards with bounded concurrency and stable identities",
42
+ items: (_inputs, context) => context.options.shards,
43
+ key: (shard) => shard.id,
44
+ concurrency: (_inputs, context) => context.options.concurrency,
45
+ progress: { itemNoun: "shards" },
46
+ mapOptions: (shard) => ({ records: shard.records, shardId: shard.id }),
47
+ });
48
+
49
+ export const FanOutPipeline = definePipeline({
50
+ id: "fan-out",
51
+ steps: [processShards],
52
+ finalize: (outputs) => outputs["process-shards"] ?? [],
53
+ });
@@ -0,0 +1,80 @@
1
+ import { createSteps, defaultPipelineContext, definePipeline, requireOutputs } from "tubeless";
2
+ import { createPipelineRunProjector, type StoredPipelineEvent } from "tubeless/run-store";
3
+ import { openSqlitePipelineRunStore } from "tubeless/run-store/sqlite";
4
+ import { definePipelineStudio } from "tubeless/workbench/studio";
5
+ import {
6
+ startPipelineRunStudio,
7
+ type PipelineRunStudioLauncher,
8
+ type PipelineRunStudioServer,
9
+ } from "tubeless/run-store/ui";
10
+
11
+ const step = createSteps<{ rows: readonly string[] }>();
12
+ const normalize = step("normalize", {
13
+ description: "Normalize imported rows for publication.",
14
+ run: (_inputs, context) => {
15
+ context.log.log("normalizing", context.options.rows.length, "rows");
16
+ return context.options.rows.map((row) => row.trim().toLowerCase());
17
+ },
18
+ });
19
+
20
+ export const LocallyObservedPipeline = definePipeline({
21
+ id: "locally-observed-import",
22
+ steps: [normalize],
23
+ targets: [normalize],
24
+ finalize: requireOutputs([normalize], ({ normalize }) => normalize),
25
+ });
26
+
27
+ /** A checked-in catalog can register many command modules in one place. */
28
+ export const LocalStudioConfig = definePipelineStudio({
29
+ commands: [{ file: "./cli-job.ts", export: "ImportCommand", name: "Import rows" }],
30
+ });
31
+
32
+ /**
33
+ * Fold paged store events without walking the full history on every refresh.
34
+ * Append only newer pages. Duplicate and out-of-order ids are ignored; `0` is a
35
+ * valid first id. `projectPipelineRunStore` remains the one-shot wrapper.
36
+ */
37
+ export function snapshotFromPages(pages: readonly (readonly StoredPipelineEvent[])[]) {
38
+ const projector = createPipelineRunProjector();
39
+ for (const page of pages) projector.append(page);
40
+ return projector.snapshot();
41
+ }
42
+
43
+ /** Recording is opt-in; ordinary calls to the pipeline remain storage-free. */
44
+ export async function runWithLocalHistory(
45
+ rows: readonly string[],
46
+ filename = ".tubeless/runs.sqlite"
47
+ ): Promise<readonly string[]> {
48
+ const store = await openSqlitePipelineRunStore(filename);
49
+ try {
50
+ return await LocallyObservedPipeline.runOrThrow(
51
+ { rows },
52
+ {
53
+ ...defaultPipelineContext(),
54
+ tracing: { exporter: store },
55
+ }
56
+ );
57
+ } finally {
58
+ await store.close();
59
+ }
60
+ }
61
+
62
+ /** The UI is another optional reader of the same append-only store. */
63
+ export async function serveLocalStudio(
64
+ filename = ".tubeless/runs.sqlite",
65
+ launcher?: PipelineRunStudioLauncher
66
+ ): Promise<PipelineRunStudioServer> {
67
+ const store = await openSqlitePipelineRunStore(filename);
68
+ const server = await startPipelineRunStudio({
69
+ history: { clear: () => store.clearHistory() },
70
+ launcher,
71
+ store,
72
+ });
73
+ return {
74
+ ...server,
75
+ close: async () => {
76
+ await server.close();
77
+ await store.close();
78
+ },
79
+ };
80
+ }
@@ -0,0 +1,56 @@
1
+ import { createSteps, definePipeline } from "tubeless";
2
+
3
+ interface PublishOptions {
4
+ source: string;
5
+ }
6
+
7
+ interface Artifact {
8
+ body: string;
9
+ size: number;
10
+ }
11
+
12
+ const step = createSteps<PublishOptions>();
13
+
14
+ const buildArtifact = step("build-artifact", {
15
+ description: "Build an artifact that can be validated and published.",
16
+ run: (_inputs, context): Artifact => ({
17
+ body: context.options.source.trim(),
18
+ size: context.options.source.trim().length,
19
+ }),
20
+ });
21
+
22
+ const validateArtifact = step("validate-artifact", {
23
+ dependsOn: [buildArtifact],
24
+ description: "Fail before publishing if the artifact is empty.",
25
+ run: ({ "build-artifact": artifact }) => {
26
+ if (artifact.size === 0) {
27
+ throw new Error("artifact is empty");
28
+ }
29
+ return { valid: true };
30
+ },
31
+ });
32
+
33
+ const publishArtifact = step("publish-artifact", {
34
+ dependsOn: [buildArtifact],
35
+ optionalDependsOn: [validateArtifact],
36
+ skipAfterFailureOf: [validateArtifact],
37
+ description: "Publish only on real runs, and only after validation has not failed.",
38
+ dryRun: "skip",
39
+ run: ({ "build-artifact": artifact }) => ({
40
+ publishedId: `artifact-${artifact.size}`,
41
+ }),
42
+ });
43
+
44
+ export const PublishPipeline = definePipeline({
45
+ id: "publish",
46
+ steps: [buildArtifact, validateArtifact, publishArtifact],
47
+ finalize: (outputs) => ({
48
+ artifact: outputs["build-artifact"],
49
+ publishedId: outputs["publish-artifact"]?.publishedId,
50
+ valid: outputs["validate-artifact"]?.valid === true,
51
+ }),
52
+ });
53
+
54
+ export async function runPublishDryRunExample() {
55
+ return PublishPipeline.runOrThrow({ source: "release notes", dryRun: true });
56
+ }