blokctl 1.1.0 → 1.2.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 (32) hide show
  1. package/dist/commands/create/project.d.ts +2 -0
  2. package/dist/commands/create/project.js +13 -10
  3. package/dist/commands/create/utils/Examples.d.ts +2 -2
  4. package/dist/commands/create/utils/Examples.js +69 -103
  5. package/dist/commands/create/workflow.js +8 -6
  6. package/dist/commands/dev/index.js +26 -0
  7. package/dist/commands/install/node.d.ts +1 -0
  8. package/dist/commands/install/node.js +3 -39
  9. package/dist/commands/migrate/index.js +25 -0
  10. package/dist/commands/migrate/nodesTs.d.ts +31 -0
  11. package/dist/commands/migrate/nodesTs.js +484 -0
  12. package/dist/commands/migrate/refs.d.ts +15 -0
  13. package/dist/commands/migrate/refs.js +706 -0
  14. package/dist/commands/nodes/index.js +10 -0
  15. package/dist/commands/nodes/listNodes.d.ts +2 -0
  16. package/dist/commands/nodes/listNodes.js +20 -15
  17. package/dist/commands/nodes/syncNodes.d.ts +7 -0
  18. package/dist/commands/nodes/syncNodes.js +138 -0
  19. package/dist/commands/nodes/syncNodes.test.d.ts +1 -0
  20. package/dist/commands/nodes/syncNodes.test.js +322 -0
  21. package/dist/commands/publish/workflow.d.ts +1 -0
  22. package/dist/commands/publish/workflow.js +9 -0
  23. package/dist/commands/publish/workflow.test.d.ts +1 -0
  24. package/dist/commands/publish/workflow.test.js +22 -0
  25. package/dist/services/observability-stack.real-prometheus.test.d.ts +1 -0
  26. package/dist/services/observability-stack.real-prometheus.test.js +95 -0
  27. package/dist/services/observability-stack.real-tempo.test.d.ts +1 -0
  28. package/dist/services/observability-stack.real-tempo.test.js +86 -0
  29. package/dist/studio-dist/assets/index-QEvKRE4T.js +42 -0
  30. package/dist/studio-dist/index.html +1 -1
  31. package/package.json +3 -2
  32. package/dist/studio-dist/assets/index-CnFqCRQe.js +0 -42
@@ -73,27 +73,24 @@ Examples:
73
73
 
74
74
  For more documentation, visit src/nodes/examples/README.md. The first three examples require a PostgreSQL database to function.
