blokctl 0.6.21 → 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 (76) 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/generate/NodeGenerator.d.ts +0 -2
  8. package/dist/commands/generate/NodeGenerator.js +0 -20
  9. package/dist/commands/generate/RuntimeGenerator.d.ts +0 -2
  10. package/dist/commands/generate/RuntimeGenerator.js +0 -19
  11. package/dist/commands/generate/RuntimeGenerator.test.js +0 -29
  12. package/dist/commands/generate/TriggerGenerator.d.ts +0 -2
  13. package/dist/commands/generate/TriggerGenerator.js +0 -19
  14. package/dist/commands/generate/WorkflowGenerator.d.ts +0 -2
  15. package/dist/commands/generate/WorkflowGenerator.js +0 -19
  16. package/dist/commands/generate/e2e/NodeGenerator.e2e.test.js +0 -12
  17. package/dist/commands/generate/e2e/RuntimeGenerator.e2e.test.js +0 -12
  18. package/dist/commands/generate/e2e/TriggerGenerator.e2e.test.js +0 -14
  19. package/dist/commands/monitor/monitor-component.js +5 -5
  20. package/dist/commands/observability/add.d.ts +2 -0
  21. package/dist/commands/observability/add.js +113 -0
  22. package/dist/commands/observability/alerting-module.test.js +43 -0
  23. package/dist/commands/observability/apply.d.ts +10 -0
  24. package/dist/commands/observability/apply.js +11 -0
  25. package/dist/commands/observability/descriptor.d.ts +37 -0
  26. package/dist/commands/observability/descriptor.js +203 -0
  27. package/dist/commands/observability/descriptor.test.d.ts +1 -0
  28. package/dist/commands/observability/descriptor.test.js +40 -0
  29. package/dist/commands/observability/index.d.ts +1 -0
  30. package/dist/commands/observability/index.js +53 -0
  31. package/dist/commands/observability/list.d.ts +2 -0
  32. package/dist/commands/observability/list.js +45 -0
  33. package/dist/commands/observability/logging-module.test.d.ts +1 -0
  34. package/dist/commands/observability/logging-module.test.js +43 -0
  35. package/dist/commands/observability/obs-stack-module.test.d.ts +1 -0
  36. package/dist/commands/observability/obs-stack-module.test.js +33 -0
  37. package/dist/commands/observability/remove.d.ts +2 -0
  38. package/dist/commands/observability/remove.js +62 -0
  39. package/dist/commands/observability/shared.d.ts +6 -0
  40. package/dist/commands/observability/shared.js +23 -0
  41. package/dist/commands/observability/status.d.ts +2 -0
  42. package/dist/commands/observability/status.js +36 -0
  43. package/dist/commands/observability/tracing-module.test.d.ts +1 -0
  44. package/dist/commands/observability/tracing-module.test.js +42 -0
  45. package/dist/commands/profile/index.js +7 -10
  46. package/dist/commands/watch/format.d.ts +23 -0
  47. package/dist/commands/watch/format.js +60 -0
  48. package/dist/commands/watch/index.d.ts +1 -0
  49. package/dist/commands/watch/index.js +53 -0
  50. package/dist/commands/watch/sse.d.ts +16 -0
  51. package/dist/commands/watch/sse.js +82 -0
  52. package/dist/index.d.ts +2 -0
  53. package/dist/index.js +4 -0
  54. package/dist/services/obs-setup.d.ts +5 -0
  55. package/dist/services/obs-setup.js +68 -0
  56. package/dist/services/obs-setup.test.d.ts +1 -0
  57. package/dist/services/obs-setup.test.js +71 -0
  58. package/dist/services/obs-tiers.d.ts +9 -0
  59. package/dist/services/obs-tiers.js +16 -0
  60. package/dist/services/observability-mutations.d.ts +4 -0
  61. package/dist/services/observability-mutations.js +46 -0
  62. package/dist/services/observability-mutations.test.d.ts +1 -0
  63. package/dist/services/observability-mutations.test.js +57 -0
  64. package/dist/services/runtime-setup.d.ts +12 -1
  65. package/dist/services/runtime-setup.js +274 -14
  66. package/dist/studio-dist/assets/{index-BD8_9YPN.js → index-CnFqCRQe.js} +17 -17
  67. package/dist/studio-dist/index.html +1 -1
  68. package/package.json +3 -3
  69. package/dist/commands/generate/GenerationAnalytics.d.ts +0 -61
  70. package/dist/commands/generate/GenerationAnalytics.js +0 -163
  71. package/dist/commands/generate/GenerationAnalytics.test.js +0 -407
  72. package/dist/commands/generate/PromptVersioning.d.ts +0 -25
  73. package/dist/commands/generate/PromptVersioning.js +0 -71
  74. package/dist/commands/generate/PromptVersioning.test.js +0 -120
  75. /package/dist/{commands/generate/GenerationAnalytics.test.d.ts → __tests__/modular-observability.capstone.e2e.test.d.ts} +0 -0
  76. /package/dist/commands/{generate/PromptVersioning.test.d.ts → observability/alerting-module.test.d.ts} +0 -0
