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.
- package/dist/__tests__/modular-observability.capstone.e2e.test.js +72 -0
- package/dist/commands/create/node.js +46 -66
- package/dist/commands/create/project.js +55 -9
- package/dist/commands/create/utils/Examples.d.ts +8 -20
- package/dist/commands/create/utils/Examples.js +138 -412
- package/dist/commands/dev/index.js +40 -1
- package/dist/commands/generate/NodeGenerator.d.ts +0 -2
- package/dist/commands/generate/NodeGenerator.js +0 -20
- package/dist/commands/generate/RuntimeGenerator.d.ts +0 -2
- package/dist/commands/generate/RuntimeGenerator.js +0 -19
- package/dist/commands/generate/RuntimeGenerator.test.js +0 -29
- package/dist/commands/generate/TriggerGenerator.d.ts +0 -2
- package/dist/commands/generate/TriggerGenerator.js +0 -19
- package/dist/commands/generate/WorkflowGenerator.d.ts +0 -2
- package/dist/commands/generate/WorkflowGenerator.js +0 -19
- package/dist/commands/generate/e2e/NodeGenerator.e2e.test.js +0 -12
- package/dist/commands/generate/e2e/RuntimeGenerator.e2e.test.js +0 -12
- package/dist/commands/generate/e2e/TriggerGenerator.e2e.test.js +0 -14
- package/dist/commands/monitor/monitor-component.js +5 -5
- package/dist/commands/observability/add.d.ts +2 -0
- package/dist/commands/observability/add.js +113 -0
- package/dist/commands/observability/alerting-module.test.js +43 -0
- package/dist/commands/observability/apply.d.ts +10 -0
- package/dist/commands/observability/apply.js +11 -0
- package/dist/commands/observability/descriptor.d.ts +37 -0
- package/dist/commands/observability/descriptor.js +203 -0
- package/dist/commands/observability/descriptor.test.d.ts +1 -0
- package/dist/commands/observability/descriptor.test.js +40 -0
- package/dist/commands/observability/index.d.ts +1 -0
- package/dist/commands/observability/index.js +53 -0
- package/dist/commands/observability/list.d.ts +2 -0
- package/dist/commands/observability/list.js +45 -0
- package/dist/commands/observability/logging-module.test.d.ts +1 -0
- package/dist/commands/observability/logging-module.test.js +43 -0
- package/dist/commands/observability/obs-stack-module.test.d.ts +1 -0
- package/dist/commands/observability/obs-stack-module.test.js +33 -0
- package/dist/commands/observability/remove.d.ts +2 -0
- package/dist/commands/observability/remove.js +62 -0
- package/dist/commands/observability/shared.d.ts +6 -0
- package/dist/commands/observability/shared.js +23 -0
- package/dist/commands/observability/status.d.ts +2 -0
- package/dist/commands/observability/status.js +36 -0
- package/dist/commands/observability/tracing-module.test.d.ts +1 -0
- package/dist/commands/observability/tracing-module.test.js +42 -0
- package/dist/commands/profile/index.js +7 -10
- package/dist/commands/watch/format.d.ts +23 -0
- package/dist/commands/watch/format.js +60 -0
- package/dist/commands/watch/index.d.ts +1 -0
- package/dist/commands/watch/index.js +53 -0
- package/dist/commands/watch/sse.d.ts +16 -0
- package/dist/commands/watch/sse.js +82 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +4 -0
- package/dist/services/obs-setup.d.ts +5 -0
- package/dist/services/obs-setup.js +68 -0
- package/dist/services/obs-setup.test.d.ts +1 -0
- package/dist/services/obs-setup.test.js +71 -0
- package/dist/services/obs-tiers.d.ts +9 -0
- package/dist/services/obs-tiers.js +16 -0
- package/dist/services/observability-mutations.d.ts +4 -0
- package/dist/services/observability-mutations.js +46 -0
- package/dist/services/observability-mutations.test.d.ts +1 -0
- package/dist/services/observability-mutations.test.js +57 -0
- package/dist/services/runtime-setup.d.ts +12 -1
- package/dist/services/runtime-setup.js +274 -14
- package/dist/studio-dist/assets/{index-BD8_9YPN.js → index-CnFqCRQe.js} +17 -17
- package/dist/studio-dist/index.html +1 -1
- package/package.json +3 -3
- package/dist/commands/generate/GenerationAnalytics.d.ts +0 -61
- package/dist/commands/generate/GenerationAnalytics.js +0 -163
- package/dist/commands/generate/GenerationAnalytics.test.js +0 -407
- package/dist/commands/generate/PromptVersioning.d.ts +0 -25
- package/dist/commands/generate/PromptVersioning.js +0 -71
- package/dist/commands/generate/PromptVersioning.test.js +0 -120
- /package/dist/{commands/generate/GenerationAnalytics.test.d.ts → __tests__/modular-observability.capstone.e2e.test.d.ts} +0 -0
- /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,
|
|
@@ -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", () => {
|
|
@@ -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
|
}
|
|
@@ -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(
|
|
124
|
-
queryPrometheus("sum(increase(
|
|
125
|
-
queryPrometheus("(sum(increase(
|
|
126
|
-
queryPrometheus("sum(increase(
|
|
127
|
-
queryPrometheus("sum(increase(
|
|
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,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
|
+
}
|