75
75
  `;
76
- const workflow_template = `
77
- {
78
- "name": "My Workflow",
79
- "description": "What this workflow does",
80
- "version": "1.0.0",
81
- "trigger": {
82
- "http": {
83
- "method": "GET",
84
- "accept": "application/json"
85
- }
76
+ const workflow_template = `import { http, node, step, workflow } from "@blokjs/core";
77
+
78
+ /**
79
+ * {{WORKFLOW_NAME}}
80
+ *
81
+ * Add steps with step("id", node, inputs). Read a step's output downstream via
82
+ * the handle it returns (const user = step(...); ... user.name). Control flow:
83
+ * branch / forEach / switchOn / tryCatch — all from @blokjs/core.
84
+ */
85
+ export default workflow(
86
+ "{{WORKFLOW_NAME}}",
87
+ { version: "1.0.0", trigger: http.get("{{WORKFLOW_PATH}}") },
88
+ (req) => {
89
+ // Echo the request body back. @blokjs/respond writes the HTTP response,
90
+ // so mark it ephemeral (no state slot needed).
91
+ step("echo", node("@blokjs/respond"), { body: req.body }, { ephemeral: true });
86
92
  },
87
- "steps": [
88
- {
89
- "id": "echo",
90
- "use": "@blokjs/respond",
91
- "inputs": {
92
- "body": "$.req.body"
93
- }
94
- }
95
- ]
96
- }
93
+ );
97
94
  `;
98
95
  const supervisord_nodejs = `
99
96
  [supervisord]
@@ -438,7 +435,7 @@ Regardless of kind, every trigger populates \`ctx.request.{body,headers,params,q
438
435
 
439
436
  ## 2. THE 9 TRIGGERS
440
437
 
441
- Each trigger below: one-line purpose, USE-WHEN / DON'T, config shape, and a canonical \`workflow({...})\` example.
438
+ Each trigger below: one-line purpose, USE-WHEN / DON'T, config shape, and a canonical typed-handle \`workflow(name, { trigger }, (entry) => { ... })\` example.
442
439
 
443
440
  ### 2.1 HTTP — \`trigger: { http: {...} }\`
444
441
 
@@ -458,16 +455,11 @@ trigger: { http: {
458
455
  \`\`\`
459
456
 
460
457
  \`\`\`ts
461
- import { workflow, $ } from "@blokjs/helper";
458
+ import { http, node, step, tpl, workflow } from "@blokjs/core";
462
459
 
463
- export default workflow({
464
- name: "Get User", version: "1.0.0",
465
- trigger: { http: { method: "GET", path: "/users/:id" } },
466
- steps: [
467
- { id: "lookup", use: "@blokjs/api-call",
468
- inputs: { url: "js/\`https://internal/users/\${ctx.request.params.id}\`" } },
469
- { id: "respond", use: "@blokjs/respond", inputs: { body: $.state.lookup }, ephemeral: true },
470
- ],
460
+ export default workflow("Get User", { version: "1.0.0", trigger: http.get("/users/:id") }, (req) => {
461
+ const user = step("lookup", node("@blokjs/api-call"), { url: tpl\`https://internal/users/\${req.params.id}\` });
462
+ step("respond", node("@blokjs/respond"), { body: user }, { ephemeral: true });
471
463
  });
472
464
  \`\`\`
473
465
 
@@ -491,15 +483,10 @@ trigger: { worker: {
491
483
  \`\`\`
492
484
 
493
485
  \`\`\`ts
494
- import { workflow } from "@blokjs/helper";
486
+ import { node, step, workflow } from "@blokjs/core";
495
487
 
496
- export default workflow({
497
- name: "Process Background Job", version: "1.0.0",
498
- trigger: { worker: { queue: "background-jobs" } },
499
- steps: [
500
- { id: "process-job", use: "@blokjs/api-call", type: "module",
501
- inputs: { url: "https://example.com/process", method: "POST", body: "js/ctx.request.body" } },
502
- ],
488
+ export default workflow("Process Background Job", { version: "1.0.0", trigger: { worker: { queue: "background-jobs" } } }, (job) => {
489
+ step("process-job", node("@blokjs/api-call"), { url: "https://example.com/process", method: "POST", body: job.body });
503
490
  });
504
491
  \`\`\`
505
492
 
@@ -521,15 +508,10 @@ trigger: { cron: {
521
508
  \`\`\`
522
509
 
523
510
  \`\`\`ts
524
- import { workflow } from "@blokjs/helper";
511
+ import { node, step, workflow } from "@blokjs/core";
525
512
 
526
- export default workflow({
527
- name: "Daily Cleanup", version: "1.0.0",
528
- trigger: { cron: { schedule: "0 2 * * *", timezone: "America/New_York" } },
529
- steps: [
530
- { id: "purge-stale", use: "@blokjs/api-call",
531
- inputs: { url: "https://api.example.com/cleanup", method: "POST" } },
532
- ],
513
+ export default workflow("Daily Cleanup", { version: "1.0.0", trigger: { cron: { schedule: "0 2 * * *", timezone: "America/New_York" } } }, () => {
514
+ step("purge-stale", node("@blokjs/api-call"), { url: "https://api.example.com/cleanup", method: "POST" });
533
515
  });
534
516
  \`\`\`
535
517
 
@@ -556,16 +538,12 @@ trigger: { pubsub: {
556
538
  \`\`\`
557
539
 
558
540
  \`\`\`ts
559
- import { workflow } from "@blokjs/helper";
541
+ import { node, step, workflow } from "@blokjs/core";
560
542
 
561
- export default workflow({
562
- name: "On Order Placed", version: "1.0.0",
563
- trigger: { pubsub: { provider: "gcp", topic: "orders.placed", subscription: "fulfillment-svc" } },
564
- steps: [
565
- { id: "fulfill", use: "@blokjs/api-call",
566
- idempotencyKey: "js/ctx.request.params.messageId", // dedup redeliveries
567
- inputs: { url: "https://fulfillment.internal/api/orders", method: "POST", body: "js/ctx.request.body" } },
568
- ],
543
+ export default workflow("On Order Placed", { version: "1.0.0", trigger: { pubsub: { provider: "gcp", topic: "orders.placed", subscription: "fulfillment-svc" } } }, (msg) => {
544
+ step("fulfill", node("@blokjs/api-call"),
545
+ { url: "https://fulfillment.internal/api/orders", method: "POST", body: msg.body },
546
+ { idempotencyKey: msg.params.messageId }); // dedup redeliveries
569
547
  });
570
548
  \`\`\`
571
549
 
@@ -590,15 +568,11 @@ trigger: { sse: {
590
568
  \`\`\`
591
569
 
592
570
  \`\`\`ts
593
- import { workflow } from "@blokjs/helper";
571
+ import { node, step, workflow } from "@blokjs/core";
594
572
 
595
- export default workflow({
596
- name: "Clock Stream", version: "1.0.0",
597
- trigger: { sse: { path: "/sse/clock", heartbeatInterval: 15000 } },
598
- steps: [
599
- { id: "sub", use: "@blokjs/sse-subscribe", inputs: { channels: ["clock"] } },
600
- { id: "stream", use: "@blokjs/sse-stream", inputs: { source: "js/ctx.state.sub" } },
601
- ],
573
+ export default workflow("Clock Stream", { version: "1.0.0", trigger: { sse: { path: "/sse/clock", heartbeatInterval: 15000 } } }, () => {
574
+ const sub = step("sub", node("@blokjs/sse-subscribe"), { channels: ["clock"] });
575
+ step("stream", node("@blokjs/sse-stream"), { source: sub });
602
576
  });
603
577
  \`\`\`
604
578
 
@@ -623,15 +597,11 @@ trigger: { websocket: {
623
597
  \`\`\`
624
598
 
625
599
  \`\`\`ts
626
- import { workflow } from "@blokjs/helper";
600
+ import { js, node, step, workflow } from "@blokjs/core";
627
601
 
628
- export default workflow({
629
- name: "WS Echo", version: "1.0.0",
630
- trigger: { websocket: { path: "/ws/echo", events: ["message", "open", "close"] } },
631
- steps: [
632
- { id: "reply", use: "@blokjs/ws-reply",
633
- inputs: { message: "js/({ echo: ctx.request.body, at: Date.now() })" } },
634
- ],
602
+ export default workflow("WS Echo", { version: "1.0.0", trigger: { websocket: { path: "/ws/echo", events: ["message", "open", "close"] } } }, (conn) => {
603
+ // \`js\` is the escape hatch for a non-structural expression (Date.now()).
604
+ step("reply", node("@blokjs/ws-reply"), { message: js\`({ echo: \${conn.body}, at: Date.now() })\` });
635
605
  });
636
606
  \`\`\`
637
607
 
@@ -662,18 +632,14 @@ trigger: { webhook: {
662
632
  \`\`\`
663
633
 
664
634
  \`\`\`ts
665
- import { workflow } from "@blokjs/helper";
666
-
667
- export default workflow({
668
- name: "Stripe Webhook", version: "1.0.0",
669
- trigger: { webhook: {
670
- provider: "stripe", namespace: "stripe",
671
- secretEnv: "STRIPE_WEBHOOK_SECRET", idempotencyKey: "js/ctx.request.body.id",
672
- }},
673
- steps: [
674
- { id: "dispatch", subworkflow: "js/ctx.request.body.type", // "invoice.paid" → "stripe.invoice.paid"
675
- inputs: { stripeEvent: "js/ctx.request.body" } },
676
- ],
635
+ import { subworkflow, workflow } from "@blokjs/core";
636
+
637
+ export default workflow("Stripe Webhook", { version: "1.0.0", trigger: { webhook: {
638
+ provider: "stripe", namespace: "stripe",
639
+ secretEnv: "STRIPE_WEBHOOK_SECRET", idempotencyKey: "js/ctx.request.body.id",
640
+ } } }, (event) => {
641
+ // polymorphic dispatch: "invoice.paid" "stripe.invoice.paid" (namespace prefix from the trigger)
642
+ subworkflow("dispatch", event.body.type, { stripeEvent: event.body });
677
643
  });
678
644
  \`\`\`
679
645
 
@@ -700,15 +666,16 @@ trigger: { mcp: {
700
666
  **Requires a workflow-level \`input:\` Zod schema** — that becomes the tool's \`inputSchema\`:
701
667
 
702
668
  \`\`\`ts
703
- import { workflow, $ } from "@blokjs/helper";
669
+ import { node, step, workflow } from "@blokjs/core";
704
670
  import { z } from "zod";
705
671
 
706
- export default workflow({
707
- name: "search_code", version: "1.0.0",
672
+ export default workflow("search_code", {
673
+ version: "1.0.0",
708
674
  input: z.object({ query: z.string(), limit: z.number().optional() }), // → tool inputSchema
709
675
  trigger: { mcp: { path: "/mcp", serverName: "my-platform",
710
676
  tool: { description: "Full-text search the indexed code" } } },
711
- steps: [ { id: "search", use: "@my/search", inputs: { query: $.req.body.query } } ],
677
+ }, (call) => {
678
+ step("search", node("@my/search"), { query: call.body.query });
712
679
  });
713
680
  \`\`\`
714
681
 
@@ -740,27 +707,22 @@ trigger: { grpc: {
740
707
  \`\`\`
741
708
 
742
709
  \`\`\`ts
743
- import { workflow } from "@blokjs/helper";
710
+ import { node, step, tpl, workflow } from "@blokjs/core";
744
711
 
745
- export default workflow({
746
- name: "GetUser", version: "1.0.0",
747
- trigger: { grpc: { service: "UserService", method: "GetUser", proto: "users.proto" } },
748
- steps: [
749
- { id: "lookup", use: "@blokjs/api-call",
750
- inputs: { url: "js/\`https://internal/users/\${ctx.request.body.userId}\`", method: "GET" } },
751
- ],
712
+ export default workflow("GetUser", { version: "1.0.0", trigger: { grpc: { service: "UserService", method: "GetUser", proto: "users.proto" } } }, (rpc) => {
713
+ step("lookup", node("@blokjs/api-call"), { url: tpl\`https://internal/users/\${rpc.body.userId}\`, method: "GET" });
752
714
  });
753
715
  \`\`\`
754
716
 
755
- The request decodes into \`ctx.request.body\`; the final step output becomes the gRPC reply. Streaming RPCs use \`@blokjs/grpc-stream\`.
717
+ The request decodes into \`ctx.request.body\`; the final step output becomes the gRPC reply. (Server-streaming RPCs are not yet supported by the inbound trigger.)
756
718
 
757
719
  > **\`trigger.queue\` is DEAD** — it has a schema but no runtime and throws at workflow construction. Use \`worker\`. **\`manual\`** has no listener (invoked programmatically only — tests / sub-workflows); not for normal authoring.
758
720
 
759
721
  ---
760
722
 
761
- ## 3. AUTHORING WORKFLOWS (v2 DSL)
723
+ ## 3. AUTHORING WORKFLOWS — object-style & JSON (legacy; see §0 for the canonical typed-handle DSL)
762
724
 
763
- Import from \`@blokjs/helper\`: \`{ workflow, $, branch, switchOn, forEach, loop, tryCatch }\`. The default export is \`workflow({...})\` — a single object literal, no chaining, no separate \`nodes{}\` map.
725
+ §0 covers the canonical typed-handle DSL (\`workflow(name, { trigger }, (entry) => { ... })\`). This section documents the **equivalent object-style form** (\`@blokjs/helper\`, one \`workflow({...})\` object literal with \`$\` reads) and JSON both still fully supported for migrating and maintaining existing workflows; all three compile to the same IR. The persistence knobs and control-flow semantics below apply to every form.
764
726
 
765
727
  \`\`\`ts
766
728
  import { workflow, $ } from "@blokjs/helper";
@@ -890,10 +852,14 @@ For HTTP, \`ttl\` requires \`delay\`. Debounce modes: \`trailing\` (default —
890
852
  \`\`\`ts
891
853
  trigger: { http: { method: "GET", middleware: ["auth-check", "request-id"] } }
892
854
  \`\`\`
893
- 2. *Defining a middleware workflow* — \`workflow({ middleware: true })\`. \`trigger\` becomes optional; it gets no public route and is referenced by \`name\`:
855
+ 2. *Defining a middleware workflow* — \`workflow(name, { middleware: true }, cb)\`. \`trigger\` becomes optional; it gets no public route and is referenced by \`name\`:
894
856
  \`\`\`ts
895
- export default workflow({ name: "auth-check", version: "1.0.0", middleware: true,
896
- steps: [ /* sets ctx.state.identity; may stop:true to short-circuit */ ] });
857
+ import { node, step, workflow } from "@blokjs/core";
858
+
859
+ export default workflow("auth-check", { version: "1.0.0", middleware: true }, () => {
860
+ // populate ctx.state.identity; a step may return stop:true to short-circuit the chain
861
+ step("identity", node("@my/verify-auth"), {});
862
+ });
897
863
  \`\`\`
898
864
 
899
865
  Process-global middleware: \`WorkflowRegistry.getInstance().setGlobalMiddleware([...])\` or \`BLOK_GLOBAL_MIDDLEWARE=a,b\`.
@@ -1294,9 +1260,9 @@ expect(result.success).toBe(true);
1294
1260
  **Do:**
1295
1261
  - Read \`.blok/config.json\` and existing \`src/workflows/\` to learn which triggers + runtimes this project uses; author for those.
1296
1262
  - Start every workflow from the **trigger decision table** in §1, not from HTTP.
1297
- - Use \`workflow({ name, version, trigger, steps })\` from \`@blokjs/helper\`.
1263
+ - Author with the typed-handle DSL — \`workflow(name, { version, trigger }, (entry) => { step("id", node("@pkg"), inputs) })\` from \`@blokjs/core\` (§0). Object-style \`workflow({...})\` and JSON stay valid for legacy/migration.
1298
1264
  - Use the typed node contract in every runtime (\`defineNode\` / \`DefineNode\` / \`TypedNode\` / \`@node\`).
1299
- - Reference cross-step outputs with \`$.state.<id>\`; use \`as:\`/\`spread:\`/\`ephemeral:\` to shape persistence.
1265
+ - Reference cross-step outputs via the handle \`step()\` returns (not \`$.state.<id>\`); use the 4th-arg \`as:\`/\`spread:\`/\`ephemeral:\` knobs to shape persistence.
1300
1266
  - Set \`type: "runtime.<lang>"\` on every sidecar step and register the node by name.
1301
1267
 
1302
1268
  **Do NOT:**
@@ -59,7 +59,7 @@ export async function createWorkflow(opts, currentPath = false) {
59
59
  const nodeProjectDirExists = fsExtra.existsSync(currentDir);
60
60
  if (!nodeProjectDirExists)
61
61
  throw new Error("ops1");
62
- const currentWorkflowsDir = `${dirPath}/workflows/json`;
62
+ const currentWorkflowsDir = `${dirPath}/workflows`;
63
63
  if (!skipPrompts) {
64
64
  fsExtra.ensureDirSync(currentWorkflowsDir);
65
65
  }
@@ -68,10 +68,10 @@ export async function createWorkflow(opts, currentPath = false) {
68
68
  if (!workflowDirExists)
69
69
  throw new Error("ops1");
70
70
  }
71
- dirPath = path.join(currentWorkflowsDir, `${workflowName.replaceAll(" ", "-").toLowerCase()}.json`);
71
+ dirPath = path.join(currentWorkflowsDir, `${workflowName.replaceAll(" ", "-").toLowerCase()}.ts`);
72
72
  }
73
73
  else {
74
- dirPath = path.join(dirPath, `${workflowName.replaceAll(" ", "-").toLowerCase()}.json`);
74
+ dirPath = path.join(dirPath, `${workflowName.replaceAll(" ", "-").toLowerCase()}.ts`);
75
75
  }
76
76
  if (!skipPrompts)
77
77
  s.message("Creating workflow...");
@@ -80,9 +80,11 @@ export async function createWorkflow(opts, currentPath = false) {
80
80
  if (workflowDirExists)
81
81
  throw new Error("ops2");
82
82
  }
83
- const workflow_json = JSON.parse(workflow_template);
84
- workflow_json.name = workflowName;
85
- fsExtra.writeFileSync(dirPath, JSON.stringify(workflow_json, null, 2));
83
+ const slug = workflowName.replaceAll(" ", "-").toLowerCase();
84
+ const workflow_ts = workflow_template
85
+ .replaceAll("{{WORKFLOW_NAME}}", workflowName)
86
+ .replaceAll("{{WORKFLOW_PATH}}", `/${slug}`);
87
+ fsExtra.writeFileSync(dirPath, workflow_ts);
86
88
  if (!skipPrompts)
87
89
  s.stop(`Node "${workflowName}" created successfully.`);
88
90
  if (!currentPath)
@@ -6,6 +6,7 @@ import fsExtra from "fs-extra";
6
6
  import { waitForGrpcPort } from "../../services/health-probe.js";
7
7
  import { detectRr } from "../../services/runtime-detector.js";
8
8
  import { generateCSharpNodeRegistry, generateGoNodeRegistry, generateJavaNodeRegistry, generateRustNodeRegistry, readProjectConfig, validateProjectRuntimes, } from "../../services/runtime-setup.js";
9
+ import { regenRuntimeStubs } from "../nodes/syncNodes.js";
9
10
  const exec = util.promisify(child_process.exec);
10
11
  const runningProcesses = [];
11
12
  function spawnProcess(cmd, args, name, currentPath, cwd, env) {
@@ -42,6 +43,25 @@ function killAllGroups(signal) {
42
43
  }
43
44
  }
44
45
  }
46
+ async function regenStubsWhenReady(baseUrl, outDir, deadlineMs = 60_000) {
47
+ const endpoint = `${baseUrl}/__blok/nodes`;
48
+ const start = Date.now();
49
+ while (Date.now() - start < deadlineMs) {
50
+ try {
51
+ const res = await fetch(endpoint);
52
+ if (res.ok) {
53
+ const count = await regenRuntimeStubs(baseUrl, outDir);
54
+ if (count > 0)
55
+ console.log(`Regenerated ${count} runtime stub file(s) → ${path.relative(process.cwd(), outDir)}`);
56
+ return;
57
+ }
58
+ }
59
+ catch {
60
+ }
61
+ await new Promise((r) => setTimeout(r, 1000));
62
+ }
63
+ console.log(` Warning: stub regen skipped — ${endpoint} did not respond within ${deadlineMs / 1000}s.`);
64
+ }
45
65
  export async function devProject(opts) {
46
66
  const currentPath = process.cwd();
47
67
  console.log("Starting the development server (transport=grpc)...");
@@ -233,6 +253,12 @@ export async function devProject(opts) {
233
253
  else {
234
254
  spawnProcess("bun", ["--watch", "run", "src/index.ts"], "Blok Runner", currentPath, undefined, triggerEnv);
235
255
  }
256
+ const httpTrigger = config?.triggers?.http;
257
+ if (httpTrigger && healthChecks.length > 0) {
258
+ const baseUrl = `http://localhost:${httpTrigger.port}`;
259
+ const outDir = path.join(currentPath, "nodes-gen");
260
+ void regenStubsWhenReady(baseUrl, outDir);
261
+ }
236
262
  const keepAlive = setInterval(() => { }, 60_000);
237
263
  let stopping = false;
238
264
  function shutdown() {
@@ -1,4 +1,5 @@
1
1
  import { Command, type OptionValues } from "../../services/commander.js";
2
2
  export declare function install(opts: OptionValues): Promise<void>;
3
+ export declare function nodeInstallHint(importName: string, importPath: string): string;
3
4
  declare const _default: Command;
4
5
  export default _default;
@@ -20,10 +20,6 @@ export async function install(opts) {
20
20
  throw new Error("Token is invalid.");
21
21
  if (!opts.node)
22
22
  throw new Error("Node name is required.");
23
- const nodeFilePath = path.resolve(opts.directory, "./src/Nodes.ts");
24
- if (!fs.existsSync(nodeFilePath)) {
25
- throw new Error("Node.ts file does not exist in the specified directory.");
26
- }
27
23
  const packageJsonPath = path.resolve(opts.directory, "./package.json");
28
24
  if (!fs.existsSync(packageJsonPath)) {
29
25
  throw new Error("package.json file does not exist in the specified directory.");
@@ -65,10 +61,7 @@ export async function install(opts) {
65
61
  p.log.info(stdout);
66
62
  else if (stderr)
67
63
  throw new Error(stderr);
68
- updateNodeFile(nodeFilePath, {
69
- importName: toPascalCase(opts.node),
70
- importPath: nodeName,
71
- });
64
+ p.log.warn(nodeInstallHint(toPascalCase(opts.node), nodeName));
72
65
  logger.stop("Node installed successfully.");
73
66
  }
74
67
  catch (error) {
@@ -84,37 +77,8 @@ export async function install(opts) {
84
77
  function toPascalCase(input) {
85
78
  return input.replace(/[^a-zA-Z0-9]+(.)/g, (_, chr) => chr.toUpperCase()).replace(/^./, (s) => s.toLowerCase());
86
79
  }
87
- function updateNodeFile(filePath, newModule) {
88
- let fileContent = fs.readFileSync(filePath, "utf-8");
89
- const { importName, importPath } = newModule;
90
- const importStatement = `import ${importName} from "${importPath}";`;
91
- if (!fileContent.includes(importStatement)) {
92
- const importLines = fileContent.match(/^import .*;$/gm) || [];
93
- const lastImport = importLines[importLines.length - 1];
94
- fileContent = fileContent.replace(lastImport, `${lastImport}\n${importStatement}`);
95
- }
96
- const nodesRegex = /const\s+nodes\s*:\s*\{[^}]+\}\s*=\s*\{([\s\S]*?)\n\};/;
97
- const match = fileContent.match(nodesRegex);
98
- if (!match)
99
- throw new Error("Could not find the `nodes` object.");
100
- const objectBody = match[1].trimEnd();
101
- const entryKey = importPath.replace(/^@[^/]+\//, "");
102
- const newEntry = `\t"${entryKey}": new ${importName}()`;
103
- if (objectBody.includes(`"${entryKey}"`)) {
104
- p.log.warn(`Node "${entryKey}" is already installed. Skipping...`);
105
- return;
106
- }
107
- const lines = objectBody
108
- .split("\n")
109
- .filter(Boolean)
110
- .map((line) => line.trimEnd());
111
- if (lines.length > 0 && !lines[lines.length - 1].endsWith(",")) {
112
- lines[lines.length - 1] += ",";
113
- }
114
- lines.push(newEntry);
115
- const newObject = `const nodes: {\n\t[key: string]: NodeBase;\n} = {\n${lines.join("\n")}\n};`;
116
- fileContent = fileContent.replace(nodesRegex, newObject);
117
- fs.writeFileSync(filePath, `${fileContent.trimEnd()}\n`, "utf-8");
80
+ export function nodeInstallHint(importName, importPath) {
81
+ return `Nodes.ts registration is deprecated. Import the node directly in a handle workflow: import ${importName} from "${importPath}"; then use step("id", ${importName}, inputs).`;
118
82
  }
119
83
  export default new Command()
120
84
  .command("node")
@@ -1,7 +1,9 @@
1
1
  import { Command } from "commander";
2
2
  import { program } from "../../services/commander.js";
3
3
  import { migrateNode } from "./node.js";
4
+ import { migrateNodesTs } from "./nodesTs.js";
4
5
  import { migratePaths } from "./paths.js";
6
+ import { migrateRefs } from "./refs.js";
5
7
  import { migrateWorkflows } from "./workflows.js";
6
8
  const migrate = new Command("migrate").description("Migrate nodes and workflows to newer patterns");
7
9
  const node = new Command("node")
@@ -30,7 +32,30 @@ const paths = new Command("paths")
30
32
  .action(async (options) => {
31
33
  await migratePaths(options);
32
34
  });
35
+ const refs = new Command("refs")
36
+ .description("Rewrite pure step input refs to structural handle refs and mark dynamic expressions")
37
+ .option("-d, --dir <value>", "Path to scan for .ts/.json workflows (defaults to current directory)")
38
+ .option("--dry-run", "Print what would change without writing files")
39
+ .option("--backup", "Create .bak files next to each migrated file (default true)")
40
+ .option("--no-backup", "Skip backup creation")
41
+ .action(async (options) => {
42
+ await migrateRefs(options);
43
+ });
44
+ const nodesTs = new Command("nodes-ts")
45
+ .description("Replace handle-DSL step() string node refs with direct imports or runtime stubs")
46
+ .option("-d, --dir <value>", "Path to scan for TypeScript workflows (defaults to ./src/workflows)")
47
+ .option("--nodes <value>", "Path to Nodes.ts (defaults to ./src/Nodes.ts)")
48
+ .option("--stubs <value>", "Path to generated runtime stubs (defaults to ./nodes-gen)")
49
+ .option("--delete-nodes", "Delete Nodes.ts only when no string workflow refs remain")
50
+ .option("--dry-run", "Print what would change without writing files")
51
+ .option("--backup", "Create .bak files next to each migrated file (default true)")
52
+ .option("--no-backup", "Skip backup creation")
53
+ .action(async (options) => {
54
+ await migrateNodesTs(options);
55
+ });
33
56
  migrate.addCommand(node);
34
57
  migrate.addCommand(workflows);
35
58
  migrate.addCommand(paths);
59
+ migrate.addCommand(refs);
60
+ migrate.addCommand(nodesTs);
36
61
  program.addCommand(migrate);
@@ -0,0 +1,31 @@
1
+ import type { OptionValues } from "commander";
2
+ type ImportKind = "default" | "named";
3
+ interface ImportRef {
4
+ kind: ImportKind;
5
+ importName: string;
6
+ importPath: string;
7
+ originFile: string;
8
+ }
9
+ interface RuntimeRef {
10
+ exportName: string;
11
+ importPath: string;
12
+ runtime: string;
13
+ }
14
+ interface Resolver {
15
+ modules: Map<string, ImportRef>;
16
+ runtimes: Map<string, RuntimeRef>;
17
+ }
18
+ interface MigrationStats {
19
+ migrated: number;
20
+ marked: number;
21
+ }
22
+ export declare function migrateNodesTs(opts: OptionValues): Promise<void>;
23
+ export declare function migrateNodesTsSource(source: string, file: string, resolver: Resolver): {
24
+ value: string;
25
+ changed: boolean;
26
+ stats: MigrationStats;
27
+ };
28
+ export declare function parseNodesMap(source: string, nodesFile: string): Promise<Map<string, ImportRef>>;
29
+ export declare function parseRuntimeStubs(stubsDir: string): Promise<Map<string, RuntimeRef>>;
30
+ export declare function countStringNodeRefs(root: string): Promise<number>;
31
+ export {};