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.
- 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/gen/appTypes.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,
|
|
@@ -126,6 +126,35 @@ async function collectTsFiles(dir) {
|
|
|
126
126
|
}
|
|
127
127
|
return out;
|
|
128
128
|
}
|
|
129
|
+
async function collectJsonWorkflowNames(dir) {
|
|
130
|
+
const names = [];
|
|
131
|
+
let dirents;
|
|
132
|
+
try {
|
|
133
|
+
dirents = await fsp.readdir(dir, { withFileTypes: true });
|
|
134
|
+
}
|
|
135
|
+
catch {
|
|
136
|
+
return names;
|
|
137
|
+
}
|
|
138
|
+
for (const d of dirents) {
|
|
139
|
+
if (d.name.startsWith("_") || d.name.startsWith("."))
|
|
140
|
+
continue;
|
|
141
|
+
const full = path.join(dir, d.name);
|
|
142
|
+
if (d.isDirectory()) {
|
|
143
|
+
names.push(...(await collectJsonWorkflowNames(full)));
|
|
144
|
+
}
|
|
145
|
+
else if (d.name.endsWith(".json")) {
|
|
146
|
+
try {
|
|
147
|
+
const parsed = JSON.parse(await fsp.readFile(full, "utf8"));
|
|
148
|
+
if (typeof parsed.name === "string" && (parsed.trigger !== undefined || parsed.steps !== undefined)) {
|
|
149
|
+
names.push(parsed.name);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
catch {
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
return names;
|
|
157
|
+
}
|
|
129
158
|
async function resolveWorkflowsDir(cwd, explicit) {
|
|
130
159
|
const candidates = explicit
|
|
131
160
|
? [explicit]
|
|
@@ -159,6 +188,14 @@ export async function generateAppTypes(opts) {
|
|
|
159
188
|
: path.join(cwd, opts.out ?? "blok-app.d.ts");
|
|
160
189
|
console.log(color.dim(`Scanning ${color.cyan(dir)} (recursive)\n`));
|
|
161
190
|
const files = await collectTsFiles(dir);
|
|
191
|
+
const jsonScanDirs = [dir, path.join(cwd, "workflows/json"), path.join(cwd, "triggers/http/workflows/json")];
|
|
192
|
+
const jsonNames = [...new Set((await Promise.all(jsonScanDirs.map((d) => collectJsonWorkflowNames(d)))).flat())];
|
|
193
|
+
const warnJsonSkipped = () => {
|
|
194
|
+
if (jsonNames.length === 0)
|
|
195
|
+
return;
|
|
196
|
+
console.log(color.yellow(`ℹ️ ${jsonNames.length} JSON workflow(s) are NOT in app-types (JSON has no TS type to import): ${jsonNames.join(", ")}.`));
|
|
197
|
+
console.log(color.dim(" Convert them to TS workflows to type them, or call them by string name on the client.\n"));
|
|
198
|
+
};
|
|
162
199
|
const entries = [];
|
|
163
200
|
const skipped = [];
|
|
164
201
|
for (const file of files) {
|
|
@@ -173,6 +210,7 @@ export async function generateAppTypes(opts) {
|
|
|
173
210
|
console.log(color.yellow("No TS workflows with a literal `name:` found — nothing to generate."));
|
|
174
211
|
if (skipped.length > 0)
|
|
175
212
|
console.log(color.dim(`Skipped (no literal name): ${skipped.join(", ")}`));
|
|
213
|
+
warnJsonSkipped();
|
|
176
214
|
return;
|
|
177
215
|
}
|
|
178
216
|
const { source, collisions } = buildAppTypeSource(entries, outFile);
|
|
@@ -191,5 +229,6 @@ export async function generateAppTypes(opts) {
|
|
|
191
229
|
if (skipped.length > 0) {
|
|
192
230
|
console.log(color.dim(`ℹ️ Skipped ${skipped.length} file(s) without a literal workflow name (dynamic name or not a workflow): ${skipped.join(", ")}`));
|
|
193
231
|
}
|
|
194
|
-
|
|
232
|
+
warnJsonSkipped();
|
|
233
|
+
console.log(color.dim('Next: `import type { BlokApp } from "<out>"` and `createBlokClient<BlokApp>({ baseUrl })`.\n'));
|
|
195
234
|
}
|
|
@@ -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
|
+
}
|