blokctl 0.6.20 → 0.7.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 (77) hide show
  1. package/dist/__tests__/modular-observability.capstone.e2e.test.js +72 -0
  2. package/dist/commands/create/node.js +46 -66
  3. package/dist/commands/create/project.js +55 -9
  4. package/dist/commands/create/utils/Examples.d.ts +8 -20
  5. package/dist/commands/create/utils/Examples.js +138 -412
  6. package/dist/commands/dev/index.js +40 -1
  7. package/dist/commands/gen/appTypes.js +40 -1
  8. package/dist/commands/generate/NodeGenerator.d.ts +0 -2
  9. package/dist/commands/generate/NodeGenerator.js +0 -20
  10. package/dist/commands/generate/RuntimeGenerator.d.ts +0 -2
  11. package/dist/commands/generate/RuntimeGenerator.js +0 -19
  12. package/dist/commands/generate/RuntimeGenerator.test.js +0 -29
  13. package/dist/commands/generate/TriggerGenerator.d.ts +0 -2
  14. package/dist/commands/generate/TriggerGenerator.js +0 -19
  15. package/dist/commands/generate/WorkflowGenerator.d.ts +0 -2
  16. package/dist/commands/generate/WorkflowGenerator.js +0 -19
  17. package/dist/commands/generate/e2e/NodeGenerator.e2e.test.js +0 -12
  18. package/dist/commands/generate/e2e/RuntimeGenerator.e2e.test.js +0 -12
  19. package/dist/commands/generate/e2e/TriggerGenerator.e2e.test.js +0 -14
  20. package/dist/commands/monitor/monitor-component.js +5 -5
  21. package/dist/commands/observability/add.d.ts +2 -0
  22. package/dist/commands/observability/add.js +113 -0
  23. package/dist/commands/observability/alerting-module.test.js +43 -0
  24. package/dist/commands/observability/apply.d.ts +10 -0
  25. package/dist/commands/observability/apply.js +11 -0
  26. package/dist/commands/observability/descriptor.d.ts +37 -0
  27. package/dist/commands/observability/descriptor.js +203 -0
  28. package/dist/commands/observability/descriptor.test.d.ts +1 -0
  29. package/dist/commands/observability/descriptor.test.js +40 -0
  30. package/dist/commands/observability/index.d.ts +1 -0
  31. package/dist/commands/observability/index.js +53 -0
  32. package/dist/commands/observability/list.d.ts +2 -0
  33. package/dist/commands/observability/list.js +45 -0
  34. package/dist/commands/observability/logging-module.test.d.ts +1 -0
  35. package/dist/commands/observability/logging-module.test.js +43 -0
  36. package/dist/commands/observability/obs-stack-module.test.d.ts +1 -0
  37. package/dist/commands/observability/obs-stack-module.test.js +33 -0
  38. package/dist/commands/observability/remove.d.ts +2 -0
  39. package/dist/commands/observability/remove.js +62 -0
  40. package/dist/commands/observability/shared.d.ts +6 -0
  41. package/dist/commands/observability/shared.js +23 -0
  42. package/dist/commands/observability/status.d.ts +2 -0
  43. package/dist/commands/observability/status.js +36 -0
  44. package/dist/commands/observability/tracing-module.test.d.ts +1 -0
  45. package/dist/commands/observability/tracing-module.test.js +42 -0
  46. package/dist/commands/profile/index.js +7 -10
  47. package/dist/commands/watch/format.d.ts +23 -0
  48. package/dist/commands/watch/format.js +60 -0
  49. package/dist/commands/watch/index.d.ts +1 -0
  50. package/dist/commands/watch/index.js +53 -0
  51. package/dist/commands/watch/sse.d.ts +16 -0
  52. package/dist/commands/watch/sse.js +82 -0
  53. package/dist/index.d.ts +2 -0
  54. package/dist/index.js +4 -0
  55. package/dist/services/obs-setup.d.ts +5 -0
  56. package/dist/services/obs-setup.js +68 -0
  57. package/dist/services/obs-setup.test.d.ts +1 -0
  58. package/dist/services/obs-setup.test.js +71 -0
  59. package/dist/services/obs-tiers.d.ts +9 -0
  60. package/dist/services/obs-tiers.js +16 -0
  61. package/dist/services/observability-mutations.d.ts +4 -0
  62. package/dist/services/observability-mutations.js +46 -0
  63. package/dist/services/observability-mutations.test.d.ts +1 -0
  64. package/dist/services/observability-mutations.test.js +57 -0
  65. package/dist/services/runtime-setup.d.ts +12 -1
  66. package/dist/services/runtime-setup.js +274 -14
  67. package/dist/studio-dist/assets/{index-BD8_9YPN.js → index-CnFqCRQe.js} +17 -17
  68. package/dist/studio-dist/index.html +1 -1
  69. package/package.json +3 -3
  70. package/dist/commands/generate/GenerationAnalytics.d.ts +0 -61
  71. package/dist/commands/generate/GenerationAnalytics.js +0 -163
  72. package/dist/commands/generate/GenerationAnalytics.test.js +0 -407
  73. package/dist/commands/generate/PromptVersioning.d.ts +0 -25
  74. package/dist/commands/generate/PromptVersioning.js +0 -71
  75. package/dist/commands/generate/PromptVersioning.test.js +0 -120
  76. /package/dist/{commands/generate/GenerationAnalytics.test.d.ts → __tests__/modular-observability.capstone.e2e.test.d.ts} +0 -0
  77. /package/dist/commands/{generate/PromptVersioning.test.d.ts → observability/alerting-module.test.d.ts} +0 -0
