blokctl 1.0.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.
- package/dist/commands/create/project.d.ts +2 -0
- package/dist/commands/create/project.js +47 -28
- package/dist/commands/create/utils/Examples.d.ts +3 -3
- package/dist/commands/create/utils/Examples.js +91 -121
- package/dist/commands/create/workflow.js +8 -6
- package/dist/commands/dev/index.js +26 -0
- package/dist/commands/install/node.d.ts +1 -0
- package/dist/commands/install/node.js +3 -39
- package/dist/commands/migrate/index.js +25 -0
- package/dist/commands/migrate/nodesTs.d.ts +31 -0
- package/dist/commands/migrate/nodesTs.js +484 -0
- package/dist/commands/migrate/refs.d.ts +15 -0
- package/dist/commands/migrate/refs.js +706 -0
- package/dist/commands/nodes/index.js +10 -0
- package/dist/commands/nodes/listNodes.d.ts +2 -0
- package/dist/commands/nodes/listNodes.js +20 -15
- package/dist/commands/nodes/syncNodes.d.ts +7 -0
- package/dist/commands/nodes/syncNodes.js +138 -0
- package/dist/commands/nodes/syncNodes.test.d.ts +1 -0
- package/dist/commands/nodes/syncNodes.test.js +322 -0
- package/dist/commands/publish/workflow.d.ts +1 -0
- package/dist/commands/publish/workflow.js +9 -0
- package/dist/commands/publish/workflow.test.d.ts +1 -0
- package/dist/commands/publish/workflow.test.js +22 -0
- package/dist/services/observability-stack.real-prometheus.test.d.ts +1 -0
- package/dist/services/observability-stack.real-prometheus.test.js +95 -0
- package/dist/services/observability-stack.real-tempo.test.d.ts +1 -0
- package/dist/services/observability-stack.real-tempo.test.js +86 -0
- package/dist/studio-dist/assets/index-QEvKRE4T.js +42 -0
- package/dist/studio-dist/index.html +1 -1
- package/package.json +3 -2
- package/dist/studio-dist/assets/index-CnFqCRQe.js +0 -42
|
@@ -1,21 +1,25 @@
|
|
|
1
|
-
const node_file = `import
|
|
1
|
+
const node_file = `import { dirname, join } from "node:path";
|
|
2
|
+
import { fileURLToPath } from "node:url";
|
|
3
|
+
import ApiCall from "@blokjs/api-call";
|
|
2
4
|
import IfElse from "@blokjs/if-else";
|
|
5
|
+
import { discoverNodes } from "@blokjs/runner";
|
|
3
6
|
import type { NodeBase } from "@blokjs/shared";
|
|
4
|
-
import ChainInit from "./nodes/chain-init/index";
|
|
5
|
-
import ChainVerify from "./nodes/chain-verify/index";
|
|
6
7
|
import ExampleNodes from "./nodes/examples/index";
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
};
|
|
8
|
+
|
|
9
|
+
// Published nodes (npm) + the example bundle are registered explicitly below.
|
|
10
|
+
// Your OWN nodes under 'nodes/<name>/index.ts' are AUTO-DISCOVERED and registered
|
|
11
|
+
// by their defineNode({ name }) — you never edit this file to add a node.
|
|
12
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
13
|
+
const local = await discoverNodes(join(here, "nodes"));
|
|
14
|
+
|
|
15
|
+
const explicit: NodeBase[] = [ApiCall, IfElse, ...(Object.values(ExampleNodes) as NodeBase[])];
|
|
16
|
+
|
|
17
|
+
// Map keys are cosmetic — the runner registers each node under its own node.name
|
|
18
|
+
// (the canonical 'use:' ref). Duplicate refs throw at startup.
|
|
19
|
+
const nodes: { [key: string]: NodeBase } = {};
|
|
20
|
+
for (const node of [...explicit, ...local]) {
|
|
21
|
+
nodes[(node as { name: string }).name] = node;
|
|
22
|
+
}
|
|
19
23
|
|
|
20
24
|
export default nodes;
|
|
21
25
|
`;
|
|
@@ -69,27 +73,24 @@ Examples:
|
|
|
69
73
|
|
|
70
74
|
For more documentation, visit src/nodes/examples/README.md. The first three examples require a PostgreSQL database to function.
|
|
71
75
|
`;
|
|
72
|
-
const workflow_template = `
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
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 });
|
|
82
92
|
},
|
|
83
|
-
|
|
84
|
-
{
|
|
85
|
-
"id": "echo",
|
|
86
|
-
"use": "@blokjs/respond",
|
|
87
|
-
"inputs": {
|
|
88
|
-
"body": "$.req.body"
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
]
|
|
92
|
-
}
|
|
93
|
+
);
|
|
93
94
|
`;
|
|
94
95
|
const supervisord_nodejs = `
|
|
95
96
|
[supervisord]
|
|
@@ -434,7 +435,7 @@ Regardless of kind, every trigger populates \`ctx.request.{body,headers,params,q
|
|
|
434
435
|
|
|
435
436
|
## 2. THE 9 TRIGGERS
|
|
436
437
|
|
|
437
|
-
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.
|
|
438
439
|
|
|
439
440
|
### 2.1 HTTP — \`trigger: { http: {...} }\`
|
|
440
441
|
|
|
@@ -454,16 +455,11 @@ trigger: { http: {
|
|
|
454
455
|
\`\`\`
|
|
455
456
|
|
|
456
457
|
\`\`\`ts
|
|
457
|
-
import {
|
|
458
|
+
import { http, node, step, tpl, workflow } from "@blokjs/core";
|
|
458
459
|
|
|
459
|
-
export default workflow({
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
steps: [
|
|
463
|
-
{ id: "lookup", use: "@blokjs/api-call",
|
|
464
|
-
inputs: { url: "js/\`https://internal/users/\${ctx.request.params.id}\`" } },
|
|
465
|
-
{ id: "respond", use: "@blokjs/respond", inputs: { body: $.state.lookup }, ephemeral: true },
|
|
466
|
-
],
|
|
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 });
|
|
467
463
|
});
|
|
468
464
|
\`\`\`
|
|
469
465
|
|
|
@@ -487,15 +483,10 @@ trigger: { worker: {
|
|
|
487
483
|
\`\`\`
|
|
488
484
|
|
|
489
485
|
\`\`\`ts
|
|
490
|
-
import { workflow } from "@blokjs/
|
|
486
|
+
import { node, step, workflow } from "@blokjs/core";
|
|
491
487
|
|
|
492
|
-
export default workflow({
|
|
493
|
-
|
|
494
|
-
trigger: { worker: { queue: "background-jobs" } },
|
|
495
|
-
steps: [
|
|
496
|
-
{ id: "process-job", use: "@blokjs/api-call", type: "module",
|
|
497
|
-
inputs: { url: "https://example.com/process", method: "POST", body: "js/ctx.request.body" } },
|
|
498
|
-
],
|
|
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 });
|
|
499
490
|
});
|
|
500
491
|
\`\`\`
|
|
501
492
|
|
|
@@ -517,15 +508,10 @@ trigger: { cron: {
|
|
|
517
508
|
\`\`\`
|
|
518
509
|
|
|
519
510
|
\`\`\`ts
|
|
520
|
-
import { workflow } from "@blokjs/
|
|
511
|
+
import { node, step, workflow } from "@blokjs/core";
|
|
521
512
|
|
|
522
|
-
export default workflow({
|
|
523
|
-
|
|
524
|
-
trigger: { cron: { schedule: "0 2 * * *", timezone: "America/New_York" } },
|
|
525
|
-
steps: [
|
|
526
|
-
{ id: "purge-stale", use: "@blokjs/api-call",
|
|
527
|
-
inputs: { url: "https://api.example.com/cleanup", method: "POST" } },
|
|
528
|
-
],
|
|
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" });
|
|
529
515
|
});
|
|
530
516
|
\`\`\`
|
|
531
517
|
|
|
@@ -552,16 +538,12 @@ trigger: { pubsub: {
|
|
|
552
538
|
\`\`\`
|
|
553
539
|
|
|
554
540
|
\`\`\`ts
|
|
555
|
-
import { workflow } from "@blokjs/
|
|
541
|
+
import { node, step, workflow } from "@blokjs/core";
|
|
556
542
|
|
|
557
|
-
export default workflow({
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
{ id: "fulfill", use: "@blokjs/api-call",
|
|
562
|
-
idempotencyKey: "js/ctx.request.params.messageId", // dedup redeliveries
|
|
563
|
-
inputs: { url: "https://fulfillment.internal/api/orders", method: "POST", body: "js/ctx.request.body" } },
|
|
564
|
-
],
|
|
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
|
|
565
547
|
});
|
|
566
548
|
\`\`\`
|
|
567
549
|
|
|
@@ -586,15 +568,11 @@ trigger: { sse: {
|
|
|
586
568
|
\`\`\`
|
|
587
569
|
|
|
588
570
|
\`\`\`ts
|
|
589
|
-
import { workflow } from "@blokjs/
|
|
571
|
+
import { node, step, workflow } from "@blokjs/core";
|
|
590
572
|
|
|
591
|
-
export default workflow({
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
steps: [
|
|
595
|
-
{ id: "sub", use: "@blokjs/sse-subscribe", inputs: { channels: ["clock"] } },
|
|
596
|
-
{ id: "stream", use: "@blokjs/sse-stream", inputs: { source: "js/ctx.state.sub" } },
|
|
597
|
-
],
|
|
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 });
|
|
598
576
|
});
|
|
599
577
|
\`\`\`
|
|
600
578
|
|
|
@@ -619,15 +597,11 @@ trigger: { websocket: {
|
|
|
619
597
|
\`\`\`
|
|
620
598
|
|
|
621
599
|
\`\`\`ts
|
|
622
|
-
import { workflow } from "@blokjs/
|
|
600
|
+
import { js, node, step, workflow } from "@blokjs/core";
|
|
623
601
|
|
|
624
|
-
export default workflow({
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
steps: [
|
|
628
|
-
{ id: "reply", use: "@blokjs/ws-reply",
|
|
629
|
-
inputs: { message: "js/({ echo: ctx.request.body, at: Date.now() })" } },
|
|
630
|
-
],
|
|
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() })\` });
|
|
631
605
|
});
|
|
632
606
|
\`\`\`
|
|
633
607
|
|
|
@@ -658,18 +632,14 @@ trigger: { webhook: {
|
|
|
658
632
|
\`\`\`
|
|
659
633
|
|
|
660
634
|
\`\`\`ts
|
|
661
|
-
import { workflow } from "@blokjs/
|
|
662
|
-
|
|
663
|
-
export default workflow({
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
}
|
|
669
|
-
steps: [
|
|
670
|
-
{ id: "dispatch", subworkflow: "js/ctx.request.body.type", // "invoice.paid" → "stripe.invoice.paid"
|
|
671
|
-
inputs: { stripeEvent: "js/ctx.request.body" } },
|
|
672
|
-
],
|
|
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 });
|
|
673
643
|
});
|
|
674
644
|
\`\`\`
|
|
675
645
|
|
|
@@ -696,15 +666,16 @@ trigger: { mcp: {
|
|
|
696
666
|
**Requires a workflow-level \`input:\` Zod schema** — that becomes the tool's \`inputSchema\`:
|
|
697
667
|
|
|
698
668
|
\`\`\`ts
|
|
699
|
-
import {
|
|
669
|
+
import { node, step, workflow } from "@blokjs/core";
|
|
700
670
|
import { z } from "zod";
|
|
701
671
|
|
|
702
|
-
export default workflow({
|
|
703
|
-
|
|
672
|
+
export default workflow("search_code", {
|
|
673
|
+
version: "1.0.0",
|
|
704
674
|
input: z.object({ query: z.string(), limit: z.number().optional() }), // → tool inputSchema
|
|
705
675
|
trigger: { mcp: { path: "/mcp", serverName: "my-platform",
|
|
706
676
|
tool: { description: "Full-text search the indexed code" } } },
|
|
707
|
-
|
|
677
|
+
}, (call) => {
|
|
678
|
+
step("search", node("@my/search"), { query: call.body.query });
|
|
708
679
|
});
|
|
709
680
|
\`\`\`
|
|
710
681
|
|
|
@@ -736,27 +707,22 @@ trigger: { grpc: {
|
|
|
736
707
|
\`\`\`
|
|
737
708
|
|
|
738
709
|
\`\`\`ts
|
|
739
|
-
import { workflow } from "@blokjs/
|
|
710
|
+
import { node, step, tpl, workflow } from "@blokjs/core";
|
|
740
711
|
|
|
741
|
-
export default workflow({
|
|
742
|
-
|
|
743
|
-
trigger: { grpc: { service: "UserService", method: "GetUser", proto: "users.proto" } },
|
|
744
|
-
steps: [
|
|
745
|
-
{ id: "lookup", use: "@blokjs/api-call",
|
|
746
|
-
inputs: { url: "js/\`https://internal/users/\${ctx.request.body.userId}\`", method: "GET" } },
|
|
747
|
-
],
|
|
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" });
|
|
748
714
|
});
|
|
749
715
|
\`\`\`
|
|
750
716
|
|
|
751
|
-
The request decodes into \`ctx.request.body\`; the final step output becomes the gRPC reply.
|
|
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.)
|
|
752
718
|
|
|
753
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.
|
|
754
720
|
|
|
755
721
|
---
|
|
756
722
|
|
|
757
|
-
## 3. AUTHORING WORKFLOWS (
|
|
723
|
+
## 3. AUTHORING WORKFLOWS — object-style & JSON (legacy; see §0 for the canonical typed-handle DSL)
|
|
758
724
|
|
|
759
|
-
|
|
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.
|
|
760
726
|
|
|
761
727
|
\`\`\`ts
|
|
762
728
|
import { workflow, $ } from "@blokjs/helper";
|
|
@@ -886,10 +852,14 @@ For HTTP, \`ttl\` requires \`delay\`. Debounce modes: \`trailing\` (default —
|
|
|
886
852
|
\`\`\`ts
|
|
887
853
|
trigger: { http: { method: "GET", middleware: ["auth-check", "request-id"] } }
|
|
888
854
|
\`\`\`
|
|
889
|
-
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\`:
|
|
890
856
|
\`\`\`ts
|
|
891
|
-
|
|
892
|
-
|
|
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
|
+
});
|
|
893
863
|
\`\`\`
|
|
894
864
|
|
|
895
865
|
Process-global middleware: \`WorkflowRegistry.getInstance().setGlobalMiddleware([...])\` or \`BLOK_GLOBAL_MIDDLEWARE=a,b\`.
|
|
@@ -1256,12 +1226,12 @@ Plus the cross-runtime rules: **the wrong input source** (typed sidecar nodes re
|
|
|
1256
1226
|
|
|
1257
1227
|
## 8. TESTING
|
|
1258
1228
|
|
|
1259
|
-
Use the \`@blokjs/
|
|
1229
|
+
Use the \`@blokjs/core/testing\` utilities with Vitest (same package you author against).
|
|
1260
1230
|
|
|
1261
1231
|
**Unit-test a node** with \`NodeTestHarness\`:
|
|
1262
1232
|
|
|
1263
1233
|
\`\`\`ts
|
|
1264
|
-
import { NodeTestHarness } from "@blokjs/
|
|
1234
|
+
import { NodeTestHarness } from "@blokjs/core/testing";
|
|
1265
1235
|
import myNode from "../src/nodes/my-node";
|
|
1266
1236
|
|
|
1267
1237
|
const harness = new NodeTestHarness(myNode);
|
|
@@ -1273,7 +1243,7 @@ harness.assertOutput(result, { user: { id: "abc-123" } });
|
|
|
1273
1243
|
**Integration-test a workflow** with \`WorkflowTestRunner\`:
|
|
1274
1244
|
|
|
1275
1245
|
\`\`\`ts
|
|
1276
|
-
import { WorkflowTestRunner } from "@blokjs/
|
|
1246
|
+
import { WorkflowTestRunner } from "@blokjs/core/testing";
|
|
1277
1247
|
|
|
1278
1248
|
const runner = new WorkflowTestRunner({ verbose: true, mockAllNodes: true });
|
|
1279
1249
|
runner.registerNode("validate", ValidateNode);
|
|
@@ -1290,9 +1260,9 @@ expect(result.success).toBe(true);
|
|
|
1290
1260
|
**Do:**
|
|
1291
1261
|
- Read \`.blok/config.json\` and existing \`src/workflows/\` to learn which triggers + runtimes this project uses; author for those.
|
|
1292
1262
|
- Start every workflow from the **trigger decision table** in §1, not from HTTP.
|
|
1293
|
-
-
|
|
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.
|
|
1294
1264
|
- Use the typed node contract in every runtime (\`defineNode\` / \`DefineNode\` / \`TypedNode\` / \`@node\`).
|
|
1295
|
-
- Reference cross-step outputs
|
|
1265
|
+
- Reference cross-step outputs via the handle \`step()\` returns (not \`$.state.<id>\`); use the 4th-arg \`as:\`/\`spread:\`/\`ephemeral:\` knobs to shape persistence.
|
|
1296
1266
|
- Set \`type: "runtime.<lang>"\` on every sidecar step and register the node by name.
|
|
1297
1267
|
|
|
1298
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
|
|
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()}.
|
|
71
|
+
dirPath = path.join(currentWorkflowsDir, `${workflowName.replaceAll(" ", "-").toLowerCase()}.ts`);
|
|
72
72
|
}
|
|
73
73
|
else {
|
|
74
|
-
dirPath = path.join(dirPath, `${workflowName.replaceAll(" ", "-").toLowerCase()}.
|
|
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
|
|
84
|
-
|
|
85
|
-
|
|
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
|
-
|
|
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
|
|
88
|
-
|
|
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 {};
|