blokctl 1.2.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 +3 -0
- package/dist/commands/create/project.js +153 -19
- package/dist/commands/create/utils/Examples.d.ts +2 -2
- package/dist/commands/create/utils/Examples.js +8 -2
- package/dist/commands/dev/index.js +13 -1
- package/dist/commands/runtime/add.js +92 -29
- package/dist/commands/runtime/index.js +1 -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/package.json +3 -3
|
@@ -2,6 +2,9 @@ import type { OptionValues } from "commander";
|
|
|
2
2
|
export declare function createProject(opts: OptionValues, version: string, currentPath?: boolean, localRepoPath?: string): Promise<void>;
|
|
3
3
|
export declare function generateSharedNodesFile(_triggers: string[], _repoSource: string): string;
|
|
4
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;
|
|
5
8
|
export declare function updateQueueProvider(triggerDestDir: string, provider: string, explicit: boolean): void;
|
|
6
9
|
export declare function getProviderDependencies(triggers: string[], pubsubProvider: string, queueProvider: string, explicitQueueProvider?: boolean): Record<string, string>;
|
|
7
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.
|
|
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 || "
|
|
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 || "
|
|
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
|
-
|
|
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.
|
|
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);
|
|
@@ -902,23 +923,31 @@ export function generateSharedWorkflowsFile(triggers, runtimeKinds = [], example
|
|
|
902
923
|
}
|
|
903
924
|
else if (trigger === "sse") {
|
|
904
925
|
imports.push('import SSEStreamDemo from "./workflows/sse/events/stream-demo";');
|
|
905
|
-
workflowEntries.push('\t"sse-stream-demo": SSEStreamDemo,');
|
|
926
|
+
workflowEntries.push('\t"sse-stream-demo": await SSEStreamDemo,');
|
|
906
927
|
if (triggers.includes("http")) {
|
|
907
928
|
imports.push('import SSEPublishDemo from "./workflows/sse/events/publish-demo";');
|
|
908
|
-
workflowEntries.push('\t"sse-publish-demo": SSEPublishDemo,');
|
|
929
|
+
workflowEntries.push('\t"sse-publish-demo": await SSEPublishDemo,');
|
|
909
930
|
}
|
|
910
931
|
}
|
|
911
932
|
else if (trigger === "websocket") {
|
|
912
933
|
imports.push('import WSEchoDemo from "./workflows/websocket/events/echo-demo";');
|
|
913
|
-
workflowEntries.push('\t"ws-echo-demo": WSEchoDemo,');
|
|
934
|
+
workflowEntries.push('\t"ws-echo-demo": await WSEchoDemo,');
|
|
914
935
|
}
|
|
915
936
|
else if (trigger === "pubsub") {
|
|
916
937
|
imports.push('import OnPubSubMessage from "./workflows/pubsub/messages/on-message";');
|
|
917
|
-
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
|
+
}
|
|
918
943
|
}
|
|
919
944
|
else if (trigger === "queue" || trigger === "worker") {
|
|
920
945
|
imports.push(`import ProcessJob from "./workflows/${trigger}/jobs/process-job";`);
|
|
921
|
-
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,');
|
|
922
951
|
}
|
|
923
952
|
}
|
|
924
953
|
if (examples) {
|
|
@@ -948,7 +977,7 @@ ${entriesSection}
|
|
|
948
977
|
export default workflows;
|
|
949
978
|
`;
|
|
950
979
|
}
|
|
951
|
-
function generateTriggerEntryFile(triggerKind, selectedTriggers = [triggerKind]) {
|
|
980
|
+
export function generateTriggerEntryFile(triggerKind, selectedTriggers = [triggerKind]) {
|
|
952
981
|
if (triggerKind === "http") {
|
|
953
982
|
const sseAlsoSelected = selectedTriggers.includes("sse");
|
|
954
983
|
const wsAlsoSelected = selectedTriggers.includes("websocket");
|
|
@@ -1288,6 +1317,63 @@ export default class App {
|
|
|
1288
1317
|
if (process.env.DISABLE_TRIGGER_RUN !== "true") {
|
|
1289
1318
|
new App().run();
|
|
1290
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
|
+
}
|
|
1291
1377
|
`;
|
|
1292
1378
|
}
|
|
1293
1379
|
return `// Entry point for ${triggerKind} trigger
|
|
@@ -1295,6 +1381,45 @@ if (process.env.DISABLE_TRIGGER_RUN !== "true") {
|
|
|
1295
1381
|
console.log("${triggerKind} trigger not yet implemented");
|
|
1296
1382
|
`;
|
|
1297
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
|
+
}
|
|
1298
1423
|
function generateSSEServerFile() {
|
|
1299
1424
|
return `import { serve } from "@hono/node-server";
|
|
1300
1425
|
import { DefaultLogger, NodeMap, WorkflowRegistry } from "@blokjs/runner";
|
|
@@ -1581,8 +1706,12 @@ function updatePubSubProvider(triggerDestDir, provider) {
|
|
|
1581
1706
|
},
|
|
1582
1707
|
};
|
|
1583
1708
|
const config = adapterConfigs[provider];
|
|
1584
|
-
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);
|
|
1585
1713
|
return;
|
|
1714
|
+
}
|
|
1586
1715
|
content = content.replace(/import \{ (\w+), (\w+) \} from ["']@blokjs\/trigger-pubsub["'];/, `import { ${config.importName}, PubSubTrigger } from "@blokjs/trigger-pubsub";`);
|
|
1587
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};`);
|
|
1588
1717
|
fsExtra.writeFileSync(serverPath, content);
|
|
@@ -1640,6 +1769,7 @@ export function updateQueueProvider(triggerDestDir, provider, explicit) {
|
|
|
1640
1769
|
export function getProviderDependencies(triggers, pubsubProvider, queueProvider, explicitQueueProvider = false) {
|
|
1641
1770
|
const deps = {};
|
|
1642
1771
|
const pubsubProviderDeps = {
|
|
1772
|
+
nats: { nats: "^2.28.0" },
|
|
1643
1773
|
gcp: { "@google-cloud/pubsub": "^5.0.0" },
|
|
1644
1774
|
aws: { "@aws-sdk/client-sns": "^3.980.0", "@aws-sdk/client-sqs": "^3.980.0" },
|
|
1645
1775
|
azure: { "@azure/service-bus": "^7.9.5" },
|
|
@@ -1664,6 +1794,10 @@ export function getProviderDependencies(triggers, pubsubProvider, queueProvider,
|
|
|
1664
1794
|
export function getProviderEnvVars(triggers, pubsubProvider, queueProvider, explicitQueueProvider = false) {
|
|
1665
1795
|
const lines = [];
|
|
1666
1796
|
const pubsubEnvVars = {
|
|
1797
|
+
nats: `
|
|
1798
|
+
# NATS (local pub/sub broker — zero cloud setup)
|
|
1799
|
+
NATS_SERVERS=localhost:4222
|
|
1800
|
+
BLOK_PUBSUB_ADAPTER=nats`,
|
|
1667
1801
|
gcp: `
|
|
1668
1802
|
# Google Cloud Pub/Sub
|
|
1669
1803
|
GCP_PROJECT_ID=my-project
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
declare const node_file = "import { dirname, join } from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport ApiCall from \"@blokjs/api-call\";\nimport IfElse from \"@blokjs/if-else\";\nimport { discoverNodes } from \"@blokjs/runner\";\nimport type { NodeBase } from \"@blokjs/shared\";\nimport ExampleNodes from \"./nodes/examples/index\";\n\n// Published nodes (npm) + the example bundle are registered explicitly below.\n// Your OWN nodes under 'nodes/<name>/index.ts' are AUTO-DISCOVERED and registered\n// by their defineNode({ name }) \u2014 you never edit this file to add a node.\nconst here = dirname(fileURLToPath(import.meta.url));\nconst local = await discoverNodes(join(here, \"nodes\"));\n\nconst explicit: NodeBase[] = [
|
|
1
|
+
declare const node_file = "import { dirname, join } from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport ApiCall from \"@blokjs/api-call\";\nimport { HELPER_NODES } from \"@blokjs/helpers\";\nimport IfElse from \"@blokjs/if-else\";\nimport { discoverNodes } from \"@blokjs/runner\";\nimport type { NodeBase } from \"@blokjs/shared\";\nimport ExampleNodes from \"./nodes/examples/index\";\n\n// Published nodes (npm) + the example bundle are registered explicitly below.\n// Your OWN nodes under 'nodes/<name>/index.ts' are AUTO-DISCOVERED and registered\n// by their defineNode({ name }) \u2014 you never edit this file to add a node.\nconst here = dirname(fileURLToPath(import.meta.url));\nconst local = await discoverNodes(join(here, \"nodes\"));\n\nconst explicit: NodeBase[] = [\n\tApiCall,\n\tIfElse,\n\t...(Object.values(HELPER_NODES) as unknown as NodeBase[]),\n\t...(Object.values(ExampleNodes) as NodeBase[]),\n];\n\n// Map keys are cosmetic \u2014 the runner registers each node under its own node.name\n// (the canonical 'use:' ref). Duplicate refs throw at startup.\nconst nodes: { [key: string]: NodeBase } = {};\nfor (const node of [...explicit, ...local]) {\n\tnodes[(node as { name: string }).name] = node;\n}\n\nexport default nodes;\n";
|
|
2
2
|
declare const package_dependencies: {
|
|
3
3
|
ai: string;
|
|
4
4
|
"@ai-sdk/openai": string;
|
|
@@ -13,7 +13,7 @@ declare const package_dev_dependencies: {
|
|
|
13
13
|
"@types/pg": string;
|
|
14
14
|
};
|
|
15
15
|
declare const python3_file = "from pydantic import BaseModel\n\nfrom blok import node\nfrom blok.types.context import Context\n\n\nclass Input(BaseModel):\n \"\"\"Validated inputs for the {{NODE_NAME}} node.\"\"\"\n\n name: str = \"world\"\n\n\nclass Output(BaseModel):\n message: str\n\n\n@node(\"{{NODE_NAME}}\", \"Describe what {{NODE_NAME}} does\")\ndef run(ctx: Context, input: Input) -> Output:\n # `input` is already validated against Input; the return is validated\n # against Output and serialized for you.\n return Output(message=f\"Hello, {input.name}!\")\n";
|
|
16
|
-
declare const examples_url = "\nExamples:\n1- Open \"workflow-docs.json\" in your browser at http://localhost:4000/workflow-docs\n2- Open \"db-manager.json\" in your browser at http://localhost:4000/db-manager\n3- Open \"dashboard-gen.json\" in your browser at http://localhost:4000/dashboard-gen\n4- Open \"countries.json\" in your browser at http://localhost:4000/countries\n5- Open \"chat.json\" in your browser at http://localhost:4000/chat (set OPENROUTER_API_KEY first)\n6- Open \"chat-memory.json\" in your browser at http://localhost:4000/chat-memory (needs OPENROUTER_API_KEY + Redis at REDIS_URL)\n7- Webhook router: POST /webhooks/{stripe,github,linear} with signed bodies \u2014 set the matching *_WEBHOOK_SECRET env vars (needs --triggers webhook)\n8- LLM agent w/ tool calls: open http://localhost:4000/agent \u2014 model picks between get_weather and calculate tools (needs OPENROUTER_API_KEY + Redis)\n9- 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)\n10- Trigger references (NOT http): workflows/json/{cron-heartbeat,pubsub-on-order
|
|
16
|
+
declare const examples_url = "\nExamples:\n1- Open \"workflow-docs.json\" in your browser at http://localhost:4000/workflow-docs\n2- Open \"db-manager.json\" in your browser at http://localhost:4000/db-manager\n3- Open \"dashboard-gen.json\" in your browser at http://localhost:4000/dashboard-gen\n4- Open \"countries.json\" in your browser at http://localhost:4000/countries\n5- Open \"chat.json\" in your browser at http://localhost:4000/chat (set OPENROUTER_API_KEY first)\n6- Open \"chat-memory.json\" in your browser at http://localhost:4000/chat-memory (needs OPENROUTER_API_KEY + Redis at REDIS_URL)\n7- Webhook router: POST /webhooks/{stripe,github,linear} with signed bodies \u2014 set the matching *_WEBHOOK_SECRET env vars (needs --triggers webhook)\n8- LLM agent w/ tool calls: open http://localhost:4000/agent \u2014 model picks between get_weather and calculate tools (needs OPENROUTER_API_KEY + Redis)\n9- 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)\n10- 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 \u2014 read AGENTS.md \"Choosing a trigger\" to pick the right one by intent instead of defaulting to HTTP.\n\nFor more documentation, visit src/nodes/examples/README.md. The first three examples require a PostgreSQL database to function.\n";
|
|
17
17
|
declare const workflow_template = "import { http, node, step, workflow } from \"@blokjs/core\";\n\n/**\n * {{WORKFLOW_NAME}}\n *\n * Add steps with step(\"id\", node, inputs). Read a step's output downstream via\n * the handle it returns (const user = step(...); ... user.name). Control flow:\n * branch / forEach / switchOn / tryCatch \u2014 all from @blokjs/core.\n */\nexport default workflow(\n\t\"{{WORKFLOW_NAME}}\",\n\t{ version: \"1.0.0\", trigger: http.get(\"{{WORKFLOW_PATH}}\") },\n\t(req) => {\n\t\t// Echo the request body back. @blokjs/respond writes the HTTP response,\n\t\t// so mark it ephemeral (no state slot needed).\n\t\tstep(\"echo\", node(\"@blokjs/respond\"), { body: req.body }, { ephemeral: true });\n\t},\n);\n";
|
|
18
18
|
declare const supervisord_nodejs = "\n[supervisord]\nnodaemon=true\n\n[program:nodejs_app]\ncommand=npm start\ndirectory=/app\nautostart=true\nautorestart=true\nstderr_logfile=/var/log/nodejs.err.log\nstdout_logfile=/var/log/nodejs.out.log\n";
|
|
19
19
|
declare const supervisord_python = "\n[program:python_app]\ncommand=python3 /app/.blok/runtimes/python3/server.py\ndirectory=/app\nautostart=true\nautorestart=true\nstderr_logfile=/var/log/python.err.log\nstdout_logfile=/var/log/python.out.log\n";
|
|
@@ -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,7 +75,7 @@ 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
|
`;
|
|
@@ -4,7 +4,7 @@ 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
9
|
import { regenRuntimeStubs } from "../nodes/syncNodes.js";
|
|
10
10
|
const exec = util.promisify(child_process.exec);
|
|
@@ -106,6 +106,12 @@ export async function devProject(opts) {
|
|
|
106
106
|
bootCmd = `${rrBin}${bootCmd.slice(2)}`;
|
|
107
107
|
}
|
|
108
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
|
+
}
|
|
109
115
|
const cmdParts = bootCmd.split(" ");
|
|
110
116
|
const cmd = cmdParts[0];
|
|
111
117
|
const args = cmdParts.slice(1);
|
|
@@ -198,6 +204,12 @@ export async function devProject(opts) {
|
|
|
198
204
|
if (brokerConsumerKinds.has(trigger.kind)) {
|
|
199
205
|
console.log(` ${trigger.label}: consumes from broker (no HTTP endpoint)`);
|
|
200
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
|
+
}
|
|
201
213
|
else {
|
|
202
214
|
console.log(` ${trigger.label}: http://localhost:${trigger.port}/health-check`);
|
|
203
215
|
}
|
|
@@ -5,7 +5,7 @@ import color from "picocolors";
|
|
|
5
5
|
import { isNonInteractive } from "../../services/non-interactive.js";
|
|
6
6
|
import { detectRuntimes, getRuntimeDefinition } from "../../services/runtime-detector.js";
|
|
7
7
|
import { ensureRuntimeGitignore, rewriteRuntimeEnvBlock, rewriteSupervisordRuntimes, runtimeEnvKey, withRuntime, } from "../../services/runtime-mutations.js";
|
|
8
|
-
import { setupRuntime } from "../../services/runtime-setup.js";
|
|
8
|
+
import { buildRuntimeConfig, setupRuntime, } from "../../services/runtime-setup.js";
|
|
9
9
|
import { RuntimeCommandError, assertGrpcPortFree, assertSidecarKind, readConfigSafe, reportRuntimeError, resolveProjectRoot, resolveSdkSource, } from "./shared.js";
|
|
10
10
|
export async function runtimeAdd(kindArg, options) {
|
|
11
11
|
try {
|
|
@@ -54,6 +54,31 @@ export async function runtimeAdd(kindArg, options) {
|
|
|
54
54
|
const sdkDir = path.join(root, ".blok", "runtimes", kind);
|
|
55
55
|
const alreadyInstalled = Boolean(config.runtimes?.[kind]) || fs.existsSync(sdkDir);
|
|
56
56
|
p.intro(color.inverse(` Add ${def.label} runtime `));
|
|
57
|
+
if (options.enable === true) {
|
|
58
|
+
if (config.runtimes?.[kind]) {
|
|
59
|
+
p.outro(color.dim(`${def.label} is already wired into .blok/config.json.`));
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
if (!fs.existsSync(sdkDir)) {
|
|
63
|
+
throw new RuntimeCommandError(`${def.label} isn't scaffolded at ${path.relative(root, sdkDir)}. Run \`blokctl runtime add ${kind}\` (without --enable) to install it first.`);
|
|
64
|
+
}
|
|
65
|
+
const rt = (detected ?? (await detectRuntimes())).find((d) => d.kind === kind);
|
|
66
|
+
if (!rt)
|
|
67
|
+
throw new RuntimeCommandError(`Unknown runtime "${kind}".`);
|
|
68
|
+
const grpcPort = grpcPortOverride ?? rt.defaultGrpcPort;
|
|
69
|
+
const clash = Object.values(config.runtimes ?? {}).find((rc) => rc.kind !== kind && rc.grpcPort === grpcPort);
|
|
70
|
+
if (clash) {
|
|
71
|
+
throw new RuntimeCommandError(`gRPC port ${grpcPort} is already used by the ${clash.label} runtime. Pass --grpc-port <n> to pick another.`);
|
|
72
|
+
}
|
|
73
|
+
const rc = buildRuntimeConfig(rt, root);
|
|
74
|
+
if (grpcPortOverride !== undefined) {
|
|
75
|
+
rc.grpcPort = grpcPortOverride;
|
|
76
|
+
if (rc.grpcStartCmd)
|
|
77
|
+
rc.grpcStartCmd = rc.grpcStartCmd.split(String(rt.defaultGrpcPort)).join(String(grpcPortOverride));
|
|
78
|
+
}
|
|
79
|
+
finalizeRuntime(root, config, rc, kind, def.label);
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
57
82
|
if (alreadyInstalled && options.force !== true) {
|
|
58
83
|
if (nonInteractive) {
|
|
59
84
|
p.outro(color.dim(`${def.label} is already installed. Re-run with --force to reinstall.`));
|
|
@@ -107,37 +132,75 @@ export async function runtimeAdd(kindArg, options) {
|
|
|
107
132
|
rc.grpcStartCmd = rc.grpcStartCmd.split(String(rt.defaultGrpcPort)).join(String(grpcPortOverride));
|
|
108
133
|
}
|
|
109
134
|
s.stop(`${def.label} runtime ready`);
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
fs.mkdirSync(path.join(root, ".blok"), { recursive: true });
|
|
113
|
-
fs.writeFileSync(path.join(root, ".blok", "config.json"), `${JSON.stringify(nextConfig, null, 2)}\n`);
|
|
114
|
-
const envPath = path.join(root, ".env.local");
|
|
115
|
-
const envContent = fs.existsSync(envPath) ? fs.readFileSync(envPath, "utf8") : "";
|
|
116
|
-
fs.writeFileSync(envPath, rewriteRuntimeEnvBlock(envContent, remaining));
|
|
117
|
-
const supervisordPath = path.join(root, "supervisord.conf");
|
|
118
|
-
if (fs.existsSync(supervisordPath)) {
|
|
119
|
-
fs.writeFileSync(supervisordPath, rewriteSupervisordRuntimes(fs.readFileSync(supervisordPath, "utf8"), remaining));
|
|
120
|
-
}
|
|
121
|
-
const gitignorePath = path.join(root, ".gitignore");
|
|
122
|
-
if (fs.existsSync(gitignorePath)) {
|
|
123
|
-
const before = fs.readFileSync(gitignorePath, "utf8");
|
|
124
|
-
const after = ensureRuntimeGitignore(before);
|
|
125
|
-
if (after !== before)
|
|
126
|
-
fs.writeFileSync(gitignorePath, after);
|
|
135
|
+
if (stampHelloExample(root, source, kind)) {
|
|
136
|
+
p.log.success(`Shipped the ${def.label} hello example — ${color.cyan(`POST /runtimes/${kind}/hello`)} (registered in src/Workflows.ts)`);
|
|
127
137
|
}
|
|
128
|
-
|
|
129
|
-
`${color.green("✓")} .blok/config.json ${color.dim(`runtimes.${kind}`)}`,
|
|
130
|
-
`${color.green("✓")} .env.local ${color.dim(`RUNTIME_${runtimeEnvKey(kind)}_GRPC_PORT=${rc.grpcPort}`)}`,
|
|
131
|
-
fs.existsSync(supervisordPath)
|
|
132
|
-
? `${color.green("✓")} supervisord.conf ${color.dim(`[program:${kind}_runtime]`)}`
|
|
133
|
-
: "",
|
|
134
|
-
`${color.green("✓")} runtimes/${kind}/nodes/ ${color.dim("(your runtime nodes go here)")}`,
|
|
135
|
-
]
|
|
136
|
-
.filter(Boolean)
|
|
137
|
-
.join("\n"), `${def.label} added`);
|
|
138
|
-
p.outro(`Run ${color.cyan("blokctl dev")} to start it, then add ${color.cyan(`type: "runtime.${kind}"`)} steps to your workflows.`);
|
|
138
|
+
finalizeRuntime(root, config, rc, kind, def.label);
|
|
139
139
|
}
|
|
140
140
|
catch (err) {
|
|
141
141
|
reportRuntimeError(err);
|
|
142
142
|
}
|
|
143
143
|
}
|
|
144
|
+
function stampHelloExample(root, source, kind) {
|
|
145
|
+
const file = `runtime-${kind}-hello.ts`;
|
|
146
|
+
const srcFile = path.join(source, "examples", "ts-workflows", file);
|
|
147
|
+
const examplesDir = path.join(root, "src", "workflows", "examples");
|
|
148
|
+
const workflowsTs = path.join(root, "src", "Workflows.ts");
|
|
149
|
+
if (!fs.existsSync(srcFile) || !fs.existsSync(examplesDir) || !fs.existsSync(workflowsTs))
|
|
150
|
+
return false;
|
|
151
|
+
const importName = `Runtime${kind.charAt(0).toUpperCase()}${kind.slice(1)}Hello`;
|
|
152
|
+
const importLine = `import ${importName} from "./workflows/examples/runtime-${kind}-hello";`;
|
|
153
|
+
const entryLine = `\t"runtime-${kind}-hello": ${importName},`;
|
|
154
|
+
const content = fs.readFileSync(workflowsTs, "utf8");
|
|
155
|
+
if (content.includes(`./workflows/examples/runtime-${kind}-hello`)) {
|
|
156
|
+
fs.copyFileSync(srcFile, path.join(examplesDir, file));
|
|
157
|
+
return true;
|
|
158
|
+
}
|
|
159
|
+
const lines = content.split("\n");
|
|
160
|
+
const openIdx = lines.findIndex((l) => /^const workflows\b.*=\s*\{\s*$/.test(l));
|
|
161
|
+
let lastImport = -1;
|
|
162
|
+
for (let i = 0; i < (openIdx === -1 ? lines.length : openIdx); i++) {
|
|
163
|
+
if (/^import /.test(lines[i]))
|
|
164
|
+
lastImport = i;
|
|
165
|
+
}
|
|
166
|
+
if (openIdx === -1 || lastImport === -1) {
|
|
167
|
+
p.log.warn(`src/Workflows.ts doesn't look generated — register the ${kind} hello example yourself:\n ${importLine}\n ${entryLine.trim()}`);
|
|
168
|
+
return false;
|
|
169
|
+
}
|
|
170
|
+
lines.splice(openIdx + 1, 0, entryLine);
|
|
171
|
+
lines.splice(lastImport + 1, 0, importLine);
|
|
172
|
+
fs.copyFileSync(srcFile, path.join(examplesDir, file));
|
|
173
|
+
fs.writeFileSync(workflowsTs, lines.join("\n"));
|
|
174
|
+
return true;
|
|
175
|
+
}
|
|
176
|
+
function finalizeRuntime(root, config, rc, kind, label) {
|
|
177
|
+
const nextConfig = withRuntime(config, rc);
|
|
178
|
+
const remaining = Object.values(nextConfig.runtimes ?? {});
|
|
179
|
+
fs.mkdirSync(path.join(root, ".blok"), { recursive: true });
|
|
180
|
+
fs.writeFileSync(path.join(root, ".blok", "config.json"), `${JSON.stringify(nextConfig, null, 2)}\n`);
|
|
181
|
+
const envPath = path.join(root, ".env.local");
|
|
182
|
+
const envContent = fs.existsSync(envPath) ? fs.readFileSync(envPath, "utf8") : "";
|
|
183
|
+
fs.writeFileSync(envPath, rewriteRuntimeEnvBlock(envContent, remaining));
|
|
184
|
+
const supervisordPath = path.join(root, "supervisord.conf");
|
|
185
|
+
if (fs.existsSync(supervisordPath)) {
|
|
186
|
+
fs.writeFileSync(supervisordPath, rewriteSupervisordRuntimes(fs.readFileSync(supervisordPath, "utf8"), remaining));
|
|
187
|
+
}
|
|
188
|
+
const gitignorePath = path.join(root, ".gitignore");
|
|
189
|
+
if (fs.existsSync(gitignorePath)) {
|
|
190
|
+
const before = fs.readFileSync(gitignorePath, "utf8");
|
|
191
|
+
const after = ensureRuntimeGitignore(before);
|
|
192
|
+
if (after !== before)
|
|
193
|
+
fs.writeFileSync(gitignorePath, after);
|
|
194
|
+
}
|
|
195
|
+
p.note([
|
|
196
|
+
`${color.green("✓")} .blok/config.json ${color.dim(`runtimes.${kind}`)}`,
|
|
197
|
+
`${color.green("✓")} .env.local ${color.dim(`RUNTIME_${runtimeEnvKey(kind)}_GRPC_PORT=${rc.grpcPort}`)}`,
|
|
198
|
+
fs.existsSync(supervisordPath)
|
|
199
|
+
? `${color.green("✓")} supervisord.conf ${color.dim(`[program:${kind}_runtime]`)}`
|
|
200
|
+
: "",
|
|
201
|
+
`${color.green("✓")} runtimes/${kind}/nodes/ ${color.dim("(your runtime nodes go here)")}`,
|
|
202
|
+
]
|
|
203
|
+
.filter(Boolean)
|
|
204
|
+
.join("\n"), `${label} added`);
|
|
205
|
+
p.outro(`Run ${color.cyan("blokctl dev")} to start it, then add ${color.cyan(`type: "runtime.${kind}"`)} steps to your workflows.`);
|
|
206
|
+
}
|
|
@@ -15,6 +15,7 @@ runtime
|
|
|
15
15
|
.option("--local <path>", "Use a local blok repo for SDK source instead of fetching by version")
|
|
16
16
|
.option("--grpc-port <port>", "Override the gRPC port for this runtime")
|
|
17
17
|
.option("--force", "Reinstall if the runtime is already present")
|
|
18
|
+
.option("--enable", "Wire an already-scaffolded runtime (SDK dir on disk) into config.json without reinstalling")
|
|
18
19
|
.option("--skip-toolchain-check", "Add even if the language toolchain isn't detected")
|
|
19
20
|
.option("-y, --yes", "Skip prompts (non-interactive)")
|
|
20
21
|
.action(async (runtimeArg, options) => {
|
|
@@ -3,6 +3,7 @@ export interface RuntimeInfo {
|
|
|
3
3
|
label: string;
|
|
4
4
|
available: boolean;
|
|
5
5
|
version?: string;
|
|
6
|
+
minVersion?: string;
|
|
6
7
|
installHint: string;
|
|
7
8
|
defaultPort: number;
|
|
8
9
|
defaultGrpcPort: number;
|
|
@@ -21,6 +22,7 @@ export interface RuntimeInfo {
|
|
|
21
22
|
};
|
|
22
23
|
}
|
|
23
24
|
export declare function detectRr(): string | null;
|
|
25
|
+
export declare function detectJava(): string | null;
|
|
24
26
|
export declare function detectRuntimes(): Promise<RuntimeInfo[]>;
|
|
25
27
|
export declare function detectRuntimeVersion(kind: string): Promise<string | undefined>;
|
|
26
28
|
export declare function getRuntimeDefinition(kind: string): Omit<RuntimeInfo, "available" | "version"> | undefined;
|
|
@@ -1,8 +1,24 @@
|
|
|
1
1
|
import child_process from "node:child_process";
|
|
2
2
|
import util from "node:util";
|
|
3
|
+
import { compareSemver } from "./semver-utils.js";
|
|
3
4
|
const exec = util.promisify(child_process.exec);
|
|
4
5
|
export function detectRr() {
|
|
5
6
|
for (const bin of ["/opt/homebrew/bin/rr", "rr"]) {
|
|
7
|
+
try {
|
|
8
|
+
const out = child_process
|
|
9
|
+
.execSync(`${bin} --version`, { stdio: ["ignore", "pipe", "ignore"] })
|
|
10
|
+
.toString()
|
|
11
|
+
.trim();
|
|
12
|
+
if (/^rr version/i.test(out))
|
|
13
|
+
return bin;
|
|
14
|
+
}
|
|
15
|
+
catch {
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
export function detectJava() {
|
|
21
|
+
for (const bin of ["java", "/opt/homebrew/opt/openjdk/bin/java"]) {
|
|
6
22
|
try {
|
|
7
23
|
child_process.execSync(`${bin} --version`, { stdio: "ignore" });
|
|
8
24
|
return bin;
|
|
@@ -45,8 +61,9 @@ const RUNTIME_DEFINITIONS = [
|
|
|
45
61
|
defaultGrpcPort: 10002,
|
|
46
62
|
commands: ["rustc --version"],
|
|
47
63
|
toolchain: "rustc + cargo",
|
|
48
|
-
installDeps: "cargo build --
|
|
64
|
+
installDeps: "cargo build --features grpc",
|
|
49
65
|
startCmd: "cargo run",
|
|
66
|
+
grpcStartCmd: "cargo run --features grpc",
|
|
50
67
|
sdkDir: "rust",
|
|
51
68
|
},
|
|
52
69
|
{
|
|
@@ -85,6 +102,7 @@ const RUNTIME_DEFINITIONS = [
|
|
|
85
102
|
{
|
|
86
103
|
kind: "php",
|
|
87
104
|
label: "PHP",
|
|
105
|
+
minVersion: "8.2.0",
|
|
88
106
|
installHint: "Install PHP 8.2+ + RoadRunner: https://php.net/downloads (rr: brew install roadrunner)",
|
|
89
107
|
defaultPort: 9005,
|
|
90
108
|
defaultGrpcPort: 10005,
|
|
@@ -103,13 +121,14 @@ const RUNTIME_DEFINITIONS = [
|
|
|
103
121
|
{
|
|
104
122
|
kind: "ruby",
|
|
105
123
|
label: "Ruby",
|
|
106
|
-
|
|
124
|
+
minVersion: "3.1.0",
|
|
125
|
+
installHint: "Install Ruby 3.1+: https://ruby-lang.org/en/downloads/ (macOS: brew install ruby)",
|
|
107
126
|
defaultPort: 9006,
|
|
108
127
|
defaultGrpcPort: 10006,
|
|
109
128
|
commands: ["ruby --version", "/opt/homebrew/opt/ruby/bin/ruby --version"],
|
|
110
129
|
toolchain: "ruby + bundler",
|
|
111
130
|
installDeps: "bundle install",
|
|
112
|
-
startCmd: "bundle exec
|
|
131
|
+
startCmd: "bundle exec ruby bin/serve.rb",
|
|
113
132
|
sdkDir: "ruby",
|
|
114
133
|
secondaryTool: {
|
|
115
134
|
name: "Bundler",
|
|
@@ -171,10 +190,12 @@ export async function detectRuntimes() {
|
|
|
171
190
|
};
|
|
172
191
|
for (const cmd of def.commands) {
|
|
173
192
|
const output = await tryExec(cmd);
|
|
174
|
-
if (output)
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
193
|
+
if (!output)
|
|
194
|
+
continue;
|
|
195
|
+
info.available = true;
|
|
196
|
+
const parsed = parseVersion(output, def.kind);
|
|
197
|
+
if (parsed && (!info.version || compareSemver(parsed, info.version) > 0)) {
|
|
198
|
+
info.version = parsed;
|
|
178
199
|
}
|
|
179
200
|
}
|
|
180
201
|
if (def.secondaryTool) {
|
|
@@ -196,13 +217,16 @@ export async function detectRuntimeVersion(kind) {
|
|
|
196
217
|
const def = RUNTIME_DEFINITIONS.find((r) => r.kind === kind);
|
|
197
218
|
if (!def)
|
|
198
219
|
return undefined;
|
|
220
|
+
let best;
|
|
199
221
|
for (const cmd of def.commands) {
|
|
200
222
|
const output = await tryExec(cmd);
|
|
201
|
-
if (output)
|
|
202
|
-
|
|
203
|
-
|
|
223
|
+
if (!output)
|
|
224
|
+
continue;
|
|
225
|
+
const parsed = parseVersion(output, def.kind);
|
|
226
|
+
if (parsed && (!best || compareSemver(parsed, best) > 0))
|
|
227
|
+
best = parsed;
|
|
204
228
|
}
|
|
205
|
-
return
|
|
229
|
+
return best;
|
|
206
230
|
}
|
|
207
231
|
export function getRuntimeDefinition(kind) {
|
|
208
232
|
return RUNTIME_DEFINITIONS.find((r) => r.kind === kind);
|
|
@@ -36,6 +36,7 @@ export interface ProjectConfig {
|
|
|
36
36
|
}
|
|
37
37
|
export type ProjectRuntimeConfig = ProjectConfig;
|
|
38
38
|
export declare function setupRuntime(runtime: RuntimeInfo, githubRepoLocal: string, projectDir: string, spinner: SpinnerHandler): Promise<RuntimeConfig>;
|
|
39
|
+
export declare function buildRuntimeConfig(runtime: RuntimeInfo, projectDir: string, startCmd?: string): RuntimeConfig;
|
|
39
40
|
export declare function generateGoNodeRegistry(projectDir: string): string;
|
|
40
41
|
export declare function generateRustNodeRegistry(projectDir: string): string;
|
|
41
42
|
export declare function generateJavaNodeRegistry(projectDir: string): string;
|
|
@@ -21,7 +21,7 @@ export async function setupRuntime(runtime, githubRepoLocal, projectDir, spinner
|
|
|
21
21
|
let startCmdOverride;
|
|
22
22
|
switch (runtime.kind) {
|
|
23
23
|
case "python3":
|
|
24
|
-
await setupPython3(blokctlRuntimeDir, spinner);
|
|
24
|
+
startCmdOverride = await setupPython3(blokctlRuntimeDir, spinner);
|
|
25
25
|
break;
|
|
26
26
|
case "go":
|
|
27
27
|
await setupGo(blokctlRuntimeDir, spinner);
|
|
@@ -39,7 +39,7 @@ export async function setupRuntime(runtime, githubRepoLocal, projectDir, spinner
|
|
|
39
39
|
await setupPhp(blokctlRuntimeDir, spinner);
|
|
40
40
|
break;
|
|
41
41
|
case "ruby":
|
|
42
|
-
startCmdOverride = await setupRuby(blokctlRuntimeDir, spinner
|
|
42
|
+
startCmdOverride = await setupRuby(blokctlRuntimeDir, spinner);
|
|
43
43
|
break;
|
|
44
44
|
}
|
|
45
45
|
if (runtime.kind === "go") {
|
|
@@ -55,16 +55,24 @@ export async function setupRuntime(runtime, githubRepoLocal, projectDir, spinner
|
|
|
55
55
|
generateCSharpNodeRegistry(projectDir);
|
|
56
56
|
}
|
|
57
57
|
spinner.message(`${runtime.label} runtime setup complete.`);
|
|
58
|
+
return buildRuntimeConfig(runtime, projectDir, startCmdOverride || runtime.startCmd);
|
|
59
|
+
}
|
|
60
|
+
export function buildRuntimeConfig(runtime, projectDir, startCmd) {
|
|
61
|
+
const blokctlRuntimeDir = path.join(projectDir, ".blok", "runtimes", runtime.kind);
|
|
58
62
|
return {
|
|
59
63
|
port: runtime.defaultPort,
|
|
60
64
|
grpcPort: runtime.defaultGrpcPort,
|
|
61
|
-
startCmd:
|
|
65
|
+
startCmd: startCmd ?? runtime.startCmd,
|
|
62
66
|
grpcStartCmd: runtime.grpcStartCmd,
|
|
63
67
|
cwd: path.relative(projectDir, blokctlRuntimeDir),
|
|
64
68
|
kind: runtime.kind,
|
|
65
69
|
label: runtime.label,
|
|
66
70
|
version: runtime.version,
|
|
67
|
-
requiredVersion: runtime.
|
|
71
|
+
requiredVersion: runtime.minVersion
|
|
72
|
+
? computeDefaultConstraint(runtime.minVersion)
|
|
73
|
+
: runtime.version
|
|
74
|
+
? computeDefaultConstraint(runtime.version)
|
|
75
|
+
: undefined,
|
|
68
76
|
transport: "grpc",
|
|
69
77
|
};
|
|
70
78
|
}
|
|
@@ -79,6 +87,7 @@ async function setupPython3(sdkDir, spinner) {
|
|
|
79
87
|
await exec(`"${venvPip}" install -r "${requirementsFile}"`, { cwd: sdkDir });
|
|
80
88
|
}
|
|
81
89
|
spinner.message("Python3 packages installed.");
|
|
90
|
+
return "python3_runtime/bin/python3 bin/serve.py";
|
|
82
91
|
}
|
|
83
92
|
async function createPythonVenv(sdkDir) {
|
|
84
93
|
await exec("python3 -m venv python3_runtime", { cwd: sdkDir, timeout: 60000 });
|
|
@@ -342,7 +351,7 @@ function collectFilesRecursive(dir, ext) {
|
|
|
342
351
|
}
|
|
343
352
|
async function setupRust(sdkDir, spinner) {
|
|
344
353
|
spinner.message("Building Rust project (this may take a few minutes on first build)...");
|
|
345
|
-
await exec("cargo build --
|
|
354
|
+
await exec("cargo build --features grpc", { cwd: sdkDir, timeout: 600000 });
|
|
346
355
|
spinner.message("Rust project built.");
|
|
347
356
|
}
|
|
348
357
|
async function setupJava(sdkDir, spinner) {
|
|
@@ -373,7 +382,7 @@ async function setupPhp(sdkDir, spinner) {
|
|
|
373
382
|
await exec("composer install --no-dev --optimize-autoloader", { cwd: sdkDir, timeout: 120000 });
|
|
374
383
|
spinner.message("PHP dependencies installed.");
|
|
375
384
|
}
|
|
376
|
-
async function setupRuby(sdkDir, spinner
|
|
385
|
+
async function setupRuby(sdkDir, spinner) {
|
|
377
386
|
const bundleCandidates = ["bundle", "/opt/homebrew/opt/ruby/bin/bundle"];
|
|
378
387
|
let resolvedBundle = "bundle";
|
|
379
388
|
for (const bin of bundleCandidates) {
|
|
@@ -392,7 +401,8 @@ async function setupRuby(sdkDir, spinner, port) {
|
|
|
392
401
|
spinner.message("Installing Ruby dependencies...");
|
|
393
402
|
await exec(`"${resolvedBundle}" install`, { cwd: sdkDir, timeout: 120000 });
|
|
394
403
|
spinner.message("Ruby dependencies installed.");
|
|
395
|
-
|
|
404
|
+
const resolvedRuby = resolvedBundle.includes("/") ? path.join(path.dirname(resolvedBundle), "ruby") : "ruby";
|
|
405
|
+
return `${resolvedBundle} exec ${resolvedRuby} bin/serve.rb`;
|
|
396
406
|
}
|
|
397
407
|
export function writeProjectConfig(projectDir, runtimeConfigs, triggerConfigs, observabilityConfigs) {
|
|
398
408
|
const config = {};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "blokctl",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.0",
|
|
4
4
|
"author": "Deskree Technologies Inc.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"description": "cli for blok",
|
|
@@ -30,8 +30,8 @@
|
|
|
30
30
|
"keywords": ["blokctl", "cli", "blok", "blok"],
|
|
31
31
|
"dependencies": {
|
|
32
32
|
"@ai-sdk/openai": "^1.3.22",
|
|
33
|
-
"@blokjs/helper": "^1.
|
|
34
|
-
"@blokjs/runner": "^1.
|
|
33
|
+
"@blokjs/helper": "^1.3.0",
|
|
34
|
+
"@blokjs/runner": "^1.3.0",
|
|
35
35
|
"@clack/prompts": "^1.0.0",
|
|
36
36
|
"ai": "^4.3.16",
|
|
37
37
|
"better-sqlite3": "^12.6.2",
|