blokctl 0.3.0 → 0.6.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/dist/commands/check/index.d.ts +2 -0
- package/dist/commands/check/index.js +39 -0
- package/dist/commands/create/project.js +20 -5
- package/dist/commands/create/utils/Examples.d.ts +2 -2
- package/dist/commands/create/utils/Examples.js +28 -31
- package/dist/commands/dev/index.js +40 -57
- package/dist/commands/generate/GenerationAnalytics.js +2 -1
- package/dist/commands/generate/e2e/NodeGenerator.e2e.test.js +1 -1
- package/dist/commands/generate/e2e/TriggerGenerator.e2e.test.js +1 -1
- package/dist/commands/generate/e2e/WorkflowGenerator.e2e.test.js +1 -1
- package/dist/commands/migrate/index.js +11 -0
- package/dist/commands/migrate/paths.d.ts +2 -0
- package/dist/commands/migrate/paths.js +267 -0
- package/dist/index.js +14 -1
- package/dist/services/local-token-manager.js +1 -1
- package/dist/services/runtime-detector.d.ts +1 -0
- package/dist/services/runtime-detector.js +12 -0
- package/dist/services/runtime-setup.d.ts +12 -1
- package/dist/services/runtime-setup.js +28 -0
- package/dist/services/semver-utils.d.ts +18 -0
- package/dist/services/semver-utils.js +82 -0
- package/dist/services/semver-utils.test.d.ts +1 -0
- package/dist/services/semver-utils.test.js +233 -0
- package/dist/studio-dist/assets/{icons-N5J4OhGx.js → icons-D-BBts99.js} +110 -65
- package/dist/studio-dist/assets/index-BD8_9YPN.js +42 -0
- package/dist/studio-dist/assets/index-D4Bc9-mb.css +1 -0
- package/dist/studio-dist/index.html +3 -3
- package/package.json +2 -2
- package/dist/studio-dist/assets/index-D6JA5F-X.js +0 -42
- package/dist/studio-dist/assets/index-mdQkg9ul.css +0 -1
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import color from "picocolors";
|
|
2
|
+
import { readProjectConfig, validateProjectRuntimes } from "../../services/runtime-setup.js";
|
|
3
|
+
export async function checkProject(_opts) {
|
|
4
|
+
const currentPath = process.cwd();
|
|
5
|
+
const config = readProjectConfig(currentPath);
|
|
6
|
+
if (!config) {
|
|
7
|
+
console.error(" No .blok/config.json found. Run this from a Blok project directory.");
|
|
8
|
+
process.exit(1);
|
|
9
|
+
}
|
|
10
|
+
console.log(`\n ${color.bold("Blok Runtime Version Check")}`);
|
|
11
|
+
console.log(" ──────────────────────────\n");
|
|
12
|
+
const results = await validateProjectRuntimes(currentPath);
|
|
13
|
+
if (results.length === 0) {
|
|
14
|
+
console.log(" No runtime version constraints configured.");
|
|
15
|
+
console.log(" Runtime versions will be pinned automatically on next project creation.\n");
|
|
16
|
+
process.exit(0);
|
|
17
|
+
}
|
|
18
|
+
console.log(` ${color.bold("Project runtimes")} (.blok/config.json):`);
|
|
19
|
+
for (const r of results) {
|
|
20
|
+
if (r.satisfied) {
|
|
21
|
+
console.log(` ${color.green("✓")} ${r.label} ${r.found} (requires ${r.required})`);
|
|
22
|
+
}
|
|
23
|
+
else {
|
|
24
|
+
console.log(` ${color.red("✗")} ${r.label} ${r.found || "not installed"} (requires ${r.required})`);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
console.log();
|
|
28
|
+
const failures = results.filter((r) => !r.satisfied);
|
|
29
|
+
if (failures.length > 0) {
|
|
30
|
+
for (const f of failures) {
|
|
31
|
+
console.log(f.message);
|
|
32
|
+
console.log();
|
|
33
|
+
}
|
|
34
|
+
console.log(` ${color.red(`${failures.length} check${failures.length > 1 ? "s" : ""} failed.`)}\n`);
|
|
35
|
+
process.exit(1);
|
|
36
|
+
}
|
|
37
|
+
console.log(` ${color.green("All checks passed.")}\n`);
|
|
38
|
+
process.exit(0);
|
|
39
|
+
}
|
|
@@ -11,12 +11,13 @@ import { isNonInteractive, parseCommaSeparated, resolveOrThrow } from "../../ser
|
|
|
11
11
|
import { manager as pm } from "../../services/package-manager.js";
|
|
12
12
|
import { detectRuntimes } from "../../services/runtime-detector.js";
|
|
13
13
|
import { createTriggerConfig, generateRuntimeEnvVars, generateSupervisordConfig, generateTriggerEnvVars, generateTriggerSupervisordConfig, setupRuntime, writeProjectConfig, } from "../../services/runtime-setup.js";
|
|
14
|
+
import { computeDefaultConstraint } from "../../services/semver-utils.js";
|
|
14
15
|
import { agents_md, claude_md, examples_url, node_file, package_dependencies, package_dev_dependencies, } from "./utils/Examples.js";
|
|
15
16
|
const exec = util.promisify(child_process.exec);
|
|
16
17
|
const HOME_DIR = `${os.homedir()}/.blok`;
|
|
17
18
|
const GITHUB_REPO_LOCAL = `${HOME_DIR}/blok`;
|
|
18
19
|
const GITHUB_REPO_REMOTE = "https://github.com/well-prado/blok.git";
|
|
19
|
-
const GITHUB_REPO_RELEASE_TAG = "v0.
|
|
20
|
+
const GITHUB_REPO_RELEASE_TAG = "v0.6.1";
|
|
20
21
|
fsExtra.ensureDirSync(HOME_DIR);
|
|
21
22
|
const options = {
|
|
22
23
|
baseDir: HOME_DIR,
|
|
@@ -86,8 +87,12 @@ export async function createProject(opts, version, currentPath = false, localRep
|
|
|
86
87
|
{ label: "NodeJS", value: "node", hint: "always included" },
|
|
87
88
|
...detectedRuntimes.map((rt) => {
|
|
88
89
|
let hint;
|
|
89
|
-
if (rt.available) {
|
|
90
|
-
|
|
90
|
+
if (rt.available && rt.version) {
|
|
91
|
+
const constraint = computeDefaultConstraint(rt.version);
|
|
92
|
+
hint = `${rt.toolchain} ${rt.version} detected (will pin ${constraint})`;
|
|
93
|
+
}
|
|
94
|
+
else if (rt.available) {
|
|
95
|
+
hint = `${rt.toolchain} detected`;
|
|
91
96
|
}
|
|
92
97
|
else if (rt.secondaryTool && !rt.secondaryTool.available) {
|
|
93
98
|
hint = `${rt.secondaryTool.name} not found - will be skipped`;
|
|
@@ -412,6 +417,7 @@ export async function createProject(opts, version, currentPath = false, localRep
|
|
|
412
417
|
"@blokjs/trigger-pubsub": "triggers/pubsub",
|
|
413
418
|
"@blokjs/trigger-queue": "triggers/queue",
|
|
414
419
|
};
|
|
420
|
+
const BLOKJS_DEP_RANGE = "^0.6.1";
|
|
415
421
|
for (const depGroup of ["dependencies", "devDependencies", "peerDependencies"]) {
|
|
416
422
|
const deps = packageJsonContent[depGroup];
|
|
417
423
|
if (!deps)
|
|
@@ -419,7 +425,7 @@ export async function createProject(opts, version, currentPath = false, localRep
|
|
|
419
425
|
for (const pkg of Object.keys(deps)) {
|
|
420
426
|
if (pkg.startsWith("@blok/")) {
|
|
421
427
|
const newPkg = pkg.replace("@blok/", "@blokjs/");
|
|
422
|
-
deps[newPkg] =
|
|
428
|
+
deps[newPkg] = BLOKJS_DEP_RANGE;
|
|
423
429
|
delete deps[pkg];
|
|
424
430
|
}
|
|
425
431
|
}
|
|
@@ -429,9 +435,18 @@ export async function createProject(opts, version, currentPath = false, localRep
|
|
|
429
435
|
deps[pkg] = `file:${path.resolve(repoSource, workspacePackageMap[pkg])}`;
|
|
430
436
|
}
|
|
431
437
|
else {
|
|
432
|
-
deps[pkg] =
|
|
438
|
+
deps[pkg] = BLOKJS_DEP_RANGE;
|
|
433
439
|
}
|
|
434
440
|
}
|
|
441
|
+
else if (typeof ver === "string" &&
|
|
442
|
+
(ver === "^0.2.0" ||
|
|
443
|
+
ver === "^0.2" ||
|
|
444
|
+
ver === "0.2.0" ||
|
|
445
|
+
ver.startsWith("^0.2.") ||
|
|
446
|
+
ver.startsWith("0.2.")) &&
|
|
447
|
+
(pkg.startsWith("@blokjs/") || pkg === "blokctl")) {
|
|
448
|
+
deps[pkg] = BLOKJS_DEP_RANGE;
|
|
449
|
+
}
|
|
435
450
|
}
|
|
436
451
|
}
|
|
437
452
|
if (localRepoPath) {
|
|
@@ -33,7 +33,7 @@ declare const php_dockerfile = "FROM php:8.2-cli-alpine AS builder\nWORKDIR /app
|
|
|
33
33
|
declare const ruby_node_file = "require_relative '../../lib/blok'\n\nmodule Blok\n module Nodes\n class {{NODE_NAME_PASCAL}}Node < Blok::NodeHandler\n def execute(ctx, config)\n # Access request body\n name = ctx.request.body.is_a?(Hash) ? ctx.request.body['name'] : nil\n name ||= 'World'\n\n # Access configuration\n prefix = config['prefix'] || 'Hello'\n\n message = \"#{prefix}, #{name}!\"\n\n # Store in context for downstream nodes\n ctx.vars['greeting'] = message\n\n # Return response\n {\n 'message' => message,\n 'timestamp' => Time.now.utc.iso8601,\n 'language' => 'Ruby'\n }\n end\n end\n end\nend\n";
|
|
34
34
|
declare const ruby_gemfile = "source 'https://rubygems.org'\n\nruby '>= 3.1'\n\ngem 'sinatra', '~> 4.0'\ngem 'puma', '~> 6.4'\ngem 'rackup', '~> 2.1'\n";
|
|
35
35
|
declare const ruby_dockerfile = "FROM ruby:3.2-alpine AS builder\nRUN apk add --no-cache build-base\nWORKDIR /app\nCOPY Gemfile Gemfile.lock ./\nRUN bundle install --without development test\n\nFROM ruby:3.2-alpine\nRUN apk --no-cache add ca-certificates wget\nWORKDIR /app\nCOPY --from=builder /usr/local/bundle /usr/local/bundle\nCOPY . .\n\nEXPOSE 8080\nENV PORT=8080\nENV RACK_ENV=production\nHEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \\\n CMD wget --no-verbose --tries=1 --spider http://localhost:8080/health || exit 1\n\nCMD [\"bundle\", \"exec\", \"puma\", \"-b\", \"tcp://0.0.0.0:8080\"]\n";
|
|
36
|
-
declare const agents_md = "# Blok Project\n\nBlok is a TypeScript-first workflow orchestration framework. It executes declarative workflows (JSON or TypeScript DSL) composed of steps (nodes) that run across 8 language runtimes: NodeJS, Python3, Go, Rust, Java, C#, PHP, and Ruby.\n\n## Project Structure\n\n```\n\u251C\u2500\u2500 src/\n\u2502 \u2514\u2500\u2500 nodes/ # TypeScript node implementations\n\u251C\u2500\u2500 runtimes/ # Non-NodeJS runtime nodes (Go, Python3, etc.)\n\u2502 \u2514\u2500\u2500 {lang}/nodes/ # Language-specific node implementations\n\u251C\u2500\u2500 workflows/\n\u2502 \u251C\u2500\u2500 json/ # Workflow definitions (JSON)\n\u2502 \u251C\u2500\u2500 yaml/ # Workflow definitions (YAML)\n\u2502 \u2514\u2500\u2500 toml/ # Workflow definitions (TOML)\n\u251C\u2500\u2500 .blok/\n\u2502 \u251C\u2500\u2500 config.json # Runtime configuration (ports, start commands)\n\u2502 \u2514\u2500\u2500 runtimes/ # Auto-generated runtime scaffolds\n\u251C\u2500\u2500 .env.local # Environment variables (ports, paths)\n\u2514\u2500\u2500 supervisord.conf # Process management config\n```\n\n## Commands\n\n```bash\nnpm run dev # Start dev server (or blokctl dev for multi-runtime)\nnpm run build # Build project\nnpm test # Run tests\nblokctl create node <name> # Scaffold a new node\nblokctl create workflow <n># Scaffold a new workflow\nblokctl trace # Open Blok Studio (trace visualization)\nblokctl studio # Alias for blokctl trace\n```\n\n## Context \u2014 Critical Data Flow\n\nThe Context type is the central execution state passed through every step.\n\n```typescript\ntype Context = {\n id: string; // Unique request ID\n request: RequestContext; // Incoming request (body, headers, params, query)\n response: ResponseContext; // Current step output \u2014 OVERWRITTEN every step\n vars?: VarsContext; // Persistent variables \u2014 PERSISTS across workflow\n config: ConfigContext; // Node config (inputs resolved by Mapper)\n env?: EnvContext; // process.env access\n logger: LoggerContext;\n error: ErrorContext;\n};\n```\n\n### The Two Critical Rules\n\n**Rule 1: \\`ctx.response.data\\` is OVERWRITTEN after every step.**\nEach step's output replaces the previous \\`ctx.response.data\\`. If you need a step's output later, store it in \\`ctx.vars\\`.\n\n**Rule 2: \\`ctx.vars\\` PERSISTS across the entire workflow.**\nUse \\`set_var: true\\` on a step to auto-store its output in \\`ctx.vars[stepName]\\`. Downstream steps access it via \\`ctx.vars['step-name']\\`.\n\n### Data Flow Example\n\n```\nStep 1: \"fetch-user\" (set_var: true)\n \u2192 ctx.response.data = { id: \"123\", name: \"Alice\" }\n \u2192 ctx.vars[\"fetch-user\"] = { id: \"123\", name: \"Alice\" }\n\nStep 2: \"transform\"\n \u2192 ctx.response.data = { result: \"done\" } \u2190 Step 1 output GONE from response\n \u2192 ctx.vars[\"fetch-user\"] still available\n\nStep 3: \"output\"\n \u2192 Can read ctx.vars[\"fetch-user\"].name \u2190 still \"Alice\"\n```\n\n### Blueprint Mapper \u2014 Expression Resolution\n\nNode inputs support dynamic expressions resolved BEFORE node execution:\n\n```json\n{\n \"inputs\": {\n \"userId\": \"js/ctx.request.body.userId\",\n \"chain\": \"js/ctx.vars['previous-step'].chain\",\n \"previous\": \"js/ctx.response.data.result\"\n }\n}\n```\n\nAvailable in js/ expressions: \\`ctx\\`, \\`data\\` (ctx.response.data), \\`func\\` (ctx.func), \\`vars\\` (ctx.vars)\n\n---\n\n## Creating Nodes with defineNode\n\nUse \\`defineNode()\\` for all new nodes. Never use the legacy class-based pattern.\n\n```typescript\nimport { defineNode } from \"@blokjs/runner\";\nimport { z } from \"zod\";\n\nexport default defineNode({\n name: \"fetch-user\",\n description: \"Fetches user by ID\",\n\n input: z.object({\n userId: z.string().uuid(),\n }),\n\n output: z.object({\n user: z.object({\n id: z.string(),\n name: z.string(),\n email: z.string().email(),\n }),\n }),\n\n async execute(ctx, input) {\n const user = await fetchUser(input.userId);\n return { user };\n },\n});\n```\n\n### Key Behaviors\n\n- Zod input/output validation runs automatically\n- ZodError is mapped to GlobalError with HTTP 400\n- \\`flow: true\\` nodes return NodeBase[] for conditional execution\n- \\`contentType\\` sets response Content-Type (e.g., \"text/html\")\n- Always \\`export default defineNode(...)\\`\n\n---\n\n## Workflow Structure (JSON)\n\n```json\n{\n \"name\": \"My Workflow\",\n \"version\": \"1.0.0\",\n \"trigger\": {\n \"http\": { \"method\": \"POST\", \"path\": \"/api/process\", \"accept\": \"application/json\" }\n },\n \"steps\": [\n { \"name\": \"fetch\", \"node\": \"@blokjs/api-call\", \"type\": \"module\" },\n { \"name\": \"process\", \"node\": \"my-node\", \"type\": \"module\", \"set_var\": true },\n { \"name\": \"go-step\", \"node\": \"chain-test\", \"type\": \"runtime.go\" }\n ],\n \"nodes\": {\n \"fetch\": { \"inputs\": { \"url\": \"https://api.example.com\", \"method\": \"GET\" } },\n \"process\": { \"inputs\": { \"data\": \"js/ctx.response.data\" } },\n \"go-step\": { \"inputs\": { \"processed\": \"js/ctx.vars['process']\" } }\n }\n}\n```\n\n### Step Types\n\n| Type | Description |\n|------|-------------|\n| \\`module\\` | TypeScript node from registered modules |\n| \\`local\\` | TypeScript node from filesystem (NODES_PATH) |\n| \\`runtime.python3\\` | Python3 SDK container (port 9007) |\n| \\`runtime.go\\` | Go SDK container (port 9001) |\n| \\`runtime.rust\\` | Rust SDK container (port 9002) |\n| \\`runtime.java\\` | Java SDK container (port 9003) |\n| \\`runtime.csharp\\` | C# SDK container (port 9004) |\n| \\`runtime.php\\` | PHP SDK container (port 9005) |\n| \\`runtime.ruby\\` | Ruby SDK container (port 9006) |\n\n### Conditional Workflow (if-else)\n\n```json\n{\n \"nodes\": {\n \"filter\": {\n \"conditions\": [\n {\n \"type\": \"if\",\n \"condition\": \"ctx.request.query.active === \\\\\"true\\\\\"\",\n \"steps\": [{ \"name\": \"active-path\", \"node\": \"handle-active\", \"type\": \"module\" }]\n },\n {\n \"type\": \"else\",\n \"steps\": [{ \"name\": \"default-path\", \"node\": \"handle-default\", \"type\": \"module\" }]\n }\n ]\n }\n }\n}\n```\n\n---\n\n## Trigger Types\n\n| Trigger | Example Config |\n|---------|---------------|\n| \\`http\\` | \\`{ \"method\": \"GET\", \"path\": \"/\", \"accept\": \"application/json\" }\\` |\n| \\`grpc\\` | \\`{ \"service\": \"UserService\", \"method\": \"GetUser\" }\\` |\n| \\`cron\\` | \\`{ \"schedule\": \"0 * * * *\", \"timezone\": \"UTC\" }\\` |\n| \\`queue\\` | \\`{ \"provider\": \"kafka\", \"topic\": \"events\" }\\` |\n| \\`pubsub\\` | \\`{ \"provider\": \"gcp\", \"topic\": \"updates\" }\\` |\n| \\`webhook\\` | \\`{ \"source\": \"github\", \"events\": [\"push\"] }\\` |\n| \\`websocket\\` | \\`{ \"events\": [\"message\"], \"path\": \"/ws\" }\\` |\n| \\`sse\\` | \\`{ \"events\": [\"update\"], \"path\": \"/stream\" }\\` |\n| \\`worker\\` | \\`{ \"queue\": \"jobs\", \"concurrency\": 5, \"retries\": 3 }\\` |\n\n### Worker Trigger\n\nThe worker trigger processes background jobs from a queue with retry logic and concurrency control.\n\n\\`\\`\\`typescript\nWorkflow({ name: \"Process Job\", version: \"1.0.0\" })\n .addTrigger(\"worker\", { queue: \"background-jobs\" })\n .addStep({\n name: \"process\",\n node: \"my-processor\",\n type: \"module\",\n inputs: { payload: \"js/ctx.request.body\", jobId: \"js/ctx.request.params.jobId\" },\n });\n\\`\\`\\`\n\nJob context: \\`ctx.request.body\\` = payload, \\`ctx.request.params.queue\\` = queue name, \\`ctx.request.params.jobId\\` = job ID, \\`ctx.request.params.attempt\\` = attempt count, \\`ctx.vars._worker_job\\` = full metadata.\n\nAdapters: NATS JetStream (recommended), BullMQ (Redis), InMemory (dev only).\n\n### NATS JetStream\n\nRecommended queue/worker backend. Environment variables:\n\\`\\`\\`\nNATS_SERVERS=localhost:4222\nNATS_STREAM_NAME=blok-queue # or blok-worker for worker trigger\nNATS_TOKEN= # optional auth\n\\`\\`\\`\n\nQueue providers: \\`kafka\\`, \\`rabbitmq\\`, \\`sqs\\`, \\`redis\\`, \\`beanstalk\\`, \\`nats\\`\n\n### Standalone Workers (Go, Rust, Python)\n\nGo, Rust, and Python SDKs include standalone NATS workers that connect directly to NATS without the TypeScript runner:\n\n\\`\\`\\`\nWORKER_CONCURRENCY=1 # Max concurrent jobs\nWORKER_MAX_RETRIES=3 # Max delivery attempts\nWORKER_QUEUES=queue1,queue2 # Queues to consume\n\\`\\`\\`\n\n---\n\n## Testing Utilities\n\n\\`@blokjs/runner\\` provides testing utilities for nodes and workflows.\n\n### NodeTestHarness \u2014 Unit test a single node:\n\\`\\`\\`typescript\nimport { NodeTestHarness } from \"@blokjs/runner\";\nconst harness = new NodeTestHarness(myNode);\nconst result = await harness.execute({ input: \"data\" });\nharness.assertSuccess(result);\nharness.assertOutput(result, { expected: \"output\" });\n\\`\\`\\`\n\n### WorkflowTestRunner \u2014 Integration test a workflow:\n\\`\\`\\`typescript\nimport { WorkflowTestRunner } from \"@blokjs/runner\";\nconst runner = new WorkflowTestRunner({ verbose: true });\nrunner.registerNode(\"validate\", ValidateNode);\nrunner.mockNode(\"external-api\", async (input) => ({ result: \"mocked\" }));\nrunner.loadWorkflow(workflowDefinition);\nconst result = await runner.execute({ input: \"data\" });\n// result.success, result.output, result.trace, result.nodeResults\n\\`\\`\\`\n\n---\n\n## Runtime Adapter System\n\nAll non-NodeJS SDKs communicate via HTTP:\n- **POST /execute** \u2014 Execute node with context\n- **GET /health** \u2014 Health check\n\nEnvironment variables: \\`RUNTIME_{LANG}_HOST\\` / \\`RUNTIME_{LANG}_PORT\\`\n\nRuntime nodes auto-save \\`result.data\\` to \\`ctx.vars[stepName]\\`.\n\n---\n\n## Blok Studio\n\nReal-time workflow trace visualization UI.\n\n- Launch: \\`blokctl trace\\` or \\`blokctl studio\\`\n- API: \\`/__blok/runs\\`, \\`/__blok/runs/:id\\`, \\`/__blok/runs/:id/stream\\` (SSE)\n- Disable: \\`BLOK_TRACE_ENABLED=false\\`\n\n---\n\n## Do NOT\n\n- Do NOT rely on \\`ctx.response.data\\` for data from non-previous steps \u2014 it gets overwritten\n- Do NOT create class-based nodes \u2014 use \\`defineNode()\\` instead\n- Do NOT use \\`any\\` type \u2014 use \\`unknown\\` and narrow with Zod\n- Do NOT hardcode runtime ports \u2014 use environment variables\n- Do NOT skip Zod input/output schemas\n- Do NOT edit files in \\`.blok/runtimes/\\` \u2014 they are auto-generated\n\n## Do\n\n- Use \\`ctx.vars\\` with \\`set_var: true\\` to pass data between non-adjacent steps\n- Use \\`js/ctx.vars['step-name'].field\\` in workflow inputs for data flow\n- Use Zod schemas for all input/output validation\n- Use \\`defineNode()\\` for all new nodes\n- Handle errors via GlobalError with appropriate HTTP status codes\n- Keep nodes focused \u2014 one responsibility per node\n";
|
|
37
|
-
declare const claude_md = "# Blok Project \u2014 Claude Code Guide\n\nRead \\`AGENTS.md\\` for full architecture and API details. This file contains Claude-specific guidance.\n\n## Quick Commands\n\n\\`\\`\\`bash\nnpm run dev # Start dev server\nblokctl dev # Multi-runtime dev server\nblokctl create node <name> # Scaffold new node\nblokctl create workflow <name> # Scaffold new workflow\nblokctl trace # Open Blok Studio\nnpm test # Run tests\n\\`\\`\\`\n\n## Context Rules (Memorize These)\n\n1. **\\`ctx.
|
|
36
|
+
declare const agents_md = "# Blok Project\n\nBlok is a TypeScript-first workflow orchestration framework. It executes declarative workflows (JSON or TypeScript DSL) composed of steps (nodes) that run across 8 language runtimes: NodeJS, Python3, Go, Rust, Java, C#, PHP, and Ruby.\n\n## Project Structure\n\n```\n\u251C\u2500\u2500 src/\n\u2502 \u2514\u2500\u2500 nodes/ # TypeScript node implementations\n\u251C\u2500\u2500 runtimes/ # Non-NodeJS runtime nodes (Go, Python3, etc.)\n\u2502 \u2514\u2500\u2500 {lang}/nodes/ # Language-specific node implementations\n\u251C\u2500\u2500 workflows/\n\u2502 \u251C\u2500\u2500 json/ # Workflow definitions (JSON)\n\u2502 \u251C\u2500\u2500 yaml/ # Workflow definitions (YAML)\n\u2502 \u2514\u2500\u2500 toml/ # Workflow definitions (TOML)\n\u251C\u2500\u2500 .blok/\n\u2502 \u251C\u2500\u2500 config.json # Runtime configuration (ports, start commands)\n\u2502 \u2514\u2500\u2500 runtimes/ # Auto-generated runtime scaffolds\n\u251C\u2500\u2500 .env.local # Environment variables (ports, paths)\n\u2514\u2500\u2500 supervisord.conf # Process management config\n```\n\n## Commands\n\n```bash\nnpm run dev # Start dev server (or blokctl dev for multi-runtime)\nnpm run build # Build project\nnpm test # Run tests\nblokctl create node <name> # Scaffold a new node\nblokctl create workflow <n># Scaffold a new workflow\nblokctl trace # Open Blok Studio (trace visualization)\nblokctl studio # Alias for blokctl trace\n```\n\n## Context \u2014 Critical Data Flow\n\nThe Context type is the central execution state passed through every step.\n\n```typescript\ntype Context = {\n id: string; // Unique request ID\n request: RequestContext; // Incoming request (body, headers, params, query)\n response: ResponseContext; // Current step output \u2014 OVERWRITTEN every step\n vars?: VarsContext; // Persistent variables \u2014 PERSISTS across workflow\n config: ConfigContext; // Node config (inputs resolved by Mapper)\n env?: EnvContext; // process.env access\n logger: LoggerContext;\n error: ErrorContext;\n};\n```\n\n### The Two Critical Rules\n\n**Rule 1: \\`ctx.prev\\` carries the immediately previous step's output.**\nEach step's output replaces \\`ctx.prev\\`. Use it for adjacent-step access only.\n\n**Rule 2: \\`ctx.state[id]\\` PERSISTS across the entire workflow.**\nEvery step's output is auto-stored at \\`ctx.state[<step-id>]\\` (the v2 default-store rule). Downstream steps reference it via \\`$.state.<id>\\` (TS DSL) or \\`\"$.state.<id>\"\\` / \\`\"js/ctx.state.<id>\"\\` (JSON). Opt out per step with \\`ephemeral: true\\`.\n\n### Data Flow Example\n\n```\nStep 1: id \"fetch-user\"\n \u2192 ctx.state[\"fetch-user\"] = { id: \"123\", name: \"Alice\" }\n \u2192 ctx.prev = { id: \"123\", name: \"Alice\" }\n\nStep 2: id \"transform\"\n \u2192 ctx.state[\"transform\"] = { result: \"done\" }\n \u2192 ctx.prev = { result: \"done\" } \u2190 Step 1 output GONE from prev\n \u2192 ctx.state[\"fetch-user\"] still available\n\nStep 3: id \"output\"\n \u2192 Can read ctx.state[\"fetch-user\"].name \u2190 still \"Alice\"\n```\n\n### Blueprint Mapper \u2014 Expression Resolution\n\nNode inputs support dynamic expressions resolved BEFORE node execution:\n\n```json\n{\n \"inputs\": {\n \"userId\": \"js/ctx.request.body.userId\",\n \"chain\": \"js/ctx.vars['previous-step'].chain\",\n \"previous\": \"js/ctx.response.data.result\"\n }\n}\n```\n\nAvailable in js/ expressions: \\`ctx\\` (full context), \\`data\\` (ctx.prev.data), \\`func\\` (ctx.func), \\`vars\\` (alias for ctx.state).\n\n---\n\n## Creating Nodes with defineNode\n\nUse \\`defineNode()\\` for all new nodes. Never use the legacy class-based pattern.\n\n```typescript\nimport { defineNode } from \"@blokjs/runner\";\nimport { z } from \"zod\";\n\nexport default defineNode({\n name: \"fetch-user\",\n description: \"Fetches user by ID\",\n\n input: z.object({\n userId: z.string().uuid(),\n }),\n\n output: z.object({\n user: z.object({\n id: z.string(),\n name: z.string(),\n email: z.string().email(),\n }),\n }),\n\n async execute(ctx, input) {\n const user = await fetchUser(input.userId);\n return { user };\n },\n});\n```\n\n### Key Behaviors\n\n- Zod input/output validation runs automatically\n- ZodError is mapped to GlobalError with HTTP 400\n- \\`flow: true\\` nodes return NodeBase[] for conditional execution\n- \\`contentType\\` sets response Content-Type (e.g., \"text/html\")\n- Always \\`export default defineNode(...)\\`\n\n---\n\n## Workflow Structure (JSON)\n\n```json\n{\n \"name\": \"My Workflow\",\n \"version\": \"1.0.0\",\n \"trigger\": {\n \"http\": { \"method\": \"POST\", \"path\": \"/api/process\", \"accept\": \"application/json\" }\n },\n \"steps\": [\n { \"id\": \"fetch\", \"use\": \"@blokjs/api-call\", \"inputs\": { \"url\": \"https://api.example.com\", \"method\": \"GET\" } },\n { \"id\": \"process\", \"use\": \"my-node\", \"inputs\": { \"data\": \"$.state.fetch\" } },\n { \"id\": \"go-step\", \"use\": \"chain-test\", \"type\": \"runtime.go\", \"inputs\": { \"processed\": \"$.state.process\" } }\n ]\n}\n```\n\n### Step Types\n\n| Type | Description |\n|------|-------------|\n| \\`module\\` | TypeScript node from registered modules |\n| \\`local\\` | TypeScript node from filesystem (NODES_PATH) |\n| \\`runtime.python3\\` | Python3 SDK container (port 9007) |\n| \\`runtime.go\\` | Go SDK container (port 9001) |\n| \\`runtime.rust\\` | Rust SDK container (port 9002) |\n| \\`runtime.java\\` | Java SDK container (port 9003) |\n| \\`runtime.csharp\\` | C# SDK container (port 9004) |\n| \\`runtime.php\\` | PHP SDK container (port 9005) |\n| \\`runtime.ruby\\` | Ruby SDK container (port 9006) |\n\n### Conditional Workflow (if-else)\n\n```json\n{\n \"nodes\": {\n \"filter\": {\n \"conditions\": [\n {\n \"type\": \"if\",\n \"condition\": \"ctx.request.query.active === \\\\\"true\\\\\"\",\n \"steps\": [{ \"name\": \"active-path\", \"node\": \"handle-active\", \"type\": \"module\" }]\n },\n {\n \"type\": \"else\",\n \"steps\": [{ \"name\": \"default-path\", \"node\": \"handle-default\", \"type\": \"module\" }]\n }\n ]\n }\n }\n}\n```\n\n---\n\n## Trigger Types\n\n| Trigger | Example Config |\n|---------|---------------|\n| \\`http\\` | \\`{ \"method\": \"GET\", \"path\": \"/\", \"accept\": \"application/json\" }\\` |\n| \\`grpc\\` | \\`{ \"service\": \"UserService\", \"method\": \"GetUser\" }\\` |\n| \\`cron\\` | \\`{ \"schedule\": \"0 * * * *\", \"timezone\": \"UTC\" }\\` |\n| \\`queue\\` | \\`{ \"provider\": \"kafka\", \"topic\": \"events\" }\\` |\n| \\`pubsub\\` | \\`{ \"provider\": \"gcp\", \"topic\": \"updates\" }\\` |\n| \\`webhook\\` | \\`{ \"source\": \"github\", \"events\": [\"push\"] }\\` |\n| \\`websocket\\` | \\`{ \"events\": [\"message\"], \"path\": \"/ws\" }\\` |\n| \\`sse\\` | \\`{ \"events\": [\"update\"], \"path\": \"/stream\" }\\` |\n| \\`worker\\` | \\`{ \"queue\": \"jobs\", \"concurrency\": 5, \"retries\": 3 }\\` |\n\n### Worker Trigger\n\nThe worker trigger processes background jobs from a queue with retry logic and concurrency control.\n\n\\`\\`\\`typescript\nWorkflow({ name: \"Process Job\", version: \"1.0.0\" })\n .addTrigger(\"worker\", { queue: \"background-jobs\" })\n .addStep({\n name: \"process\",\n node: \"my-processor\",\n type: \"module\",\n inputs: { payload: \"js/ctx.request.body\", jobId: \"js/ctx.request.params.jobId\" },\n });\n\\`\\`\\`\n\nJob context: \\`ctx.request.body\\` = payload, \\`ctx.request.params.queue\\` = queue name, \\`ctx.request.params.jobId\\` = job ID, \\`ctx.request.params.attempt\\` = attempt count, \\`ctx.vars._worker_job\\` = full metadata.\n\nAdapters: NATS JetStream (recommended), BullMQ (Redis), InMemory (dev only).\n\n### NATS JetStream\n\nRecommended queue/worker backend. Environment variables:\n\\`\\`\\`\nNATS_SERVERS=localhost:4222\nNATS_STREAM_NAME=blok-queue # or blok-worker for worker trigger\nNATS_TOKEN= # optional auth\n\\`\\`\\`\n\nQueue providers: \\`kafka\\`, \\`rabbitmq\\`, \\`sqs\\`, \\`redis\\`, \\`beanstalk\\`, \\`nats\\`\n\n### Standalone Workers (Go, Rust, Python)\n\nGo, Rust, and Python SDKs include standalone NATS workers that connect directly to NATS without the TypeScript runner:\n\n\\`\\`\\`\nWORKER_CONCURRENCY=1 # Max concurrent jobs\nWORKER_MAX_RETRIES=3 # Max delivery attempts\nWORKER_QUEUES=queue1,queue2 # Queues to consume\n\\`\\`\\`\n\n---\n\n## Testing Utilities\n\n\\`@blokjs/runner\\` provides testing utilities for nodes and workflows.\n\n### NodeTestHarness \u2014 Unit test a single node:\n\\`\\`\\`typescript\nimport { NodeTestHarness } from \"@blokjs/runner\";\nconst harness = new NodeTestHarness(myNode);\nconst result = await harness.execute({ input: \"data\" });\nharness.assertSuccess(result);\nharness.assertOutput(result, { expected: \"output\" });\n\\`\\`\\`\n\n### WorkflowTestRunner \u2014 Integration test a workflow:\n\\`\\`\\`typescript\nimport { WorkflowTestRunner } from \"@blokjs/runner\";\nconst runner = new WorkflowTestRunner({ verbose: true });\nrunner.registerNode(\"validate\", ValidateNode);\nrunner.mockNode(\"external-api\", async (input) => ({ result: \"mocked\" }));\nrunner.loadWorkflow(workflowDefinition);\nconst result = await runner.execute({ input: \"data\" });\n// result.success, result.output, result.trace, result.nodeResults\n\\`\\`\\`\n\n---\n\n## Runtime Adapter System\n\nAll non-NodeJS SDKs communicate via HTTP:\n- **POST /execute** \u2014 Execute node with context\n- **GET /health** \u2014 Health check\n\nEnvironment variables: \\`RUNTIME_{LANG}_HOST\\` / \\`RUNTIME_{LANG}_PORT\\`\n\nRuntime nodes auto-save \\`result.data\\` to \\`ctx.vars[stepName]\\`.\n\n---\n\n## Blok Studio\n\nReal-time workflow trace visualization UI.\n\n- Launch: \\`blokctl trace\\` or \\`blokctl studio\\`\n- API: \\`/__blok/runs\\`, \\`/__blok/runs/:id\\`, \\`/__blok/runs/:id/stream\\` (SSE)\n- Disable: \\`BLOK_TRACE_ENABLED=false\\`\n\n---\n\n## Do NOT\n\n- Do NOT rely on \\`ctx.response.data\\` for data from non-previous steps \u2014 it gets overwritten\n- Do NOT create class-based nodes \u2014 use \\`defineNode()\\` instead\n- Do NOT use \\`any\\` type \u2014 use \\`unknown\\` and narrow with Zod\n- Do NOT hardcode runtime ports \u2014 use environment variables\n- Do NOT skip Zod input/output schemas\n- Do NOT edit files in \\`.blok/runtimes/\\` \u2014 they are auto-generated\n\n## Do\n\n- Use \\`$.state.<id>\\` (or \\`js/ctx.state.<id>\\`) to pass data between non-adjacent steps \u2014 every step default-stores its output there\n- Opt out per step with \\`ephemeral: true\\` when the step is a side effect only\n- Use Zod schemas for all input/output validation\n- Use \\`defineNode()\\` for all new nodes\n- Handle errors via GlobalError with appropriate HTTP status codes\n- Keep nodes focused \u2014 one responsibility per node\n";
|
|
37
|
+
declare const claude_md = "# Blok Project \u2014 Claude Code Guide\n\nRead \\`AGENTS.md\\` for full architecture and API details. This file contains Claude-specific guidance.\n\n## Quick Commands\n\n\\`\\`\\`bash\nnpm run dev # Start dev server\nblokctl dev # Multi-runtime dev server\nblokctl create node <name> # Scaffold new node\nblokctl create workflow <name> # Scaffold new workflow\nblokctl trace # Open Blok Studio\nnpm test # Run tests\n\\`\\`\\`\n\n## Context Rules (Memorize These)\n\n1. **\\`ctx.prev\\` is the immediately previous step's output.** Overwritten every step.\n2. **\\`ctx.state[<id>]\\` PERSISTS across the workflow.** Every step default-stores its output there; reference via \\`$.state.<id>\\` or \\`js/ctx.state.<id>\\`. Opt out with \\`ephemeral: true\\`.\n3. **Blueprint Mapper resolves \\`$.<path>\\` and \\`js/\\` expressions BEFORE node execution.**\n\nWhen users have data flow issues, check these three things first.\n\n## Debugging Workflows\n\n1. **Verify structure**: Every step has an \\`id\\` and a \\`use\\` (v2). v1's \\`name\\` + \\`nodes{}\\` still works but is normalized at load time.\n2. **Trace data flow**: Does the target step reference the correct source id (\\`$.state.<id>\\`)? Did the source step have \\`ephemeral: true\\` accidentally?\n3. **Check runtimes**: SDK containers running? \\`GET http://localhost:{port}/health\\`\n4. **Check Studio traces**: \\`/__blok/runs/:id\\` shows step-by-step inputs/outputs/errors\n\n### Common Errors\n\n| Error | Fix |\n|-------|-----|\n| \\`Node type X not found\\` | Wrong \\`type\\` in step \u2014 use module, local, or runtime.* |\n| \\`Validation failed\\` | Zod schema mismatch \u2014 check input schema vs actual data |\n| \\`Runtime execution error\\` | SDK container not running \u2014 check health endpoint |\n| \\`ctx.state['X'] undefined\\` | Source step has \\`ephemeral: true\\`, or the id doesn't match what's referenced in \\`$.state.<id>\\` |\n| \\`set_var, which was removed in v0.5\\` | Drop \\`set_var: true\\` (it's the default) or replace \\`set_var: false\\` with \\`ephemeral: true\\`. Run \\`blokctl migrate workflows\\`. |\n\n## Generating Code\n\nAlways use \\`defineNode()\\`. Never class-based BlokService.\n\n\\`\\`\\`typescript\nimport { defineNode } from \"@blokjs/runner\";\nimport { z } from \"zod\";\n\nexport default defineNode({\n name: \"node-name\",\n description: \"What this node does\",\n input: z.object({ /* Zod schema */ }),\n output: z.object({ /* Zod schema */ }),\n async execute(ctx, input) {\n return { /* must match output schema */ };\n },\n});\n\\`\\`\\`\n\n### Checklist:\n- Zod input schema covers all inputs\n- Zod output schema matches execute() return\n- Node name matches workflow references\n- No \\`any\\` types \u2014 use \\`z.unknown()\\` if dynamic\n- \\`export default defineNode(...)\\`\n\n## Worker Workflows\n\nWorker trigger processes background jobs from a queue:\n\n\\`\\`\\`typescript\nWorkflow({ name: \"Process Job\", version: \"1.0.0\" })\n .addTrigger(\"worker\", { queue: \"background-jobs\" })\n .addStep({ name: \"process\", node: \"my-processor\", type: \"module\",\n inputs: { payload: \"js/ctx.request.body\", jobId: \"js/ctx.request.params.jobId\" } });\n\\`\\`\\`\n\nJob data: \\`ctx.request.body\\` = payload, \\`ctx.request.params.queue/jobId/attempt\\` = metadata.\nAdapters: NATS JetStream (recommended), BullMQ (Redis), InMemory (dev).\n\n## Testing\n\n\\`\\`\\`typescript\nimport { NodeTestHarness, WorkflowTestRunner } from \"@blokjs/runner\";\n\n// Unit test a node\nconst harness = new NodeTestHarness(myNode);\nconst result = await harness.execute({ input: \"data\" });\nharness.assertSuccess(result);\n\n// Integration test a workflow\nconst runner = new WorkflowTestRunner({ mockAllNodes: true });\nrunner.loadWorkflow(definition);\nconst wfResult = await runner.execute({ input: \"data\" });\n\\`\\`\\`\n\n## Blok Studio Help\n\n- Launch: \\`blokctl trace\\` or navigate to \\`/__blok\\`\n- \"No output\" \u2192 Node not returning data or Zod output validation failed\n- \"Step error\" \u2192 Expand error \u2014 check if 400 (validation) or 500 (runtime)\n- \"State not passing\" \u2192 Source step has \\`ephemeral: true\\`, OR target's \\`$.state.<id>\\` references a non-existent step id\n\n## Debugging Workers\n\n- NATS not reachable \u2192 Check \\`NATS_SERVERS\\` env var, ensure NATS is running\n- Job timeout \u2192 Increase \\`timeout\\` in trigger config or optimize node\n- Max retries exceeded \u2192 Check node errors, job moves to DLQ\n\n## Do NOT\n\n- Do NOT suggest class-based BlokService for new nodes\n- Do NOT generate code with \\`any\\` types\n- Do NOT assume \\`ctx.response.data\\` persists across steps\n- Do NOT skip Zod schemas when creating nodes\n- Do NOT edit files in \\`.blok/runtimes/\\`\n";
|
|
38
38
|
declare const function_first_node_file = "import { defineNode } from \"@blokjs/runner\";\nimport { z } from \"zod\";\n\n/**\n * A function-first node that demonstrates the modern defineNode pattern.\n * This node is type-safe, validated, and requires 60% less boilerplate.\n */\nexport default defineNode({\n\tname: \"{{NODE_NAME}}\",\n\tdescription: \"A function-first node with Zod validation\",\n\n\t// Input schema using Zod - automatically validated\n\tinput: z.object({\n\t\tmessage: z.string().optional().default(\"Hello World\"),\n\t}),\n\n\t// Output schema using Zod - automatically validated\n\toutput: z.object({\n\t\tmessage: z.string(),\n\t\ttimestamp: z.string(),\n\t}),\n\n\t// Execute function - type-safe with inferred types from Zod schemas\n\tasync execute(ctx, input) {\n\t\t// Your business logic here\n\t\t// - ctx.vars: Access workflow variables\n\t\t// - ctx.request: Access HTTP request data\n\t\t// - ctx.logger: Log messages\n\t\t// - ctx.env: Access environment variables\n\n\t\t// Example: Store data for downstream nodes\n\t\tctx.vars[\"processed-message\"] = input.message;\n\n\t\t// Return type-safe output (validated automatically)\n\t\treturn {\n\t\t\tmessage: `Processed: ${input.message}`,\n\t\t\ttimestamp: new Date().toISOString(),\n\t\t};\n\t},\n});\n";
|
|
39
39
|
export { node_file, package_dependencies, package_dev_dependencies, python3_file, examples_url, workflow_template, supervisord_nodejs, supervisord_python, go_node_file, go_mod_file, go_dockerfile, java_node_file, java_pom_file, java_dockerfile, rust_node_file, rust_cargo_file, rust_dockerfile, csharp_node_file, csharp_csproj_file, csharp_dockerfile, php_node_file, php_composer_file, php_dockerfile, ruby_node_file, ruby_gemfile, ruby_dockerfile, function_first_node_file, agents_md, claude_md, };
|
|
@@ -657,25 +657,26 @@ type Context = {
|
|
|
657
657
|
|
|
658
658
|
### The Two Critical Rules
|
|
659
659
|
|
|
660
|
-
**Rule 1: \\\`ctx.
|
|
661
|
-
Each step's output replaces
|
|
660
|
+
**Rule 1: \\\`ctx.prev\\\` carries the immediately previous step's output.**
|
|
661
|
+
Each step's output replaces \\\`ctx.prev\\\`. Use it for adjacent-step access only.
|
|
662
662
|
|
|
663
|
-
**Rule 2: \\\`ctx.
|
|
664
|
-
|
|
663
|
+
**Rule 2: \\\`ctx.state[id]\\\` PERSISTS across the entire workflow.**
|
|
664
|
+
Every step's output is auto-stored at \\\`ctx.state[<step-id>]\\\` (the v2 default-store rule). Downstream steps reference it via \\\`$.state.<id>\\\` (TS DSL) or \\\`"$.state.<id>"\\\` / \\\`"js/ctx.state.<id>"\\\` (JSON). Opt out per step with \\\`ephemeral: true\\\`.
|
|
665
665
|
|
|
666
666
|
### Data Flow Example
|
|
667
667
|
|
|
668
668
|
\`\`\`
|
|
669
|
-
Step 1: "fetch-user"
|
|
670
|
-
→ ctx.
|
|
671
|
-
→ ctx.
|
|
669
|
+
Step 1: id "fetch-user"
|
|
670
|
+
→ ctx.state["fetch-user"] = { id: "123", name: "Alice" }
|
|
671
|
+
→ ctx.prev = { id: "123", name: "Alice" }
|
|
672
672
|
|
|
673
|
-
Step 2: "transform"
|
|
674
|
-
→ ctx.
|
|
675
|
-
→ ctx.
|
|
673
|
+
Step 2: id "transform"
|
|
674
|
+
→ ctx.state["transform"] = { result: "done" }
|
|
675
|
+
→ ctx.prev = { result: "done" } ← Step 1 output GONE from prev
|
|
676
|
+
→ ctx.state["fetch-user"] still available
|
|
676
677
|
|
|
677
|
-
Step 3: "output"
|
|
678
|
-
→ Can read ctx.
|
|
678
|
+
Step 3: id "output"
|
|
679
|
+
→ Can read ctx.state["fetch-user"].name ← still "Alice"
|
|
679
680
|
\`\`\`
|
|
680
681
|
|
|
681
682
|
### Blueprint Mapper — Expression Resolution
|
|
@@ -692,7 +693,7 @@ Node inputs support dynamic expressions resolved BEFORE node execution:
|
|
|
692
693
|
}
|
|
693
694
|
\`\`\`
|
|
694
695
|
|
|
695
|
-
Available in js/ expressions: \\\`ctx
|
|
696
|
+
Available in js/ expressions: \\\`ctx\\\` (full context), \\\`data\\\` (ctx.prev.data), \\\`func\\\` (ctx.func), \\\`vars\\\` (alias for ctx.state).
|
|
696
697
|
|
|
697
698
|
---
|
|
698
699
|
|
|
@@ -747,15 +748,10 @@ export default defineNode({
|
|
|
747
748
|
"http": { "method": "POST", "path": "/api/process", "accept": "application/json" }
|
|
748
749
|
},
|
|
749
750
|
"steps": [
|
|
750
|
-
{ "
|
|
751
|
-
{ "
|
|
752
|
-
{ "
|
|
753
|
-
]
|
|
754
|
-
"nodes": {
|
|
755
|
-
"fetch": { "inputs": { "url": "https://api.example.com", "method": "GET" } },
|
|
756
|
-
"process": { "inputs": { "data": "js/ctx.response.data" } },
|
|
757
|
-
"go-step": { "inputs": { "processed": "js/ctx.vars['process']" } }
|
|
758
|
-
}
|
|
751
|
+
{ "id": "fetch", "use": "@blokjs/api-call", "inputs": { "url": "https://api.example.com", "method": "GET" } },
|
|
752
|
+
{ "id": "process", "use": "my-node", "inputs": { "data": "$.state.fetch" } },
|
|
753
|
+
{ "id": "go-step", "use": "chain-test", "type": "runtime.go", "inputs": { "processed": "$.state.process" } }
|
|
754
|
+
]
|
|
759
755
|
}
|
|
760
756
|
\`\`\`
|
|
761
757
|
|
|
@@ -912,8 +908,8 @@ Real-time workflow trace visualization UI.
|
|
|
912
908
|
|
|
913
909
|
## Do
|
|
914
910
|
|
|
915
|
-
- Use \\\`ctx.
|
|
916
|
-
-
|
|
911
|
+
- Use \\\`$.state.<id>\\\` (or \\\`js/ctx.state.<id>\\\`) to pass data between non-adjacent steps — every step default-stores its output there
|
|
912
|
+
- Opt out per step with \\\`ephemeral: true\\\` when the step is a side effect only
|
|
917
913
|
- Use Zod schemas for all input/output validation
|
|
918
914
|
- Use \\\`defineNode()\\\` for all new nodes
|
|
919
915
|
- Handle errors via GlobalError with appropriate HTTP status codes
|
|
@@ -936,16 +932,16 @@ npm test # Run tests
|
|
|
936
932
|
|
|
937
933
|
## Context Rules (Memorize These)
|
|
938
934
|
|
|
939
|
-
1. **\\\`ctx.
|
|
940
|
-
2. **\\\`ctx.
|
|
941
|
-
3. **Blueprint Mapper resolves \\\`js/\\\` expressions BEFORE node execution.**
|
|
935
|
+
1. **\\\`ctx.prev\\\` is the immediately previous step's output.** Overwritten every step.
|
|
936
|
+
2. **\\\`ctx.state[<id>]\\\` PERSISTS across the workflow.** Every step default-stores its output there; reference via \\\`$.state.<id>\\\` or \\\`js/ctx.state.<id>\\\`. Opt out with \\\`ephemeral: true\\\`.
|
|
937
|
+
3. **Blueprint Mapper resolves \\\`$.<path>\\\` and \\\`js/\\\` expressions BEFORE node execution.**
|
|
942
938
|
|
|
943
939
|
When users have data flow issues, check these three things first.
|
|
944
940
|
|
|
945
941
|
## Debugging Workflows
|
|
946
942
|
|
|
947
|
-
1. **Verify structure**: Every \\\`
|
|
948
|
-
2. **Trace data flow**:
|
|
943
|
+
1. **Verify structure**: Every step has an \\\`id\\\` and a \\\`use\\\` (v2). v1's \\\`name\\\` + \\\`nodes{}\\\` still works but is normalized at load time.
|
|
944
|
+
2. **Trace data flow**: Does the target step reference the correct source id (\\\`$.state.<id>\\\`)? Did the source step have \\\`ephemeral: true\\\` accidentally?
|
|
949
945
|
3. **Check runtimes**: SDK containers running? \\\`GET http://localhost:{port}/health\\\`
|
|
950
946
|
4. **Check Studio traces**: \\\`/__blok/runs/:id\\\` shows step-by-step inputs/outputs/errors
|
|
951
947
|
|
|
@@ -956,7 +952,8 @@ When users have data flow issues, check these three things first.
|
|
|
956
952
|
| \\\`Node type X not found\\\` | Wrong \\\`type\\\` in step — use module, local, or runtime.* |
|
|
957
953
|
| \\\`Validation failed\\\` | Zod schema mismatch — check input schema vs actual data |
|
|
958
954
|
| \\\`Runtime execution error\\\` | SDK container not running — check health endpoint |
|
|
959
|
-
| \\\`ctx.
|
|
955
|
+
| \\\`ctx.state['X'] undefined\\\` | Source step has \\\`ephemeral: true\\\`, or the id doesn't match what's referenced in \\\`$.state.<id>\\\` |
|
|
956
|
+
| \\\`set_var, which was removed in v0.5\\\` | Drop \\\`set_var: true\\\` (it's the default) or replace \\\`set_var: false\\\` with \\\`ephemeral: true\\\`. Run \\\`blokctl migrate workflows\\\`. |
|
|
960
957
|
|
|
961
958
|
## Generating Code
|
|
962
959
|
|
|
@@ -1019,7 +1016,7 @@ const wfResult = await runner.execute({ input: "data" });
|
|
|
1019
1016
|
- Launch: \\\`blokctl trace\\\` or navigate to \\\`/__blok\\\`
|
|
1020
1017
|
- "No output" → Node not returning data or Zod output validation failed
|
|
1021
1018
|
- "Step error" → Expand error — check if 400 (validation) or 500 (runtime)
|
|
1022
|
-
- "
|
|
1019
|
+
- "State not passing" → Source step has \\\`ephemeral: true\\\`, OR target's \\\`$.state.<id>\\\` references a non-existent step id
|
|
1023
1020
|
|
|
1024
1021
|
## Debugging Workers
|
|
1025
1022
|
|
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
|
-
import http from "node:http";
|
|
3
2
|
import path from "node:path";
|
|
4
3
|
import fsExtra from "fs-extra";
|
|
5
4
|
import { waitForGrpcPort } from "../../services/health-probe.js";
|
|
6
5
|
import { detectRr } from "../../services/runtime-detector.js";
|
|
7
|
-
import { readProjectConfig } from "../../services/runtime-setup.js";
|
|
6
|
+
import { readProjectConfig, validateProjectRuntimes } from "../../services/runtime-setup.js";
|
|
8
7
|
const runningProcesses = [];
|
|
9
8
|
function spawnProcess(cmd, args, name, currentPath, cwd, env) {
|
|
10
9
|
const child = spawn(cmd, args, {
|
|
@@ -40,55 +39,45 @@ function killAllGroups(signal) {
|
|
|
40
39
|
}
|
|
41
40
|
}
|
|
42
41
|
}
|
|
43
|
-
function waitForHttpHealth(port, timeoutMs, proc) {
|
|
44
|
-
return new Promise((resolve) => {
|
|
45
|
-
if (proc && proc.exitCode !== null) {
|
|
46
|
-
resolve(false);
|
|
47
|
-
return;
|
|
48
|
-
}
|
|
49
|
-
const start = Date.now();
|
|
50
|
-
let done = false;
|
|
51
|
-
function finish(result) {
|
|
52
|
-
if (done)
|
|
53
|
-
return;
|
|
54
|
-
done = true;
|
|
55
|
-
clearInterval(interval);
|
|
56
|
-
resolve(result);
|
|
57
|
-
}
|
|
58
|
-
proc?.on("exit", () => finish(false));
|
|
59
|
-
const interval = setInterval(() => {
|
|
60
|
-
if (done)
|
|
61
|
-
return;
|
|
62
|
-
if (Date.now() - start > timeoutMs) {
|
|
63
|
-
finish(false);
|
|
64
|
-
return;
|
|
65
|
-
}
|
|
66
|
-
const req = http.get(`http://localhost:${port}/health`, (res) => {
|
|
67
|
-
res.resume();
|
|
68
|
-
if (res.statusCode === 200)
|
|
69
|
-
finish(true);
|
|
70
|
-
});
|
|
71
|
-
req.on("error", () => {
|
|
72
|
-
});
|
|
73
|
-
req.setTimeout(1000, () => req.destroy());
|
|
74
|
-
}, 500);
|
|
75
|
-
});
|
|
76
|
-
}
|
|
77
42
|
export async function devProject(opts) {
|
|
78
43
|
const currentPath = process.cwd();
|
|
79
|
-
|
|
80
|
-
const transport = useHttpFallback ? "http" : "grpc";
|
|
81
|
-
console.log(`Starting the development server (transport=${transport})...`);
|
|
44
|
+
console.log("Starting the development server (transport=grpc)...");
|
|
82
45
|
console.log("Current path: ", currentPath);
|
|
83
|
-
if (useHttpFallback) {
|
|
84
|
-
console.log(" ⚠ --with-http-fallback is deprecated and will be removed in v0.4.0.");
|
|
85
|
-
}
|
|
86
46
|
const config = readProjectConfig(currentPath);
|
|
47
|
+
const skipVersionCheck = opts.skipVersionCheck === true;
|
|
48
|
+
const validationResults = await validateProjectRuntimes(currentPath);
|
|
49
|
+
if (validationResults.length > 0) {
|
|
50
|
+
const failures = validationResults.filter((r) => !r.satisfied);
|
|
51
|
+
const successes = validationResults.filter((r) => r.satisfied);
|
|
52
|
+
if (failures.length > 0 && !skipVersionCheck) {
|
|
53
|
+
console.error("\n Runtime version requirements not met:\n");
|
|
54
|
+
for (const f of failures) {
|
|
55
|
+
console.error(f.message);
|
|
56
|
+
console.error();
|
|
57
|
+
}
|
|
58
|
+
console.error(" Tip: Use --skip-version-check to bypass this check.\n");
|
|
59
|
+
process.exit(1);
|
|
60
|
+
}
|
|
61
|
+
if (failures.length > 0 && skipVersionCheck) {
|
|
62
|
+
console.log("\n Runtime version warnings:");
|
|
63
|
+
for (const f of failures) {
|
|
64
|
+
console.log(` ! ${f.label} ${f.found || "not installed"} (requires ${f.required}) — SKIPPED`);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
if (successes.length > 0) {
|
|
68
|
+
if (failures.length === 0)
|
|
69
|
+
console.log("\n Runtime version check:");
|
|
70
|
+
for (const s of successes) {
|
|
71
|
+
console.log(s.message);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
console.log();
|
|
75
|
+
}
|
|
87
76
|
const runtimeDefs = [];
|
|
88
77
|
if (config?.runtimes) {
|
|
89
78
|
for (const [, rt] of Object.entries(config.runtimes)) {
|
|
90
|
-
let bootCmd =
|
|
91
|
-
if (rt.kind === "php" &&
|
|
79
|
+
let bootCmd = rt.grpcStartCmd ?? rt.startCmd;
|
|
80
|
+
if (rt.kind === "php" && bootCmd.startsWith("rr ")) {
|
|
92
81
|
const rrBin = detectRr();
|
|
93
82
|
if (rrBin && rrBin !== "rr") {
|
|
94
83
|
bootCmd = `${rrBin}${bootCmd.slice(2)}`;
|
|
@@ -103,20 +92,19 @@ export async function devProject(opts) {
|
|
|
103
92
|
continue;
|
|
104
93
|
}
|
|
105
94
|
const grpcPort = rt.grpcPort ?? rt.port + 1000;
|
|
106
|
-
const probePort = useHttpFallback ? rt.port : grpcPort;
|
|
107
95
|
const env = {
|
|
108
96
|
PORT: String(rt.port),
|
|
109
97
|
GRPC_PORT: String(grpcPort),
|
|
110
98
|
HOST: "0.0.0.0",
|
|
111
|
-
BLOK_TRANSPORT:
|
|
99
|
+
BLOK_TRANSPORT: "grpc",
|
|
112
100
|
};
|
|
113
101
|
runtimeDefs.push({
|
|
114
102
|
cmd,
|
|
115
103
|
args,
|
|
116
|
-
name: `${rt.label} Runtime (
|
|
104
|
+
name: `${rt.label} Runtime (grpc port ${grpcPort})`,
|
|
117
105
|
cwd: runtimeCwd,
|
|
118
106
|
env,
|
|
119
|
-
port:
|
|
107
|
+
port: grpcPort,
|
|
120
108
|
});
|
|
121
109
|
}
|
|
122
110
|
}
|
|
@@ -153,19 +141,14 @@ export async function devProject(opts) {
|
|
|
153
141
|
if (config?.runtimes && Object.keys(config.runtimes).length > 0) {
|
|
154
142
|
console.log("\nRuntime listeners:");
|
|
155
143
|
for (const [, rt] of Object.entries(config.runtimes)) {
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
}
|
|
159
|
-
else {
|
|
160
|
-
const grpcPort = rt.grpcPort ?? rt.port + 1000;
|
|
161
|
-
console.log(` ${rt.label}: gRPC 127.0.0.1:${grpcPort}`);
|
|
162
|
-
}
|
|
144
|
+
const grpcPort = rt.grpcPort ?? rt.port + 1000;
|
|
145
|
+
console.log(` ${rt.label}: gRPC 127.0.0.1:${grpcPort}`);
|
|
163
146
|
}
|
|
164
147
|
}
|
|
165
148
|
if (healthChecks.length > 0) {
|
|
166
149
|
console.log("\nWaiting for runtimes to be ready...");
|
|
167
150
|
const maxWait = 120_000;
|
|
168
|
-
const results = await Promise.all(healthChecks.map((hc) =>
|
|
151
|
+
const results = await Promise.all(healthChecks.map((hc) => waitForGrpcPort(hc.port, maxWait, hc.proc)));
|
|
169
152
|
const allReady = results.every(Boolean);
|
|
170
153
|
if (allReady) {
|
|
171
154
|
console.log("All runtimes ready.\n");
|
|
@@ -185,7 +168,7 @@ export async function devProject(opts) {
|
|
|
185
168
|
}
|
|
186
169
|
const triggerEnv = {
|
|
187
170
|
...traceEnv,
|
|
188
|
-
BLOK_TRANSPORT:
|
|
171
|
+
BLOK_TRANSPORT: "grpc",
|
|
189
172
|
};
|
|
190
173
|
if (config?.triggers && Object.keys(config.triggers).length > 0) {
|
|
191
174
|
console.log("Starting triggers...");
|
|
@@ -106,7 +106,8 @@ export class GenerationAnalytics {
|
|
|
106
106
|
filtered = filtered.filter((e) => e.success === filter.success);
|
|
107
107
|
}
|
|
108
108
|
if (filter?.since) {
|
|
109
|
-
|
|
109
|
+
const since = filter.since;
|
|
110
|
+
filtered = filtered.filter((e) => e.timestamp >= since);
|
|
110
111
|
}
|
|
111
112
|
return filtered;
|
|
112
113
|
}
|
|
@@ -197,7 +197,7 @@ describe("NodeGenerator E2E", () => {
|
|
|
197
197
|
});
|
|
198
198
|
describe("code with markdown fences", () => {
|
|
199
199
|
it("should return code as-is from LLM (cleanup happens at CLI layer)", async () => {
|
|
200
|
-
const wrappedCode =
|
|
200
|
+
const wrappedCode = `\`\`\`typescript\n${VALID_FUNCTION_FIRST_NODE}\n\`\`\``;
|
|
201
201
|
mockedGenerateText.mockResolvedValueOnce({ text: wrappedCode });
|
|
202
202
|
mockValidPass();
|
|
203
203
|
const result = await generator.generateNode("fetch-user", "Create a user fetcher", "test-api-key", false, "function");
|
|
@@ -252,7 +252,7 @@ describe("TriggerGenerator E2E", () => {
|
|
|
252
252
|
});
|
|
253
253
|
describe("markdown fence cleanup", () => {
|
|
254
254
|
it("should strip markdown TypeScript fences from LLM response", async () => {
|
|
255
|
-
const wrappedCode =
|
|
255
|
+
const wrappedCode = `\`\`\`typescript\n${VALID_QUEUE_TRIGGER}\n\`\`\``;
|
|
256
256
|
mockedGenerateText.mockResolvedValueOnce({ text: wrappedCode });
|
|
257
257
|
mockedValidateCode.mockReturnValue({ success: true, errors: [], warnings: [] });
|
|
258
258
|
const result = await generator.generateTrigger("kafka-queue", "queue", "Create a queue trigger", "test-api-key");
|
|
@@ -210,7 +210,7 @@ describe("WorkflowGenerator E2E", () => {
|
|
|
210
210
|
});
|
|
211
211
|
describe("markdown fence cleanup", () => {
|
|
212
212
|
it("should strip markdown JSON fences from LLM response", async () => {
|
|
213
|
-
const wrappedJson =
|
|
213
|
+
const wrappedJson = `\`\`\`json\n${VALID_HTTP_WORKFLOW}\n\`\`\``;
|
|
214
214
|
mockedGenerateText.mockResolvedValueOnce({
|
|
215
215
|
text: wrappedJson,
|
|
216
216
|
});
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Command } from "commander";
|
|
2
2
|
import { program } from "../../services/commander.js";
|
|
3
3
|
import { migrateNode } from "./node.js";
|
|
4
|
+
import { migratePaths } from "./paths.js";
|
|
4
5
|
import { migrateWorkflows } from "./workflows.js";
|
|
5
6
|
const migrate = new Command("migrate").description("Migrate nodes and workflows to newer patterns");
|
|
6
7
|
const node = new Command("node")
|
|
@@ -20,6 +21,16 @@ const workflows = new Command("workflows")
|
|
|
20
21
|
.action(async (options) => {
|
|
21
22
|
await migrateWorkflows(options);
|
|
22
23
|
});
|
|
24
|
+
const paths = new Command("paths")
|
|
25
|
+
.description("Add explicit `trigger.http.path` to every JSON HTTP workflow (prep for v0.4 explicit-path-only routing)")
|
|
26
|
+
.option("-d, --dir <value>", "Path to the JSON workflows directory (defaults to ./workflows/json or ./triggers/http/workflows/json)")
|
|
27
|
+
.option("--dry-run", "Print what would change without writing files")
|
|
28
|
+
.option("--backup", "Create .bak files next to each migrated workflow (default true)")
|
|
29
|
+
.option("--no-backup", "Skip backup creation")
|
|
30
|
+
.action(async (options) => {
|
|
31
|
+
await migratePaths(options);
|
|
32
|
+
});
|
|
23
33
|
migrate.addCommand(node);
|
|
24
34
|
migrate.addCommand(workflows);
|
|
35
|
+
migrate.addCommand(paths);
|
|
25
36
|
program.addCommand(migrate);
|