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.
Files changed (38) hide show
  1. package/dist/commands/create/project.d.ts +5 -0
  2. package/dist/commands/create/project.js +164 -27
  3. package/dist/commands/create/utils/Examples.d.ts +4 -4
  4. package/dist/commands/create/utils/Examples.js +77 -105
  5. package/dist/commands/create/workflow.js +8 -6
  6. package/dist/commands/dev/index.js +39 -1
  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/commands/runtime/add.js +92 -29
  26. package/dist/commands/runtime/index.js +1 -0
  27. package/dist/services/observability-stack.real-prometheus.test.d.ts +1 -0
  28. package/dist/services/observability-stack.real-prometheus.test.js +95 -0
  29. package/dist/services/observability-stack.real-tempo.test.d.ts +1 -0
  30. package/dist/services/observability-stack.real-tempo.test.js +86 -0
  31. package/dist/services/runtime-detector.d.ts +2 -0
  32. package/dist/services/runtime-detector.js +35 -11
  33. package/dist/services/runtime-setup.d.ts +1 -0
  34. package/dist/services/runtime-setup.js +17 -7
  35. package/dist/studio-dist/assets/index-QEvKRE4T.js +42 -0
  36. package/dist/studio-dist/index.html +1 -1
  37. package/package.json +3 -2
  38. package/dist/studio-dist/assets/index-CnFqCRQe.js +0 -42
@@ -1,5 +1,10 @@
1
1
  import type { OptionValues } from "commander";
2
2
  export declare function createProject(opts: OptionValues, version: string, currentPath?: boolean, localRepoPath?: string): Promise<void>;
3
+ export declare function generateSharedNodesFile(_triggers: string[], _repoSource: string): string;
4
+ export declare function generateSharedWorkflowsFile(triggers: string[], runtimeKinds?: string[], examples?: boolean): string;
5
+ export declare function generateTriggerEntryFile(triggerKind: string, selectedTriggers?: string[]): string;
6
+ export declare function generateCronExampleWorkflowFile(): string;
7
+ export declare function generateCronServerFile(): string;
3
8
  export declare function updateQueueProvider(triggerDestDir: string, provider: string, explicit: boolean): void;
4
9
  export declare function getProviderDependencies(triggers: string[], pubsubProvider: string, queueProvider: string, explicitQueueProvider?: boolean): Record<string, string>;
5
10
  export declare function getProviderEnvVars(triggers: string[], pubsubProvider: string, queueProvider: string, explicitQueueProvider?: boolean): string;
@@ -14,7 +14,7 @@ import { rewriteObservabilityEnvBlock } from "../../services/observability-mutat
14
14
  import { manager as pm } from "../../services/package-manager.js";
15
15
  import { detectRuntimes } from "../../services/runtime-detector.js";
16
16
  import { createTriggerConfig, generateRuntimeEnvVars, generateSupervisordConfig, generateTriggerEnvVars, generateTriggerSupervisordConfig, setupRuntime, writeProjectConfig, } from "../../services/runtime-setup.js";
17
- import { computeDefaultConstraint } from "../../services/semver-utils.js";
17
+ import { computeDefaultConstraint, formatVersionMismatch, satisfiesConstraint } from "../../services/semver-utils.js";
18
18
  import { resolveObservabilitySelection } from "../observability/apply.js";
19
19
  import { OBSERVABILITY_MODULE_IDS, allObservabilityModules, getObservabilityModule, } from "../observability/descriptor.js";
20
20
  import { agents_md, claude_md, examples_url, node_file, package_dependencies, package_dev_dependencies, } from "./utils/Examples.js";
@@ -22,7 +22,7 @@ const exec = util.promisify(child_process.exec);
22
22
  const HOME_DIR = `${os.homedir()}/.blok`;
23
23
  const GITHUB_REPO_LOCAL = `${HOME_DIR}/blok`;
24
24
  const GITHUB_REPO_REMOTE = "https://github.com/well-prado/blok.git";
