@intentius/behold 0.6.0 → 0.8.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/AGENTS.md +9 -2
- package/README.md +12 -2
- package/demos.json +8 -0
- package/dist/cli.js +769 -139
- package/example-argo-estate/README.md +46 -0
- package/example-argo-estate/app-a/chant.config.ts +10 -0
- package/example-argo-estate/app-a/manifests/app.yaml +54 -0
- package/example-argo-estate/app-a/package.json +13 -0
- package/example-argo-estate/app-a/src/app.ts +35 -0
- package/example-argo-estate/app-a/tsconfig.json +1 -0
- package/example-argo-estate/app-b/chant.config.ts +9 -0
- package/example-argo-estate/app-b/manifests/app.yaml +54 -0
- package/example-argo-estate/app-b/package.json +13 -0
- package/example-argo-estate/app-b/src/app.ts +34 -0
- package/example-argo-estate/app-b/tsconfig.json +1 -0
- package/example-argo-estate/control-plane/chant.config.ts +14 -0
- package/example-argo-estate/control-plane/package.json +13 -0
- package/example-argo-estate/control-plane/src/argo.ts +69 -0
- package/example-argo-estate/control-plane/tsconfig.json +1 -0
- package/example-argo-estate/package-lock.json +1949 -0
- package/example-argo-estate/package.json +10 -0
- package/example-flux-estate/README.md +13 -8
- package/example-flux-estate/app-a/package.json +3 -3
- package/example-flux-estate/app-b/package.json +3 -3
- package/example-flux-estate/control-plane/package.json +2 -2
- package/example-flux-estate/control-plane/src/flux.ts +6 -0
- package/example-flux-estate/package-lock.json +17 -17
- package/example-k8s/README.md +16 -2
- package/example-k8s/base/deployment.yaml +21 -0
- package/example-k8s/base/kustomization.yaml +3 -0
- package/example-k8s/base/service.yaml +12 -0
- package/example-k8s/chant.config.ts +10 -0
- package/example-k8s/overlays/dev/kustomization.yaml +6 -0
- package/example-k8s/package-lock.json +43 -40
- package/example-k8s/package.json +4 -4
- package/example-writes/package-lock.json +18 -16
- package/example-writes/package.json +3 -3
- package/package.json +3 -2
- package/web/app.js +359 -21
- package/web/icons/cncf/argo.svg +1 -0
- package/web/icons/cncf/flux.svg +1 -0
- package/web/icons/cncf/helm.svg +1 -0
- package/web/icons/k8s/c-role.svg +1 -0
- package/web/icons/k8s/cm.svg +1 -0
- package/web/icons/k8s/crb.svg +1 -0
- package/web/icons/k8s/crd.svg +1 -0
- package/web/icons/k8s/cronjob.svg +1 -0
- package/web/icons/k8s/deploy.svg +1 -0
- package/web/icons/k8s/ds.svg +1 -0
- package/web/icons/k8s/ep.svg +1 -0
- package/web/icons/k8s/group.svg +1 -0
- package/web/icons/k8s/hpa.svg +1 -0
- package/web/icons/k8s/ing.svg +1 -0
- package/web/icons/k8s/job.svg +1 -0
- package/web/icons/k8s/limits.svg +1 -0
- package/web/icons/k8s/netpol.svg +1 -0
- package/web/icons/k8s/ns.svg +1 -0
- package/web/icons/k8s/pod.svg +1 -0
- package/web/icons/k8s/psp.svg +1 -0
- package/web/icons/k8s/pv.svg +1 -0
- package/web/icons/k8s/pvc.svg +1 -0
- package/web/icons/k8s/quota.svg +1 -0
- package/web/icons/k8s/rb.svg +1 -0
- package/web/icons/k8s/role.svg +1 -0
- package/web/icons/k8s/rs.svg +1 -0
- package/web/icons/k8s/sa.svg +1 -0
- package/web/icons/k8s/sc.svg +1 -0
- package/web/icons/k8s/secret.svg +1 -0
- package/web/icons/k8s/sts.svg +1 -0
- package/web/icons/k8s/svc.svg +1 -0
- package/web/icons/k8s/user.svg +1 -0
- package/web/icons/k8s/vol.svg +1 -0
- package/web/index.html +192 -84
- package/web/layout-store.js +118 -0
- package/web/layout-store.test.js +163 -0
- package/web/theme.js +58 -6
package/dist/cli.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
// src/cli.ts
|
|
2
|
-
import { resolve as
|
|
2
|
+
import { resolve as resolve4, dirname as dirname6, join as join14, relative as relative3, sep } from "node:path";
|
|
3
3
|
import { realpathSync, existsSync as existsSync10, cpSync } from "node:fs";
|
|
4
4
|
import { spawnSync as spawnSync2 } from "node:child_process";
|
|
5
|
-
import { fileURLToPath as
|
|
5
|
+
import { fileURLToPath as fileURLToPath5 } from "node:url";
|
|
6
6
|
|
|
7
7
|
// src/server.ts
|
|
8
8
|
import { Hono } from "hono";
|
|
@@ -87,11 +87,11 @@ function contextBindsCluster(context, clusterName) {
|
|
|
87
87
|
import { streamSSE } from "hono/streaming";
|
|
88
88
|
import { serveStatic } from "@hono/node-server/serve-static";
|
|
89
89
|
import { serve } from "@hono/node-server";
|
|
90
|
-
import { fileURLToPath } from "node:url";
|
|
90
|
+
import { fileURLToPath as fileURLToPath3 } from "node:url";
|
|
91
91
|
import { execFile as execFile4 } from "node:child_process";
|
|
92
92
|
import { promisify as promisify4 } from "node:util";
|
|
93
|
-
import { existsSync as existsSync9, readFileSync as
|
|
94
|
-
import { dirname as
|
|
93
|
+
import { existsSync as existsSync9, readFileSync as readFileSync9 } from "node:fs";
|
|
94
|
+
import { dirname as dirname4, join as join11, relative, resolve as resolve2 } from "node:path";
|
|
95
95
|
|
|
96
96
|
// src/recents.ts
|
|
97
97
|
import { readFileSync, writeFileSync, mkdirSync, existsSync } from "node:fs";
|
|
@@ -125,8 +125,9 @@ function addRecent(dir, now = () => (/* @__PURE__ */ new Date()).toISOString())
|
|
|
125
125
|
|
|
126
126
|
// src/chant.ts
|
|
127
127
|
import { spawn } from "node:child_process";
|
|
128
|
-
import { existsSync as existsSync3 } from "node:fs";
|
|
128
|
+
import { existsSync as existsSync3, readFileSync as readFileSync3 } from "node:fs";
|
|
129
129
|
import { createRequire } from "node:module";
|
|
130
|
+
import { fileURLToPath } from "node:url";
|
|
130
131
|
import { dirname as dirname2, join as join3, resolve } from "node:path";
|
|
131
132
|
|
|
132
133
|
// src/project.ts
|
|
@@ -135,7 +136,7 @@ import { join as join2 } from "node:path";
|
|
|
135
136
|
import { pathToFileURL } from "node:url";
|
|
136
137
|
var CONFIG_NAMES = ["chant.config.ts", "chant.config.mts", "chant.config.js", "chant.config.mjs"];
|
|
137
138
|
var BEHOLD_CONFIG_NAME = ".behold.json";
|
|
138
|
-
function
|
|
139
|
+
function chantConfigPath(projectDir) {
|
|
139
140
|
for (const name of CONFIG_NAMES) {
|
|
140
141
|
const p = join2(projectDir, name);
|
|
141
142
|
if (existsSync2(p)) return p;
|
|
@@ -222,7 +223,7 @@ function parseStringLiteral(content, key) {
|
|
|
222
223
|
return m?.[1];
|
|
223
224
|
}
|
|
224
225
|
async function detectProject(projectDir) {
|
|
225
|
-
const path =
|
|
226
|
+
const path = chantConfigPath(projectDir);
|
|
226
227
|
if (!path) return { environments: [], lexicons: [] };
|
|
227
228
|
try {
|
|
228
229
|
const mod = await import(pathToFileURL(path).href);
|
|
@@ -253,11 +254,30 @@ function loadBeholdConfig(projectDir) {
|
|
|
253
254
|
try {
|
|
254
255
|
const raw = JSON.parse(readFileSync2(path, "utf8"));
|
|
255
256
|
const tiers = readTiers(raw);
|
|
256
|
-
|
|
257
|
+
const members = Array.isArray(raw.members) ? raw.members.filter((m) => typeof m === "string" && !!m) : [];
|
|
258
|
+
return { ...tiers ? { tiers } : {}, ...members.length ? { members } : {} };
|
|
257
259
|
} catch {
|
|
258
260
|
return {};
|
|
259
261
|
}
|
|
260
262
|
}
|
|
263
|
+
function readWorkspaces(projectDir) {
|
|
264
|
+
try {
|
|
265
|
+
const raw = JSON.parse(readFileSync2(join2(projectDir, "package.json"), "utf8"));
|
|
266
|
+
const ws = Array.isArray(raw.workspaces) ? raw.workspaces : raw.workspaces?.packages;
|
|
267
|
+
return Array.isArray(ws) ? ws.filter((w) => typeof w === "string") : [];
|
|
268
|
+
} catch {
|
|
269
|
+
return [];
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
function detectProjectShape(projectDir) {
|
|
273
|
+
const configFile = chantConfigPath(projectDir);
|
|
274
|
+
if (configFile) return { kind: "project", configFile };
|
|
275
|
+
const declared = loadBeholdConfig(projectDir).members;
|
|
276
|
+
const from = declared ? "behold-config" : "workspaces";
|
|
277
|
+
const members = (declared ?? readWorkspaces(projectDir)).filter((m) => !!chantConfigPath(join2(projectDir, m)));
|
|
278
|
+
if (members.length) return { kind: "estate", members, membersFrom: from };
|
|
279
|
+
return { kind: "none" };
|
|
280
|
+
}
|
|
261
281
|
|
|
262
282
|
// node_modules/@intentius/chant/src/yaml.ts
|
|
263
283
|
function parseYAML(content) {
|
|
@@ -545,19 +565,18 @@ function graphFlags(opts) {
|
|
|
545
565
|
if (opts.overlay) flags.push("--overlay");
|
|
546
566
|
return flags;
|
|
547
567
|
}
|
|
548
|
-
function
|
|
568
|
+
function packageManifestFrom(req, name) {
|
|
549
569
|
let entry;
|
|
550
570
|
try {
|
|
551
|
-
entry = req.resolve(
|
|
571
|
+
entry = req.resolve(name);
|
|
552
572
|
} catch {
|
|
553
573
|
return void 0;
|
|
554
574
|
}
|
|
555
575
|
let dir = dirname2(entry);
|
|
556
576
|
for (; ; ) {
|
|
557
|
-
const manifest = join3(dir, "package.json");
|
|
558
577
|
try {
|
|
559
|
-
const pkg = createRequire(import.meta.url)(
|
|
560
|
-
if (pkg.name ===
|
|
578
|
+
const pkg = createRequire(import.meta.url)(join3(dir, "package.json"));
|
|
579
|
+
if (pkg.name === name) return { root: dir, manifest: pkg };
|
|
561
580
|
} catch {
|
|
562
581
|
}
|
|
563
582
|
const parent = dirname2(dir);
|
|
@@ -566,14 +585,52 @@ function chantBinFrom(req) {
|
|
|
566
585
|
}
|
|
567
586
|
return void 0;
|
|
568
587
|
}
|
|
569
|
-
function
|
|
588
|
+
function chantPackageFrom(req) {
|
|
589
|
+
const found = packageManifestFrom(req, "@intentius/chant");
|
|
590
|
+
if (!found) return void 0;
|
|
591
|
+
const { root, manifest } = found;
|
|
592
|
+
return { root, bin: join3(root, manifest.bin?.chant ?? "bin/chant"), ...manifest.version ? { version: manifest.version } : {} };
|
|
593
|
+
}
|
|
594
|
+
function resolveLexicons(projectDir, lexicons) {
|
|
595
|
+
const req = createRequire(join3(resolve(projectDir), "noop.js"));
|
|
596
|
+
return lexicons.map((lexicon) => {
|
|
597
|
+
const pkg = `@intentius/chant-lexicon-${lexicon}`;
|
|
598
|
+
const found = packageManifestFrom(req, pkg);
|
|
599
|
+
return { lexicon, pkg, installed: !!found, ...found?.manifest.version ? { version: found.manifest.version } : {} };
|
|
600
|
+
});
|
|
601
|
+
}
|
|
602
|
+
function resolveChant(projectDir) {
|
|
603
|
+
const own = chantPackageFrom(createRequire(import.meta.url));
|
|
570
604
|
if (projectDir) {
|
|
571
|
-
const fromProject =
|
|
572
|
-
if (fromProject) return fromProject;
|
|
605
|
+
const fromProject = chantPackageFrom(createRequire(join3(resolve(projectDir), "noop.js")));
|
|
606
|
+
if (fromProject) return { source: fromProject.root === own?.root ? "behold" : "project", ...fromProject };
|
|
607
|
+
}
|
|
608
|
+
if (own) return { source: "behold", ...own };
|
|
609
|
+
return { source: "path", bin: "chant" };
|
|
610
|
+
}
|
|
611
|
+
function chantFloor() {
|
|
612
|
+
try {
|
|
613
|
+
const here = dirname2(fileURLToPath(import.meta.url));
|
|
614
|
+
const pkg = JSON.parse(readFileSync3(join3(here, "..", "package.json"), "utf8"));
|
|
615
|
+
return pkg.dependencies?.["@intentius/chant"]?.replace(/^[^0-9]*/, "") || void 0;
|
|
616
|
+
} catch {
|
|
617
|
+
return void 0;
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
function meetsFloor(version, floor) {
|
|
621
|
+
if (!version || !floor) return true;
|
|
622
|
+
const parts = (v) => v.split("-")[0].split(".").map((n) => Number.parseInt(n, 10));
|
|
623
|
+
const a = parts(version);
|
|
624
|
+
const b = parts(floor);
|
|
625
|
+
if (a.some(Number.isNaN) || b.some(Number.isNaN)) return true;
|
|
626
|
+
for (let i = 0; i < Math.max(a.length, b.length); i++) {
|
|
627
|
+
const d = (a[i] ?? 0) - (b[i] ?? 0);
|
|
628
|
+
if (d !== 0) return d > 0;
|
|
573
629
|
}
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
630
|
+
return true;
|
|
631
|
+
}
|
|
632
|
+
function chantBin(projectDir) {
|
|
633
|
+
return resolveChant(projectDir).bin;
|
|
577
634
|
}
|
|
578
635
|
function runChantRaw(args, projectDir, envOverride) {
|
|
579
636
|
return new Promise((resolvePromise, reject) => {
|
|
@@ -798,7 +855,7 @@ function mergeClusterRoot(ir, clusterIr, running) {
|
|
|
798
855
|
}
|
|
799
856
|
|
|
800
857
|
// src/helm-releases.ts
|
|
801
|
-
import { existsSync as existsSync4, readdirSync, readFileSync as
|
|
858
|
+
import { existsSync as existsSync4, readdirSync, readFileSync as readFileSync4, statSync } from "node:fs";
|
|
802
859
|
import { join as join4 } from "node:path";
|
|
803
860
|
var RELEASE_KEY = /^release\/([^/]+)\/(.+)$/;
|
|
804
861
|
function declaredChartNames(ir) {
|
|
@@ -839,7 +896,7 @@ function discoverReleaseUnits(projectDir) {
|
|
|
839
896
|
for (const file of files) {
|
|
840
897
|
let content;
|
|
841
898
|
try {
|
|
842
|
-
content =
|
|
899
|
+
content = readFileSync4(file, "utf8");
|
|
843
900
|
} catch {
|
|
844
901
|
continue;
|
|
845
902
|
}
|
|
@@ -892,7 +949,7 @@ function synthesizeHelmReleases(ir, observed, owners) {
|
|
|
892
949
|
|
|
893
950
|
// src/gh-run.ts
|
|
894
951
|
import { spawn as spawn2 } from "node:child_process";
|
|
895
|
-
import { readdirSync as readdirSync2, readFileSync as
|
|
952
|
+
import { readdirSync as readdirSync2, readFileSync as readFileSync5 } from "node:fs";
|
|
896
953
|
import { join as join5 } from "node:path";
|
|
897
954
|
|
|
898
955
|
// src/ci-run.ts
|
|
@@ -947,19 +1004,19 @@ function finishPipelineProgress(state, exitCode) {
|
|
|
947
1004
|
}
|
|
948
1005
|
|
|
949
1006
|
// src/gh-run.ts
|
|
950
|
-
var defaultGhExec = (args) => new Promise((
|
|
1007
|
+
var defaultGhExec = (args) => new Promise((resolve5) => {
|
|
951
1008
|
let out = "";
|
|
952
1009
|
let proc;
|
|
953
1010
|
try {
|
|
954
1011
|
proc = spawn2("gh", args, { stdio: ["ignore", "pipe", "pipe"] });
|
|
955
1012
|
} catch {
|
|
956
|
-
|
|
1013
|
+
resolve5({ code: 127, out: "" });
|
|
957
1014
|
return;
|
|
958
1015
|
}
|
|
959
1016
|
proc.stdout.on("data", (d) => out += d);
|
|
960
1017
|
proc.stderr.on("data", (d) => out += d);
|
|
961
|
-
proc.on("error", () =>
|
|
962
|
-
proc.on("close", (code) =>
|
|
1018
|
+
proc.on("error", () => resolve5({ code: 127, out }));
|
|
1019
|
+
proc.on("close", (code) => resolve5({ code: code ?? 1, out }));
|
|
963
1020
|
});
|
|
964
1021
|
async function ghReady(exec = defaultGhExec) {
|
|
965
1022
|
const { code, out } = await exec(["auth", "status"]);
|
|
@@ -993,7 +1050,7 @@ function pickWorkflow(projectDir, pipeline) {
|
|
|
993
1050
|
for (const f of files) {
|
|
994
1051
|
let text;
|
|
995
1052
|
try {
|
|
996
|
-
text =
|
|
1053
|
+
text = readFileSync5(join5(dir, f), "utf8");
|
|
997
1054
|
} catch {
|
|
998
1055
|
continue;
|
|
999
1056
|
}
|
|
@@ -1309,13 +1366,22 @@ var FLUX_SOURCE_KINDS = /* @__PURE__ */ new Set([
|
|
|
1309
1366
|
"K8s::Flux::HelmChart",
|
|
1310
1367
|
"K8s::Flux::Bucket"
|
|
1311
1368
|
]);
|
|
1312
|
-
function sourceRefOf(refObj, referrer) {
|
|
1369
|
+
function sourceRefOf(refObj, referrer, defaultKind) {
|
|
1313
1370
|
const ref = rec(refObj);
|
|
1314
|
-
const kind = str2(ref?.kind);
|
|
1371
|
+
const kind = str2(ref?.kind) ?? defaultKind;
|
|
1315
1372
|
const name = str2(ref?.name);
|
|
1316
1373
|
if (!kind || !name) return void 0;
|
|
1317
1374
|
return { kind, name, namespace: str2(ref?.namespace) ?? namespaceOf(referrer) };
|
|
1318
1375
|
}
|
|
1376
|
+
function namedRefOf(refObj, referrer) {
|
|
1377
|
+
const ref = rec(refObj);
|
|
1378
|
+
const name = str2(ref?.name);
|
|
1379
|
+
if (!name) return void 0;
|
|
1380
|
+
return { name, namespace: str2(ref?.namespace) ?? namespaceOf(referrer) };
|
|
1381
|
+
}
|
|
1382
|
+
function list(v) {
|
|
1383
|
+
return Array.isArray(v) ? v : [];
|
|
1384
|
+
}
|
|
1319
1385
|
function helmReleaseSourceRef(n) {
|
|
1320
1386
|
return rec(rec(rec(n.attrs?.spec)?.chart)?.spec)?.sourceRef;
|
|
1321
1387
|
}
|
|
@@ -1343,19 +1409,38 @@ function deriveK8sEdges(nodes) {
|
|
|
1343
1409
|
if (from !== to) out.push({ from, to, kind: "ref", viaAttr, inferred: true });
|
|
1344
1410
|
};
|
|
1345
1411
|
const workloads = k8s.filter((n) => WORKLOAD_KINDS.has(n.kind));
|
|
1346
|
-
const
|
|
1347
|
-
const
|
|
1412
|
+
const byKindNsName = /* @__PURE__ */ new Map();
|
|
1413
|
+
const appProjectsByName = /* @__PURE__ */ new Map();
|
|
1348
1414
|
for (const n of k8s) {
|
|
1349
1415
|
const name = nameOf(n);
|
|
1350
1416
|
if (!name) continue;
|
|
1351
|
-
|
|
1352
|
-
if (
|
|
1417
|
+
byKindNsName.set(`${n.kind}\0${namespaceOf(n)}\0${name}`, n);
|
|
1418
|
+
if (n.kind === "K8s::Argo::AppProject") {
|
|
1419
|
+
const same = appProjectsByName.get(name) ?? [];
|
|
1420
|
+
same.push(n);
|
|
1421
|
+
appProjectsByName.set(name, same);
|
|
1422
|
+
}
|
|
1353
1423
|
}
|
|
1354
|
-
const
|
|
1355
|
-
|
|
1424
|
+
const lookup = (kind, namespace, name) => byKindNsName.get(`${kind}\0${namespace}\0${name}`);
|
|
1425
|
+
const addFluxRef = (n, refObj, viaAttr, allowed, defaultKind) => {
|
|
1426
|
+
const ref = sourceRefOf(refObj, n, defaultKind);
|
|
1427
|
+
if (!ref || ref.name === "*") return;
|
|
1428
|
+
const kind = `K8s::Flux::${ref.kind}`;
|
|
1429
|
+
if (allowed && !allowed.has(kind)) return;
|
|
1430
|
+
const target = lookup(kind, ref.namespace, ref.name);
|
|
1431
|
+
if (target) add(n.id, target.id, viaAttr);
|
|
1432
|
+
};
|
|
1433
|
+
const addSourceRef = (n, refObj, viaAttr, defaultKind) => addFluxRef(n, refObj, viaAttr, FLUX_SOURCE_KINDS, defaultKind);
|
|
1434
|
+
const addNamedRef = (n, refObj, kind, viaAttr) => {
|
|
1435
|
+
const ref = namedRefOf(refObj, n);
|
|
1356
1436
|
if (!ref) return;
|
|
1357
|
-
const
|
|
1358
|
-
if (
|
|
1437
|
+
const target = lookup(kind, ref.namespace, ref.name);
|
|
1438
|
+
if (target) add(n.id, target.id, viaAttr);
|
|
1439
|
+
};
|
|
1440
|
+
const addProject = (n, project, viaAttr) => {
|
|
1441
|
+
const candidates = project ? appProjectsByName.get(project) ?? [] : [];
|
|
1442
|
+
const target = candidates.length === 1 ? candidates[0] : candidates.find((p) => namespaceOf(p) === namespaceOf(n));
|
|
1443
|
+
if (target) add(n.id, target.id, viaAttr);
|
|
1359
1444
|
};
|
|
1360
1445
|
for (const n of k8s) {
|
|
1361
1446
|
if (n.kind === "K8s::Core::Service") {
|
|
@@ -1370,7 +1455,7 @@ function deriveK8sEdges(nodes) {
|
|
|
1370
1455
|
}
|
|
1371
1456
|
if (n.kind === "K8s::Networking::Ingress") {
|
|
1372
1457
|
for (const svcName of ingressBackendServices(n)) {
|
|
1373
|
-
const svc =
|
|
1458
|
+
const svc = lookup("K8s::Core::Service", namespaceOf(n), svcName);
|
|
1374
1459
|
if (svc) add(n.id, svc.id, "ingress backend");
|
|
1375
1460
|
}
|
|
1376
1461
|
continue;
|
|
@@ -1388,10 +1473,36 @@ function deriveK8sEdges(nodes) {
|
|
|
1388
1473
|
}
|
|
1389
1474
|
if (n.kind === "K8s::Flux::Kustomization") {
|
|
1390
1475
|
addSourceRef(n, rec(n.attrs?.spec)?.sourceRef, "sourceRef");
|
|
1476
|
+
for (const dep of list(rec(n.attrs?.spec)?.dependsOn)) {
|
|
1477
|
+
addNamedRef(n, dep, "K8s::Flux::Kustomization", "dependsOn");
|
|
1478
|
+
}
|
|
1391
1479
|
continue;
|
|
1392
1480
|
}
|
|
1393
1481
|
if (n.kind === "K8s::Flux::HelmRelease") {
|
|
1394
1482
|
addSourceRef(n, helmReleaseSourceRef(n), "chart sourceRef");
|
|
1483
|
+
continue;
|
|
1484
|
+
}
|
|
1485
|
+
if (n.kind === "K8s::Flux::ImagePolicy") {
|
|
1486
|
+
addNamedRef(n, rec(n.attrs?.spec)?.imageRepositoryRef, "K8s::Flux::ImageRepository", "imageRepositoryRef");
|
|
1487
|
+
continue;
|
|
1488
|
+
}
|
|
1489
|
+
if (n.kind === "K8s::Flux::ImageUpdateAutomation") {
|
|
1490
|
+
addSourceRef(n, rec(n.attrs?.spec)?.sourceRef, "sourceRef", "GitRepository");
|
|
1491
|
+
continue;
|
|
1492
|
+
}
|
|
1493
|
+
if (n.kind === "K8s::Flux::Alert") {
|
|
1494
|
+
addNamedRef(n, rec(n.attrs?.spec)?.providerRef, "K8s::Flux::Provider", "providerRef");
|
|
1495
|
+
for (const src of list(rec(n.attrs?.spec)?.eventSources)) {
|
|
1496
|
+
addFluxRef(n, src, "eventSource");
|
|
1497
|
+
}
|
|
1498
|
+
continue;
|
|
1499
|
+
}
|
|
1500
|
+
if (n.kind === "K8s::Argo::Application") {
|
|
1501
|
+
addProject(n, str2(rec(n.attrs?.spec)?.project), "project");
|
|
1502
|
+
continue;
|
|
1503
|
+
}
|
|
1504
|
+
if (n.kind === "K8s::Argo::ApplicationSet") {
|
|
1505
|
+
addProject(n, str2(rec(rec(rec(n.attrs?.spec)?.template)?.spec)?.project), "template project");
|
|
1395
1506
|
}
|
|
1396
1507
|
}
|
|
1397
1508
|
return out;
|
|
@@ -1883,6 +1994,13 @@ function metaStr(node, key) {
|
|
|
1883
1994
|
const v = meta?.[key];
|
|
1884
1995
|
return typeof v === "string" ? v : void 0;
|
|
1885
1996
|
}
|
|
1997
|
+
function destinationNamespace(n) {
|
|
1998
|
+
if (n.kind !== "K8s::Argo::Application") return void 0;
|
|
1999
|
+
const spec = n.attrs?.spec;
|
|
2000
|
+
const dest = spec?.destination;
|
|
2001
|
+
const ns = dest?.namespace;
|
|
2002
|
+
return typeof ns === "string" && ns.length > 0 ? ns : void 0;
|
|
2003
|
+
}
|
|
1886
2004
|
function declaredNamespaces(nodes) {
|
|
1887
2005
|
const out = /* @__PURE__ */ new Set();
|
|
1888
2006
|
for (const n of nodes) {
|
|
@@ -1893,6 +2011,8 @@ function declaredNamespaces(nodes) {
|
|
|
1893
2011
|
}
|
|
1894
2012
|
const ns = metaStr(n, "namespace");
|
|
1895
2013
|
if (ns) out.add(ns);
|
|
2014
|
+
const dest = destinationNamespace(n);
|
|
2015
|
+
if (dest) out.add(dest);
|
|
1896
2016
|
}
|
|
1897
2017
|
return out;
|
|
1898
2018
|
}
|
|
@@ -1972,6 +2092,12 @@ import { join as join6 } from "node:path";
|
|
|
1972
2092
|
function overlayBoxTitle(name) {
|
|
1973
2093
|
return `overlay ${name}`;
|
|
1974
2094
|
}
|
|
2095
|
+
var ROOT_ANNOTATION = "chant.intentius.io/kustomize-root";
|
|
2096
|
+
function annotatedRoot(node) {
|
|
2097
|
+
const metadata2 = node.attrs?.metadata;
|
|
2098
|
+
const v = metadata2?.annotations?.[ROOT_ANNOTATION];
|
|
2099
|
+
return typeof v === "string" && v ? v : void 0;
|
|
2100
|
+
}
|
|
1975
2101
|
function dirOf2(node) {
|
|
1976
2102
|
const file = node.sourceLoc?.file;
|
|
1977
2103
|
if (typeof file !== "string") return "";
|
|
@@ -2002,12 +2128,15 @@ function projectKustomizeLogical(ir, sourceRoots, exists = existsSync5) {
|
|
|
2002
2128
|
};
|
|
2003
2129
|
const rootCache = /* @__PURE__ */ new Map();
|
|
2004
2130
|
for (const n of k8s) {
|
|
2005
|
-
|
|
2006
|
-
if (
|
|
2007
|
-
|
|
2008
|
-
|
|
2131
|
+
let root = annotatedRoot(n);
|
|
2132
|
+
if (root === void 0) {
|
|
2133
|
+
const dir = dirOf2(n);
|
|
2134
|
+
if (dir === "") continue;
|
|
2135
|
+
if (!rootCache.has(dir)) rootCache.set(dir, kustomizationRoot(bases, dir, exists));
|
|
2136
|
+
root = rootCache.get(dir);
|
|
2137
|
+
}
|
|
2009
2138
|
if (root === void 0) continue;
|
|
2010
|
-
const name = root.split("/").pop() || root;
|
|
2139
|
+
const name = root.replace(/\/+$/, "").split("/").pop() || root;
|
|
2011
2140
|
child(overlayBoxTitle(name), n.id);
|
|
2012
2141
|
}
|
|
2013
2142
|
return { ir: { nodes: [], edges: [], groups: {} }, byContainer };
|
|
@@ -2214,9 +2343,9 @@ function enrichedRefs(ir, aws, byId) {
|
|
|
2214
2343
|
const producersByName = /* @__PURE__ */ new Map();
|
|
2215
2344
|
for (const ex of ir.exports ?? []) {
|
|
2216
2345
|
if (!ex.node || !byId.has(ex.node)) continue;
|
|
2217
|
-
const
|
|
2218
|
-
if (isExample(byId.get(ex.node)))
|
|
2219
|
-
else
|
|
2346
|
+
const list3 = producersByName.get(ex.name) ?? producersByName.set(ex.name, []).get(ex.name);
|
|
2347
|
+
if (isExample(byId.get(ex.node))) list3.push(ex.node);
|
|
2348
|
+
else list3.unshift(ex.node);
|
|
2220
2349
|
}
|
|
2221
2350
|
const exportNames = [...producersByName.keys()].sort((a, b) => b.length - a.length);
|
|
2222
2351
|
const bridge = (paramId, producers) => {
|
|
@@ -2704,7 +2833,83 @@ function summarizePlan(plan, byComponent, nonResource) {
|
|
|
2704
2833
|
}
|
|
2705
2834
|
|
|
2706
2835
|
// src/render.ts
|
|
2707
|
-
import { layoutIr, layoutArchitecture, renderSvg, cardSizes } from "@intentius/pinhole";
|
|
2836
|
+
import { layoutIr, layoutArchitecture, renderSvg, cardSizes, registerPack } from "@intentius/pinhole";
|
|
2837
|
+
|
|
2838
|
+
// src/icon-packs.ts
|
|
2839
|
+
import { readFileSync as readFileSync6 } from "node:fs";
|
|
2840
|
+
import { dirname as dirname3, join as join7 } from "node:path";
|
|
2841
|
+
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
2842
|
+
var ICON_ROOT = join7(dirname3(fileURLToPath2(import.meta.url)), "..", "web", "icons");
|
|
2843
|
+
var cache = /* @__PURE__ */ new Map();
|
|
2844
|
+
function unprefix(body) {
|
|
2845
|
+
return body.replace(/\s(?:inkscape|sodipodi):[\w-]+\s*=\s*(["'])[\s\S]*?\1/g, "").replace(/\sxlink:href\s*=/g, " href=");
|
|
2846
|
+
}
|
|
2847
|
+
function loadIcon(rel) {
|
|
2848
|
+
const hit = cache.get(rel);
|
|
2849
|
+
if (hit) return hit;
|
|
2850
|
+
const raw = readFileSync6(join7(ICON_ROOT, `${rel}.svg`), "utf8");
|
|
2851
|
+
const open = raw.indexOf("<svg");
|
|
2852
|
+
const openEnd = raw.indexOf(">", open);
|
|
2853
|
+
const close = raw.lastIndexOf("</svg");
|
|
2854
|
+
if (open < 0 || openEnd < 0 || close < 0) throw new Error(`malformed vendored icon: ${rel}.svg`);
|
|
2855
|
+
const viewBox = /\bviewBox\s*=\s*(["'])(.*?)\1/.exec(raw.slice(open, openEnd))?.[2];
|
|
2856
|
+
if (!viewBox) throw new Error(`vendored icon has no viewBox: ${rel}.svg`);
|
|
2857
|
+
const glyph = { body: unprefix(raw.slice(openEnd + 1, close).trim()), colored: true, viewBox };
|
|
2858
|
+
cache.set(rel, glyph);
|
|
2859
|
+
return glyph;
|
|
2860
|
+
}
|
|
2861
|
+
var K8S_ICON_BY_KIND = {
|
|
2862
|
+
"K8s::Apiextensions::CustomResourceDefinition": "crd",
|
|
2863
|
+
"K8s::Apps::DaemonSet": "ds",
|
|
2864
|
+
"K8s::Apps::Deployment": "deploy",
|
|
2865
|
+
"K8s::Apps::ReplicaSet": "rs",
|
|
2866
|
+
"K8s::Apps::StatefulSet": "sts",
|
|
2867
|
+
"K8s::Autoscaling::HorizontalPodAutoscaler": "hpa",
|
|
2868
|
+
"K8s::Batch::CronJob": "cronjob",
|
|
2869
|
+
"K8s::Batch::Job": "job",
|
|
2870
|
+
"K8s::Core::ConfigMap": "cm",
|
|
2871
|
+
"K8s::Core::Endpoints": "ep",
|
|
2872
|
+
"K8s::Core::LimitRange": "limits",
|
|
2873
|
+
"K8s::Core::Namespace": "ns",
|
|
2874
|
+
"K8s::Core::PersistentVolume": "pv",
|
|
2875
|
+
"K8s::Core::PersistentVolumeClaim": "pvc",
|
|
2876
|
+
"K8s::Core::Pod": "pod",
|
|
2877
|
+
"K8s::Core::PodTemplate": "pod",
|
|
2878
|
+
"K8s::Core::ReplicationController": "rs",
|
|
2879
|
+
// the pre-ReplicaSet spelling of the same thing
|
|
2880
|
+
"K8s::Core::ResourceQuota": "quota",
|
|
2881
|
+
"K8s::Core::Secret": "secret",
|
|
2882
|
+
"K8s::Core::Service": "svc",
|
|
2883
|
+
"K8s::Core::ServiceAccount": "sa",
|
|
2884
|
+
"K8s::Core::Volume": "vol",
|
|
2885
|
+
"K8s::Networking::Ingress": "ing",
|
|
2886
|
+
"K8s::Networking::IngressClass": "ing",
|
|
2887
|
+
"K8s::Networking::NetworkPolicy": "netpol",
|
|
2888
|
+
"K8s::Rbac::ClusterRole": "c-role",
|
|
2889
|
+
"K8s::Rbac::ClusterRoleBinding": "crb",
|
|
2890
|
+
"K8s::Rbac::Role": "role",
|
|
2891
|
+
"K8s::Rbac::RoleBinding": "rb",
|
|
2892
|
+
"K8s::Storage::StorageClass": "sc"
|
|
2893
|
+
};
|
|
2894
|
+
var K8S_MARK_BY_PREFIX = [
|
|
2895
|
+
["K8s::Flux::", "cncf/flux"],
|
|
2896
|
+
["K8s::Argo::", "cncf/argo"]
|
|
2897
|
+
];
|
|
2898
|
+
var HELM_MARK_KINDS = /* @__PURE__ */ new Set(["Helm::Chart", "Helm::Release"]);
|
|
2899
|
+
function k8sIconFor(kind) {
|
|
2900
|
+
for (const [prefix, rel] of K8S_MARK_BY_PREFIX) {
|
|
2901
|
+
if (kind.startsWith(prefix)) return loadIcon(rel);
|
|
2902
|
+
}
|
|
2903
|
+
const stem = K8S_ICON_BY_KIND[kind];
|
|
2904
|
+
return stem ? loadIcon(`k8s/${stem}`) : void 0;
|
|
2905
|
+
}
|
|
2906
|
+
function helmIconFor(kind) {
|
|
2907
|
+
return HELM_MARK_KINDS.has(kind) ? loadIcon("cncf/helm") : void 0;
|
|
2908
|
+
}
|
|
2909
|
+
|
|
2910
|
+
// src/render.ts
|
|
2911
|
+
registerPack({ lexicon: "k8s", iconFor: k8sIconFor });
|
|
2912
|
+
registerPack({ lexicon: "helm", iconFor: helmIconFor });
|
|
2708
2913
|
function renderArchitecture(ir, byContainer, opts = {}) {
|
|
2709
2914
|
const spread = Math.min(1.5, ir.edges.length / Math.max(ir.nodes.length, 1));
|
|
2710
2915
|
const layout = layoutArchitecture(ir, byContainer, {
|
|
@@ -2900,8 +3105,8 @@ function radializeLayout(layout, groupOf, size = /* @__PURE__ */ new Map()) {
|
|
|
2900
3105
|
}
|
|
2901
3106
|
|
|
2902
3107
|
// src/ops.ts
|
|
2903
|
-
import { readdirSync as readdirSync3, readFileSync as
|
|
2904
|
-
import { join as
|
|
3108
|
+
import { readdirSync as readdirSync3, readFileSync as readFileSync7, existsSync as existsSync6 } from "node:fs";
|
|
3109
|
+
import { join as join8 } from "node:path";
|
|
2905
3110
|
var APPLY_TARGET_LEXICON = {
|
|
2906
3111
|
cloudformation: "aws",
|
|
2907
3112
|
kubectl: "k8s",
|
|
@@ -2923,11 +3128,11 @@ function discoverOps(projectDir) {
|
|
|
2923
3128
|
const seen = /* @__PURE__ */ new Set();
|
|
2924
3129
|
const out = [];
|
|
2925
3130
|
for (const sub of ["ops", "src", "."]) {
|
|
2926
|
-
const dir =
|
|
3131
|
+
const dir = join8(projectDir, sub);
|
|
2927
3132
|
if (!existsSync6(dir)) continue;
|
|
2928
3133
|
for (const f of readdirSync3(dir)) {
|
|
2929
3134
|
if (!f.endsWith(".op.ts")) continue;
|
|
2930
|
-
const content =
|
|
3135
|
+
const content = readFileSync7(join8(dir, f), "utf8");
|
|
2931
3136
|
const name = content.match(/name:\s*["'`]([^"'`]+)["'`]/)?.[1];
|
|
2932
3137
|
if (!name || seen.has(name)) continue;
|
|
2933
3138
|
seen.add(name);
|
|
@@ -2984,16 +3189,19 @@ function nodeDiff(json, nodeId) {
|
|
|
2984
3189
|
if (drift) return { category: "drifted", changes: drift.changes ?? [] };
|
|
2985
3190
|
const unobserved = r.unobserved?.find((u) => u.name === nodeId);
|
|
2986
3191
|
if (unobserved) {
|
|
3192
|
+
const queried = unobserved.queried ?? r.queried?.[nodeId];
|
|
2987
3193
|
return {
|
|
2988
3194
|
category: "unobserved",
|
|
2989
3195
|
changes: [],
|
|
2990
3196
|
unobservedReason: unobserved.reason,
|
|
2991
|
-
...unobserved.detail ? { unobservedDetail: unobserved.detail } : {}
|
|
3197
|
+
...unobserved.detail ? { unobservedDetail: unobserved.detail } : {},
|
|
3198
|
+
...queried ? { queried } : {}
|
|
2992
3199
|
};
|
|
2993
3200
|
}
|
|
2994
3201
|
const runtimeChild = r.runtimeChildren?.find((rc) => rc.name === nodeId);
|
|
2995
3202
|
if (runtimeChild) return { category: "runtime", changes: [], runtimeOwner: runtimeChild.owner };
|
|
2996
|
-
if (r.missing?.includes(nodeId))
|
|
3203
|
+
if (r.missing?.includes(nodeId))
|
|
3204
|
+
return { category: "missing", changes: [], ...r.queried?.[nodeId] ? { queried: r.queried[nodeId] } : {} };
|
|
2997
3205
|
if (r.orphan?.includes(nodeId)) return { category: "orphan", changes: [] };
|
|
2998
3206
|
if (r.disappeared?.includes(nodeId)) return { category: "disappeared", changes: [] };
|
|
2999
3207
|
if (r.newlyObserved?.includes(nodeId)) return { category: "newlyObserved", changes: [] };
|
|
@@ -3037,6 +3245,117 @@ function classifyHealth(status) {
|
|
|
3037
3245
|
if (HEALTHY.test(text)) return "healthy";
|
|
3038
3246
|
return "unknown";
|
|
3039
3247
|
}
|
|
3248
|
+
function rec2(v) {
|
|
3249
|
+
return typeof v === "object" && v !== null && !Array.isArray(v) ? v : void 0;
|
|
3250
|
+
}
|
|
3251
|
+
function str3(v) {
|
|
3252
|
+
return typeof v === "string" && v.length > 0 ? v : void 0;
|
|
3253
|
+
}
|
|
3254
|
+
function statusTree(o) {
|
|
3255
|
+
const attrs = rec2(o.attributes);
|
|
3256
|
+
if (!attrs) return void 0;
|
|
3257
|
+
return rec2(attrs.status) ?? attrs;
|
|
3258
|
+
}
|
|
3259
|
+
var ARGO_HEALTH = {
|
|
3260
|
+
Healthy: "healthy",
|
|
3261
|
+
Degraded: "degraded",
|
|
3262
|
+
Progressing: "progressing",
|
|
3263
|
+
Suspended: "progressing",
|
|
3264
|
+
Missing: "progressing",
|
|
3265
|
+
Unknown: "unknown"
|
|
3266
|
+
};
|
|
3267
|
+
var ARGO_SYNC = {
|
|
3268
|
+
Synced: "healthy",
|
|
3269
|
+
OutOfSync: "progressing",
|
|
3270
|
+
Unknown: "unknown"
|
|
3271
|
+
};
|
|
3272
|
+
function argoVerdict(o) {
|
|
3273
|
+
if (o.type !== "K8s::Argo::Application") return void 0;
|
|
3274
|
+
const tree = statusTree(o);
|
|
3275
|
+
let health = str3(rec2(tree?.health)?.status);
|
|
3276
|
+
let sync = str3(rec2(tree?.sync)?.status);
|
|
3277
|
+
if (!health && !sync) {
|
|
3278
|
+
const word = str3(o.status);
|
|
3279
|
+
if (word === "READY") {
|
|
3280
|
+
health = "Healthy";
|
|
3281
|
+
sync = "Synced";
|
|
3282
|
+
} else if (word && word in ARGO_HEALTH) health = word;
|
|
3283
|
+
else if (word && word in ARGO_SYNC) sync = word;
|
|
3284
|
+
}
|
|
3285
|
+
const verdicts = [health ? ARGO_HEALTH[health] : void 0, sync ? ARGO_SYNC[sync] : void 0].filter(
|
|
3286
|
+
(v) => v !== void 0
|
|
3287
|
+
);
|
|
3288
|
+
if (verdicts.length === 0) return void 0;
|
|
3289
|
+
const detail = [health ? `health=${health}` : void 0, sync ? `sync=${sync}` : void 0].filter(Boolean).join(", ");
|
|
3290
|
+
for (const rung of ["degraded", "progressing", "unknown"]) {
|
|
3291
|
+
if (verdicts.includes(rung)) return { health: rung, detail };
|
|
3292
|
+
}
|
|
3293
|
+
return { health: "healthy", detail };
|
|
3294
|
+
}
|
|
3295
|
+
var FLUX_PENDING_REASON = /^(progressing|reconciling)/i;
|
|
3296
|
+
function readyFromConditions(tree) {
|
|
3297
|
+
const conditions = tree?.conditions;
|
|
3298
|
+
if (!Array.isArray(conditions)) return void 0;
|
|
3299
|
+
for (const c of conditions) {
|
|
3300
|
+
const cond = rec2(c);
|
|
3301
|
+
if (!cond || cond.type !== "Ready") continue;
|
|
3302
|
+
const status = str3(cond.status);
|
|
3303
|
+
if (status !== "True" && status !== "False" && status !== "Unknown") continue;
|
|
3304
|
+
return { status, reason: str3(cond.reason) };
|
|
3305
|
+
}
|
|
3306
|
+
return void 0;
|
|
3307
|
+
}
|
|
3308
|
+
function readyFromChantConditions(o) {
|
|
3309
|
+
const conditions = rec2(o.attributes)?.conditions;
|
|
3310
|
+
if (!Array.isArray(conditions)) return void 0;
|
|
3311
|
+
for (const c of conditions) {
|
|
3312
|
+
if (typeof c !== "string") continue;
|
|
3313
|
+
const m = /^Ready(?:=([^:]*))?(?::|$)/.exec(c.trim());
|
|
3314
|
+
if (!m) continue;
|
|
3315
|
+
const reason = m[1]?.trim() || void 0;
|
|
3316
|
+
return { status: reason && FLUX_PENDING_REASON.test(reason) ? "Unknown" : "False", reason };
|
|
3317
|
+
}
|
|
3318
|
+
return void 0;
|
|
3319
|
+
}
|
|
3320
|
+
function readyFromStatusWord(o) {
|
|
3321
|
+
const word = str3(o.status);
|
|
3322
|
+
if (!word || word === "PRESENT") return void 0;
|
|
3323
|
+
if (word === "READY") return { status: "True" };
|
|
3324
|
+
if (word === "NOT-READY") return { status: "False" };
|
|
3325
|
+
return { status: FLUX_PENDING_REASON.test(word) ? "Unknown" : "False", reason: word };
|
|
3326
|
+
}
|
|
3327
|
+
function describeReady(ready) {
|
|
3328
|
+
return `Ready=${ready.status}${ready.reason ? ` (${ready.reason})` : ""}`;
|
|
3329
|
+
}
|
|
3330
|
+
function revisionVerdict(tree) {
|
|
3331
|
+
const applied = str3(tree?.lastAppliedRevision);
|
|
3332
|
+
const attempted = str3(tree?.lastAttemptedRevision);
|
|
3333
|
+
if (!applied || !attempted || applied === attempted) return void 0;
|
|
3334
|
+
return { health: "progressing", detail: `applied ${applied}, attempted ${attempted}` };
|
|
3335
|
+
}
|
|
3336
|
+
var FLUX_TYPE_PREFIX = "K8s::Flux::";
|
|
3337
|
+
function fluxVerdict(o) {
|
|
3338
|
+
if (!o.type?.startsWith(FLUX_TYPE_PREFIX)) return void 0;
|
|
3339
|
+
const tree = statusTree(o);
|
|
3340
|
+
const ready = readyFromConditions(tree) ?? readyFromChantConditions(o) ?? readyFromStatusWord(o);
|
|
3341
|
+
if (ready?.status === "False") return { health: "degraded", detail: describeReady(ready) };
|
|
3342
|
+
if (ready?.status === "Unknown") return { health: "progressing", detail: describeReady(ready) };
|
|
3343
|
+
if (ready?.status === "True") return revisionVerdict(tree) ?? { health: "healthy", detail: describeReady(ready) };
|
|
3344
|
+
const conditions = tree?.conditions;
|
|
3345
|
+
const reconciling = Array.isArray(conditions) && conditions.some((c) => {
|
|
3346
|
+
const cond = rec2(c);
|
|
3347
|
+
return cond?.type === "Reconciling" && cond.status === "True";
|
|
3348
|
+
});
|
|
3349
|
+
if (reconciling) return { health: "progressing", detail: "Reconciling=True" };
|
|
3350
|
+
if (!str3(o.status) || o.status === "PRESENT") {
|
|
3351
|
+
return { health: "progressing", detail: "no Ready condition yet" };
|
|
3352
|
+
}
|
|
3353
|
+
return void 0;
|
|
3354
|
+
}
|
|
3355
|
+
function classifyObservedHealth(observed) {
|
|
3356
|
+
if (!observed) return { health: "unknown" };
|
|
3357
|
+
return argoVerdict(observed) ?? fluxVerdict(observed) ?? { health: classifyHealth(observed.status) };
|
|
3358
|
+
}
|
|
3040
3359
|
|
|
3041
3360
|
// src/apply.ts
|
|
3042
3361
|
var PROGRESS_EVENT_TYPES = /* @__PURE__ */ new Set([
|
|
@@ -3323,23 +3642,23 @@ var OpRunner = class {
|
|
|
3323
3642
|
|
|
3324
3643
|
// src/substrates.ts
|
|
3325
3644
|
import { spawn as spawn3 } from "node:child_process";
|
|
3326
|
-
import { existsSync as existsSync7, readFileSync as
|
|
3327
|
-
import { join as
|
|
3645
|
+
import { existsSync as existsSync7, readFileSync as readFileSync8 } from "node:fs";
|
|
3646
|
+
import { join as join9 } from "node:path";
|
|
3328
3647
|
import { platform } from "node:os";
|
|
3329
3648
|
function probe(cmd, args) {
|
|
3330
|
-
return new Promise((
|
|
3649
|
+
return new Promise((resolve5) => {
|
|
3331
3650
|
let out = "";
|
|
3332
3651
|
let proc;
|
|
3333
3652
|
try {
|
|
3334
3653
|
proc = spawn3(cmd, args, { stdio: ["ignore", "pipe", "pipe"] });
|
|
3335
3654
|
} catch {
|
|
3336
|
-
|
|
3655
|
+
resolve5({ code: 127, out: "" });
|
|
3337
3656
|
return;
|
|
3338
3657
|
}
|
|
3339
3658
|
proc.stdout.on("data", (d) => out += d);
|
|
3340
3659
|
proc.stderr.on("data", (d) => out += d);
|
|
3341
|
-
proc.on("error", () =>
|
|
3342
|
-
proc.on("close", (code) =>
|
|
3660
|
+
proc.on("error", () => resolve5({ code: 127, out }));
|
|
3661
|
+
proc.on("close", (code) => resolve5({ code: code ?? 1, out }));
|
|
3343
3662
|
});
|
|
3344
3663
|
}
|
|
3345
3664
|
async function dockerAvailable() {
|
|
@@ -3352,11 +3671,11 @@ async function dockerRunning(nameFilter) {
|
|
|
3352
3671
|
return out.split(/\r?\n/).map((s) => s.trim()).filter(Boolean);
|
|
3353
3672
|
}
|
|
3354
3673
|
function scriptBringUp(projectDir, relPath, label) {
|
|
3355
|
-
return existsSync7(
|
|
3674
|
+
return existsSync7(join9(projectDir, relPath)) ? { label, cmd: "bash", args: [relPath] } : void 0;
|
|
3356
3675
|
}
|
|
3357
3676
|
function projectLexicons(projectDir) {
|
|
3358
3677
|
try {
|
|
3359
|
-
const src =
|
|
3678
|
+
const src = readFileSync8(join9(projectDir, "chant.config.ts"), "utf-8");
|
|
3360
3679
|
const m = src.match(/lexicons\s*:\s*\[([^\]]*)\]/);
|
|
3361
3680
|
if (!m) return [];
|
|
3362
3681
|
return [...m[1].matchAll(/["']([^"']+)["']/g)].map((x) => x[1]);
|
|
@@ -3406,7 +3725,7 @@ async function detectSubstrates(projectDir, preview = false, boundContext) {
|
|
|
3406
3725
|
["forgejo", "Forgejo", ".forgejo", "test/forgejo-runtime-e2e.sh"]
|
|
3407
3726
|
];
|
|
3408
3727
|
for (const [name, label, marker, script] of forges) {
|
|
3409
|
-
if (!existsSync7(
|
|
3728
|
+
if (!existsSync7(join9(projectDir, marker))) continue;
|
|
3410
3729
|
const c = docker ? await dockerRunning(name) : [];
|
|
3411
3730
|
const d = dep(c.length > 0, "container up", "on-demand (pipeline run)");
|
|
3412
3731
|
subs.push({
|
|
@@ -3436,7 +3755,7 @@ async function detectSubstrates(projectDir, preview = false, boundContext) {
|
|
|
3436
3755
|
detail: endpoint ? `targeting ${endpoint}` : "real Fly (FLY_FLAPS_BASE_URL unset)"
|
|
3437
3756
|
});
|
|
3438
3757
|
}
|
|
3439
|
-
if (existsSync7(
|
|
3758
|
+
if (existsSync7(join9(projectDir, ".github", "workflows"))) {
|
|
3440
3759
|
const gh = await probe("gh", ["auth", "status"]);
|
|
3441
3760
|
const ready = gh.code === 0;
|
|
3442
3761
|
subs.push({
|
|
@@ -3449,7 +3768,7 @@ async function detectSubstrates(projectDir, preview = false, boundContext) {
|
|
|
3449
3768
|
if (lexicons.includes("temporal")) {
|
|
3450
3769
|
let hasProfiles = false;
|
|
3451
3770
|
try {
|
|
3452
|
-
hasProfiles = /temporal\s*:\s*\{[\s\S]{0,400}?profiles\s*:/.test(
|
|
3771
|
+
hasProfiles = /temporal\s*:\s*\{[\s\S]{0,400}?profiles\s*:/.test(readFileSync8(join9(projectDir, "chant.config.ts"), "utf-8"));
|
|
3453
3772
|
} catch {
|
|
3454
3773
|
}
|
|
3455
3774
|
subs.push({
|
|
@@ -3566,6 +3885,13 @@ var firstLine = (e) => {
|
|
|
3566
3885
|
const m = msg.match(/exited \d+:\s*([\s\S]*)/);
|
|
3567
3886
|
return (m ? m[1] : msg).split("\n")[0].trim().slice(0, 160);
|
|
3568
3887
|
};
|
|
3888
|
+
function namespaceRuntimeOwners(name, ir) {
|
|
3889
|
+
const own = new Set(ir.nodes.map((n) => n.id));
|
|
3890
|
+
for (const n of ir.nodes) {
|
|
3891
|
+
if (n.runtimeOwner && own.has(n.runtimeOwner)) n.runtimeOwner = `${name}/${n.runtimeOwner}`;
|
|
3892
|
+
}
|
|
3893
|
+
return ir;
|
|
3894
|
+
}
|
|
3569
3895
|
async function composeEstateOverlay(projectDirs, opts, classify) {
|
|
3570
3896
|
const names = shortStackNames(projectDirs);
|
|
3571
3897
|
const unobserved = [];
|
|
@@ -3575,7 +3901,7 @@ async function composeEstateOverlay(projectDirs, opts, classify) {
|
|
|
3575
3901
|
projectDirs.map(async (dir, i) => {
|
|
3576
3902
|
const name = names[i];
|
|
3577
3903
|
try {
|
|
3578
|
-
stacks[i] = { name, ir: classify(await graphIr(dir, { ...opts, live: true, overlay: true })) };
|
|
3904
|
+
stacks[i] = { name, ir: namespaceRuntimeOwners(name, classify(await graphIr(dir, { ...opts, live: true, overlay: true }))) };
|
|
3579
3905
|
} catch (err) {
|
|
3580
3906
|
const reason = firstLine(err);
|
|
3581
3907
|
try {
|
|
@@ -3602,7 +3928,7 @@ async function composeEstateOverlay(projectDirs, opts, classify) {
|
|
|
3602
3928
|
|
|
3603
3929
|
// src/events.ts
|
|
3604
3930
|
import { watch, existsSync as existsSync8 } from "node:fs";
|
|
3605
|
-
import { join as
|
|
3931
|
+
import { join as join10 } from "node:path";
|
|
3606
3932
|
var Broadcaster = class {
|
|
3607
3933
|
listeners = /* @__PURE__ */ new Set();
|
|
3608
3934
|
subscribe(fn) {
|
|
@@ -3620,7 +3946,7 @@ var Broadcaster = class {
|
|
|
3620
3946
|
};
|
|
3621
3947
|
var IGNORE = /(^|[\\/])(node_modules|dist|\.git)([\\/]|$)/;
|
|
3622
3948
|
function watchSource(projectDir, onChange, debounceMs = 200) {
|
|
3623
|
-
const dir = existsSync8(
|
|
3949
|
+
const dir = existsSync8(join10(projectDir, "src")) ? join10(projectDir, "src") : projectDir;
|
|
3624
3950
|
let timer;
|
|
3625
3951
|
const watcher = watch(dir, { recursive: true }, (_event, file) => {
|
|
3626
3952
|
const name = typeof file === "string" ? file : "";
|
|
@@ -3728,19 +4054,21 @@ import { renderMorphHtml, layoutIr as layoutIr2 } from "@intentius/pinhole";
|
|
|
3728
4054
|
function safeJson(value) {
|
|
3729
4055
|
return JSON.stringify(value).replace(/</g, "\\u003c");
|
|
3730
4056
|
}
|
|
4057
|
+
var LANES_MONO = `ui-monospace, "SF Mono", SFMono-Regular, "Cascadia Code", "JetBrains Mono", "IBM Plex Mono", Menlo, Consolas, monospace`;
|
|
3731
4058
|
var LANES_CSS = `
|
|
3732
|
-
body { background: var(--bg, #
|
|
3733
|
-
#behold-lanes { position: fixed; left: 0; right: 0; bottom: 0; background: var(--bg, #
|
|
3734
|
-
border-top: 1px solid var(--line, #
|
|
4059
|
+
body { background: var(--bg, #1e1e2e); color: var(--fg, #cdd6f4); padding-bottom: 150px; }
|
|
4060
|
+
#behold-lanes { position: fixed; left: 0; right: 0; bottom: 0; background: var(--bg, #1e1e2e);
|
|
4061
|
+
border-top: 1px solid var(--line, #3b3c4a); padding: 8px 12px 10px; font: 11px/1.5 ${LANES_MONO}; color: var(--muted, #777a8c); }
|
|
3735
4062
|
#behold-lanes .hd { display: flex; gap: 14px; align-items: baseline; margin-bottom: 4px; }
|
|
3736
|
-
#behold-lanes .hd .rt { color: var(--foreign, #
|
|
3737
|
-
#behold-lanes .hd a { color: var(--pending, #
|
|
4063
|
+
#behold-lanes .hd .rt { color: var(--foreign, #f9e2af); }
|
|
4064
|
+
#behold-lanes .hd a { color: var(--pending, #89b4fa); text-decoration: none; margin-left: auto; }
|
|
3738
4065
|
#behold-lanes canvas { display: block; width: 100%; cursor: pointer; }
|
|
3739
4066
|
#behold-diff { position: fixed; right: 12px; bottom: 156px; width: 260px; max-height: 40vh; overflow: auto;
|
|
3740
|
-
background: var(--panel, #
|
|
3741
|
-
color: var(--fg, #
|
|
3742
|
-
#behold-diff h4 { margin: 0 0 6px; font
|
|
3743
|
-
|
|
4067
|
+
background: var(--panel, #2e2f40); border: 1px solid var(--rule, #4e4f64); border-radius: 3px; padding: 10px 12px; font: 11px/1.5 ${LANES_MONO};
|
|
4068
|
+
color: var(--fg, #cdd6f4); display: none; }
|
|
4069
|
+
#behold-diff h4 { margin: 0 0 6px; font: 600 10.5px/1.4 ui-sans-serif, system-ui, sans-serif;
|
|
4070
|
+
text-transform: uppercase; letter-spacing: .07em; color: var(--muted, #777a8c); }
|
|
4071
|
+
#behold-diff .a { color: var(--managed, #a6e3a1); } #behold-diff .r { color: var(--degraded, #f38ba8); } #behold-diff .c { color: var(--foreign, #f9e2af); }`;
|
|
3744
4072
|
var THEME_BOOT = `<script type="module">import { initTheme, onThemeChange, mountThemePicker } from "/theme.js"; initTheme(); mountThemePicker(document.getElementById("behold-lanes-pickers")); onThemeChange(() => window.dispatchEvent(new Event("behold-theme")));</script>`;
|
|
3745
4073
|
function laneStripScript(frames) {
|
|
3746
4074
|
return `<script>
|
|
@@ -3766,7 +4094,7 @@ const LF = ${safeJson(frames)};
|
|
|
3766
4094
|
const inNow = now !== undefined, inPrev = prev !== undefined;
|
|
3767
4095
|
return inNow !== inPrev || (inNow && inPrev && now !== prev);
|
|
3768
4096
|
}
|
|
3769
|
-
const color = s => s === "good" ? css("--managed", "#
|
|
4097
|
+
const color = s => s === "good" ? css("--managed", "#a6e3a1") : s === "warn" ? css("--foreign", "#f9e2af") : s === "accent" ? css("--pending", "#89b4fa") : css("--edge", "#56596d");
|
|
3770
4098
|
|
|
3771
4099
|
function draw() {
|
|
3772
4100
|
const dpr = window.devicePixelRatio || 1;
|
|
@@ -3775,22 +4103,22 @@ const LF = ${safeJson(frames)};
|
|
|
3775
4103
|
c.clearRect(0, 0, host.clientWidth, H); c.font = "12px ui-sans-serif, system-ui, sans-serif";
|
|
3776
4104
|
subs.forEach((s, r) => {
|
|
3777
4105
|
const y = padT + r * rowH + rowH / 2;
|
|
3778
|
-
c.fillStyle = offset[s] ? css("--foreign", "#
|
|
3779
|
-
c.strokeStyle = css("--line", "#
|
|
4106
|
+
c.fillStyle = offset[s] ? css("--foreign", "#f9e2af") : css("--muted", "#777a8c"); c.textAlign = "left"; c.fillText(s, 8, y + 4);
|
|
4107
|
+
c.strokeStyle = css("--line", "#3b3c4a"); c.beginPath(); c.moveTo(padL, y); c.lineTo(host.clientWidth - padR, y); c.stroke();
|
|
3780
4108
|
LF.forEach((f, i) => {
|
|
3781
4109
|
if (!f.byLexicon[s]) return;
|
|
3782
4110
|
// dot per substrate; brighter when a node in this substrate changed at i
|
|
3783
4111
|
const changed = Object.keys(f.status).some(id => f.lexicon[id] === s && changedAt(id, i));
|
|
3784
4112
|
const hi = focus && f.lexicon[focus] === s && changedAt(focus, i);
|
|
3785
|
-
c.fillStyle = hi ? css("--fg", "#
|
|
4113
|
+
c.fillStyle = hi ? css("--fg", "#cdd6f4") : changed ? color(mode(f, s)) : css("--line", "#3b3c4a");
|
|
3786
4114
|
c.beginPath(); c.arc(xOf(i, s), y, i === cur ? 5 : hi ? 4.5 : 3.5, 0, 7); c.fill();
|
|
3787
4115
|
});
|
|
3788
4116
|
});
|
|
3789
|
-
const px = baseX(cur); c.strokeStyle = css("--pending", "#
|
|
4117
|
+
const px = baseX(cur); c.strokeStyle = css("--pending", "#89b4fa"); c.lineWidth = 1.5;
|
|
3790
4118
|
c.beginPath(); c.moveTo(px, padT - 2); c.lineTo(px, padT + subs.length * rowH); c.stroke();
|
|
3791
|
-
if (pair != null) { const qx = baseX(pair); c.strokeStyle = css("--foreign", "#
|
|
4119
|
+
if (pair != null) { const qx = baseX(pair); c.strokeStyle = css("--foreign", "#f9e2af"); c.setLineDash([3,3]);
|
|
3792
4120
|
c.beginPath(); c.moveTo(qx, padT - 2); c.lineTo(qx, padT + subs.length * rowH); c.stroke(); c.setLineDash([]); }
|
|
3793
|
-
c.fillStyle = css("--fg", "#
|
|
4121
|
+
c.fillStyle = css("--fg", "#cdd6f4"); c.textAlign = "center"; c.fillText(LF[cur].name, px, padT + subs.length * rowH + 16);
|
|
3794
4122
|
document.getElementById("behold-lanes-meta").textContent =
|
|
3795
4123
|
LF.length + " frames \xB7 frame " + (cur + 1) + "/" + LF.length + (focus ? " \xB7 focus " + focus : "");
|
|
3796
4124
|
document.getElementById("behold-lanes-rt").style.display = anyOffset() ? "inline" : "none";
|
|
@@ -3896,7 +4224,7 @@ async function emulatorDown(projectDir) {
|
|
|
3896
4224
|
}
|
|
3897
4225
|
|
|
3898
4226
|
// src/server.ts
|
|
3899
|
-
var webRoot =
|
|
4227
|
+
var webRoot = join11(dirname4(fileURLToPath3(import.meta.url)), "..", "web");
|
|
3900
4228
|
var execFileP = async (cmd, args) => (await promisify4(execFile4)(cmd, args, { encoding: "utf8", timeout: 1e4 })).stdout;
|
|
3901
4229
|
function optsFromQuery(url, tierEnvVar, projectDir) {
|
|
3902
4230
|
const q = url.searchParams;
|
|
@@ -3945,8 +4273,8 @@ function tierFailure(tier, message) {
|
|
|
3945
4273
|
}
|
|
3946
4274
|
function beholdVersion() {
|
|
3947
4275
|
try {
|
|
3948
|
-
const here =
|
|
3949
|
-
return JSON.parse(
|
|
4276
|
+
const here = dirname4(fileURLToPath3(import.meta.url));
|
|
4277
|
+
return JSON.parse(readFileSync9(join11(here, "..", "package.json"), "utf8")).version ?? "unknown";
|
|
3950
4278
|
} catch {
|
|
3951
4279
|
return "unknown";
|
|
3952
4280
|
}
|
|
@@ -4007,13 +4335,14 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
|
|
|
4007
4335
|
return void 0;
|
|
4008
4336
|
}
|
|
4009
4337
|
};
|
|
4338
|
+
const estateSourceRoots = async (opts) => (await Promise.all((cfg.projectDirs ?? []).map(async (dir) => [await graphPath(dir, opts), dir]))).flat();
|
|
4010
4339
|
app.get("/healthz", (c) => c.json({ ok: true, projectDir: cfg.projectDir, env: cfg.env ?? null, frames: frames.size }));
|
|
4011
4340
|
app.get("/api/frames", (c) => c.json({ frames: frames.summaries() }));
|
|
4012
4341
|
app.get("/lanes", (c) => {
|
|
4013
4342
|
const all = frames.all();
|
|
4014
4343
|
if (all.length < 2) {
|
|
4015
4344
|
return c.html(
|
|
4016
|
-
`<!doctype html><meta charset=utf-8><script type="module">import { initTheme } from "/theme.js"; initTheme();</script><body style="font:14px system-ui;background:var(--bg,#
|
|
4345
|
+
`<!doctype html><meta charset=utf-8><script type="module">import { initTheme } from "/theme.js"; initTheme();</script><body style="font:14px system-ui;background:var(--bg,#1e1e2e);color:var(--muted,#777a8c);padding:2rem"><h3 style="color:var(--fg,#cdd6f4)">deployment lanes</h3><p>${all.length} frame(s) captured \u2014 need at least two to scrub.</p><p>Frames accrue when the estate moves: hit <b style="color:var(--fg,#cdd6f4)">\u21BB Refresh</b> (captures the current live state), run a <b style="color:var(--fg,#cdd6f4)">Sync</b>/Adopt, edit the source, or serve with <code>--poll</code> against a moving environment. Then reload.</p><p><a href="/" style="color:var(--pending,#58a6ff);text-decoration:none">\u2190 back to the graph</a></p></body>`
|
|
4017
4346
|
);
|
|
4018
4347
|
}
|
|
4019
4348
|
return c.html(renderLanes(all, frames.summaries()));
|
|
@@ -4079,8 +4408,8 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
|
|
|
4079
4408
|
return c.json({ started: true, name, ran: label });
|
|
4080
4409
|
});
|
|
4081
4410
|
app.post("/api/local/reset", (c) => {
|
|
4082
|
-
const down =
|
|
4083
|
-
const up =
|
|
4411
|
+
const down = join11(cfg.projectDir, "scripts/local/local-down.sh");
|
|
4412
|
+
const up = join11(cfg.projectDir, "scripts/local/local-up.sh");
|
|
4084
4413
|
if (!existsSync9(down) || !existsSync9(up)) {
|
|
4085
4414
|
return c.json({ error: "no local-down.sh / local-up.sh in scripts/local \u2014 reset is only for local emulator projects" }, 400);
|
|
4086
4415
|
}
|
|
@@ -4160,7 +4489,7 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
|
|
|
4160
4489
|
const body = await c.req.json().catch(() => ({}));
|
|
4161
4490
|
const dir = typeof body.dir === "string" && body.dir.trim() ? resolve2(body.dir.trim()) : "";
|
|
4162
4491
|
if (!dir || !existsSync9(dir)) return c.json({ error: `no such directory: ${dir || "(no dir given)"}` }, 400);
|
|
4163
|
-
if (!existsSync9(
|
|
4492
|
+
if (!existsSync9(join11(dir, "chant.config.ts"))) {
|
|
4164
4493
|
return c.json({ error: `${dir} doesn't look like a chant project \u2014 no chant.config.ts` }, 400);
|
|
4165
4494
|
}
|
|
4166
4495
|
addRecent(cfg.projectDir);
|
|
@@ -4232,10 +4561,31 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
|
|
|
4232
4561
|
let mode;
|
|
4233
4562
|
let metaEnv = cfg.env ?? null;
|
|
4234
4563
|
if (multi) {
|
|
4235
|
-
ir = await composeEstate(cfg.projectDirs, opts);
|
|
4564
|
+
ir = await composeEstate(cfg.projectDirs, logical ? { ...opts, detail: 3 } : opts);
|
|
4236
4565
|
ir = addValueMatchEdges(ir);
|
|
4237
4566
|
ir = addK8sDeclaredEdges(ir);
|
|
4238
|
-
|
|
4567
|
+
const estateContext = await boundK8sContext(metaEnv ?? void 0);
|
|
4568
|
+
ir = addClusterAnchorEdges(ir, estateContext);
|
|
4569
|
+
if (logical) {
|
|
4570
|
+
const logicalBefore = ir.nodes.length;
|
|
4571
|
+
const { ir: projected, byContainer } = projectTopology(ir, metaEnv ?? void 0, estateContext, await estateSourceRoots(opts));
|
|
4572
|
+
const { svg: svg2 } = renderArchitecture(projected, byContainer);
|
|
4573
|
+
const logicalNote = notesFor("logical", projected, void 0, logicalBefore);
|
|
4574
|
+
return c.json({
|
|
4575
|
+
ir: projected,
|
|
4576
|
+
svg: svg2,
|
|
4577
|
+
byContainer,
|
|
4578
|
+
meta: {
|
|
4579
|
+
projectDir: cfg.projectDir,
|
|
4580
|
+
env: metaEnv,
|
|
4581
|
+
tier: opts.tier ?? null,
|
|
4582
|
+
target: opts.target ?? null,
|
|
4583
|
+
mode: "logical",
|
|
4584
|
+
estate: cfg.projectDirs.length,
|
|
4585
|
+
...logicalNote ? { note: logicalNote } : {}
|
|
4586
|
+
}
|
|
4587
|
+
});
|
|
4588
|
+
}
|
|
4239
4589
|
} else if (components) {
|
|
4240
4590
|
ir = await componentGraphIr(cfg.projectDir, opts);
|
|
4241
4591
|
const env = opts.env ?? cfg.env;
|
|
@@ -4271,13 +4621,13 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
|
|
|
4271
4621
|
srcCompositeEdgesAttached = 0;
|
|
4272
4622
|
}
|
|
4273
4623
|
}
|
|
4274
|
-
if (!multi && !components && !logical && !opts.lens && ir.nodes.length === 0 && !existsSync9(
|
|
4624
|
+
if (!multi && !components && !logical && !opts.lens && ir.nodes.length === 0 && !existsSync9(join11(cfg.projectDir, "chant.config.ts"))) {
|
|
4275
4625
|
return c.json(noProjectError(cfg.projectDir), 404);
|
|
4276
4626
|
}
|
|
4277
4627
|
const radial = new URL(c.req.url).searchParams.get("radial") === "1";
|
|
4278
4628
|
const { svg } = renderGraph(ir, multi ? { boxes: "byStack" } : { radial });
|
|
4279
4629
|
const srcZoom = components ? "components" : logical ? "logical" : opts.detail === 1 ? "composites" : opts.detail === 3 ? "attributes" : "resources";
|
|
4280
|
-
const estateLensNote = multi &&
|
|
4630
|
+
const estateLensNote = multi && components ? "the components lens doesn't apply to a composed estate yet \u2014 showing the composed entity graph" : void 0;
|
|
4281
4631
|
const srcNote = multi ? estateLensNote : notesFor(srcZoom, ir, srcCompositeEdgesAttached);
|
|
4282
4632
|
return c.json({
|
|
4283
4633
|
ir,
|
|
@@ -4379,22 +4729,36 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
|
|
|
4379
4729
|
try {
|
|
4380
4730
|
if (cfg.projectDirs && cfg.projectDirs.length > 1) {
|
|
4381
4731
|
const runtime = new URL(c.req.url).searchParams.get("runtime") === "1";
|
|
4382
|
-
const
|
|
4732
|
+
const detail = logical ? 3 : query.detail;
|
|
4733
|
+
const est = await composeEstateOverlay(cfg.projectDirs, { ...tierTargetOpts(query), detail, env }, reclassifyOverlay);
|
|
4383
4734
|
if (est.dropped.length === est.total) {
|
|
4384
4735
|
return c.json({ error: `no project in the estate could be graphed \u2014 ${est.dropped.map((d) => `${d.name}: ${d.reason}`).join("; ")}` }, 500);
|
|
4385
4736
|
}
|
|
4386
|
-
|
|
4387
|
-
|
|
4737
|
+
const composed = est.ir;
|
|
4738
|
+
let ir2 = runtime ? attachRuntimeContainment(composed) : pruneRuntimeChildren(composed);
|
|
4739
|
+
if ((detail ?? 2) < 3) ir2 = pruneImports(ir2);
|
|
4388
4740
|
ir2 = addValueMatchEdges(ir2);
|
|
4389
4741
|
ir2 = addK8sDeclaredEdges(ir2);
|
|
4390
|
-
|
|
4391
|
-
|
|
4392
|
-
const lensNote = logical || runtime ? `the ${logical ? "logical" : "runtime"} lens doesn't apply to a composed estate yet \u2014 showing the composed entity overlay` : void 0;
|
|
4742
|
+
const boundContext2 = await boundK8sContext(env);
|
|
4743
|
+
ir2 = addClusterAnchorEdges(ir2, boundContext2);
|
|
4393
4744
|
const coverNote = est.unobserved.length || est.dropped.length ? `live observe covered ${est.observed} of ${est.total} projects \u2014 ` + [
|
|
4394
4745
|
...est.unobserved.map((u) => `${u.name}: ${u.reason} (painted unobserved)`),
|
|
4395
4746
|
...est.dropped.map((d) => `${d.name}: dropped (${d.reason})`)
|
|
4396
4747
|
].join("; ") : void 0;
|
|
4397
|
-
|
|
4748
|
+
if (logical) {
|
|
4749
|
+
const logicalBefore = ir2.nodes.length;
|
|
4750
|
+
const { ir: projected, byContainer } = projectTopology(ir2, env, boundContext2, await estateSourceRoots(query));
|
|
4751
|
+
const { svg: svg3 } = renderArchitecture(projected, byContainer);
|
|
4752
|
+
const note3 = [notesFor("logical", projected, void 0, logicalBefore), coverNote, namespaceMismatchNote(projected.nodes)].filter(Boolean).join(" \xB7 ");
|
|
4753
|
+
return c.json({
|
|
4754
|
+
ir: projected,
|
|
4755
|
+
svg: svg3,
|
|
4756
|
+
byContainer,
|
|
4757
|
+
meta: { projectDir: cfg.projectDir, env, mode: "logical", estate: est.total, ...note3 ? { note: note3 } : {} }
|
|
4758
|
+
});
|
|
4759
|
+
}
|
|
4760
|
+
const { svg: svg2 } = renderGraph(ir2, { boxes: runtime ? "byContainer" : "byStack" });
|
|
4761
|
+
const note2 = [coverNote, namespaceMismatchNote(ir2.nodes), runtime ? notesFor("runtime", ir2) : void 0].filter(Boolean).join(" \xB7 ");
|
|
4398
4762
|
return c.json({
|
|
4399
4763
|
ir: ir2,
|
|
4400
4764
|
svg: svg2,
|
|
@@ -4500,10 +4864,12 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
|
|
|
4500
4864
|
const nodes = {};
|
|
4501
4865
|
for (const id of ids) {
|
|
4502
4866
|
const observed = nodeObserved(parsed, id);
|
|
4867
|
+
const verdict = classifyObservedHealth(observed);
|
|
4503
4868
|
nodes[id] = {
|
|
4504
4869
|
observed,
|
|
4505
4870
|
diff: nodeDiff(parsed, id),
|
|
4506
|
-
health:
|
|
4871
|
+
health: verdict.health,
|
|
4872
|
+
...verdict.detail ? { healthDetail: verdict.detail } : {},
|
|
4507
4873
|
// Field-level (per-manager) drift (#87, chant#1181) — null when no
|
|
4508
4874
|
// lexicon in this diff carries a `deep` section at all.
|
|
4509
4875
|
fieldDrift: nodeFieldDrift(parsed, id)
|
|
@@ -4527,12 +4893,14 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
|
|
|
4527
4893
|
return c.json({ error: "diff output was not JSON \u2014 chant may predate --live --json (needs 0.18.7+)" }, 500);
|
|
4528
4894
|
}
|
|
4529
4895
|
const observed = nodeObserved(parsed, node);
|
|
4896
|
+
const verdict = classifyObservedHealth(observed);
|
|
4530
4897
|
return c.json({
|
|
4531
4898
|
node,
|
|
4532
4899
|
env,
|
|
4533
4900
|
diff: nodeDiff(parsed, node),
|
|
4534
4901
|
observed,
|
|
4535
|
-
health:
|
|
4902
|
+
health: verdict.health,
|
|
4903
|
+
...verdict.detail ? { healthDetail: verdict.detail } : {},
|
|
4536
4904
|
// Field-level (per-manager) drift (#87, chant#1181) — null when no
|
|
4537
4905
|
// lexicon in this diff carries a `deep` section at all.
|
|
4538
4906
|
fieldDrift: nodeFieldDrift(parsed, node)
|
|
@@ -4583,7 +4951,7 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
|
|
|
4583
4951
|
});
|
|
4584
4952
|
const rel = relative(process.cwd(), webRoot) || ".";
|
|
4585
4953
|
app.use("/*", serveStatic({ root: rel }));
|
|
4586
|
-
app.get("/", serveStatic({ path:
|
|
4954
|
+
app.get("/", serveStatic({ path: join11(rel, "index.html") }));
|
|
4587
4955
|
return app;
|
|
4588
4956
|
}
|
|
4589
4957
|
async function startServer(cfg) {
|
|
@@ -4612,7 +4980,7 @@ async function startServer(cfg) {
|
|
|
4612
4980
|
}
|
|
4613
4981
|
}
|
|
4614
4982
|
for (const dir of cfg.projectDirs ?? [cfg.projectDir]) {
|
|
4615
|
-
if (existsSync9(
|
|
4983
|
+
if (existsSync9(join11(dir, "chant.config.ts"))) addRecent(dir);
|
|
4616
4984
|
}
|
|
4617
4985
|
const broadcaster = new Broadcaster();
|
|
4618
4986
|
const frames = new FrameBuffer();
|
|
@@ -4718,19 +5086,19 @@ async function startServer(cfg) {
|
|
|
4718
5086
|
}
|
|
4719
5087
|
|
|
4720
5088
|
// src/demos.ts
|
|
4721
|
-
import { readFileSync as
|
|
4722
|
-
import { join as
|
|
5089
|
+
import { readFileSync as readFileSync10 } from "node:fs";
|
|
5090
|
+
import { join as join12 } from "node:path";
|
|
4723
5091
|
import { spawnSync } from "node:child_process";
|
|
4724
5092
|
function loadDemoRegistry(pkgRoot) {
|
|
4725
5093
|
let raw;
|
|
4726
5094
|
try {
|
|
4727
|
-
raw = JSON.parse(
|
|
5095
|
+
raw = JSON.parse(readFileSync10(join12(pkgRoot, "demos.json"), "utf8"));
|
|
4728
5096
|
} catch {
|
|
4729
5097
|
return [];
|
|
4730
5098
|
}
|
|
4731
|
-
const
|
|
4732
|
-
if (!Array.isArray(
|
|
4733
|
-
return
|
|
5099
|
+
const list3 = raw?.demos;
|
|
5100
|
+
if (!Array.isArray(list3)) return [];
|
|
5101
|
+
return list3.filter((e) => {
|
|
4734
5102
|
const d = e;
|
|
4735
5103
|
if (!d || typeof d.name !== "string" || !d.name || typeof d.description !== "string") return false;
|
|
4736
5104
|
if (d.source === "bundled") {
|
|
@@ -4754,9 +5122,9 @@ function missingRequirements(entry) {
|
|
|
4754
5122
|
}
|
|
4755
5123
|
|
|
4756
5124
|
// src/export.ts
|
|
4757
|
-
import { mkdirSync as mkdirSync2, writeFileSync as writeFileSync2, copyFileSync, readFileSync as
|
|
4758
|
-
import { join as
|
|
4759
|
-
import { fileURLToPath as
|
|
5125
|
+
import { mkdirSync as mkdirSync2, writeFileSync as writeFileSync2, copyFileSync, readFileSync as readFileSync11, readdirSync as readdirSync4 } from "node:fs";
|
|
5126
|
+
import { join as join13, dirname as dirname5, basename } from "node:path";
|
|
5127
|
+
import { fileURLToPath as fileURLToPath4 } from "node:url";
|
|
4760
5128
|
var LENS_PARAMS = ["components", "detail", "env", "logical", "radial", "tier"];
|
|
4761
5129
|
function canonicalKey(path, params) {
|
|
4762
5130
|
const flat = params.get("components") === "1" || params.get("logical") === "1";
|
|
@@ -4803,7 +5171,7 @@ function captureKeys(axes) {
|
|
|
4803
5171
|
return [...keys];
|
|
4804
5172
|
}
|
|
4805
5173
|
function webDir() {
|
|
4806
|
-
return
|
|
5174
|
+
return join13(dirname5(fileURLToPath4(import.meta.url)), "..", "web");
|
|
4807
5175
|
}
|
|
4808
5176
|
function workerName(project, override) {
|
|
4809
5177
|
const raw = override ?? `behold-${basename(project)}`;
|
|
@@ -4814,7 +5182,7 @@ async function runExport(cfg, outDir, opts = {}) {
|
|
|
4814
5182
|
const app = createApp(cfg);
|
|
4815
5183
|
const proj = await (await app.request("/api/project")).json();
|
|
4816
5184
|
const axes = { environments: proj.environments ?? [], tiers: proj.tiers ?? [] };
|
|
4817
|
-
const snapDir =
|
|
5185
|
+
const snapDir = join13(outDir, "snapshots");
|
|
4818
5186
|
mkdirSync2(snapDir, { recursive: true });
|
|
4819
5187
|
const keyToFile = {};
|
|
4820
5188
|
let ok = 0;
|
|
@@ -4823,7 +5191,7 @@ async function runExport(cfg, outDir, opts = {}) {
|
|
|
4823
5191
|
const res = await app.request(key);
|
|
4824
5192
|
const body = await res.text();
|
|
4825
5193
|
const file = slug(key);
|
|
4826
|
-
writeFileSync2(
|
|
5194
|
+
writeFileSync2(join13(snapDir, file), body);
|
|
4827
5195
|
keyToFile[key] = `snapshots/${file}`;
|
|
4828
5196
|
if (res.ok) ok++;
|
|
4829
5197
|
else failed++;
|
|
@@ -4835,21 +5203,21 @@ async function runExport(cfg, outDir, opts = {}) {
|
|
|
4835
5203
|
axes,
|
|
4836
5204
|
keyToFile
|
|
4837
5205
|
};
|
|
4838
|
-
writeFileSync2(
|
|
4839
|
-
const html =
|
|
5206
|
+
writeFileSync2(join13(outDir, "manifest.json"), JSON.stringify(manifest, null, 2));
|
|
5207
|
+
const html = readFileSync11(join13(webDir(), "index.html"), "utf8").replace(
|
|
4840
5208
|
/<\/head>/i,
|
|
4841
5209
|
` <script>window.__BEHOLD_STATIC__ = true;</script>
|
|
4842
5210
|
</head>`
|
|
4843
5211
|
);
|
|
4844
|
-
writeFileSync2(
|
|
5212
|
+
writeFileSync2(join13(outDir, "index.html"), html);
|
|
4845
5213
|
for (const f of readdirSync4(webDir())) {
|
|
4846
5214
|
if (f === "index.html") continue;
|
|
4847
|
-
copyFileSync(
|
|
5215
|
+
copyFileSync(join13(webDir(), f), join13(outDir, f));
|
|
4848
5216
|
}
|
|
4849
|
-
writeFileSync2(
|
|
5217
|
+
writeFileSync2(join13(outDir, "README.md"), BUNDLE_README);
|
|
4850
5218
|
const name = workerName(cfg.projectDir, opts.name);
|
|
4851
5219
|
writeFileSync2(
|
|
4852
|
-
|
|
5220
|
+
join13(outDir, "wrangler.jsonc"),
|
|
4853
5221
|
JSON.stringify(
|
|
4854
5222
|
{ $schema: "node_modules/wrangler/config-schema.json", name, compatibility_date: "2025-06-01", assets: { directory: "." } },
|
|
4855
5223
|
null,
|
|
@@ -4896,15 +5264,239 @@ It's just files \u2014 GitHub Pages, S3, nginx, or Cloudflare Pages
|
|
|
4896
5264
|
(\`wrangler pages deploy .\`) all work.
|
|
4897
5265
|
`;
|
|
4898
5266
|
|
|
5267
|
+
// src/doctor.ts
|
|
5268
|
+
import { relative as relative2, resolve as resolve3 } from "node:path";
|
|
5269
|
+
var list2 = (xs) => xs.join(", ");
|
|
5270
|
+
var EMPTY_KUBECONFIG = { contexts: /* @__PURE__ */ new Map(), servers: /* @__PURE__ */ new Map() };
|
|
5271
|
+
function summarize(xs, max = 4) {
|
|
5272
|
+
return xs.length <= max ? list2(xs) : `${list2(xs.slice(0, max))}, +${xs.length - max} more`;
|
|
5273
|
+
}
|
|
5274
|
+
function labelFor(root, target, estate) {
|
|
5275
|
+
if (!estate) return "";
|
|
5276
|
+
const rel = relative2(root, target);
|
|
5277
|
+
return `${rel || "."}: `;
|
|
5278
|
+
}
|
|
5279
|
+
function chantCheck(root, targets, estate) {
|
|
5280
|
+
const floor = chantFloor();
|
|
5281
|
+
const resolutions = targets.map((t) => ({ target: t, res: resolveChant(t) }));
|
|
5282
|
+
const missing = resolutions.filter(({ res }) => res.source !== "project");
|
|
5283
|
+
const describe = ({ target, res }) => `${labelFor(root, target, estate)}${res.source === "project" ? `chant ${res.version ?? "(unknown version)"}` : "no chant installed"}`;
|
|
5284
|
+
if (missing.length) {
|
|
5285
|
+
return {
|
|
5286
|
+
name: "chant",
|
|
5287
|
+
status: "fail",
|
|
5288
|
+
detail: `${list2(resolutions.map(describe))} \u2014 behold shells the project's own chant`,
|
|
5289
|
+
fix: `Run \`npm install\` in ${list2(missing.map(({ target }) => relative2(root, target) || target))}`
|
|
5290
|
+
};
|
|
5291
|
+
}
|
|
5292
|
+
const stale = resolutions.filter(({ res }) => !meetsFloor(res.version, floor));
|
|
5293
|
+
const versions = list2(resolutions.map(describe));
|
|
5294
|
+
if (stale.length) {
|
|
5295
|
+
return {
|
|
5296
|
+
name: "chant",
|
|
5297
|
+
status: "warn",
|
|
5298
|
+
detail: `${versions} \u2014 below behold's floor ${floor}`,
|
|
5299
|
+
fix: "Bump @intentius/chant in the project (`npm install @intentius/chant@latest`) \u2014 older versions observe less."
|
|
5300
|
+
};
|
|
5301
|
+
}
|
|
5302
|
+
return { name: "chant", status: "pass", detail: `${versions}${floor ? ` (behold's floor ${floor})` : ""}` };
|
|
5303
|
+
}
|
|
5304
|
+
function lexiconCheck(root, declared, estate) {
|
|
5305
|
+
const missing = [];
|
|
5306
|
+
const found = [];
|
|
5307
|
+
for (const [target, lexicons] of declared) {
|
|
5308
|
+
const label = labelFor(root, target, estate);
|
|
5309
|
+
for (const r of resolveLexicons(target, lexicons)) {
|
|
5310
|
+
(r.installed ? found : missing).push(`${label}${r.lexicon}${r.installed && r.version ? ` ${r.version}` : ""}`);
|
|
5311
|
+
}
|
|
5312
|
+
}
|
|
5313
|
+
if (missing.length) {
|
|
5314
|
+
return {
|
|
5315
|
+
name: "lexicons",
|
|
5316
|
+
status: "fail",
|
|
5317
|
+
detail: `declared but not installed: ${list2(missing)}${found.length ? ` (installed: ${list2(found)})` : ""}`,
|
|
5318
|
+
fix: "Run `npm install` in the project \u2014 chant can't graph source importing a lexicon it can't resolve."
|
|
5319
|
+
};
|
|
5320
|
+
}
|
|
5321
|
+
if (!found.length) {
|
|
5322
|
+
return { name: "lexicons", status: "pass", detail: "none declared" };
|
|
5323
|
+
}
|
|
5324
|
+
return { name: "lexicons", status: "pass", detail: `${found.length} installed: ${list2(found)}` };
|
|
5325
|
+
}
|
|
5326
|
+
function envCheck(envs, serveArg) {
|
|
5327
|
+
if (!envs.length) {
|
|
5328
|
+
return {
|
|
5329
|
+
name: "envs",
|
|
5330
|
+
status: "warn",
|
|
5331
|
+
detail: "no environments declared \u2014 the declared graph serves, but there is no live overlay to pick",
|
|
5332
|
+
fix: "Declare `environments` (or `k8s.profiles`) in chant.config.ts, then serve with `--env <name>`."
|
|
5333
|
+
};
|
|
5334
|
+
}
|
|
5335
|
+
return { name: "envs", status: "pass", detail: `${list2(envs)} \u2014 \`behold serve ${serveArg} --env ${envs[0]}\` for the live overlay` };
|
|
5336
|
+
}
|
|
5337
|
+
function kubeCheck(lexicons, profiles, envs, kubeconfig) {
|
|
5338
|
+
if (!lexicons.includes("k8s") && !lexicons.includes("helm")) {
|
|
5339
|
+
return { name: "kube", status: "pass", detail: "no k8s or helm lexicon \u2014 no cluster binding to check" };
|
|
5340
|
+
}
|
|
5341
|
+
const ambient = kubeconfig.currentContext;
|
|
5342
|
+
if (!kubeconfig.contexts.size) {
|
|
5343
|
+
return {
|
|
5344
|
+
name: "kube",
|
|
5345
|
+
status: "warn",
|
|
5346
|
+
detail: "no kubeconfig readable (kubectl absent, or it has no contexts) \u2014 the declared graph still serves",
|
|
5347
|
+
fix: "Install kubectl and point KUBECONFIG at your cluster, or run `behold demo k8s` for a throwaway k3d one."
|
|
5348
|
+
};
|
|
5349
|
+
}
|
|
5350
|
+
const bound = envs.filter((e) => profiles[e]?.context);
|
|
5351
|
+
if (!bound.length) {
|
|
5352
|
+
return {
|
|
5353
|
+
name: "kube",
|
|
5354
|
+
status: "warn",
|
|
5355
|
+
detail: `no k8s.profiles binding \u2014 chant falls back to the ambient current-context${ambient ? ` (${ambient})` : " (none set)"}`,
|
|
5356
|
+
fix: "Declare `k8s.profiles.<env>.context` in chant.config.ts so the cluster the reads hit is explicit, not ambient."
|
|
5357
|
+
};
|
|
5358
|
+
}
|
|
5359
|
+
const lines = [];
|
|
5360
|
+
const unresolved = [];
|
|
5361
|
+
const mismatched = [];
|
|
5362
|
+
for (const env of bound) {
|
|
5363
|
+
const context = profiles[env].context;
|
|
5364
|
+
const target = resolveK8sTarget(profiles, env, kubeconfig);
|
|
5365
|
+
if (!target) {
|
|
5366
|
+
unresolved.push(`${env}: ${context}`);
|
|
5367
|
+
continue;
|
|
5368
|
+
}
|
|
5369
|
+
lines.push(`${env}: ${target.label} at ${target.endpoint}`);
|
|
5370
|
+
if (ambient && ambient !== target.label) mismatched.push(`${env}: ${target.label}`);
|
|
5371
|
+
}
|
|
5372
|
+
if (unresolved.length) {
|
|
5373
|
+
return {
|
|
5374
|
+
name: "kube",
|
|
5375
|
+
status: "warn",
|
|
5376
|
+
detail: `context not in the kubeconfig \u2014 ${list2(unresolved)}${lines.length ? `; bound: ${list2(lines)}` : ""}`,
|
|
5377
|
+
fix: `Add the context to your kubeconfig, or point k8s.profiles at one you have (${summarize([...kubeconfig.contexts.keys()])}).`
|
|
5378
|
+
};
|
|
5379
|
+
}
|
|
5380
|
+
if (mismatched.length) {
|
|
5381
|
+
return {
|
|
5382
|
+
name: "kube",
|
|
5383
|
+
status: "warn",
|
|
5384
|
+
detail: `bound ${list2(lines)}, but the ambient current-context is ${ambient} \u2014 chant refuses a live read on a mismatch`,
|
|
5385
|
+
fix: `Run \`kubectl config use-context ${mismatched[0].split(": ")[1]}\` before the live overlay.`
|
|
5386
|
+
};
|
|
5387
|
+
}
|
|
5388
|
+
return { name: "kube", status: "pass", detail: `bound ${list2(lines)}` };
|
|
5389
|
+
}
|
|
5390
|
+
function substrateCheck(substrates) {
|
|
5391
|
+
if (!substrates.length) return { name: "substrates", status: "pass", detail: "none needed by this project" };
|
|
5392
|
+
const detail = list2(substrates.map((s) => `${s.label} ${s.status}`));
|
|
5393
|
+
const down = substrates.filter((s) => s.status === "down" || s.status === "blocked");
|
|
5394
|
+
if (!down.length) return { name: "substrates", status: "pass", detail };
|
|
5395
|
+
const named = down.map((s) => `${s.label} (${s.bringUp ? `${s.bringUp.cmd} ${s.bringUp.args.join(" ")}` : s.detail})`);
|
|
5396
|
+
return {
|
|
5397
|
+
name: "substrates",
|
|
5398
|
+
status: "warn",
|
|
5399
|
+
detail,
|
|
5400
|
+
fix: `Bring up: ${list2(named)} \u2014 or \`behold serve <dir> --local\`, which boots the project's own emulators (needs Docker).`
|
|
5401
|
+
};
|
|
5402
|
+
}
|
|
5403
|
+
function opsCheck(root, ops, chantSource, estate) {
|
|
5404
|
+
const mcp = chantSource === "project" ? "chant MCP available (the project's own chant)" : "chant MCP unavailable until the project's chant is installed";
|
|
5405
|
+
if (!ops.length) {
|
|
5406
|
+
return {
|
|
5407
|
+
name: "ops",
|
|
5408
|
+
status: "warn",
|
|
5409
|
+
detail: `no committed Ops (*.op.ts) \u2014 behold can show drift but has nothing to trigger; ${mcp}`,
|
|
5410
|
+
fix: "Add an ApplyOp (code to cloud) or ReconcileOp (cloud to PR) under ops/ \u2014 behold triggers your Ops, it holds no apply creds."
|
|
5411
|
+
};
|
|
5412
|
+
}
|
|
5413
|
+
const names = ops.map((o) => `${labelFor(root, o.dir, estate)}${o.name} (${o.kind})`);
|
|
5414
|
+
return { name: "ops", status: "pass", detail: `${ops.length} committed: ${list2(names)}; ${mcp}` };
|
|
5415
|
+
}
|
|
5416
|
+
async function diagnose(dir, probes = {}) {
|
|
5417
|
+
const root = resolve3(dir);
|
|
5418
|
+
const shape = detectProjectShape(root);
|
|
5419
|
+
const behold = beholdVersion();
|
|
5420
|
+
if (shape.kind === "none") {
|
|
5421
|
+
return {
|
|
5422
|
+
behold,
|
|
5423
|
+
dir: root,
|
|
5424
|
+
kind: "none",
|
|
5425
|
+
ok: false,
|
|
5426
|
+
checks: [
|
|
5427
|
+
{
|
|
5428
|
+
name: "project",
|
|
5429
|
+
status: "fail",
|
|
5430
|
+
detail: `no chant.config.ts here, and no estate members declared (.behold.json \`members\`, or npm workspaces)`,
|
|
5431
|
+
fix: "Point behold at a chant project (`behold doctor <dir>`), or run `behold demo` for a bundled working example."
|
|
5432
|
+
}
|
|
5433
|
+
]
|
|
5434
|
+
};
|
|
5435
|
+
}
|
|
5436
|
+
const estate = shape.kind === "estate";
|
|
5437
|
+
const members = (shape.members ?? []).map((m) => resolve3(root, m));
|
|
5438
|
+
const targets = estate ? members : [root];
|
|
5439
|
+
const primary = targets[0];
|
|
5440
|
+
const projectCheck = estate ? {
|
|
5441
|
+
name: "project",
|
|
5442
|
+
status: "pass",
|
|
5443
|
+
detail: `estate of ${members.length} projects (${shape.membersFrom === "behold-config" ? ".behold.json members" : "npm workspaces"}): ${list2(
|
|
5444
|
+
members.map((m) => relative2(root, m))
|
|
5445
|
+
)}`
|
|
5446
|
+
} : { name: "project", status: "pass", detail: `chant project (${relative2(root, shape.configFile)})` };
|
|
5447
|
+
const infos = await Promise.all(targets.map(async (t) => ({ target: t, info: await detectProject(t) })));
|
|
5448
|
+
const declared = new Map(infos.map(({ target, info }) => [target, info.lexicons]));
|
|
5449
|
+
const lexicons = [...new Set(infos.flatMap(({ info }) => info.lexicons))];
|
|
5450
|
+
const envs = [...new Set(infos.flatMap(({ info }) => info.environments))];
|
|
5451
|
+
const profiles = {};
|
|
5452
|
+
for (const { info } of infos) for (const [env, p] of Object.entries(info.k8sProfiles ?? {})) profiles[env] ??= p;
|
|
5453
|
+
const kubeconfig = lexicons.includes("k8s") || lexicons.includes("helm") ? await (probes.loadKubeconfig ?? loadKubeconfig)() : EMPTY_KUBECONFIG;
|
|
5454
|
+
const kube = kubeCheck(lexicons, profiles, envs, kubeconfig);
|
|
5455
|
+
const boundContext = resolveK8sTarget(profiles, envs[0], kubeconfig)?.label;
|
|
5456
|
+
const substrates = await (probes.detectSubstrates ?? detectSubstrates)(primary, false, boundContext);
|
|
5457
|
+
const checks = [
|
|
5458
|
+
projectCheck,
|
|
5459
|
+
chantCheck(root, targets, estate),
|
|
5460
|
+
lexiconCheck(root, declared, estate),
|
|
5461
|
+
// An estate root is not itself servable — the hint has to name its members
|
|
5462
|
+
// (`behold serve a b c`, #31), which is what a stranger would otherwise
|
|
5463
|
+
// discover by having the root serve nothing.
|
|
5464
|
+
envCheck(envs, estate ? shape.members.map((m) => `${dir.replace(/\/$/, "")}/${m}`).join(" ") : dir),
|
|
5465
|
+
kube,
|
|
5466
|
+
substrateCheck(substrates),
|
|
5467
|
+
opsCheck(root, discoverEstateOps(targets), resolveChant(primary).source, estate)
|
|
5468
|
+
];
|
|
5469
|
+
return { behold, dir: root, kind: shape.kind, ok: !checks.some((c) => c.status === "fail"), checks };
|
|
5470
|
+
}
|
|
5471
|
+
function formatReport(report) {
|
|
5472
|
+
const lines = [`behold doctor ${report.behold} \u2014 ${report.dir}`, ""];
|
|
5473
|
+
for (const c of report.checks) {
|
|
5474
|
+
lines.push(` ${c.status.padEnd(4)} ${c.name.padEnd(10)} ${c.detail}`);
|
|
5475
|
+
if (c.fix) lines.push(` ${"fix".padEnd(10)} ${c.fix}`);
|
|
5476
|
+
}
|
|
5477
|
+
const tally = (s) => report.checks.filter((c) => c.status === s).length;
|
|
5478
|
+
lines.push("", ` ${tally("pass")} pass, ${tally("warn")} warn, ${tally("fail")} fail`, "");
|
|
5479
|
+
return lines.join("\n");
|
|
5480
|
+
}
|
|
5481
|
+
|
|
4899
5482
|
// src/cli.ts
|
|
4900
5483
|
var USAGE = `behold \u2014 a live control plane on chant (read-only core)
|
|
4901
5484
|
|
|
4902
5485
|
Usage:
|
|
4903
5486
|
behold demo [name] [target-dir] [--port <n>] [--list]
|
|
5487
|
+
behold doctor [project-dir] [--json]
|
|
4904
5488
|
behold preview [project-dir] [--port <n>] [--emulator]
|
|
4905
5489
|
behold export [project-dir] [--out <dir>] [--env <name>] [--name <worker>] [--emulator]
|
|
4906
5490
|
behold serve <project-dir\u2026> [--port <n>] [--env <name>] [--poll <secs>] [--local]
|
|
4907
5491
|
|
|
5492
|
+
doctor Why won't this project serve well? A read-only diagnosis of
|
|
5493
|
+
everything behold needs \u2014 the project's kind, its own chant install
|
|
5494
|
+
and version, declared lexicons, the envs the picker will infer, the
|
|
5495
|
+
kube context chant binds versus the ambient one, substrate
|
|
5496
|
+
readiness, committed Ops \u2014 each line pass/warn/fail with a one-line
|
|
5497
|
+
fix. Defaults to the current directory. Exits 0 iff nothing failed;
|
|
5498
|
+
--json for machines. Starts no server and changes nothing.
|
|
5499
|
+
|
|
4908
5500
|
demo The five-minute path from npm \u2014 no chant project needed. A catalog
|
|
4909
5501
|
of demo estates (behold demo --list): bundled ones copy out of the
|
|
4910
5502
|
package into a directory that's yours to edit; git ones shallow-
|
|
@@ -4965,6 +5557,8 @@ Options:
|
|
|
4965
5557
|
names, unlike --local; kept separate because Loom's own
|
|
4966
5558
|
\`scripts/local/local-up.sh\` Floci setup clashes on :4566
|
|
4967
5559
|
with chant's generic \`chant emulator up\`.
|
|
5560
|
+
--json doctor only: the report as JSON (stable keys) instead of
|
|
5561
|
+
the console lines \u2014 the AGENTS.md audience.
|
|
4968
5562
|
--out <dir> export only: output directory (default ./behold-export).
|
|
4969
5563
|
--name <worker> export only: Cloudflare Worker name in the generated
|
|
4970
5564
|
wrangler.jsonc.
|
|
@@ -4985,6 +5579,10 @@ async function run3(argv) {
|
|
|
4985
5579
|
await runDemo(rest);
|
|
4986
5580
|
return;
|
|
4987
5581
|
}
|
|
5582
|
+
if (cmd === "doctor") {
|
|
5583
|
+
await runDoctor(rest);
|
|
5584
|
+
return;
|
|
5585
|
+
}
|
|
4988
5586
|
if (cmd === "preview") {
|
|
4989
5587
|
await runPreview(rest);
|
|
4990
5588
|
return;
|
|
@@ -5048,7 +5646,7 @@ ${USAGE}`);
|
|
|
5048
5646
|
process.stderr.write("behold serve: --auto-sync needs --env and --poll (it acts on polled drift)\n");
|
|
5049
5647
|
process.exit(2);
|
|
5050
5648
|
}
|
|
5051
|
-
const dirs = projectDirs.map((d) =>
|
|
5649
|
+
const dirs = projectDirs.map((d) => resolve4(d));
|
|
5052
5650
|
for (const d of dirs) warnIfNotChantProject(d);
|
|
5053
5651
|
await startServer({
|
|
5054
5652
|
projectDir: dirs[0],
|
|
@@ -5062,15 +5660,47 @@ ${USAGE}`);
|
|
|
5062
5660
|
});
|
|
5063
5661
|
}
|
|
5064
5662
|
function warnIfNotChantProject(dir) {
|
|
5065
|
-
|
|
5663
|
+
const shape = detectProjectShape(dir);
|
|
5664
|
+
if (shape.kind === "project") return;
|
|
5665
|
+
if (shape.kind === "estate") {
|
|
5666
|
+
process.stderr.write(
|
|
5667
|
+
`behold: warning \u2014 ${dir} is an estate root, not a chant project itself.
|
|
5668
|
+
Serve its members composed: behold serve ${shape.members.map((m) => join14(dir, m)).join(" ")}
|
|
5669
|
+
`
|
|
5670
|
+
);
|
|
5671
|
+
return;
|
|
5672
|
+
}
|
|
5066
5673
|
process.stderr.write(
|
|
5067
5674
|
`behold: warning \u2014 ${dir} has no chant.config.ts; this doesn't look like a chant project.
|
|
5068
|
-
No project yet? \`behold demo\` serves a bundled working example (needs Docker).
|
|
5675
|
+
\`behold doctor ${dir}\` says what's missing. No project yet? \`behold demo\` serves a bundled working example (needs Docker).
|
|
5069
5676
|
`
|
|
5070
5677
|
);
|
|
5071
5678
|
}
|
|
5679
|
+
async function runDoctor(rest) {
|
|
5680
|
+
let json = false;
|
|
5681
|
+
let dirArg;
|
|
5682
|
+
for (const a of rest) {
|
|
5683
|
+
if (a === "--json") json = true;
|
|
5684
|
+
else if (a === "-h" || a === "--help") return void process.stdout.write(USAGE);
|
|
5685
|
+
else if (!a.startsWith("-")) dirArg = a;
|
|
5686
|
+
else {
|
|
5687
|
+
process.stderr.write(`behold doctor: unexpected argument '${a}'
|
|
5688
|
+
`);
|
|
5689
|
+
process.exit(2);
|
|
5690
|
+
}
|
|
5691
|
+
}
|
|
5692
|
+
const dir = dirArg ?? ".";
|
|
5693
|
+
if (!existsSync10(resolve4(dir))) {
|
|
5694
|
+
process.stderr.write(`behold doctor: no such directory: ${resolve4(dir)}
|
|
5695
|
+
`);
|
|
5696
|
+
process.exit(2);
|
|
5697
|
+
}
|
|
5698
|
+
const report = await diagnose(dir);
|
|
5699
|
+
process.stdout.write(json ? JSON.stringify(report, null, 2) + "\n" : formatReport(report));
|
|
5700
|
+
if (!report.ok) process.exitCode = 1;
|
|
5701
|
+
}
|
|
5072
5702
|
async function runDemo(rest) {
|
|
5073
|
-
const pkgRoot =
|
|
5703
|
+
const pkgRoot = join14(dirname6(fileURLToPath5(import.meta.url)), "..");
|
|
5074
5704
|
const registry = loadDemoRegistry(pkgRoot);
|
|
5075
5705
|
let port = 4600;
|
|
5076
5706
|
let name;
|
|
@@ -5122,12 +5752,12 @@ async function runDemo(rest) {
|
|
|
5122
5752
|
`);
|
|
5123
5753
|
process.exit(2);
|
|
5124
5754
|
}
|
|
5125
|
-
const target =
|
|
5126
|
-
dirArg ?? (entry.name === "writes" && existsSync10("behold-demo") ? "behold-demo" :
|
|
5755
|
+
const target = resolve4(
|
|
5756
|
+
dirArg ?? (entry.name === "writes" && existsSync10("behold-demo") ? "behold-demo" : join14("behold-demos", entry.name))
|
|
5127
5757
|
);
|
|
5128
5758
|
if (!existsSync10(target)) {
|
|
5129
5759
|
if (entry.source === "bundled") {
|
|
5130
|
-
const bundled =
|
|
5760
|
+
const bundled = join14(pkgRoot, entry.dir);
|
|
5131
5761
|
if (!existsSync10(bundled)) {
|
|
5132
5762
|
process.stderr.write(`behold demo ${entry.name}: this install has no bundled ${entry.dir}
|
|
5133
5763
|
`);
|
|
@@ -5137,7 +5767,7 @@ async function runDemo(rest) {
|
|
|
5137
5767
|
`);
|
|
5138
5768
|
cpSync(bundled, target, {
|
|
5139
5769
|
recursive: true,
|
|
5140
|
-
filter: (src) => !
|
|
5770
|
+
filter: (src) => !relative3(bundled, src).split(sep).includes("node_modules")
|
|
5141
5771
|
});
|
|
5142
5772
|
} else {
|
|
5143
5773
|
process.stdout.write(`behold demo ${entry.name} \u2192 cloning ${entry.repo} to ${target}
|
|
@@ -5153,7 +5783,7 @@ async function runDemo(rest) {
|
|
|
5153
5783
|
process.stdout.write(`behold demo ${entry.name} \u2192 reusing ${target}
|
|
5154
5784
|
`);
|
|
5155
5785
|
}
|
|
5156
|
-
if (existsSync10(
|
|
5786
|
+
if (existsSync10(join14(target, "package.json")) && !existsSync10(join14(target, "node_modules"))) {
|
|
5157
5787
|
process.stdout.write(`behold demo ${entry.name} \u2192 npm install\u2026
|
|
5158
5788
|
`);
|
|
5159
5789
|
const r = spawnSync2("npm", ["install"], { cwd: target, stdio: "inherit", shell: process.platform === "win32" });
|
|
@@ -5175,7 +5805,7 @@ async function runDemo(rest) {
|
|
|
5175
5805
|
}
|
|
5176
5806
|
process.stdout.write(`behold demo ${entry.name} \u2192 serving. Blue = declared; Deploy turns it green.
|
|
5177
5807
|
`);
|
|
5178
|
-
const serveDirs = entry.serve.dirs?.length ? entry.serve.dirs.map((d) =>
|
|
5808
|
+
const serveDirs = entry.serve.dirs?.length ? entry.serve.dirs.map((d) => join14(target, d)) : [target];
|
|
5179
5809
|
const serveArgs = ["serve", ...serveDirs, "--port", String(port)];
|
|
5180
5810
|
if (entry.serve.local) serveArgs.push("--local");
|
|
5181
5811
|
if (entry.serve.env) serveArgs.push("--env", entry.serve.env);
|
|
@@ -5203,7 +5833,7 @@ async function runPreview(rest) {
|
|
|
5203
5833
|
process.stderr.write("behold preview: --port must be a number\n");
|
|
5204
5834
|
process.exit(2);
|
|
5205
5835
|
}
|
|
5206
|
-
const projectDir =
|
|
5836
|
+
const projectDir = resolve4(dirArg ?? process.cwd());
|
|
5207
5837
|
if (!existsSync10(projectDir)) {
|
|
5208
5838
|
process.stderr.write(`behold preview: project not found at ${projectDir}
|
|
5209
5839
|
`);
|
|
@@ -5224,21 +5854,21 @@ async function runPreview(rest) {
|
|
|
5224
5854
|
await startServer({ projectDir, port, env: "local", previewMode: true });
|
|
5225
5855
|
}
|
|
5226
5856
|
async function runExportCmd(rest) {
|
|
5227
|
-
let outDir =
|
|
5857
|
+
let outDir = resolve4("behold-export");
|
|
5228
5858
|
let env;
|
|
5229
5859
|
let name;
|
|
5230
5860
|
let dirArg;
|
|
5231
5861
|
let emulator = false;
|
|
5232
5862
|
for (let i = 0; i < rest.length; i++) {
|
|
5233
5863
|
const a = rest[i];
|
|
5234
|
-
if (a === "--out") outDir =
|
|
5864
|
+
if (a === "--out") outDir = resolve4(rest[++i]);
|
|
5235
5865
|
else if (a === "--env") env = rest[++i];
|
|
5236
5866
|
else if (a === "--name") name = rest[++i];
|
|
5237
5867
|
else if (a === "--emulator") emulator = true;
|
|
5238
5868
|
else if (a === "-h" || a === "--help") return void process.stdout.write(USAGE);
|
|
5239
5869
|
else if (!a.startsWith("-")) dirArg = a;
|
|
5240
5870
|
}
|
|
5241
|
-
const projectDir =
|
|
5871
|
+
const projectDir = resolve4(dirArg ?? process.cwd());
|
|
5242
5872
|
if (emulator) {
|
|
5243
5873
|
injectEmulatorEnv(env);
|
|
5244
5874
|
env ??= "local";
|
|
@@ -5254,7 +5884,7 @@ function isMainModule() {
|
|
|
5254
5884
|
const entry = process.argv[1];
|
|
5255
5885
|
if (!entry) return false;
|
|
5256
5886
|
try {
|
|
5257
|
-
return realpathSync(entry) === realpathSync(
|
|
5887
|
+
return realpathSync(entry) === realpathSync(fileURLToPath5(import.meta.url));
|
|
5258
5888
|
} catch {
|
|
5259
5889
|
return false;
|
|
5260
5890
|
}
|