@@ -0,0 +1,57 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { rewriteObservabilityEnvBlock, withObservabilityModule, withoutObservabilityModule, } from "./observability-mutations.js";
3
+ const mod = (addedAt = "2026-01-01T00:00:00.000Z") => ({ enabled: true, addedAt });
4
+ describe("withObservabilityModule / withoutObservabilityModule", () => {
5
+ it("adds a module while preserving runtimes + triggers", () => {
6
+ const base = { runtimes: { go: { port: 0, startCmd: "", cwd: "", kind: "go", label: "Go" } } };
7
+ const next = withObservabilityModule(base, "metrics", mod());
8
+ expect(next.observability).toEqual({ metrics: mod() });
9
+ expect(next.runtimes).toBe(base.runtimes);
10
+ });
11
+ it("re-adding the same module is a no-op (identical output)", () => {
12
+ const a = withObservabilityModule({}, "tracing", mod());
13
+ const b = withObservabilityModule(a, "tracing", mod());
14
+ expect(b).toEqual(a);
15
+ });
16
+ it("removing the last module drops the observability key entirely", () => {
17
+ const one = withObservabilityModule({}, "metrics", mod());
18
+ const gone = withoutObservabilityModule(one, "metrics");
19
+ expect(gone.observability).toBeUndefined();
20
+ });
21
+ it("removing an absent module is a no-op", () => {
22
+ const cfg = { observability: { metrics: mod() } };
23
+ expect(withoutObservabilityModule(cfg, "tracing")).toBe(cfg);
24
+ });
25
+ it("removing one of several keeps the rest", () => {
26
+ const cfg = withObservabilityModule(withObservabilityModule({}, "metrics", mod()), "tracing", mod());
27
+ const next = withoutObservabilityModule(cfg, "metrics");
28
+ expect(Object.keys(next.observability ?? {})).toEqual(["tracing"]);
29
+ });
30
+ });
31
+ describe("rewriteObservabilityEnvBlock", () => {
32
+ it("appends a fenced block and is idempotent (run twice = identical)", () => {
33
+ const blocks = ["BLOK_TRACE_STORE=sqlite", "# BLOK_METRICS_DISABLED=1"];
34
+ const once = rewriteObservabilityEnvBlock("PORT=4000\n", blocks);
35
+ const twice = rewriteObservabilityEnvBlock(once, blocks);
36
+ expect(twice).toBe(once);
37
+ expect(once).toContain("PORT=4000");
38
+ expect(once).toContain("BLOK_TRACE_STORE=sqlite");
39
+ expect(once.match(/managed by blokctl/g)?.length).toBe(2);
40
+ });
41
+ it("preserves unrelated env vars when the module set changes", () => {
42
+ const first = rewriteObservabilityEnvBlock("SECRET=abc\n", ["BLOK_TRACE_STORE=sqlite"]);
43
+ const second = rewriteObservabilityEnvBlock(first, ["CONSOLE_LOG_ACTIVE=true"]);
44
+ expect(second).toContain("SECRET=abc");
45
+ expect(second).toContain("CONSOLE_LOG_ACTIVE=true");
46
+ expect(second).not.toContain("BLOK_TRACE_STORE");
47
+ });
48
+ it("removes the block when no modules remain", () => {
49
+ const withBlock = rewriteObservabilityEnvBlock("SECRET=abc\n", ["BLOK_TRACE_STORE=sqlite"]);
50
+ const cleared = rewriteObservabilityEnvBlock(withBlock, []);
51
+ expect(cleared).toBe("SECRET=abc\n");
52
+ expect(cleared).not.toContain("managed by blokctl");
53
+ });
54
+ it("rejects BLOK_METRICS_ENABLED (only the DISABLED kill-switch is supported)", () => {
55
+ expect(() => rewriteObservabilityEnvBlock("", ["BLOK_METRICS_ENABLED=false"])).toThrow(/BLOK_METRICS_ENABLED/);
56
+ });
57
+ });
@@ -23,13 +23,24 @@ export interface TriggerConfig {
23
23
  entryPoint: string;
24
24
  startCmd: string;
25
25
  }
