blokctl 1.1.0 → 1.3.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 +5 -0
- package/dist/commands/create/project.js +164 -27
- package/dist/commands/create/utils/Examples.d.ts +4 -4
- package/dist/commands/create/utils/Examples.js +77 -105
- package/dist/commands/create/workflow.js +8 -6
- package/dist/commands/dev/index.js +39 -1
- 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/commands/runtime/add.js +92 -29
- package/dist/commands/runtime/index.js +1 -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/services/runtime-detector.d.ts +2 -0
- package/dist/services/runtime-detector.js +35 -11
- package/dist/services/runtime-setup.d.ts +1 -0
- package/dist/services/runtime-setup.js +17 -7
- 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,6 +1,7 @@
|
|
|
1
1
|
const node_file = `import { dirname, join } from "node:path";
|
|
2
2
|
import { fileURLToPath } from "node:url";
|
|
3
3
|
import ApiCall from "@blokjs/api-call";
|
|
4
|
+
import { HELPER_NODES } from "@blokjs/helpers";
|
|
4
5
|
import IfElse from "@blokjs/if-else";
|
|
5
6
|
import { discoverNodes } from "@blokjs/runner";
|
|
6
7
|
import type { NodeBase } from "@blokjs/shared";
|
|
@@ -12,7 +13,12 @@ import ExampleNodes from "./nodes/examples/index";
|
|
|
12
13
|
const here = dirname(fileURLToPath(import.meta.url));
|
|
13
14
|
const local = await discoverNodes(join(here, "nodes"));
|
|
14
15
|
|
|
15
|
-
const explicit: NodeBase[] = [
|
|
16
|
+
const explicit: NodeBase[] = [
|
|
17
|
+
ApiCall,
|
|
18
|
+
IfElse,
|
|
19
|
+
...(Object.values(HELPER_NODES) as unknown as NodeBase[]),
|
|
20
|
+
...(Object.values(ExampleNodes) as NodeBase[]),
|
|
21
|
+
];
|
|
16
22
|
|
|
17
23
|
// Map keys are cosmetic — the runner registers each node under its own node.name
|
|
18
24
|
// (the canonical 'use:' ref). Duplicate refs throw at startup.
|
|
@@ -69,31 +75,28 @@ Examples:
|
|
|
69
75
|
7- Webhook router: POST /webhooks/{stripe,github,linear} with signed bodies — set the matching *_WEBHOOK_SECRET env vars (needs --triggers webhook)
|
|
70
76
|
8- LLM agent w/ tool calls: open http://localhost:4000/agent — model picks between get_weather and calculate tools (needs OPENROUTER_API_KEY + Redis)
|
|
71
77
|
9- Worker fan-out: POST /fanout/jobs with body '{items:[...], tenantId?:"..."}' to enqueue N worker jobs (needs --triggers worker; BLOK_WORKER_ADAPTER=in-memory works single-process)
|
|
72
|
-
10- Trigger references (NOT http): workflows/json/{cron-heartbeat,pubsub-on-order
|
|
78
|
+
10- Trigger references (NOT http): workflows/json/{cron-heartbeat,pubsub-on-order}.json demonstrate the cron and pubsub triggers; src/workflows/websocket/events/echo-demo.ts demonstrates websocket — read AGENTS.md "Choosing a trigger" to pick the right one by intent instead of defaulting to HTTP.
|
|
73
79
|
|
|
74
80
|
For more documentation, visit src/nodes/examples/README.md. The first three examples require a PostgreSQL database to function.
|
|
75
81
|
`;
|
|
76
|
-
const workflow_template = `
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
82
|
+
const workflow_template = `import { http, node, step, workflow } from "@blokjs/core";
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* {{WORKFLOW_NAME}}
|
|
86
|
+
*
|
|
87
|
+
* Add steps with step("id", node, inputs). Read a step's output downstream via
|
|
88
|
+
* the handle it returns (const user = step(...); ... user.name). Control flow:
|
|
89
|
+
* branch / forEach / switchOn / tryCatch — all from @blokjs/core.
|
|
90
|
+
*/
|
|
91
|
+
export default workflow(
|
|
92
|
+
"{{WORKFLOW_NAME}}",
|
|
93
|
+
{ version: "1.0.0", trigger: http.get("{{WORKFLOW_PATH}}") },
|
|
94
|
+
(req) => {
|
|
95
|
+
// Echo the request body back. @blokjs/respond writes the HTTP response,
|
|
96
|
+
// so mark it ephemeral (no state slot needed).
|
|
97
|
+
step("echo", node("@blokjs/respond"), { body: req.body }, { ephemeral: true });
|
|
86
98
|
},
|
|
87
|
-
|
|
88
|
-
{
|
|
89
|
-
"id": "echo",
|
|
90
|
-
"use": "@blokjs/respond",
|
|
91
|
-
"inputs": {
|
|
92
|
-
"body": "$.req.body"
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
]
|
|
96
|
-
}
|
|
99
|
+
);
|
|
97
100
|
`;
|
|
98
101
|
const supervisord_nodejs = `
|
|
99
102
|
[supervisord]
|
|
@@ -438,7 +441,7 @@ Regardless of kind, every trigger populates \`ctx.request.{body,headers,params,q
|
|
|
438
441
|
|
|
439
442
|
## 2. THE 9 TRIGGERS
|
|
440
443
|
|
|
441
|
-
Each trigger below: one-line purpose, USE-WHEN / DON'T, config shape, and a canonical \`workflow({...})\` example.
|
|
444
|
+
Each trigger below: one-line purpose, USE-WHEN / DON'T, config shape, and a canonical typed-handle \`workflow(name, { trigger }, (entry) => { ... })\` example.
|
|
442
445
|
|
|
443
446
|
### 2.1 HTTP — \`trigger: { http: {...} }\`
|
|
444
447
|
|
|
@@ -458,16 +461,11 @@ trigger: { http: {
|
|
|
458
461
|
\`\`\`
|
|
459
462
|
|
|
460
463
|
\`\`\`ts
|
|
461
|
-
import {
|
|
464
|
+
import { http, node, step, tpl, workflow } from "@blokjs/core";
|
|
462
465
|
|
|
463
|
-
export default workflow({
|
|
464
|
-
|
|
465
|
-
|
|
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
|
-
],
|
|
466
|
+
export default workflow("Get User", { version: "1.0.0", trigger: http.get("/users/:id") }, (req) => {
|
|
467
|
+
const user = step("lookup", node("@blokjs/api-call"), { url: tpl\`https://internal/users/\${req.params.id}\` });
|
|
468
|
+
step("respond", node("@blokjs/respond"), { body: user }, { ephemeral: true });
|
|
471
469
|
});
|
|
472
470
|
\`\`\`
|
|
473
471
|
|
|
@@ -491,15 +489,10 @@ trigger: { worker: {
|
|
|
491
489
|
\`\`\`
|
|
492
490
|
|
|
493
491
|
\`\`\`ts
|
|
494
|
-
import { workflow } from "@blokjs/
|
|
492
|
+
import { node, step, workflow } from "@blokjs/core";
|
|
495
493
|
|
|
496
|
-
export default workflow({
|
|
497
|
-
|
|
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
|
-
],
|
|
494
|
+
export default workflow("Process Background Job", { version: "1.0.0", trigger: { worker: { queue: "background-jobs" } } }, (job) => {
|
|
495
|
+
step("process-job", node("@blokjs/api-call"), { url: "https://example.com/process", method: "POST", body: job.body });
|
|
503
496
|
});
|
|
504
497
|
\`\`\`
|
|
505
498
|
|
|
@@ -521,15 +514,10 @@ trigger: { cron: {
|
|
|
521
514
|
\`\`\`
|
|
522
515
|
|
|
523
516
|
\`\`\`ts
|
|
524
|
-
import { workflow } from "@blokjs/
|
|
517
|
+
import { node, step, workflow } from "@blokjs/core";
|
|
525
518
|
|
|
526
|
-
export default workflow({
|
|
527
|
-
|
|
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
|
-
],
|
|
519
|
+
export default workflow("Daily Cleanup", { version: "1.0.0", trigger: { cron: { schedule: "0 2 * * *", timezone: "America/New_York" } } }, () => {
|
|
520
|
+
step("purge-stale", node("@blokjs/api-call"), { url: "https://api.example.com/cleanup", method: "POST" });
|
|
533
521
|
});
|
|
534
522
|
\`\`\`
|
|
535
523
|
|
|
@@ -556,16 +544,12 @@ trigger: { pubsub: {
|
|
|
556
544
|
\`\`\`
|
|
557
545
|
|
|
558
546
|
\`\`\`ts
|
|
559
|
-
import { workflow } from "@blokjs/
|
|
547
|
+
import { node, step, workflow } from "@blokjs/core";
|
|
560
548
|
|
|
561
|
-
export default workflow({
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
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
|
-
],
|
|
549
|
+
export default workflow("On Order Placed", { version: "1.0.0", trigger: { pubsub: { provider: "gcp", topic: "orders.placed", subscription: "fulfillment-svc" } } }, (msg) => {
|
|
550
|
+
step("fulfill", node("@blokjs/api-call"),
|
|
551
|
+
{ url: "https://fulfillment.internal/api/orders", method: "POST", body: msg.body },
|
|
552
|
+
{ idempotencyKey: msg.params.messageId }); // dedup redeliveries
|
|
569
553
|
});
|
|
570
554
|
\`\`\`
|
|
571
555
|
|
|
@@ -590,15 +574,11 @@ trigger: { sse: {
|
|
|
590
574
|
\`\`\`
|
|
591
575
|
|
|
592
576
|
\`\`\`ts
|
|
593
|
-
import { workflow } from "@blokjs/
|
|
577
|
+
import { node, step, workflow } from "@blokjs/core";
|
|
594
578
|
|
|
595
|
-
export default workflow({
|
|
596
|
-
|
|
597
|
-
|
|
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
|
-
],
|
|
579
|
+
export default workflow("Clock Stream", { version: "1.0.0", trigger: { sse: { path: "/sse/clock", heartbeatInterval: 15000 } } }, () => {
|
|
580
|
+
const sub = step("sub", node("@blokjs/sse-subscribe"), { channels: ["clock"] });
|
|
581
|
+
step("stream", node("@blokjs/sse-stream"), { source: sub });
|
|
602
582
|
});
|
|
603
583
|
\`\`\`
|
|
604
584
|
|
|
@@ -623,15 +603,11 @@ trigger: { websocket: {
|
|
|
623
603
|
\`\`\`
|
|
624
604
|
|
|
625
605
|
\`\`\`ts
|
|
626
|
-
import { workflow } from "@blokjs/
|
|
606
|
+
import { js, node, step, workflow } from "@blokjs/core";
|
|
627
607
|
|
|
628
|
-
export default workflow({
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
steps: [
|
|
632
|
-
{ id: "reply", use: "@blokjs/ws-reply",
|
|
633
|
-
inputs: { message: "js/({ echo: ctx.request.body, at: Date.now() })" } },
|
|
634
|
-
],
|
|
608
|
+
export default workflow("WS Echo", { version: "1.0.0", trigger: { websocket: { path: "/ws/echo", events: ["message", "open", "close"] } } }, (conn) => {
|
|
609
|
+
// \`js\` is the escape hatch for a non-structural expression (Date.now()).
|
|
610
|
+
step("reply", node("@blokjs/ws-reply"), { message: js\`({ echo: \${conn.body}, at: Date.now() })\` });
|
|
635
611
|
});
|
|
636
612
|
\`\`\`
|
|
637
613
|
|
|
@@ -662,18 +638,14 @@ trigger: { webhook: {
|
|
|
662
638
|
\`\`\`
|
|
663
639
|
|
|
664
640
|
\`\`\`ts
|
|
665
|
-
import { workflow } from "@blokjs/
|
|
666
|
-
|
|
667
|
-
export default workflow({
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
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
|
-
],
|
|
641
|
+
import { subworkflow, workflow } from "@blokjs/core";
|
|
642
|
+
|
|
643
|
+
export default workflow("Stripe Webhook", { version: "1.0.0", trigger: { webhook: {
|
|
644
|
+
provider: "stripe", namespace: "stripe",
|
|
645
|
+
secretEnv: "STRIPE_WEBHOOK_SECRET", idempotencyKey: "js/ctx.request.body.id",
|
|
646
|
+
} } }, (event) => {
|
|
647
|
+
// polymorphic dispatch: "invoice.paid" → "stripe.invoice.paid" (namespace prefix from the trigger)
|
|
648
|
+
subworkflow("dispatch", event.body.type, { stripeEvent: event.body });
|
|
677
649
|
});
|
|
678
650
|
\`\`\`
|
|
679
651
|
|
|
@@ -700,15 +672,16 @@ trigger: { mcp: {
|
|
|
700
672
|
**Requires a workflow-level \`input:\` Zod schema** — that becomes the tool's \`inputSchema\`:
|
|
701
673
|
|
|
702
674
|
\`\`\`ts
|
|
703
|
-
import {
|
|
675
|
+
import { node, step, workflow } from "@blokjs/core";
|
|
704
676
|
import { z } from "zod";
|
|
705
677
|
|
|
706
|
-
export default workflow({
|
|
707
|
-
|
|
678
|
+
export default workflow("search_code", {
|
|
679
|
+
version: "1.0.0",
|
|
708
680
|
input: z.object({ query: z.string(), limit: z.number().optional() }), // → tool inputSchema
|
|
709
681
|
trigger: { mcp: { path: "/mcp", serverName: "my-platform",
|
|
710
682
|
tool: { description: "Full-text search the indexed code" } } },
|
|
711
|
-
|
|
683
|
+
}, (call) => {
|
|
684
|
+
step("search", node("@my/search"), { query: call.body.query });
|
|
712
685
|
});
|
|
713
686
|
\`\`\`
|
|
714
687
|
|
|
@@ -740,27 +713,22 @@ trigger: { grpc: {
|
|
|
740
713
|
\`\`\`
|
|
741
714
|
|
|
742
715
|
\`\`\`ts
|
|
743
|
-
import { workflow } from "@blokjs/
|
|
716
|
+
import { node, step, tpl, workflow } from "@blokjs/core";
|
|
744
717
|
|
|
745
|
-
export default workflow({
|
|
746
|
-
|
|
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
|
-
],
|
|
718
|
+
export default workflow("GetUser", { version: "1.0.0", trigger: { grpc: { service: "UserService", method: "GetUser", proto: "users.proto" } } }, (rpc) => {
|
|
719
|
+
step("lookup", node("@blokjs/api-call"), { url: tpl\`https://internal/users/\${rpc.body.userId}\`, method: "GET" });
|
|
752
720
|
});
|
|
753
721
|
\`\`\`
|
|
754
722
|
|
|
755
|
-
The request decodes into \`ctx.request.body\`; the final step output becomes the gRPC reply.
|
|
723
|
+
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
724
|
|
|
757
725
|
> **\`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
726
|
|
|
759
727
|
---
|
|
760
728
|
|
|
761
|
-
## 3. AUTHORING WORKFLOWS (
|
|
729
|
+
## 3. AUTHORING WORKFLOWS — object-style & JSON (legacy; see §0 for the canonical typed-handle DSL)
|
|
762
730
|
|
|
763
|
-
|
|
731
|
+
§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
732
|
|
|
765
733
|
\`\`\`ts
|
|
766
734
|
import { workflow, $ } from "@blokjs/helper";
|
|
@@ -890,10 +858,14 @@ For HTTP, \`ttl\` requires \`delay\`. Debounce modes: \`trailing\` (default —
|
|
|
890
858
|
\`\`\`ts
|
|
891
859
|
trigger: { http: { method: "GET", middleware: ["auth-check", "request-id"] } }
|
|
892
860
|
\`\`\`
|
|
893
|
-
2. *Defining a middleware workflow* — \`workflow({ middleware: true })\`. \`trigger\` becomes optional; it gets no public route and is referenced by \`name\`:
|
|
861
|
+
2. *Defining a middleware workflow* — \`workflow(name, { middleware: true }, cb)\`. \`trigger\` becomes optional; it gets no public route and is referenced by \`name\`:
|
|
894
862
|
\`\`\`ts
|
|
895
|
-
|
|
896
|
-
|
|
863
|
+
import { node, step, workflow } from "@blokjs/core";
|
|
864
|
+
|
|
865
|
+
export default workflow("auth-check", { version: "1.0.0", middleware: true }, () => {
|
|
866
|
+
// populate ctx.state.identity; a step may return stop:true to short-circuit the chain
|
|
867
|
+
step("identity", node("@my/verify-auth"), {});
|
|
868
|
+
});
|
|
897
869
|
\`\`\`
|
|
898
870
|
|
|
899
871
|
Process-global middleware: \`WorkflowRegistry.getInstance().setGlobalMiddleware([...])\` or \`BLOK_GLOBAL_MIDDLEWARE=a,b\`.
|
|
@@ -1294,9 +1266,9 @@ expect(result.success).toBe(true);
|
|
|
1294
1266
|
**Do:**
|
|
1295
1267
|
- Read \`.blok/config.json\` and existing \`src/workflows/\` to learn which triggers + runtimes this project uses; author for those.
|
|
1296
1268
|
- Start every workflow from the **trigger decision table** in §1, not from HTTP.
|
|
1297
|
-
-
|
|
1269
|
+
- 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
1270
|
- Use the typed node contract in every runtime (\`defineNode\` / \`DefineNode\` / \`TypedNode\` / \`@node\`).
|
|
1299
|
-
- Reference cross-step outputs
|
|
1271
|
+
- Reference cross-step outputs via the handle \`step()\` returns (not \`$.state.<id>\`); use the 4th-arg \`as:\`/\`spread:\`/\`ephemeral:\` knobs to shape persistence.
|
|
1300
1272
|
- Set \`type: "runtime.<lang>"\` on every sidecar step and register the node by name.
|
|
1301
1273
|
|
|
1302
1274
|
**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)
|
|
@@ -4,8 +4,9 @@ import path from "node:path";
|
|
|
4
4
|
import util from "node:util";
|
|
5
5
|
import fsExtra from "fs-extra";
|
|
6
6
|
import { waitForGrpcPort } from "../../services/health-probe.js";
|
|
7
|
-
import { detectRr } from "../../services/runtime-detector.js";
|
|
7
|
+
import { detectJava, 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)...");
|
|
@@ -86,6 +106,12 @@ export async function devProject(opts) {
|
|
|
86
106
|
bootCmd = `${rrBin}${bootCmd.slice(2)}`;
|
|
87
107
|
}
|
|
88
108
|
}
|
|
109
|
+
if (rt.kind === "java" && bootCmd.startsWith("java ")) {
|
|
110
|
+
const javaBin = detectJava();
|
|
111
|
+
if (javaBin && javaBin !== "java") {
|
|
112
|
+
bootCmd = `${javaBin}${bootCmd.slice(4)}`;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
89
115
|
const cmdParts = bootCmd.split(" ");
|
|
90
116
|
const cmd = cmdParts[0];
|
|
91
117
|
const args = cmdParts.slice(1);
|
|
@@ -178,6 +204,12 @@ export async function devProject(opts) {
|
|
|
178
204
|
if (brokerConsumerKinds.has(trigger.kind)) {
|
|
179
205
|
console.log(` ${trigger.label}: consumes from broker (no HTTP endpoint)`);
|
|
180
206
|
}
|
|
207
|
+
else if (trigger.kind === "cron") {
|
|
208
|
+
console.log(` ${trigger.label}: scheduled (no HTTP endpoint)`);
|
|
209
|
+
}
|
|
210
|
+
else if (trigger.kind === "grpc") {
|
|
211
|
+
console.log(` ${trigger.label}: gRPC 127.0.0.1:${trigger.port}`);
|
|
212
|
+
}
|
|
181
213
|
else {
|
|
182
214
|
console.log(` ${trigger.label}: http://localhost:${trigger.port}/health-check`);
|
|
183
215
|
}
|
|
@@ -233,6 +265,12 @@ export async function devProject(opts) {
|
|
|
233
265
|
else {
|
|
234
266
|
spawnProcess("bun", ["--watch", "run", "src/index.ts"], "Blok Runner", currentPath, undefined, triggerEnv);
|
|
235
267
|
}
|
|
268
|
+
const httpTrigger = config?.triggers?.http;
|
|
269
|
+
if (httpTrigger && healthChecks.length > 0) {
|
|
270
|
+
const baseUrl = `http://localhost:${httpTrigger.port}`;
|
|
271
|
+
const outDir = path.join(currentPath, "nodes-gen");
|
|
272
|
+
void regenStubsWhenReady(baseUrl, outDir);
|
|
273
|
+
}
|
|
236
274
|
const keepAlive = setInterval(() => { }, 60_000);
|
|
237
275
|
let stopping = false;
|
|
238
276
|
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 {};
|