@@ -1,9 +1,12 @@
1
1
  import { spawn } from "node:child_process";
2
+ import child_process from "node:child_process";
2
3
  import path from "node:path";
4
+ import util from "node:util";
3
5
  import fsExtra from "fs-extra";
4
6
  import { waitForGrpcPort } from "../../services/health-probe.js";
5
7
  import { detectRr } from "../../services/runtime-detector.js";
6
- import { readProjectConfig, validateProjectRuntimes } from "../../services/runtime-setup.js";
8
+ import { generateCSharpNodeRegistry, generateGoNodeRegistry, generateJavaNodeRegistry, generateRustNodeRegistry, readProjectConfig, validateProjectRuntimes, } from "../../services/runtime-setup.js";
9
+ const exec = util.promisify(child_process.exec);
7
10
  const runningProcesses = [];
8
11
  function spawnProcess(cmd, args, name, currentPath, cwd, env) {
9
12
  const child = spawn(cmd, args, {
@@ -98,6 +101,42 @@ export async function devProject(opts) {
98
101
  HOST: "0.0.0.0",
99
102
  BLOK_TRANSPORT: "grpc",
100
103
  };
104
+ if (rt.kind === "python3" || rt.kind === "ruby" || rt.kind === "php") {
105
+ env.BLOK_NODES_DIR = path.resolve(currentPath, "runtimes", rt.kind, "nodes");
106
+ }
107
+ if (rt.kind === "go") {
108
+ try {
109
+ generateGoNodeRegistry(currentPath);
110
+ }
111
+ catch (err) {
112
+ console.log(` Warning: Go user-node codegen failed: ${err.message}`);
113
+ }
114
+ }
115
+ if (rt.kind === "rust") {
116
+ try {
117
+ generateRustNodeRegistry(currentPath);
118
+ }
119
+ catch (err) {
120
+ console.log(` Warning: Rust user-node codegen failed: ${err.message}`);
121
+ }
122
+ }
123
+ if (rt.kind === "csharp") {
124
+ try {
125
+ generateCSharpNodeRegistry(currentPath);
126
+ }
127
+ catch (err) {
128
+ console.log(` Warning: C# user-node codegen failed: ${err.message}`);
129
+ }
130
+ }
131
+ if (rt.kind === "java") {
132
+ try {
133
+ generateJavaNodeRegistry(currentPath);
134
+ await exec("mvn package -q -DskipTests", { cwd: runtimeCwd, timeout: 300000 });
135
+ }
136
+ catch (err) {
137
+ console.log(` Warning: Java user-node codegen/build failed: ${err.message}`);
138
+ }
139
+ }
101
140
  runtimeDefs.push({
102
141
  cmd,
103
142
  args,
@@ -7,8 +7,6 @@ type NodeInformation = {
7
7
  errors: string[];
8
8
  warnings: string[];
9
9
  attempts: number;
10
- promptVersion?: string;
11
- durationMs?: number;
12
10
  };
13
11
  };
14
12
  export type { NodeInformation };
@@ -1,8 +1,6 @@
1
1
  import * as fs from "node:fs";
2
2
  import { createOpenAI } from "@ai-sdk/openai";
3
3
  import { generateText } from "ai";
4
- import { GenerationAnalytics } from "./GenerationAnalytics.js";
5
- import { getVersionStamp, registerPromptContent } from "./PromptVersioning.js";
6
4
  import createFnNodeSystemPrompt from "./prompts/create-fn-node.system.js";
7
5
  import createNodeSystemPrompt from "./prompts/create-node.system.js";
8
6
  import * as CompilationValidator from "./validators/CompilationValidator.js";
@@ -10,10 +8,6 @@ import * as NodeValidator from "./validators/NodeValidator.js";
10
8
  export default class NodeGenerator {
11
9
  MAX_VALIDATION_ATTEMPTS = 3;
12
10
  async generateNode(nodeName, userPrompt, apiKey, update = false, nodeStyle = "function") {
13
- const analytics = GenerationAnalytics.getInstance();
14
- const getElapsed = analytics.startTimer();
15
- const promptId = nodeStyle === "function" ? "create-fn-node" : "create-node";
16
- const promptVersion = getVersionStamp(promptId);
17
11
  const openai = createOpenAI({
18
12
  compatibility: "strict",
19
13
  apiKey: apiKey,
@@ -21,7 +15,6 @@ export default class NodeGenerator {
21
15
  const promptTemplate = nodeStyle === "function" ? createFnNodeSystemPrompt : createNodeSystemPrompt;
22
16
  let prompt = promptTemplate.prompt;
23
17
  let existingCode = null;
24
- registerPromptContent(promptId, prompt);
25
18
  if (update) {
26
19
  const dirName = nodeName.toLowerCase().replace(/\s+/g, "-");
27
20
  const dirPath = process.cwd();
@@ -72,17 +65,6 @@ export default class NodeGenerator {
72
65
  console.log(`⚠️ Validation failed (attempt ${attempts}/${this.MAX_VALIDATION_ATTEMPTS}). Retrying with feedback...`);
73
66
  }
74
67
  }
75
- const durationMs = getElapsed();
76
- analytics.recordEvent({
77
- type: "node",
78
- subtype: nodeStyle,
79
- name: nodeName,
80
- success: isValid,
81
- attempts,
82
- durationMs,
83
- errors: allErrors,
84
- promptVersion,
85
- });
86
68
  return {
87
69
  nodeName,
88
70
  userPrompt,
@@ -92,8 +74,6 @@ export default class NodeGenerator {
92
74
  errors: validationErrors,
93
75
  warnings: validationWarnings,
94
76
  attempts,
95
- promptVersion,
96
- durationMs,
97
77
  },
98
78
  };
99
79
  }
@@ -11,8 +11,6 @@ export type RuntimeInformation = {
11
11
  errors: string[];
12
12
  warnings: string[];
13
13
  attempts: number;
14
- promptVersion?: string;
15
- durationMs?: number;
16
14
  };
17
15
  };
18
16
  declare const SUPPORTED_LANGUAGES: readonly ["go", "java", "rust", "python", "csharp", "php", "ruby"];
@@ -1,8 +1,6 @@
1
1
  import * as fs from "node:fs";
2
2
  import { createOpenAI } from "@ai-sdk/openai";
3
3
  import { generateText } from "ai";
4
- import { GenerationAnalytics } from "./GenerationAnalytics.js";
5
- import { getVersionStamp, registerPromptContent } from "./PromptVersioning.js";
6
4
  import createRuntimeSystemPrompt from "./prompts/create-runtime.system.js";
7
5
  const SUPPORTED_LANGUAGES = ["go", "java", "rust", "python", "csharp", "php", "ruby"];
8
6
  export function isSupportedLanguage(lang) {
@@ -11,15 +9,11 @@ export function isSupportedLanguage(lang) {
11
9
  export default class RuntimeGenerator {
12
10
  MAX_VALIDATION_ATTEMPTS = 3;
13
11
  async generateRuntime(language, userPrompt, apiKey, update = false, existingPath) {
14
- const analytics = GenerationAnalytics.getInstance();
15
- const getElapsed = analytics.startTimer();
16
- const promptVersion = getVersionStamp("create-runtime");
17
12
  const openai = createOpenAI({
18
13
  compatibility: "strict",
19
14
  apiKey: apiKey,
20
15
  });
21
16
  let prompt = createRuntimeSystemPrompt.prompt;
22
- registerPromptContent("create-runtime", prompt);
23
17
  if (update && existingPath) {
24
18
  const existingContent = this.readExistingRuntime(existingPath);
25
19
  prompt = `${createRuntimeSystemPrompt.updatePrompt}\n\n${existingContent}`;
@@ -54,17 +48,6 @@ export default class RuntimeGenerator {
54
48
  }
55
49
  }
56
50
  const files = this.parseFiles(generatedCode, language);
57
- const durationMs = getElapsed();
58
- analytics.recordEvent({
59
- type: "node",
60
- subtype: `runtime-${language}`,
61
- name: `runtime-${language}`,
62
- success: isValid,
63
- attempts,
64
- durationMs,
65
- errors: allErrors,
66
- promptVersion,
67
- });
68
51
  return {
69
52
  language,
70
53
  userPrompt,
@@ -75,8 +58,6 @@ export default class RuntimeGenerator {
75
58
  errors: validationErrors,
76
59
  warnings: validationWarnings,
77
60
  attempts,
78
- promptVersion,
79
- durationMs,
80
61
  },
81
62
  };
82
63
  }
@@ -6,7 +6,6 @@ vi.mock("ai", () => ({
6
6
  generateText: vi.fn(),
7
7
  }));
8
8
  import { generateText } from "ai";
9
- import { GenerationAnalytics } from "./GenerationAnalytics.js";
10
9
  import RuntimeGenerator, { isSupportedLanguage } from "./RuntimeGenerator.js";
11
10
  const mockedGenerateText = vi.mocked(generateText);
12
11
  describe("RuntimeGenerator", () => {
@@ -15,7 +14,6 @@ describe("RuntimeGenerator", () => {
15
14
  generator = new RuntimeGenerator();
16
15
  vi.clearAllMocks();
17
16
  vi.spyOn(console, "log").mockImplementation(() => { });
18
- GenerationAnalytics.resetInstance();
19
17
  });
20
18
  afterEach(() => {
21
19
  vi.restoreAllMocks();
@@ -433,39 +431,12 @@ func main() {
433
431
  expect(result.validationResult?.attempts).toBe(3);
434
432
  expect(result.validationResult?.errors.length).toBeGreaterThan(0);
435
433
  });
436
- it("should record analytics event", async () => {
437
- mockedGenerateText.mockResolvedValueOnce({ text: validGoRuntime });
438
- await generator.generateRuntime("go", "test", "test-key");
439
- const analytics = GenerationAnalytics.getInstance();
440
- const stats = analytics.getStats();
441
- expect(stats.totalGenerations).toBe(1);
442
- expect(stats.successCount).toBe(1);
443
- });
444
- it("should record failed analytics event", async () => {
445
- mockedGenerateText.mockResolvedValue({ text: "invalid" });
446
- await generator.generateRuntime("java", "test", "test-key");
447
- const analytics = GenerationAnalytics.getInstance();
448
- const stats = analytics.getStats();
449
- expect(stats.totalGenerations).toBe(1);
450
- expect(stats.failureCount).toBe(1);
451
- });
452
434
  it("should strip markdown fences from LLM output", async () => {
453
435
  const wrappedCode = `\`\`\`go\n${validGoRuntime}\n\`\`\``;
454
436
  mockedGenerateText.mockResolvedValueOnce({ text: wrappedCode });
455
437
  const result = await generator.generateRuntime("go", "test", "test-key");
456
438
  expect(result.validationResult?.valid).toBe(true);
457
439
  });
458
- it("should include prompt version in validation result", async () => {
459
- mockedGenerateText.mockResolvedValueOnce({ text: validGoRuntime });
460
- const result = await generator.generateRuntime("go", "test", "test-key");
461
- expect(result.validationResult?.promptVersion).toBe("create-runtime@1.0.0");
462
- });
463
- it("should include duration in validation result", async () => {
464
- mockedGenerateText.mockResolvedValueOnce({ text: validGoRuntime });
465
- const result = await generator.generateRuntime("go", "test", "test-key");
466
- expect(result.validationResult?.durationMs).toBeDefined();
467
- expect(typeof result.validationResult?.durationMs).toBe("number");
468
- });
469
440
  });
470
441
  describe("language-specific validation", () => {
471
442
  it("should warn Go code without go.mod", () => {
@@ -8,8 +8,6 @@ export type TriggerInformation = {
8
8
  errors: string[];
9
9
  warnings: string[];
10
10
  attempts: number;
11
- promptVersion?: string;
12
- durationMs?: number;
13
11
  };
14
12
  };
15
13
  export default class TriggerGenerator {
@@ -1,22 +1,16 @@
1
1
  import * as fs from "node:fs";
2
2
  import { createOpenAI } from "@ai-sdk/openai";
3
3
  import { generateText } from "ai";
4
- import { GenerationAnalytics } from "./GenerationAnalytics.js";
5
- import { getVersionStamp, registerPromptContent } from "./PromptVersioning.js";
6
4
  import createTriggerSystemPrompt from "./prompts/create-trigger.system.js";
7
5
  import * as CompilationValidator from "./validators/CompilationValidator.js";
8
6
  export default class TriggerGenerator {
9
7
  MAX_VALIDATION_ATTEMPTS = 3;
10
8
  async generateTrigger(triggerName, triggerType, userPrompt, apiKey, update = false, existingTriggerPath) {
11
- const analytics = GenerationAnalytics.getInstance();
12
- const getElapsed = analytics.startTimer();
13
- const promptVersion = getVersionStamp("create-trigger");
14
9
  const openai = createOpenAI({
15
10
  compatibility: "strict",
16
11
  apiKey: apiKey,
17
12
  });
18
13
  let prompt = createTriggerSystemPrompt.prompt;
19
- registerPromptContent("create-trigger", prompt);
20
14
  if (update && existingTriggerPath) {
21
15
  const existingContent = fs.readFileSync(existingTriggerPath, "utf8");
22
16
  prompt = `${createTriggerSystemPrompt.updatePrompt}\n\n${existingContent}`;
@@ -58,17 +52,6 @@ export default class TriggerGenerator {
58
52
  console.log(`⚠️ Trigger validation failed (attempt ${attempts}/${this.MAX_VALIDATION_ATTEMPTS}). Retrying with feedback...`);
59
53
  }
60
54
  }
61
- const durationMs = getElapsed();
62
- analytics.recordEvent({
63
- type: "trigger",
64
- subtype: triggerType,
65
- name: triggerName,
66
- success: isValid,
67
- attempts,
68
- durationMs,
69
- errors: allErrors,
70
- promptVersion,
71
- });
72
55
  return {
73
56
  triggerName,
74
57
  triggerType,
@@ -79,8 +62,6 @@ export default class TriggerGenerator {
79
62
  errors: validationErrors,
80
63
  warnings: validationWarnings,
81
64
  attempts,
82
- promptVersion,
83
- durationMs,
84
65
  },
85
66
  };
86
67
  }
@@ -8,8 +8,6 @@ export type WorkflowInformation = {
8
8
  errors: string[];
9
9
  warnings: string[];
10
10
  attempts: number;
11
- promptVersion?: string;
12
- durationMs?: number;
13
11
  };
14
12
  };
15
13
  export default class WorkflowGenerator {
@@ -1,22 +1,16 @@
1
1
  import * as fs from "node:fs";
2
2
  import { createOpenAI } from "@ai-sdk/openai";
3
3
  import { generateText } from "ai";
4
- import { GenerationAnalytics } from "./GenerationAnalytics.js";
5
- import { getVersionStamp, registerPromptContent } from "./PromptVersioning.js";
6
4
  import createWorkflowSystemPrompt from "./prompts/create-workflow.system.js";
7
5
  import * as WorkflowValidator from "./validators/WorkflowValidator.js";
8
6
  export default class WorkflowGenerator {
9
7
  MAX_VALIDATION_ATTEMPTS = 3;
10
8
  async generateWorkflow(workflowName, userPrompt, apiKey, triggerType, update = false, existingWorkflowPath) {
11
- const analytics = GenerationAnalytics.getInstance();
12
- const getElapsed = analytics.startTimer();
13
- const promptVersion = getVersionStamp("create-workflow");
14
9
  const openai = createOpenAI({
15
10
  compatibility: "strict",
16
11
  apiKey: apiKey,
17
12
  });
18
13
  let prompt = createWorkflowSystemPrompt.prompt;
19
- registerPromptContent("create-workflow", prompt);
20
14
  if (update && existingWorkflowPath) {
21
15
  const existingContent = fs.readFileSync(existingWorkflowPath, "utf8");
22
16
  prompt = `${createWorkflowSystemPrompt.updatePrompt}\n\n${existingContent}`;
@@ -50,17 +44,6 @@ export default class WorkflowGenerator {
50
44
  console.log(`⚠️ Workflow validation failed (attempt ${attempts}/${this.MAX_VALIDATION_ATTEMPTS}). Retrying with feedback...`);
51
45
  }
52
46
  }
53
- const durationMs = getElapsed();
54
- analytics.recordEvent({
55
- type: "workflow",
56
- subtype: triggerType,
57
- name: workflowName,
58
- success: isValid,
59
- attempts,
60
- durationMs,
61
- errors: allErrors,
62
- promptVersion,
63
- });
64
47
  return {
65
48
  workflowName,
66
49
  userPrompt,
@@ -71,8 +54,6 @@ export default class WorkflowGenerator {
71
54
  errors: validationErrors,
72
55
  warnings: validationWarnings,
73
56
  attempts,
74
- promptVersion,
75
- durationMs,
76
57
  },
77
58
  };
78
59
  }
@@ -115,18 +115,6 @@ describe("NodeGenerator E2E", () => {
115
115
  const callArgs = mockedGenerateText.mock.calls[0][0];
116
116
  expect(callArgs.temperature).toBe(0.2);
117
117
  });
118
- it("should include prompt version in validation result", async () => {
119
- mockedGenerateText.mockResolvedValueOnce({ text: VALID_FUNCTION_FIRST_NODE });
120
- mockValidPass();
121
- const result = await generator.generateNode("test-node", "Create a test node", "test-api-key", false, "function");
122
- expect(result.validationResult.promptVersion).toContain("create-fn-node@");
123
- });
124
- it("should include duration in validation result", async () => {
125
- mockedGenerateText.mockResolvedValueOnce({ text: VALID_FUNCTION_FIRST_NODE });
126
- mockValidPass();
127
- const result = await generator.generateNode("test-node", "Create a test node", "test-api-key", false, "function");
128
- expect(result.validationResult.durationMs).toBeGreaterThanOrEqual(0);
129
- });
130
118
  });
131
119
  describe("validation feedback loop", () => {
132
120
  it("should retry with feedback on first failure and succeed on second attempt", async () => {
@@ -645,18 +645,6 @@ describe("RuntimeGenerator E2E", () => {
645
645
  expect(mockedGenerateText).toHaveBeenCalledTimes(1);
646
646
  });
647
647
  });
648
- describe("analytics integration", () => {
649
- it("should include prompt version in validation result", async () => {
650
- mockedGenerateText.mockResolvedValueOnce({ text: VALID_GO_RUNTIME });
651
- const result = await generator.generateRuntime("go", "Create a runtime", "test-api-key");
652
- expect(result.validationResult.promptVersion).toContain("create-runtime@");
653
- });
654
- it("should include duration in validation result", async () => {
655
- mockedGenerateText.mockResolvedValueOnce({ text: VALID_GO_RUNTIME });
656
- const result = await generator.generateRuntime("go", "Create a runtime", "test-api-key");
657
- expect(result.validationResult.durationMs).toBeGreaterThanOrEqual(0);
658
- });
659
- });
660
648
  describe("multi-file parsing", () => {
661
649
  it("should parse files separated by // FILE: markers", async () => {
662
650
  const code = [
@@ -278,18 +278,4 @@ describe("TriggerGenerator E2E", () => {
278
278
  expect(system).toContain("loadNodes");
279
279
  });
280
280
  });
281
- describe("analytics integration", () => {
282
- it("should include prompt version in validation result", async () => {
283
- mockedGenerateText.mockResolvedValueOnce({ text: VALID_QUEUE_TRIGGER });
284
- mockedValidateCode.mockReturnValue({ success: true, errors: [], warnings: [] });
285
- const result = await generator.generateTrigger("test-trigger", "queue", "Create a test trigger", "test-api-key");
286
- expect(result.validationResult.promptVersion).toContain("create-trigger@");
287
- });
288
- it("should include duration in validation result", async () => {
289
- mockedGenerateText.mockResolvedValueOnce({ text: VALID_QUEUE_TRIGGER });
290
- mockedValidateCode.mockReturnValue({ success: true, errors: [], warnings: [] });
291
- const result = await generator.generateTrigger("test-trigger", "queue", "Create a test trigger", "test-api-key");
292
- expect(result.validationResult.durationMs).toBeGreaterThanOrEqual(0);
293
- });
294
- });
295
281
  });
@@ -120,11 +120,11 @@ const fetchPrometheusMetrics = async (host, token) => {
120
120
  mapWorkflow(wfErrors, "errors", (v) => Math.round(+v));
121
121
  mapWorkflow(wfReqs, "requests", (v) => Math.round(+v));
122
122
  const nodeMetricsRaw = await Promise.all([
123
- queryPrometheus("(sum(increase(node_total[1m])) by (node_name, workflow_path)) > 0", host, token),
124
- queryPrometheus("sum(increase(node_time[1m])) by (node_name, workflow_path)", host, token),
125
- queryPrometheus("(sum(increase(node_errors_total[1m])) by (node_name, workflow_path)) > 0", host, token),
126
- queryPrometheus("sum(increase(node_cpu[1m])) by (node_name, workflow_path)", host, token),
127
- queryPrometheus("sum(increase(node_memory[1m])) by (node_name, workflow_path)", host, token),
123
+ queryPrometheus("(sum(increase(blok_node_executions_total[1m])) by (node_name, workflow_path)) > 0", host, token),
124
+ queryPrometheus("sum(increase(blok_node_duration_seconds_sum[1m])) by (node_name, workflow_path)", host, token),
125
+ queryPrometheus("(sum(increase(blok_node_errors_total[1m])) by (node_name, workflow_path)) > 0", host, token),
126
+ queryPrometheus("sum(increase(blok_node_cpu_usage[1m])) by (node_name, workflow_path)", host, token),
127
+ queryPrometheus("sum(increase(blok_node_memory_bytes[1m])) by (node_name, workflow_path)", host, token),
128
128
  ]);
129
129
  const nodeMap = {};
130
130
  const setNodeMetric = (list, key, convert) => {
@@ -0,0 +1,2 @@
1
+ import type { OptionValues } from "../../services/commander.js";
2
+ export declare function observabilityAdd(moduleArg: string | undefined, options: OptionValues): Promise<void>;
@@ -0,0 +1,113 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import * as p from "@clack/prompts";
4
+ import color from "picocolors";
5
+ import { isNonInteractive } from "../../services/non-interactive.js";
6
+ import { rewriteObservabilityEnvBlock, withObservabilityModule } from "../../services/observability-mutations.js";
7
+ import { OBSERVABILITY_MODULE_IDS, allObservabilityModules, getObservabilityModule, resolveWithDependencies, } from "./descriptor.js";
8
+ import { ObservabilityCommandError, readConfigSafe, readFrameworkVersion, reportObservabilityError, resolveProjectRoot, } from "./shared.js";
9
+ export async function observabilityAdd(moduleArg, options) {
10
+ try {
11
+ const root = resolveProjectRoot(options.directory);
12
+ const config = readConfigSafe(root);
13
+ const enabled = config.observability ?? {};
14
+ const nonInteractive = isNonInteractive() || options.yes === true;
15
+ let id = moduleArg?.trim().toLowerCase();
16
+ if (!id) {
17
+ if (nonInteractive) {
18
+ throw new ObservabilityCommandError(`Specify a module: blokctl observability add <${OBSERVABILITY_MODULE_IDS.join("|")}>`);
19
+ }
20
+ const choices = allObservabilityModules()
21
+ .filter((m) => !enabled[m.id])
22
+ .map((m) => ({ value: m.id, label: m.label, hint: m.description }));
23
+ if (choices.length === 0) {
24
+ p.intro(color.inverse(" Add observability module "));
25
+ p.outro(color.dim("All observability modules are already enabled."));
26
+ return;
27
+ }
28
+ const picked = await p.select({ message: "Which observability module do you want to add?", options: choices });
29
+ if (p.isCancel(picked)) {
30
+ p.cancel("Cancelled.");
31
+ return;
32
+ }
33
+ id = picked;
34
+ }
35
+ const mod = getObservabilityModule(id);
36
+ if (!mod) {
37
+ throw new ObservabilityCommandError(`Unknown observability module "${id}". Known: ${OBSERVABILITY_MODULE_IDS.join(", ")}.`);
38
+ }
39
+ p.intro(color.inverse(` Add ${mod.label} `));
40
+ if (enabled[mod.id] && options.force !== true) {
41
+ p.outro(color.dim(`${mod.label} is already enabled. Re-run with --force to re-apply its scaffold.`));
42
+ return;
43
+ }
44
+ const { resolved, added } = resolveWithDependencies([mod.id]);
45
+ const newDeps = added.filter((d) => !enabled[d]);
46
+ if (newDeps.length > 0) {
47
+ const labels = newDeps.map((d) => getObservabilityModule(d)?.label ?? d).join(", ");
48
+ if (nonInteractive) {
49
+ p.log.info(color.dim(`Also enabling required dependencies: ${labels}`));
50
+ }
51
+ else {
52
+ const ok = await p.confirm({
53
+ message: `${mod.label} requires ${labels}. Enable ${newDeps.length > 1 ? "them" : "it"} too?`,
54
+ initialValue: true,
55
+ });
56
+ if (p.isCancel(ok) || !ok) {
57
+ p.outro(color.dim("Left unchanged."));
58
+ return;
59
+ }
60
+ }
61
+ }
62
+ const toApply = resolved.filter((rid) => rid === mod.id || !enabled[rid]);
63
+ const addedAt = new Date().toISOString();
64
+ const version = readFrameworkVersion(root);
65
+ const s = p.spinner();
66
+ let nextConfig = config;
67
+ const scaffoldOpts = {
68
+ projectDir: root,
69
+ nonInteractive,
70
+ tier: options.tier,
71
+ localRepo: options.local,
72
+ };
73
+ for (const rid of toApply) {
74
+ const d = getObservabilityModule(rid);
75
+ if (!d)
76
+ continue;
77
+ if (d.validate)
78
+ await d.validate(root);
79
+ if (d.scaffold) {
80
+ s.start(`Scaffolding ${d.label}…`);
81
+ await d.scaffold(scaffoldOpts);
82
+ s.stop(`${d.label} ready`);
83
+ }
84
+ if (d.setup)
85
+ await d.setup(scaffoldOpts);
86
+ const extra = rid === "obs-stack" ? { settings: { tier: options.tier ?? "lite" } } : {};
87
+ nextConfig = withObservabilityModule(nextConfig, rid, { enabled: true, addedAt, version, ...extra });
88
+ }
89
+ fs.mkdirSync(path.join(root, ".blok"), { recursive: true });
90
+ fs.writeFileSync(path.join(root, ".blok", "config.json"), `${JSON.stringify(nextConfig, null, 2)}\n`);
91
+ const enabledIds = Object.keys(nextConfig.observability ?? {});
92
+ const envBlocks = enabledIds.map((eid) => getObservabilityModule(eid)?.envBlock({ projectDir: root }) ?? "");
93
+ const envPath = path.join(root, ".env.local");
94
+ const envContent = fs.existsSync(envPath) ? fs.readFileSync(envPath, "utf8") : "";
95
+ fs.writeFileSync(envPath, rewriteObservabilityEnvBlock(envContent, envBlocks));
96
+ const deps = {};
97
+ for (const rid of toApply)
98
+ Object.assign(deps, getObservabilityModule(rid)?.packageDeps ?? {});
99
+ if (Object.keys(deps).length > 0)
100
+ mergePackageDeps(root, deps);
101
+ p.note(toApply.map((rid) => `${color.green("✓")} ${getObservabilityModule(rid)?.label ?? rid}`).join("\n"), `${mod.label} added`);
102
+ p.outro(color.dim(`Enabled modules: ${enabledIds.join(", ")}.`));
103
+ }
104
+ catch (err) {
105
+ reportObservabilityError(err);
106
+ }
107
+ }
108
+ function mergePackageDeps(root, deps) {
109
+ const pkgPath = path.join(root, "package.json");
110
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf8"));
111
+ pkg.dependencies = { ...(pkg.dependencies ?? {}), ...deps };
112
+ fs.writeFileSync(pkgPath, `${JSON.stringify(pkg, null, 2)}\n`);
113
+ }
@@ -0,0 +1,43 @@
1
+ import fs from "node:fs";
2
+ import os from "node:os";
3
+ import path from "node:path";
4
+ import { afterEach, beforeEach, describe, expect, it } from "vitest";
5
+ import { getObservabilityModule } from "./descriptor.js";
6
+ const alerting = getObservabilityModule("alerting");
7
+ const errorSink = getObservabilityModule("error-sink");
8
+ describe("alerting + error-sink modules (MO-ALERTS)", () => {
9
+ it("alerting depends on metrics, declares alertmanager, and enables alerting", () => {
10
+ expect(alerting.dependencies).toEqual(["metrics"]);
11
+ expect(alerting.composeServices).toEqual(["alertmanager"]);
12
+ expect(alerting.envBlock({ projectDir: "/x" })).toContain("BLOK_ALERTING_ENABLED=true");
13
+ });
14
+ it("error-sink ships inert — SENTRY_DSN is commented out", () => {
15
+ for (const line of errorSink.envBlock({ projectDir: "/x" }).split("\n")) {
16
+ if (/SENTRY_DSN=/.test(line))
17
+ expect(line.trim().startsWith("#")).toBe(true);
18
+ }
19
+ });
20
+ describe("verify()", () => {
21
+ let tmp;
22
+ beforeEach(() => {
23
+ tmp = fs.mkdtempSync(path.join(os.tmpdir(), "blok-alerts-"));
24
+ });
25
+ afterEach(() => fs.rmSync(tmp, { recursive: true, force: true }));
26
+ it("error-sink: inert when no SENTRY_DSN", async () => {
27
+ fs.writeFileSync(path.join(tmp, ".env.local"), "# SENTRY_DSN=\n");
28
+ expect((await errorSink.verify?.(tmp))?.message).toMatch(/inert/);
29
+ });
30
+ it("error-sink: flags the missing dep when DSN is set but @sentry/node is absent", async () => {
31
+ fs.writeFileSync(path.join(tmp, ".env.local"), "SENTRY_DSN=https://k@e.test/1\n");
32
+ expect((await errorSink.verify?.(tmp))?.message).toMatch(/@sentry\/node missing/);
33
+ });
34
+ it("alerting: points at obs-stack when the rules file isn't in the project", async () => {
35
+ expect((await alerting.verify?.(tmp))?.message).toMatch(/obs-stack=full/);
36
+ });
37
+ it("alerting: reports rules present when they exist", async () => {
38
+ fs.mkdirSync(path.join(tmp, "infra", "metrics", "rules"), { recursive: true });
39
+ fs.writeFileSync(path.join(tmp, "infra", "metrics", "rules", "blok-alerts.yml"), "groups: []\n");
40
+ expect((await alerting.verify?.(tmp))?.message).toMatch(/alert rules present/);
41
+ });
42
+ });
43
+ });
@@ -0,0 +1,10 @@
1
+ import type { ObservabilityModuleConfig } from "../../services/runtime-setup.js";
2
+ export declare function resolveObservabilitySelection(moduleIds: string[], opts: {
3
+ addedAt: string;
4
+ version?: string;
5
+ projectDir: string;
6
+ }): {
7
+ configMap: Record<string, ObservabilityModuleConfig>;
8
+ envBlocks: string[];
9
+ added: string[];
10
+ };
@@ -0,0 +1,11 @@
1
+ import { getObservabilityModule, resolveWithDependencies } from "./descriptor.js";
2
+ export function resolveObservabilitySelection(moduleIds, opts) {
3
+ if (moduleIds.length === 0)
4
+ return { configMap: {}, envBlocks: [], added: [] };
5
+ const { resolved, added } = resolveWithDependencies(moduleIds);
6
+ const configMap = {};
7
+ for (const id of resolved)
8
+ configMap[id] = { enabled: true, addedAt: opts.addedAt, version: opts.version };
9
+ const envBlocks = resolved.map((id) => getObservabilityModule(id)?.envBlock({ projectDir: opts.projectDir }) ?? "");
10
+ return { configMap, envBlocks, added };
11
+ }