25
- const GITHUB_REPO_RELEASE_TAG = "v1.1.0";
25
+ const GITHUB_REPO_RELEASE_TAG = "v1.3.0";
26
26
  const RUNTIME_HELLO_EXAMPLES = {
27
27
  go: "runtime-go-hello.ts",
28
28
  rust: "runtime-rust-hello.ts",
@@ -55,7 +55,7 @@ export async function createProject(opts, version, currentPath = false, localRep
55
55
  let examples = opts.examples ?? false;
56
56
  let selectedRuntimeKinds = opts.runtimes ? parseCommaSeparated(opts.runtimes) : ["node"];
57
57
  let selectedManager = opts.packageManager || "npm";
58
- let pubsubProvider = opts.pubsubProvider || "gcp";
58
+ let pubsubProvider = opts.pubsubProvider || "nats";
59
59
  let queueProvider = opts.queueProvider || "kafka";
60
60
  let explicitQueueProvider = Boolean(opts.queueProvider);
61
61
  let selectedObsTier = opts.obsStack ? parseObsTier(opts.obsStack) : "none";
@@ -154,6 +154,7 @@ export async function createProject(opts, version, currentPath = false, localRep
154
154
  ? p.select({
155
155
  message: "Select Pub/Sub provider",
156
156
  options: [
157
+ { label: "NATS (local, zero cloud setup)", value: "nats" },
157
158
  { label: "Google Cloud Pub/Sub", value: "gcp" },
158
159
  { label: "AWS SNS/SQS", value: "aws" },
159
160
  { label: "Azure Service Bus", value: "azure" },
@@ -212,7 +213,7 @@ export async function createProject(opts, version, currentPath = false, localRep
212
213
  });
213
214
  projectName = blokctlProject.projectName;
214
215
  selectedTriggers = blokctlProject.triggers;
215
- pubsubProvider = blokctlProject.pubsubProvider || "gcp";
216
+ pubsubProvider = blokctlProject.pubsubProvider || "nats";
216
217
  explicitQueueProvider = blokctlProject.queueProvider != null;
217
218
  queueProvider = blokctlProject.queueProvider || "kafka";
218
219
  selectedRuntimeKinds = blokctlProject.runtimes;
@@ -453,6 +454,14 @@ export async function createProject(opts, version, currentPath = false, localRep
453
454
  fsExtra.ensureDirSync(wsServerDir);
454
455
  fsExtra.writeFileSync(`${wsServerDir}/WSServer.ts`, generateWSServerFile());
455
456
  }
457
+ if (selectedTriggers.includes("cron")) {
458
+ const cronServerDir = `${dirPath}/src/triggers/cron/runner`;
459
+ fsExtra.ensureDirSync(cronServerDir);
460
+ fsExtra.writeFileSync(`${cronServerDir}/CronServer.ts`, generateCronServerFile());
461
+ const cronWorkflowDir = `${dirPath}/src/workflows/cron`;
462
+ fsExtra.ensureDirSync(cronWorkflowDir);
463
+ fsExtra.writeFileSync(`${cronWorkflowDir}/heartbeat.ts`, generateCronExampleWorkflowFile());
464
+ }
456
465
  for (const triggerKind of selectedTriggers) {
457
466
  const triggerNodesDir = `${repoSource}/triggers/${triggerKind}/src/nodes`;
458
467
  if (fsExtra.existsSync(triggerNodesDir)) {
@@ -500,13 +509,7 @@ export async function createProject(opts, version, currentPath = false, localRep
500
509
  fsExtra.ensureDirSync(`${dirPath}/infra/milvus`);
501
510
  fsExtra.copySync(`${repoSource}/infra/development`, `${dirPath}/infra/postgresql`);
502
511
  fsExtra.copySync(`${repoSource}/infra/milvus`, `${dirPath}/infra/milvus`);
503
- const needsHelpers = selectedTriggers.includes("sse") || selectedTriggers.includes("websocket") || selectedTriggers.includes("mcp");
504
- const examplesNodesContent = needsHelpers
505
- ? node_file
506
- .replace(`import type { NodeBase } from "@blokjs/shared";`, `import type { NodeBase } from "@blokjs/shared";\nimport { HELPER_NODES } from "@blokjs/helpers";`)
507
- .replace(`} = {\n\t"@blokjs/api-call": ApiCall,`, `} = {\n\t...HELPER_NODES,\n\t"@blokjs/api-call": ApiCall,`)
508
- : node_file;
509
- fsExtra.writeFileSync(`${dirPath}/src/Nodes.ts`, examplesNodesContent);
512
+ fsExtra.writeFileSync(`${dirPath}/src/Nodes.ts`, node_file);
510
513
  fsExtra.copySync(`${repoSource}/sdk`, `${dirPath}/public/sdk`);
511
514
  const tsExamplesSrc = `${repoSource}/examples/ts-workflows`;
512
515
  const tsExamplesDest = `${dirPath}/src/workflows/examples`;
@@ -551,7 +554,7 @@ export async function createProject(opts, version, currentPath = false, localRep
551
554
  "@blokjs/trigger-worker": "triggers/worker",
552
555
  "@blokjs/core": "core/core",
553
556
  };
554
- const BLOKJS_DEP_RANGE = "^1.1.0";
557
+ const BLOKJS_DEP_RANGE = "^1.3.0";
555
558
  for (const depGroup of ["dependencies", "devDependencies", "peerDependencies"]) {
556
559
  const deps = packageJsonContent[depGroup];
557
560
  if (!deps)
@@ -636,6 +639,16 @@ export async function createProject(opts, version, currentPath = false, localRep
636
639
  ? `file:${path.resolve(repoSource, "triggers/worker")}`
637
640
  : BLOKJS_DEP_RANGE;
638
641
  }
642
+ if (selectedTriggers.includes("cron")) {
643
+ triggerPackageDeps["@blokjs/trigger-cron"] = localRepoPath
644
+ ? `file:${path.resolve(repoSource, "triggers/cron")}`
645
+ : BLOKJS_DEP_RANGE;
646
+ }
647
+ if (selectedTriggers.includes("grpc")) {
648
+ triggerPackageDeps["@blokjs/trigger-grpc"] = localRepoPath
649
+ ? `file:${path.resolve(repoSource, "triggers/grpc")}`
650
+ : BLOKJS_DEP_RANGE;
651
+ }
639
652
  if (selectedTriggers.includes("sse")) {
640
653
  const sseDeps = {
641
654
  "@blokjs/api-call": localRepoPath
@@ -705,6 +718,14 @@ export async function createProject(opts, version, currentPath = false, localRep
705
718
  const rt = detectedRuntimes.find((r) => r.kind === kind);
706
719
  if (!rt)
707
720
  continue;
721
+ if (rt.minVersion) {
722
+ const constraint = computeDefaultConstraint(rt.minVersion);
723
+ if (!rt.version || !satisfiesConstraint(rt.version, constraint)) {
724
+ console.log(`\n${formatVersionMismatch(rt.label, rt.version, constraint, rt.installHint)}`);
725
+ console.log(color.yellow(` Skipping ${rt.label} setup. After upgrading, add it with \`blokctl runtime add ${rt.kind}\`.\n`));
726
+ continue;
727
+ }
728
+ }
708
729
  try {
709
730
  const config = await setupRuntime(rt, repoSource, dirPath, s);
710
731
  runtimeConfigs.push(config);
@@ -858,20 +879,21 @@ export async function createProject(opts, version, currentPath = false, localRep
858
879
  console.log(error.message);
859
880
  }
860
881
  }
861
- function generateSharedNodesFile(triggers, _repoSource) {
882
+ export function generateSharedNodesFile(_triggers, _repoSource) {
862
883
  const imports = [
863
884
  'import { dirname, join } from "node:path";',
864
885
  'import { fileURLToPath } from "node:url";',
865
886
  'import ApiCall from "@blokjs/api-call";',
866
887
  'import IfElse from "@blokjs/if-else";',
888
+ 'import { HELPER_NODES } from "@blokjs/helpers";',
867
889
  'import { discoverNodes } from "@blokjs/runner";',
868
890
  'import type { NodeBase } from "@blokjs/shared";',
869
891
  ];
870
- const explicit = ["ApiCall as unknown as NodeBase", "IfElse as unknown as NodeBase"];
871
- if (triggers.includes("sse") || triggers.includes("websocket")) {
872
- imports.push('import { HELPER_NODES } from "@blokjs/helpers";');
873
- explicit.push("...(Object.values(HELPER_NODES) as unknown as NodeBase[])");
874
- }
892
+ const explicit = [
893
+ "ApiCall as unknown as NodeBase",
894
+ "IfElse as unknown as NodeBase",
895
+ "...(Object.values(HELPER_NODES) as unknown as NodeBase[])",
896
+ ];
875
897
  return `${imports.join("\n")}
876
898
 
877
899
  // Published nodes (npm) are registered explicitly below. Your OWN nodes under
@@ -890,32 +912,42 @@ for (const node of [${explicit.join(", ")}, ...local]) {
890
912
  export default nodes;
891
913
  `;
892
914
  }
893
- function generateSharedWorkflowsFile(triggers, runtimeKinds = [], examples = false) {
915
+ export function generateSharedWorkflowsFile(triggers, runtimeKinds = [], examples = false) {
894
916
  const imports = [];
895
917
  const workflowEntries = [];
896
918
  for (const trigger of triggers) {
897
919
  if (trigger === "http") {
898
- imports.push("// HTTP workflows are auto-discovered from workflows/json/");
920
+ imports.push("// HTTP JSON workflows are auto-discovered from workflows/json/");
921
+ imports.push('import CountriesHandleDsl from "./workflows/http/countries-handle-dsl";');
922
+ workflowEntries.push('\t"countries-dsl": await CountriesHandleDsl,');
899
923
  }
900
924
  else if (trigger === "sse") {
901
925
  imports.push('import SSEStreamDemo from "./workflows/sse/events/stream-demo";');
902
- workflowEntries.push('\t"sse-stream-demo": SSEStreamDemo,');
926
+ workflowEntries.push('\t"sse-stream-demo": await SSEStreamDemo,');
903
927
  if (triggers.includes("http")) {
904
928
  imports.push('import SSEPublishDemo from "./workflows/sse/events/publish-demo";');
905
- workflowEntries.push('\t"sse-publish-demo": SSEPublishDemo,');
929
+ workflowEntries.push('\t"sse-publish-demo": await SSEPublishDemo,');
906
930
  }
907
931
  }
908
932
  else if (trigger === "websocket") {
909
933
  imports.push('import WSEchoDemo from "./workflows/websocket/events/echo-demo";');
910
- workflowEntries.push('\t"ws-echo-demo": WSEchoDemo,');
934
+ workflowEntries.push('\t"ws-echo-demo": await WSEchoDemo,');
911
935
  }
912
936
  else if (trigger === "pubsub") {
913
937
  imports.push('import OnPubSubMessage from "./workflows/pubsub/messages/on-message";');
914
- workflowEntries.push('\t"on-pubsub-message": OnPubSubMessage,');
938
+ workflowEntries.push('\t"on-pubsub-message": await OnPubSubMessage,');
939
+ if (triggers.includes("http")) {
940
+ imports.push('import PublishOrder from "./workflows/pubsub/publish-order";');
941
+ workflowEntries.push('\t"publish-order": await PublishOrder,');
942
+ }
915
943
  }
916
944
  else if (trigger === "queue" || trigger === "worker") {
917
945
  imports.push(`import ProcessJob from "./workflows/${trigger}/jobs/process-job";`);
918
- workflowEntries.push('\t"process-job": ProcessJob,');
946
+ workflowEntries.push('\t"process-job": await ProcessJob,');
947
+ }
948
+ else if (trigger === "cron") {
949
+ imports.push('import CronHeartbeat from "./workflows/cron/heartbeat";');
950
+ workflowEntries.push('\t"cron-heartbeat": await CronHeartbeat,');
919
951
  }
920
952
  }
921
953
  if (examples) {
@@ -945,7 +977,7 @@ ${entriesSection}
945
977
  export default workflows;
946
978
  `;
947
979
  }
948
- function generateTriggerEntryFile(triggerKind, selectedTriggers = [triggerKind]) {
980
+ export function generateTriggerEntryFile(triggerKind, selectedTriggers = [triggerKind]) {
949
981
  if (triggerKind === "http") {
950
982
  const sseAlsoSelected = selectedTriggers.includes("sse");
951
983
  const wsAlsoSelected = selectedTriggers.includes("websocket");
@@ -1285,6 +1317,63 @@ export default class App {
1285
1317
  if (process.env.DISABLE_TRIGGER_RUN !== "true") {
1286
1318
  new App().run();
1287
1319
  }
1320
+ `;
1321
+ }
1322
+ if (triggerKind === "cron") {
1323
+ return `import { DefaultLogger } from "@blokjs/runner";
1324
+ import { type Span, metrics, trace } from "@opentelemetry/api";
1325
+ import CronServer from "./runner/CronServer";
1326
+
1327
+ export default class App {
1328
+ private cronServer: CronServer = <CronServer>{};
1329
+ protected trigger_initializer = 0;
1330
+ protected initializer = 0;
1331
+ protected tracer = trace.getTracer(
1332
+ process.env.PROJECT_NAME || "trigger-cron-server",
1333
+ process.env.PROJECT_VERSION || "0.0.1",
1334
+ );
1335
+ private logger = new DefaultLogger();
1336
+ protected app_cold_start = metrics.getMeter("default").createGauge("initialization", {
1337
+ description: "Application cold start",
1338
+ });
1339
+
1340
+ constructor() {
1341
+ this.initializer = performance.now();
1342
+ this.cronServer = new CronServer();
1343
+ }
1344
+
1345
+ async run() {
1346
+ this.tracer.startActiveSpan("initialization", async (span: Span) => {
1347
+ await this.cronServer.listen();
1348
+ this.initializer = performance.now() - this.initializer;
1349
+
1350
+ this.logger.log(\`Cron trigger initialized in \${(this.initializer).toFixed(2)}ms\`);
1351
+ this.app_cold_start.record(this.initializer, {
1352
+ pid: process.pid,
1353
+ env: process.env.NODE_ENV,
1354
+ app: process.env.APP_NAME,
1355
+ });
1356
+ span.end();
1357
+ });
1358
+ }
1359
+ }
1360
+
1361
+ if (process.env.DISABLE_TRIGGER_RUN !== "true") {
1362
+ new App().run();
1363
+ }
1364
+ `;
1365
+ }
1366
+ if (triggerKind === "grpc") {
1367
+ return `import { GrpcServer } from "@blokjs/trigger-grpc";
1368
+ import nodes from "../../Nodes";
1369
+ import workflows from "../../Workflows";
1370
+
1371
+ const host = process.env.GRPC_HOST || "0.0.0.0";
1372
+ const port = Number(process.env.GRPC_PORT || process.env.PORT || 4003);
1373
+
1374
+ if (process.env.DISABLE_TRIGGER_RUN !== "true") {
1375
+ new GrpcServer({ host, port, nodes, workflows }).start();
1376
+ }
1288
1377
  `;
1289
1378
  }
1290
1379
  return `// Entry point for ${triggerKind} trigger
@@ -1292,6 +1381,45 @@ if (process.env.DISABLE_TRIGGER_RUN !== "true") {
1292
1381
  console.log("${triggerKind} trigger not yet implemented");
1293
1382
  `;
1294
1383
  }
1384
+ export function generateCronExampleWorkflowFile() {
1385
+ return `import { node, step, workflow } from "@blokjs/core";
1386
+
1387
+ /**
1388
+ * Cron heartbeat — fires on a schedule, NOT on an HTTP request. \`blokctl dev\`
1389
+ * boots it via src/triggers/cron/index.ts and it fires every minute. The
1390
+ * schedule accepts an optional leading seconds field (e.g. \`* * * * * *\` for
1391
+ * every second). Add more cron workflows by exporting them and registering
1392
+ * them in src/Workflows.ts.
1393
+ */
1394
+ export default workflow(
1395
+ "Cron Heartbeat",
1396
+ { version: "1.0.0", trigger: { cron: { schedule: "* * * * *", timezone: "UTC" } } },
1397
+ () => {
1398
+ step("heartbeat", node("@blokjs/expr"), { expression: "({ ok: true, at: Date.now() })" }, { ephemeral: true });
1399
+ },
1400
+ );
1401
+ `;
1402
+ }
1403
+ export function generateCronServerFile() {
1404
+ return `import { CronTrigger } from "@blokjs/trigger-cron";
1405
+ import nodes from "../../../Nodes";
1406
+ import workflows from "../../../Workflows";
1407
+
1408
+ /**
1409
+ * CronServer — the cron trigger for this project.
1410
+ *
1411
+ * CronTrigger.listen() populates the NodeMap, registers workflows with the
1412
+ * WorkflowRegistry, then schedules a CronJob for every workflow whose trigger
1413
+ * is \`{ cron: { schedule, timezone? } }\`. There is no port to bind — the
1414
+ * scheduled job timers keep the process alive. Non-cron workflows sharing this
1415
+ * Workflows.ts are ignored by the scheduler (they run under their own trigger).
1416
+ */
1417
+ export default class CronServer extends CronTrigger {
1418
+ protected nodes: Record<string, import("@blokjs/runner").BlokService<unknown>> = nodes;
1419
+ protected workflows: Record<string, import("@blokjs/helper").WorkflowV2Builder> = workflows;
1420
+ }
1421
+ `;
1422
+ }
1295
1423
  function generateSSEServerFile() {
1296
1424
  return `import { serve } from "@hono/node-server";
1297
1425
  import { DefaultLogger, NodeMap, WorkflowRegistry } from "@blokjs/runner";
@@ -1578,8 +1706,12 @@ function updatePubSubProvider(triggerDestDir, provider) {
1578
1706
  },
1579
1707
  };
1580
1708
  const config = adapterConfigs[provider];
1581
- if (!config)
1709
+ if (!config) {
1710
+ content = content.replace(/import \{ GCPPubSubAdapter, PubSubTrigger \} from ["']@blokjs\/trigger-pubsub["'];/, 'import { PubSubTrigger } from "@blokjs/trigger-pubsub";');
1711
+ content = content.replace(/\n\tprotected adapter = new GCPPubSubAdapter\(\{[\s\S]*?\}\);\n/, "");
1712
+ fsExtra.writeFileSync(serverPath, content);
1582
1713
  return;
1714
+ }
1583
1715
  content = content.replace(/import \{ (\w+), (\w+) \} from ["']@blokjs\/trigger-pubsub["'];/, `import { ${config.importName}, PubSubTrigger } from "@blokjs/trigger-pubsub";`);
1584
1716
  content = content.replace(/(export default class \w+ extends PubSubTrigger \{[\s\S]*?)\n\tprotected adapter = new \w+\(\{[\s\S]*?\}\);/, `$1\n\tprotected adapter = ${config.init};`);
1585
1717
  fsExtra.writeFileSync(serverPath, content);
@@ -1637,6 +1769,7 @@ export function updateQueueProvider(triggerDestDir, provider, explicit) {
1637
1769
  export function getProviderDependencies(triggers, pubsubProvider, queueProvider, explicitQueueProvider = false) {
1638
1770
  const deps = {};
1639
1771
  const pubsubProviderDeps = {
1772
+ nats: { nats: "^2.28.0" },
1640
1773
  gcp: { "@google-cloud/pubsub": "^5.0.0" },
1641
1774
  aws: { "@aws-sdk/client-sns": "^3.980.0", "@aws-sdk/client-sqs": "^3.980.0" },
1642
1775
  azure: { "@azure/service-bus": "^7.9.5" },
@@ -1661,6 +1794,10 @@ export function getProviderDependencies(triggers, pubsubProvider, queueProvider,
1661
1794
  export function getProviderEnvVars(triggers, pubsubProvider, queueProvider, explicitQueueProvider = false) {
1662
1795
  const lines = [];
1663
1796
  const pubsubEnvVars = {
1797
+ nats: `
1798
+ # NATS (local pub/sub broker — zero cloud setup)
1799
+ NATS_SERVERS=localhost:4222
1800
+ BLOK_PUBSUB_ADAPTER=nats`,
1664
1801
  gcp: `
1665
1802
  # Google Cloud Pub/Sub
1666
1803
  GCP_PROJECT_ID=my-project