26
+ export interface ObservabilityModuleConfig {
27
+ enabled: boolean;
28
+ addedAt: string;
29
+ version?: string;
30
+ settings?: Record<string, unknown>;
31
+ }
26
32
  export interface ProjectConfig {
27
33
  triggers?: Record<string, TriggerConfig>;
28
34
  runtimes?: Record<string, RuntimeConfig>;
35
+ observability?: Record<string, ObservabilityModuleConfig>;
29
36
  }
30
37
  export type ProjectRuntimeConfig = ProjectConfig;
31
38
  export declare function setupRuntime(runtime: RuntimeInfo, githubRepoLocal: string, projectDir: string, spinner: SpinnerHandler): Promise<RuntimeConfig>;
32
- export declare function writeProjectConfig(projectDir: string, runtimeConfigs: RuntimeConfig[], triggerConfigs?: TriggerConfig[]): void;
39
+ export declare function generateGoNodeRegistry(projectDir: string): string;
40
+ export declare function generateRustNodeRegistry(projectDir: string): string;
41
+ export declare function generateJavaNodeRegistry(projectDir: string): string;
42
+ export declare function generateCSharpNodeRegistry(projectDir: string): string;
43
+ export declare function writeProjectConfig(projectDir: string, runtimeConfigs: RuntimeConfig[], triggerConfigs?: TriggerConfig[], observabilityConfigs?: Record<string, ObservabilityModuleConfig>): void;
33
44
  export declare function readProjectConfig(projectDir: string): ProjectConfig | null;
34
45
  export declare function generateRuntimeEnvVars(runtimeConfigs: RuntimeConfig[]): string;
35
46
  export declare function generateSupervisordConfig(runtimeConfigs: RuntimeConfig[]): 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, projectRuntimeDir, spinner);
24
+ await setupPython3(blokctlRuntimeDir, spinner);
25
25
  break;
26
26
  case "go":
27
27
  await setupGo(blokctlRuntimeDir, spinner);
@@ -42,6 +42,18 @@ export async function setupRuntime(runtime, githubRepoLocal, projectDir, spinner
42
42
  startCmdOverride = await setupRuby(blokctlRuntimeDir, spinner, runtime.defaultPort);
43
43
  break;
44
44
  }
45
+ if (runtime.kind === "go") {
46
+ generateGoNodeRegistry(projectDir);
47
+ }
48
+ else if (runtime.kind === "rust") {
49
+ generateRustNodeRegistry(projectDir);
50
+ }
51
+ else if (runtime.kind === "java") {
52
+ generateJavaNodeRegistry(projectDir);
53
+ }
54
+ else if (runtime.kind === "csharp") {
55
+ generateCSharpNodeRegistry(projectDir);
56
+ }
45
57
  spinner.message(`${runtime.label} runtime setup complete.`);
46
58
  return {
47
59
  port: runtime.defaultPort,
@@ -56,7 +68,7 @@ export async function setupRuntime(runtime, githubRepoLocal, projectDir, spinner
56
68
  transport: "grpc",
57
69
  };
58
70
  }
