blokctl 0.6.21 → 1.0.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 +60 -9
- package/dist/commands/create/utils/Examples.d.ts +11 -23
- package/dist/commands/create/utils/Examples.js +206 -421
- 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 +4 -23
- package/dist/commands/generate/WorkflowGenerator.test.js +2 -2
- 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/generate/prompts/create-fn-node.system.js +10 -17
- package/dist/commands/generate/prompts/create-workflow.system.js +114 -375
- package/dist/commands/generate/validators/WorkflowValidator.js +80 -5
- package/dist/commands/generate/validators/WorkflowValidator.test.js +66 -0
- 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
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
function parseFrame(frame) {
|
|
2
|
+
let eventType = null;
|
|
3
|
+
const dataLines = [];
|
|
4
|
+
for (const raw of frame.split("\n")) {
|
|
5
|
+
const line = raw.replace(/\r$/, "");
|
|
6
|
+
if (line.length === 0 || line.startsWith(":"))
|
|
7
|
+
continue;
|
|
8
|
+
if (line.startsWith("event:"))
|
|
9
|
+
eventType = line.slice(6).trim();
|
|
10
|
+
else if (line.startsWith("data:"))
|
|
11
|
+
dataLines.push(line.slice(5).replace(/^ /, ""));
|
|
12
|
+
}
|
|
13
|
+
if (dataLines.length === 0)
|
|
14
|
+
return null;
|
|
15
|
+
if (eventType === "connected" || eventType === "stream-end")
|
|
16
|
+
return null;
|
|
17
|
+
try {
|
|
18
|
+
const parsed = JSON.parse(dataLines.join("\n"));
|
|
19
|
+
if (parsed && typeof parsed === "object" && "runId" in parsed && "type" in parsed) {
|
|
20
|
+
return parsed;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
}
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
export function parseSseBuffer(buffer) {
|
|
28
|
+
const events = [];
|
|
29
|
+
let rest = buffer.replace(/\r\n/g, "\n");
|
|
30
|
+
let sep = rest.indexOf("\n\n");
|
|
31
|
+
while (sep !== -1) {
|
|
32
|
+
const frame = rest.slice(0, sep);
|
|
33
|
+
rest = rest.slice(sep + 2);
|
|
34
|
+
const event = parseFrame(frame);
|
|
35
|
+
if (event)
|
|
36
|
+
events.push(event);
|
|
37
|
+
sep = rest.indexOf("\n\n");
|
|
38
|
+
}
|
|
39
|
+
return { events, rest };
|
|
40
|
+
}
|
|
41
|
+
export async function connectEventStream(baseUrl, opts, handlers) {
|
|
42
|
+
const url = new URL("/__blok/stream", baseUrl);
|
|
43
|
+
if (opts.workflows && opts.workflows.length > 0) {
|
|
44
|
+
url.searchParams.set("workflows", opts.workflows.join(","));
|
|
45
|
+
}
|
|
46
|
+
const headers = { accept: "text/event-stream" };
|
|
47
|
+
if (opts.token)
|
|
48
|
+
headers.authorization = `Bearer ${opts.token}`;
|
|
49
|
+
let res;
|
|
50
|
+
try {
|
|
51
|
+
res = await fetch(url, { headers, signal: opts.signal });
|
|
52
|
+
}
|
|
53
|
+
catch (err) {
|
|
54
|
+
if (err?.name !== "AbortError")
|
|
55
|
+
handlers.onError?.(err);
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
if (!res.ok || !res.body) {
|
|
59
|
+
handlers.onError?.(new Error(`stream connect failed: ${res.status} ${res.statusText || ""}`.trim()));
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
handlers.onOpen?.();
|
|
63
|
+
const reader = res.body.getReader();
|
|
64
|
+
const decoder = new TextDecoder();
|
|
65
|
+
let buffer = "";
|
|
66
|
+
try {
|
|
67
|
+
while (true) {
|
|
68
|
+
const { value, done } = await reader.read();
|
|
69
|
+
if (done)
|
|
70
|
+
break;
|
|
71
|
+
buffer += decoder.decode(value, { stream: true });
|
|
72
|
+
const { events, rest } = parseSseBuffer(buffer);
|
|
73
|
+
buffer = rest;
|
|
74
|
+
for (const event of events)
|
|
75
|
+
handlers.onEvent(event);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
catch (err) {
|
|
79
|
+
if (err?.name !== "AbortError")
|
|
80
|
+
handlers.onError?.(err);
|
|
81
|
+
}
|
|
82
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -13,8 +13,10 @@ import "./commands/nodes/index.js";
|
|
|
13
13
|
import "./commands/config/index.js";
|
|
14
14
|
import "./commands/migrate/index.js";
|
|
15
15
|
import "./commands/runtime/index.js";
|
|
16
|
+
import "./commands/observability/index.js";
|
|
16
17
|
import "./commands/graph/index.js";
|
|
17
18
|
import "./commands/profile/index.js";
|
|
18
19
|
import "./commands/cost/index.js";
|
|
19
20
|
import "./commands/trace/index.js";
|
|
21
|
+
import "./commands/watch/index.js";
|
|
20
22
|
export declare const CLI_NAME = "blokctl";
|
package/dist/index.js
CHANGED
|
@@ -28,10 +28,12 @@ import "./commands/nodes/index.js";
|
|
|
28
28
|
import "./commands/config/index.js";
|
|
29
29
|
import "./commands/migrate/index.js";
|
|
30
30
|
import "./commands/runtime/index.js";
|
|
31
|
+
import "./commands/observability/index.js";
|
|
31
32
|
import "./commands/graph/index.js";
|
|
32
33
|
import "./commands/profile/index.js";
|
|
33
34
|
import "./commands/cost/index.js";
|
|
34
35
|
import "./commands/trace/index.js";
|
|
36
|
+
import "./commands/watch/index.js";
|
|
35
37
|
import { Command } from "commander";
|
|
36
38
|
const version = await getPackageVersion();
|
|
37
39
|
const exec = util.promisify(child_process.exec);
|
|
@@ -89,6 +91,8 @@ async function main() {
|
|
|
89
91
|
.option("-m, --package-manager <value>", "Package manager: npm, yarn, pnpm, bun")
|
|
90
92
|
.option("--pubsub-provider <value>", "Pub/Sub provider: gcp, aws, azure (default: gcp)")
|
|
91
93
|
.option("--queue-provider <value>", "Queue provider: kafka, rabbitmq, sqs, redis (default: kafka)")
|
|
94
|
+
.option("--obs-stack <tier>", "Observability dev stack: none, lite, full (default: none)")
|
|
95
|
+
.option("--observability <list>", "Comma-separated observability modules: metrics,tracing,trace-store,logging,alerting,error-sink")
|
|
92
96
|
.option("--examples", "Install example workflows and nodes")
|
|
93
97
|
.action(async (options) => {
|
|
94
98
|
await analytics.trackCommandExecution({
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { type ObsStackTier } from "./obs-tiers.js";
|
|
2
|
+
export declare function setupObservabilityStack(repoSource: string, projectDir: string, tier: ObsStackTier): {
|
|
3
|
+
copied: string[];
|
|
4
|
+
};
|
|
5
|
+
export declare function trimComposeServices(composePath: string, keep: string[]): void;
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import fsExtra from "fs-extra";
|
|
3
|
+
import { parse, stringify } from "yaml";
|
|
4
|
+
import { TIER_DEFINITIONS } from "./obs-tiers.js";
|
|
5
|
+
export function setupObservabilityStack(repoSource, projectDir, tier) {
|
|
6
|
+
const def = TIER_DEFINITIONS[tier];
|
|
7
|
+
if (tier === "none")
|
|
8
|
+
return { copied: [] };
|
|
9
|
+
const src = path.join(repoSource, "infra", "metrics");
|
|
10
|
+
const dest = path.join(projectDir, "infra", "metrics");
|
|
11
|
+
fsExtra.ensureDirSync(dest);
|
|
12
|
+
const copied = [];
|
|
13
|
+
if (def.files === "*") {
|
|
14
|
+
fsExtra.copySync(src, dest);
|
|
15
|
+
copied.push(...fsExtra.readdirSync(dest));
|
|
16
|
+
}
|
|
17
|
+
else {
|
|
18
|
+
for (const entry of def.files) {
|
|
19
|
+
const from = path.join(src, entry);
|
|
20
|
+
if (fsExtra.existsSync(from)) {
|
|
21
|
+
fsExtra.copySync(from, path.join(dest, entry));
|
|
22
|
+
copied.push(entry);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
trimComposeServices(path.join(dest, "docker-compose.yml"), def.services);
|
|
27
|
+
return { copied };
|
|
28
|
+
}
|
|
29
|
+
export function trimComposeServices(composePath, keep) {
|
|
30
|
+
if (!fsExtra.existsSync(composePath))
|
|
31
|
+
return;
|
|
32
|
+
const doc = parse(fsExtra.readFileSync(composePath, "utf8"));
|
|
33
|
+
if (!doc.services)
|
|
34
|
+
return;
|
|
35
|
+
const keepSet = new Set(keep);
|
|
36
|
+
let changed = false;
|
|
37
|
+
for (const name of Object.keys(doc.services)) {
|
|
38
|
+
if (!keepSet.has(name)) {
|
|
39
|
+
delete doc.services[name];
|
|
40
|
+
changed = true;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
for (const svc of Object.values(doc.services)) {
|
|
44
|
+
const dep = svc.depends_on;
|
|
45
|
+
if (Array.isArray(dep)) {
|
|
46
|
+
const next = dep.filter((d) => keepSet.has(d));
|
|
47
|
+
if (next.length !== dep.length) {
|
|
48
|
+
changed = true;
|
|
49
|
+
if (next.length > 0)
|
|
50
|
+
svc.depends_on = next;
|
|
51
|
+
else
|
|
52
|
+
svc.depends_on = undefined;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
else if (dep && typeof dep === "object") {
|
|
56
|
+
for (const d of Object.keys(dep)) {
|
|
57
|
+
if (!keepSet.has(d)) {
|
|
58
|
+
delete dep[d];
|
|
59
|
+
changed = true;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
if (Object.keys(dep).length === 0)
|
|
63
|
+
svc.depends_on = undefined;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
if (changed)
|
|
67
|
+
fsExtra.writeFileSync(composePath, stringify(doc));
|
|
68
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,71 @@
|
|
|
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 { parse } from "yaml";
|
|
6
|
+
import { setupObservabilityStack } from "./obs-setup.js";
|
|
7
|
+
import { ALL_OBS_SERVICES } from "./obs-tiers.js";
|
|
8
|
+
function findRepoRoot() {
|
|
9
|
+
let dir = import.meta.dirname;
|
|
10
|
+
for (let i = 0; i < 8; i++) {
|
|
11
|
+
if (fs.existsSync(path.join(dir, "infra", "metrics", "docker-compose.yml")))
|
|
12
|
+
return dir;
|
|
13
|
+
const up = path.dirname(dir);
|
|
14
|
+
if (up === dir)
|
|
15
|
+
break;
|
|
16
|
+
dir = up;
|
|
17
|
+
}
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
const REPO = findRepoRoot();
|
|
21
|
+
const composeServices = (dir) => {
|
|
22
|
+
const doc = parse(fs.readFileSync(path.join(dir, "infra", "metrics", "docker-compose.yml"), "utf8"));
|
|
23
|
+
return Object.keys(doc.services ?? {});
|
|
24
|
+
};
|
|
25
|
+
describe.skipIf(!REPO)("setupObservabilityStack — tiers (MO-STACK)", () => {
|
|
26
|
+
const repo = REPO;
|
|
27
|
+
let tmp;
|
|
28
|
+
beforeEach(() => {
|
|
29
|
+
tmp = fs.mkdtempSync(path.join(os.tmpdir(), "blok-obs-tier-"));
|
|
30
|
+
});
|
|
31
|
+
afterEach(() => {
|
|
32
|
+
fs.rmSync(tmp, { recursive: true, force: true });
|
|
33
|
+
});
|
|
34
|
+
it("none: writes nothing — no infra/metrics dir", () => {
|
|
35
|
+
const res = setupObservabilityStack(repo, tmp, "none");
|
|
36
|
+
expect(res.copied).toEqual([]);
|
|
37
|
+
expect(fs.existsSync(path.join(tmp, "infra", "metrics"))).toBe(false);
|
|
38
|
+
});
|
|
39
|
+
it("lite: exactly prometheus + grafana, and no loki/tempo/alloy", () => {
|
|
40
|
+
setupObservabilityStack(repo, tmp, "lite");
|
|
41
|
+
expect(composeServices(tmp).sort()).toEqual(["grafana", "prometheus"]);
|
|
42
|
+
const m = path.join(tmp, "infra", "metrics");
|
|
43
|
+
expect(fs.existsSync(path.join(m, "prometheus.yml"))).toBe(true);
|
|
44
|
+
expect(fs.existsSync(path.join(m, "loki-config.yaml"))).toBe(false);
|
|
45
|
+
expect(fs.existsSync(path.join(m, "tempo.yaml"))).toBe(false);
|
|
46
|
+
});
|
|
47
|
+
it("lite: no kept service depends_on a trimmed-away service", () => {
|
|
48
|
+
setupObservabilityStack(repo, tmp, "lite");
|
|
49
|
+
const doc = parse(fs.readFileSync(path.join(tmp, "infra", "metrics", "docker-compose.yml"), "utf8"));
|
|
50
|
+
for (const svc of Object.values(doc.services ?? {})) {
|
|
51
|
+
const dep = svc.depends_on;
|
|
52
|
+
const deps = Array.isArray(dep) ? dep : dep ? Object.keys(dep) : [];
|
|
53
|
+
for (const d of deps)
|
|
54
|
+
expect(["prometheus", "grafana"]).toContain(d);
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
it("full: all services + the whole file set", () => {
|
|
58
|
+
setupObservabilityStack(repo, tmp, "full");
|
|
59
|
+
expect(composeServices(tmp).sort()).toEqual([...ALL_OBS_SERVICES].sort());
|
|
60
|
+
const m = path.join(tmp, "infra", "metrics");
|
|
61
|
+
expect(fs.existsSync(path.join(m, "loki-config.yaml"))).toBe(true);
|
|
62
|
+
expect(fs.existsSync(path.join(m, "tempo.yaml"))).toBe(true);
|
|
63
|
+
});
|
|
64
|
+
it("is idempotent — re-running lite yields an identical compose file", () => {
|
|
65
|
+
setupObservabilityStack(repo, tmp, "lite");
|
|
66
|
+
const composePath = path.join(tmp, "infra", "metrics", "docker-compose.yml");
|
|
67
|
+
const first = fs.readFileSync(composePath, "utf8");
|
|
68
|
+
setupObservabilityStack(repo, tmp, "lite");
|
|
69
|
+
expect(fs.readFileSync(composePath, "utf8")).toBe(first);
|
|
70
|
+
});
|
|
71
|
+
});
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export type ObsStackTier = "none" | "lite" | "full";
|
|
2
|
+
export declare const OBS_STACK_TIERS: readonly ObsStackTier[];
|
|
3
|
+
export declare const ALL_OBS_SERVICES: readonly ["prometheus", "alertmanager", "grafana", "tempo", "loki", "nginx", "alloy"];
|
|
4
|
+
export interface TierDefinition {
|
|
5
|
+
services: string[];
|
|
6
|
+
files: string[] | "*";
|
|
7
|
+
}
|
|
8
|
+
export declare const TIER_DEFINITIONS: Record<ObsStackTier, TierDefinition>;
|
|
9
|
+
export declare function parseObsTier(value: string): ObsStackTier;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export const OBS_STACK_TIERS = ["none", "lite", "full"];
|
|
2
|
+
export const ALL_OBS_SERVICES = ["prometheus", "alertmanager", "grafana", "tempo", "loki", "nginx", "alloy"];
|
|
3
|
+
export const TIER_DEFINITIONS = {
|
|
4
|
+
none: { services: [], files: [] },
|
|
5
|
+
lite: {
|
|
6
|
+
services: ["prometheus", "grafana"],
|
|
7
|
+
files: ["docker-compose.yml", "prometheus.yml", "rules", "datasources.yml", "dashboards", "dashboard.json"],
|
|
8
|
+
},
|
|
9
|
+
full: { services: [...ALL_OBS_SERVICES], files: "*" },
|
|
10
|
+
};
|
|
11
|
+
export function parseObsTier(value) {
|
|
12
|
+
const v = value.trim().toLowerCase();
|
|
13
|
+
if (OBS_STACK_TIERS.includes(v))
|
|
14
|
+
return v;
|
|
15
|
+
throw new Error(`Invalid --obs-stack "${value}". Choose one of: ${OBS_STACK_TIERS.join(", ")}.`);
|
|
16
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { ObservabilityModuleConfig, ProjectConfig } from "./runtime-setup.js";
|
|
2
|
+
export declare function withObservabilityModule(config: ProjectConfig, id: string, moduleConfig: ObservabilityModuleConfig): ProjectConfig;
|
|
3
|
+
export declare function withoutObservabilityModule(config: ProjectConfig, id: string): ProjectConfig;
|
|
4
|
+
export declare function rewriteObservabilityEnvBlock(envContent: string, moduleBlocks: string[]): string;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
const OBS_START = "# >>> Blok observability (managed by blokctl) >>>";
|
|
2
|
+
const OBS_END = "# <<< Blok observability (managed by blokctl) <<<";
|
|
3
|
+
export function withObservabilityModule(config, id, moduleConfig) {
|
|
4
|
+
return { ...config, observability: { ...(config.observability ?? {}), [id]: moduleConfig } };
|
|
5
|
+
}
|
|
6
|
+
export function withoutObservabilityModule(config, id) {
|
|
7
|
+
if (!config.observability || !(id in config.observability))
|
|
8
|
+
return config;
|
|
9
|
+
const rest = Object.fromEntries(Object.entries(config.observability).filter(([k]) => k !== id));
|
|
10
|
+
return { ...config, observability: Object.keys(rest).length === 0 ? undefined : rest };
|
|
11
|
+
}
|
|
12
|
+
function stripManagedBlock(envContent) {
|
|
13
|
+
const out = [];
|
|
14
|
+
let inside = false;
|
|
15
|
+
for (const line of envContent.split("\n")) {
|
|
16
|
+
if (line.trim() === OBS_START) {
|
|
17
|
+
inside = true;
|
|
18
|
+
continue;
|
|
19
|
+
}
|
|
20
|
+
if (inside) {
|
|
21
|
+
if (line.trim() === OBS_END)
|
|
22
|
+
inside = false;
|
|
23
|
+
continue;
|
|
24
|
+
}
|
|
25
|
+
out.push(line);
|
|
26
|
+
}
|
|
27
|
+
return out.join("\n");
|
|
28
|
+
}
|
|
29
|
+
export function rewriteObservabilityEnvBlock(envContent, moduleBlocks) {
|
|
30
|
+
for (const block of moduleBlocks) {
|
|
31
|
+
if (/\bBLOK_METRICS_ENABLED\b/.test(block)) {
|
|
32
|
+
throw new Error("BLOK_METRICS_ENABLED is not a supported flag — metrics are ON by default; disable with BLOK_METRICS_DISABLED=1.");
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
const cleaned = stripManagedBlock(envContent)
|
|
36
|
+
.replace(/\n{3,}/g, "\n\n")
|
|
37
|
+
.replace(/\n+$/, "");
|
|
38
|
+
const body = moduleBlocks
|
|
39
|
+
.map((b) => b.trim())
|
|
40
|
+
.filter(Boolean)
|
|
41
|
+
.join("\n\n");
|
|
42
|
+
if (!body)
|
|
43
|
+
return cleaned.length > 0 ? `${cleaned}\n` : "";
|
|
44
|
+
const block = `${OBS_START}\n${body}\n${OBS_END}`;
|
|
45
|
+
return cleaned.length > 0 ? `${cleaned}\n\n${block}\n` : `${block}\n`;
|
|
46
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { rewriteObservabilityEnvBlock, withObservabilityModule, withoutObservabilityModule, } from "./observability-mutations.js";
|
|
3
|
+
const mod = (addedAt = "2026-01-01T00:00:00.000Z") => ({ enabled: true, addedAt });
|
|
4
|
+
describe("withObservabilityModule / withoutObservabilityModule", () => {
|
|
5
|
+
it("adds a module while preserving runtimes + triggers", () => {
|
|
6
|
+
const base = { runtimes: { go: { port: 0, startCmd: "", cwd: "", kind: "go", label: "Go" } } };
|
|
7
|
+
const next = withObservabilityModule(base, "metrics", mod());
|
|
8
|
+
expect(next.observability).toEqual({ metrics: mod() });
|
|
9
|
+
expect(next.runtimes).toBe(base.runtimes);
|
|
10
|
+
});
|
|
11
|
+
it("re-adding the same module is a no-op (identical output)", () => {
|
|
12
|
+
const a = withObservabilityModule({}, "tracing", mod());
|
|
13
|
+
const b = withObservabilityModule(a, "tracing", mod());
|
|
14
|
+
expect(b).toEqual(a);
|
|
15
|
+
});
|
|
16
|
+
it("removing the last module drops the observability key entirely", () => {
|
|
17
|
+
const one = withObservabilityModule({}, "metrics", mod());
|
|
18
|
+
const gone = withoutObservabilityModule(one, "metrics");
|
|
19
|
+
expect(gone.observability).toBeUndefined();
|
|
20
|
+
});
|
|
21
|
+
it("removing an absent module is a no-op", () => {
|
|
22
|
+
const cfg = { observability: { metrics: mod() } };
|
|
23
|
+
expect(withoutObservabilityModule(cfg, "tracing")).toBe(cfg);
|
|
24
|
+
});
|
|
25
|
+
it("removing one of several keeps the rest", () => {
|
|
26
|
+
const cfg = withObservabilityModule(withObservabilityModule({}, "metrics", mod()), "tracing", mod());
|
|
27
|
+
const next = withoutObservabilityModule(cfg, "metrics");
|
|
28
|
+
expect(Object.keys(next.observability ?? {})).toEqual(["tracing"]);
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
describe("rewriteObservabilityEnvBlock", () => {
|
|
32
|
+
it("appends a fenced block and is idempotent (run twice = identical)", () => {
|
|
33
|
+
const blocks = ["BLOK_TRACE_STORE=sqlite", "# BLOK_METRICS_DISABLED=1"];
|
|
34
|
+
const once = rewriteObservabilityEnvBlock("PORT=4000\n", blocks);
|
|
35
|
+
const twice = rewriteObservabilityEnvBlock(once, blocks);
|
|
36
|
+
expect(twice).toBe(once);
|
|
37
|
+
expect(once).toContain("PORT=4000");
|
|
38
|
+
expect(once).toContain("BLOK_TRACE_STORE=sqlite");
|
|
39
|
+
expect(once.match(/managed by blokctl/g)?.length).toBe(2);
|
|
40
|
+
});
|
|
41
|
+
it("preserves unrelated env vars when the module set changes", () => {
|
|
42
|
+
const first = rewriteObservabilityEnvBlock("SECRET=abc\n", ["BLOK_TRACE_STORE=sqlite"]);
|
|
43
|
+
const second = rewriteObservabilityEnvBlock(first, ["CONSOLE_LOG_ACTIVE=true"]);
|
|
44
|
+
expect(second).toContain("SECRET=abc");
|
|
45
|
+
expect(second).toContain("CONSOLE_LOG_ACTIVE=true");
|
|
46
|
+
expect(second).not.toContain("BLOK_TRACE_STORE");
|
|
47
|
+
});
|
|
48
|
+
it("removes the block when no modules remain", () => {
|
|
49
|
+
const withBlock = rewriteObservabilityEnvBlock("SECRET=abc\n", ["BLOK_TRACE_STORE=sqlite"]);
|
|
50
|
+
const cleared = rewriteObservabilityEnvBlock(withBlock, []);
|
|
51
|
+
expect(cleared).toBe("SECRET=abc\n");
|
|
52
|
+
expect(cleared).not.toContain("managed by blokctl");
|
|
53
|
+
});
|
|
54
|
+
it("rejects BLOK_METRICS_ENABLED (only the DISABLED kill-switch is supported)", () => {
|
|
55
|
+
expect(() => rewriteObservabilityEnvBlock("", ["BLOK_METRICS_ENABLED=false"])).toThrow(/BLOK_METRICS_ENABLED/);
|
|
56
|
+
});
|
|
57
|
+
});
|
|
@@ -23,13 +23,24 @@ export interface TriggerConfig {
|
|
|
23
23
|
entryPoint: string;
|
|
24
24
|
startCmd: string;
|
|
25
25
|
}
|
|
26
|
+
export interface ObservabilityModuleConfig {
|
|
27
|
+
enabled: boolean;
|
|
28
|
+
addedAt: string;
|
|
29
|
+
version?: string;
|
|
30
|
+
settings?: Record<string, unknown>;
|
|
31
|
+
}
|
|
26
32
|
export interface ProjectConfig {
|
|
27
33
|
triggers?: Record<string, TriggerConfig>;
|
|
28
34
|
runtimes?: Record<string, RuntimeConfig>;
|
|
35
|
+
observability?: Record<string, ObservabilityModuleConfig>;
|
|
29
36
|
}
|
|
30
37
|
export type ProjectRuntimeConfig = ProjectConfig;
|
|
31
38
|
export declare function setupRuntime(runtime: RuntimeInfo, githubRepoLocal: string, projectDir: string, spinner: SpinnerHandler): Promise<RuntimeConfig>;
|
|
32
|
-
export declare function
|
|
39
|
+
export declare function generateGoNodeRegistry(projectDir: string): string;
|
|
40
|
+
export declare function generateRustNodeRegistry(projectDir: string): string;
|
|
41
|
+
export declare function generateJavaNodeRegistry(projectDir: string): string;
|
|
42
|
+
export declare function generateCSharpNodeRegistry(projectDir: string): string;
|
|
43
|
+
export declare function writeProjectConfig(projectDir: string, runtimeConfigs: RuntimeConfig[], triggerConfigs?: TriggerConfig[], observabilityConfigs?: Record<string, ObservabilityModuleConfig>): void;
|
|
33
44
|
export declare function readProjectConfig(projectDir: string): ProjectConfig | null;
|
|
34
45
|
export declare function generateRuntimeEnvVars(runtimeConfigs: RuntimeConfig[]): string;
|
|
35
46
|
export declare function generateSupervisordConfig(runtimeConfigs: RuntimeConfig[]): string;
|