59
- async function setupPython3(sdkDir, projectRuntimeDir, spinner) {
71
+ async function setupPython3(sdkDir, spinner) {
60
72
  spinner.message("Creating Python3 virtual environment...");
61
73
  await createPythonVenv(sdkDir);
62
74
  spinner.message("Python3 virtual environment created.");
@@ -67,16 +79,6 @@ async function setupPython3(sdkDir, projectRuntimeDir, spinner) {
67
79
  await exec(`"${venvPip}" install -r "${requirementsFile}"`, { cwd: sdkDir });
68
80
  }
69
81
  spinner.message("Python3 packages installed.");
70
- const nodesLink = path.join(projectRuntimeDir, "nodes");
71
- const sdkNodesDir = path.join(sdkDir, "nodes");
72
- if (fsExtra.existsSync(sdkNodesDir) && !fsExtra.existsSync(nodesLink)) {
73
- fsExtra.symlinkSync(sdkNodesDir, nodesLink, "junction");
74
- }
75
- const coreLink = path.join(projectRuntimeDir, "core");
76
- const sdkCoreDir = path.join(sdkDir, "core");
77
- if (fsExtra.existsSync(sdkCoreDir) && !fsExtra.existsSync(coreLink)) {
78
- fsExtra.symlinkSync(sdkCoreDir, coreLink, "junction");
79
- }
80
82
  }
81
83
  async function createPythonVenv(sdkDir) {
82
84
  await exec("python3 -m venv python3_runtime", { cwd: sdkDir, timeout: 60000 });
@@ -86,6 +88,258 @@ async function setupGo(sdkDir, spinner) {
86
88
  await exec("go mod download", { cwd: sdkDir, timeout: 120000 });
87
89
  spinner.message("Go dependencies installed.");
88
90
  }
91
+ export function generateGoNodeRegistry(projectDir) {
92
+ const goSdkDir = path.join(projectDir, ".blok", "runtimes", "go");
93
+ const nodesSrcDir = path.join(projectDir, "runtimes", "go", "nodes");
94
+ const usernodesDir = path.join(goSdkDir, "usernodes");
95
+ const registryFile = path.join(goSdkDir, "cmd", "server", "register_user_nodes.go");
96
+ fsExtra.removeSync(usernodesDir);
97
+ const nodes = [];
98
+ if (fsExtra.existsSync(nodesSrcDir)) {
99
+ let i = 0;
100
+ for (const entry of fsExtra.readdirSync(nodesSrcDir, { withFileTypes: true })) {
101
+ if (!entry.isDirectory())
102
+ continue;
103
+ const srcDir = path.join(nodesSrcDir, entry.name);
104
+ const goFiles = fsExtra.readdirSync(srcDir).filter((f) => f.endsWith(".go"));
105
+ if (goFiles.length === 0)
106
+ continue;
107
+ const exportsRegister = goFiles.some((f) => /func\s+Register\s*\(/.test(fsExtra.readFileSync(path.join(srcDir, f), "utf8")));
108
+ if (!exportsRegister) {
109
+ console.warn(`[blokctl] skipping Go node '${entry.name}': no exported Register(registry) found`);
110
+ continue;
111
+ }
112
+ const destDir = path.join(usernodesDir, entry.name);
113
+ fsExtra.ensureDirSync(destDir);
114
+ for (const f of goFiles) {
115
+ fsExtra.copySync(path.join(srcDir, f), path.join(destDir, f));
116
+ }
117
+ nodes.push({
118
+ alias: `usernode${i++}`,
119
+ importPath: `github.com/nickincloud/blok-go/usernodes/${entry.name}`,
120
+ });
121
+ }
122
+ }
123
+ const importLines = [
124
+ '\tblok "github.com/nickincloud/blok-go"',
125
+ ...nodes.map((n) => `\t${n.alias} "${n.importPath}"`),
126
+ ];
127
+ const callLines = nodes.map((n) => `\t${n.alias}.Register(registry)`);
128
+ const content = `// Code generated by blokctl. DO NOT EDIT.
129
+ package main
130
+
131
+ import (
132
+ ${importLines.join("\n")}
133
+ )
134
+
135
+ // registerUserNodes registers nodes scaffolded under runtimes/go/nodes.
136
+ func registerUserNodes(registry *blok.NodeRegistry) {
137
+ ${callLines.join("\n")}
138
+ }
139
+ `;
140
+ fsExtra.ensureDirSync(path.dirname(registryFile));
141
+ fsExtra.writeFileSync(registryFile, content);
142
+ return registryFile;
143
+ }
144
+ export function generateRustNodeRegistry(projectDir) {
145
+ const rustSdkDir = path.join(projectDir, ".blok", "runtimes", "rust");
146
+ const nodesSrcDir = path.join(projectDir, "runtimes", "rust", "nodes");
147
+ const usernodesDir = path.join(rustSdkDir, "src", "user_nodes");
148
+ fsExtra.removeSync(usernodesDir);
149
+ fsExtra.ensureDirSync(usernodesDir);
150
+ const toModIdent = (name) => {
151
+ let id = name.replace(/[^a-zA-Z0-9_]/g, "_");
152
+ if (/^[0-9]/.test(id))
153
+ id = `_${id}`;
154
+ return id;
155
+ };
156
+ const mods = [];
157
+ if (fsExtra.existsSync(nodesSrcDir)) {
158
+ for (const entry of fsExtra.readdirSync(nodesSrcDir, { withFileTypes: true })) {
159
+ if (!entry.isDirectory())
160
+ continue;
161
+ const srcDir = path.join(nodesSrcDir, entry.name);
162
+ const rsFiles = fsExtra.readdirSync(srcDir).filter((f) => f.endsWith(".rs"));
163
+ if (rsFiles.length === 0)
164
+ continue;
165
+ const rootFile = rsFiles.find((f) => /fn\s+register\s*\(/.test(fsExtra.readFileSync(path.join(srcDir, f), "utf8")));
166
+ if (!rootFile) {
167
+ console.warn(`[blokctl] skipping Rust node '${entry.name}': no .rs exporting fn register(registry) found`);
168
+ continue;
169
+ }
170
+ const modIdent = toModIdent(entry.name);
171
+ const destDir = path.join(usernodesDir, modIdent);
172
+ fsExtra.ensureDirSync(destDir);
173
+ const siblings = rsFiles.filter((f) => f !== rootFile);
174
+ for (const f of siblings) {
175
+ fsExtra.copySync(path.join(srcDir, f), path.join(destDir, f));
176
+ }
177
+ let rootSrc = fsExtra.readFileSync(path.join(srcDir, rootFile), "utf8");
178
+ if (siblings.length > 0) {
179
+ const subMods = siblings.map((f) => `mod ${path.basename(f, ".rs")};`).join("\n");
180
+ rootSrc = `${subMods}\n\n${rootSrc}`;
181
+ }
182
+ fsExtra.writeFileSync(path.join(destDir, "mod.rs"), rootSrc);
183
+ mods.push(modIdent);
184
+ }
185
+ }
186
+ const modLines = mods.map((m) => `pub mod ${m};`);
187
+ const callLines = mods.map((m) => `\t${m}::register(registry);`);
188
+ const content = `// Code generated by blokctl. DO NOT EDIT.
189
+ use blok::registry::NodeRegistry;
190
+
191
+ ${modLines.join("\n")}
192
+
193
+ /// Registers nodes scaffolded under runtimes/rust/nodes.
194
+ pub fn register_user_nodes(${mods.length > 0 ? "registry" : "_registry"}: &mut NodeRegistry) {
195
+ ${callLines.join("\n")}
196
+ }
197
+ `;
198
+ const registryFile = path.join(usernodesDir, "mod.rs");
199
+ fsExtra.writeFileSync(registryFile, content);
200
+ return registryFile;
201
+ }
202
+ export function generateJavaNodeRegistry(projectDir) {
203
+ const javaSdkDir = path.join(projectDir, ".blok", "runtimes", "java");
204
+ const nodesSrcDir = path.join(projectDir, "runtimes", "java", "nodes");
205
+ const sdkJavaRoot = path.join(javaSdkDir, "src", "main", "java");
206
+ const usernodesDir = path.join(sdkJavaRoot, "usernodes");
207
+ const registryFile = path.join(sdkJavaRoot, "com", "blok", "blok", "UserNodeRegistry.java");
208
+ fsExtra.removeSync(usernodesDir);
209
+ const registrations = [];
210
+ if (fsExtra.existsSync(nodesSrcDir)) {
211
+ for (const entry of fsExtra.readdirSync(nodesSrcDir, { withFileTypes: true })) {
212
+ if (!entry.isDirectory())
213
+ continue;
214
+ const nodeSrcRoot = path.join(nodesSrcDir, entry.name, "src", "main", "java");
215
+ if (!fsExtra.existsSync(nodeSrcRoot)) {
216
+ console.warn(`[blokctl] skipping Java node '${entry.name}': no src/main/java found`);
217
+ continue;
218
+ }
219
+ const javaFiles = [];
220
+ const walk = (dir) => {
221
+ for (const f of fsExtra.readdirSync(dir, { withFileTypes: true })) {
222
+ const full = path.join(dir, f.name);
223
+ if (f.isDirectory())
224
+ walk(full);
225
+ else if (f.name.endsWith(".java"))
226
+ javaFiles.push(full);
227
+ }
228
+ };
229
+ walk(nodeSrcRoot);
230
+ let fqcn;
231
+ for (const file of javaFiles) {
232
+ const src = fsExtra.readFileSync(file, "utf8");
233
+ const classMatch = src.match(/class\s+(\w+)\s+implements\s+NodeHandler/);
234
+ if (!classMatch)
235
+ continue;
236
+ const pkgMatch = src.match(/package\s+([\w.]+)\s*;/);
237
+ fqcn = pkgMatch ? `${pkgMatch[1]}.${classMatch[1]}` : classMatch[1];
238
+ break;
239
+ }
240
+ if (!fqcn) {
241
+ console.warn(`[blokctl] skipping Java node '${entry.name}': no class implementing NodeHandler found`);
242
+ continue;
243
+ }
244
+ fsExtra.copySync(nodeSrcRoot, path.join(usernodesDir, entry.name));
245
+ registrations.push(`\t\tregistry.register("${entry.name}", new ${fqcn}());`);
246
+ }
247
+ }
248
+ const content = `// Code generated by blokctl. DO NOT EDIT.
249
+ package com.blok.blok;
250
+
251
+ import com.blok.blok.node.NodeRegistry;
252
+
253
+ public final class UserNodeRegistry {
254
+
255
+ \tprivate UserNodeRegistry() {
256
+ \t}
257
+
258
+ \t/** Registers nodes scaffolded under runtimes/java/nodes. */
259
+ \tpublic static void registerUserNodes(NodeRegistry registry) {
260
+ ${registrations.join("\n")}
261
+ \t}
262
+ }
263
+ `;
264
+ fsExtra.ensureDirSync(path.dirname(registryFile));
265
+ fsExtra.writeFileSync(registryFile, content);
266
+ return registryFile;
267
+ }
268
+ export function generateCSharpNodeRegistry(projectDir) {
269
+ const csSdkDir = path.join(projectDir, ".blok", "runtimes", "csharp");
270
+ const nodesSrcDir = path.join(projectDir, "runtimes", "csharp", "nodes");
271
+ const usernodesDir = path.join(csSdkDir, "src", "Blok.Core", "Nodes", "UserNodes");
272
+ const registryFile = path.join(csSdkDir, "src", "Blok.Core", "UserNodeRegistry.cs");
273
+ fsExtra.removeSync(usernodesDir);
274
+ const seenClasses = new Set();
275
+ const registrations = [];
276
+ if (fsExtra.existsSync(nodesSrcDir)) {
277
+ for (const entry of fsExtra.readdirSync(nodesSrcDir, { withFileTypes: true })) {
278
+ if (!entry.isDirectory())
279
+ continue;
280
+ const srcDir = path.join(nodesSrcDir, entry.name);
281
+ const csFiles = collectFilesRecursive(srcDir, ".cs");
282
+ if (csFiles.length === 0)
283
+ continue;
284
+ let className;
285
+ for (const file of csFiles) {
286
+ const match = fsExtra.readFileSync(file, "utf8").match(/class\s+(\w+)\s*:\s*INodeHandler\b/);
287
+ if (match) {
288
+ className = match[1];
289
+ break;
290
+ }
291
+ }
292
+ if (!className) {
293
+ console.warn(`[blokctl] skipping C# node '${entry.name}': no 'class X : INodeHandler' found`);
294
+ continue;
295
+ }
296
+ if (seenClasses.has(className)) {
297
+ console.warn(`[blokctl] skipping C# node '${entry.name}': duplicate class name '${className}' (single namespace Blok.Core.Nodes)`);
298
+ continue;
299
+ }
300
+ seenClasses.add(className);
301
+ const destDir = path.join(usernodesDir, entry.name);
302
+ fsExtra.ensureDirSync(destDir);
303
+ for (const file of csFiles) {
304
+ fsExtra.copySync(file, path.join(destDir, path.basename(file)));
305
+ }
306
+ registrations.push({ nodeName: entry.name, className });
307
+ }
308
+ }
309
+ const callLines = registrations.map((r) => `\t\tregistry.Register("${r.nodeName}", new Blok.Core.Nodes.${r.className}());`);
310
+ const content = `// Code generated by blokctl. DO NOT EDIT.
311
+ using Blok.Core.Node;
312
+
313
+ namespace Blok.Core;
314
+
315
+ /// <summary>
316
+ /// Registers user nodes scaffolded under runtimes/csharp/nodes.
317
+ /// </summary>
318
+ public static class UserNodeRegistry
319
+ {
320
+ public static void RegisterUserNodes(NodeRegistry registry)
321
+ {
322
+ ${callLines.join("\n")}
323
+ }
324
+ }
325
+ `;
326
+ fsExtra.ensureDirSync(path.dirname(registryFile));
327
+ fsExtra.writeFileSync(registryFile, content);
328
+ return registryFile;
329
+ }
330
+ function collectFilesRecursive(dir, ext) {
331
+ const out = [];
332
+ for (const entry of fsExtra.readdirSync(dir, { withFileTypes: true })) {
333
+ const full = path.join(dir, entry.name);
334
+ if (entry.isDirectory()) {
335
+ out.push(...collectFilesRecursive(full, ext));
336
+ }
337
+ else if (entry.name.endsWith(ext)) {
338
+ out.push(full);
339
+ }
340
+ }
341
+ return out;
342
+ }
89
343
  async function setupRust(sdkDir, spinner) {
90
344
  spinner.message("Building Rust project (this may take a few minutes on first build)...");
91
345
  await exec("cargo build --release", { cwd: sdkDir, timeout: 600000 });
@@ -140,7 +394,7 @@ async function setupRuby(sdkDir, spinner, port) {
140
394
  spinner.message("Ruby dependencies installed.");
141
395
  return `${resolvedBundle} exec rackup --host 0.0.0.0 -p ${port} config.ru`;
142
396
  }
143
- export function writeProjectConfig(projectDir, runtimeConfigs, triggerConfigs) {
397
+ export function writeProjectConfig(projectDir, runtimeConfigs, triggerConfigs, observabilityConfigs) {
144
398
  const config = {};
145
399
  if (runtimeConfigs.length > 0) {
146
400
  config.runtimes = {};
@@ -154,6 +408,9 @@ export function writeProjectConfig(projectDir, runtimeConfigs, triggerConfigs) {
154
408
  config.triggers[tc.kind] = tc;
155
409
  }
156
410
  }
411
+ if (observabilityConfigs && Object.keys(observabilityConfigs).length > 0) {
412
+ config.observability = observabilityConfigs;
413
+ }
157
414
  const configPath = path.join(projectDir, ".blok", "config.json");
158
415
  fsExtra.ensureDirSync(path.dirname(configPath));
159
416
  fsExtra.writeFileSync(configPath, JSON.stringify(config, null, 2));
@@ -185,11 +442,14 @@ export function generateSupervisordConfig(runtimeConfigs) {
185
442
  for (const rc of runtimeConfigs) {
186
443
  const cmd = rc.grpcStartCmd ?? rc.startCmd;
187
444
  const grpcPortLine = rc.grpcPort !== undefined ? `,GRPC_PORT="${rc.grpcPort}"` : "";
445
+ const nodesDirLine = rc.kind === "python3" || rc.kind === "ruby" || rc.kind === "php"
446
+ ? `,BLOK_NODES_DIR="/app/runtimes/${rc.kind}/nodes"`
447
+ : "";
188
448
  config += `
189
449
  [program:${rc.kind}_runtime]
190
450
  command=${cmd}
191
451
  directory=/app/${rc.cwd}
192
- environment=PORT="${rc.port}"${grpcPortLine},HOST="0.0.0.0",BLOK_TRANSPORT="grpc"
452
+ environment=PORT="${rc.port}"${grpcPortLine}${nodesDirLine},HOST="0.0.0.0",BLOK_TRANSPORT="grpc"
193
453
  autostart=true
194
454
  autorestart=true
195
455
  stderr_logfile=/var/log/${rc.kind}.err.log