@intentius/behold 0.7.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 +750 -132
- 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 +2 -2
- package/example-flux-estate/app-b/package.json +2 -2
- 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 +18 -18
- package/example-k8s/package.json +3 -3
- package/example-writes/package-lock.json +14 -14
- package/example-writes/package.json +3 -3
- package/package.json +3 -2
- package/web/app.js +350 -22
- 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
|
}
|
|
@@ -2223,9 +2343,9 @@ function enrichedRefs(ir, aws, byId) {
|
|
|
2223
2343
|
const producersByName = /* @__PURE__ */ new Map();
|
|
2224
2344
|
for (const ex of ir.exports ?? []) {
|
|
2225
2345
|
if (!ex.node || !byId.has(ex.node)) continue;
|
|
2226
|
-
const
|
|
2227
|
-
if (isExample(byId.get(ex.node)))
|
|
2228
|
-
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);
|
|
2229
2349
|
}
|
|
2230
2350
|
const exportNames = [...producersByName.keys()].sort((a, b) => b.length - a.length);
|
|
2231
2351
|
const bridge = (paramId, producers) => {
|
|
@@ -2713,7 +2833,83 @@ function summarizePlan(plan, byComponent, nonResource) {
|
|
|
2713
2833
|
}
|
|
2714
2834
|
|
|
2715
2835
|
// src/render.ts
|
|
2716
|
-
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 });
|
|
2717
2913
|
function renderArchitecture(ir, byContainer, opts = {}) {
|
|
2718
2914
|
const spread = Math.min(1.5, ir.edges.length / Math.max(ir.nodes.length, 1));
|
|
2719
2915
|
const layout = layoutArchitecture(ir, byContainer, {
|
|
@@ -2909,8 +3105,8 @@ function radializeLayout(layout, groupOf, size = /* @__PURE__ */ new Map()) {
|
|
|
2909
3105
|
}
|
|
2910
3106
|
|
|
2911
3107
|
// src/ops.ts
|
|
2912
|
-
import { readdirSync as readdirSync3, readFileSync as
|
|
2913
|
-
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";
|
|
2914
3110
|
var APPLY_TARGET_LEXICON = {
|
|
2915
3111
|
cloudformation: "aws",
|
|
2916
3112
|
kubectl: "k8s",
|
|
@@ -2932,11 +3128,11 @@ function discoverOps(projectDir) {
|
|
|
2932
3128
|
const seen = /* @__PURE__ */ new Set();
|
|
2933
3129
|
const out = [];
|
|
2934
3130
|
for (const sub of ["ops", "src", "."]) {
|
|
2935
|
-
const dir =
|
|
3131
|
+
const dir = join8(projectDir, sub);
|
|
2936
3132
|
if (!existsSync6(dir)) continue;
|
|
2937
3133
|
for (const f of readdirSync3(dir)) {
|
|
2938
3134
|
if (!f.endsWith(".op.ts")) continue;
|
|
2939
|
-
const content =
|
|
3135
|
+
const content = readFileSync7(join8(dir, f), "utf8");
|
|
2940
3136
|
const name = content.match(/name:\s*["'`]([^"'`]+)["'`]/)?.[1];
|
|
2941
3137
|
if (!name || seen.has(name)) continue;
|
|
2942
3138
|
seen.add(name);
|
|
@@ -3049,6 +3245,117 @@ function classifyHealth(status) {
|
|
|
3049
3245
|
if (HEALTHY.test(text)) return "healthy";
|
|
3050
3246
|
return "unknown";
|
|
3051
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
|
+
}
|
|
3052
3359
|
|
|
3053
3360
|
// src/apply.ts
|
|
3054
3361
|
var PROGRESS_EVENT_TYPES = /* @__PURE__ */ new Set([
|
|
@@ -3335,23 +3642,23 @@ var OpRunner = class {
|
|
|
3335
3642
|
|
|
3336
3643
|
// src/substrates.ts
|
|
3337
3644
|
import { spawn as spawn3 } from "node:child_process";
|
|
3338
|
-
import { existsSync as existsSync7, readFileSync as
|
|
3339
|
-
import { join as
|
|
3645
|
+
import { existsSync as existsSync7, readFileSync as readFileSync8 } from "node:fs";
|
|
3646
|
+
import { join as join9 } from "node:path";
|
|
3340
3647
|
import { platform } from "node:os";
|
|
3341
3648
|
function probe(cmd, args) {
|
|
3342
|
-
return new Promise((
|
|
3649
|
+
return new Promise((resolve5) => {
|
|
3343
3650
|
let out = "";
|
|
3344
3651
|
let proc;
|
|
3345
3652
|
try {
|
|
3346
3653
|
proc = spawn3(cmd, args, { stdio: ["ignore", "pipe", "pipe"] });
|
|
3347
3654
|
} catch {
|
|
3348
|
-
|
|
3655
|
+
resolve5({ code: 127, out: "" });
|
|
3349
3656
|
return;
|
|
3350
3657
|
}
|
|
3351
3658
|
proc.stdout.on("data", (d) => out += d);
|
|
3352
3659
|
proc.stderr.on("data", (d) => out += d);
|
|
3353
|
-
proc.on("error", () =>
|
|
3354
|
-
proc.on("close", (code) =>
|
|
3660
|
+
proc.on("error", () => resolve5({ code: 127, out }));
|
|
3661
|
+
proc.on("close", (code) => resolve5({ code: code ?? 1, out }));
|
|
3355
3662
|
});
|
|
3356
3663
|
}
|
|
3357
3664
|
async function dockerAvailable() {
|
|
@@ -3364,11 +3671,11 @@ async function dockerRunning(nameFilter) {
|
|
|
3364
3671
|
return out.split(/\r?\n/).map((s) => s.trim()).filter(Boolean);
|
|
3365
3672
|
}
|
|
3366
3673
|
function scriptBringUp(projectDir, relPath, label) {
|
|
3367
|
-
return existsSync7(
|
|
3674
|
+
return existsSync7(join9(projectDir, relPath)) ? { label, cmd: "bash", args: [relPath] } : void 0;
|
|
3368
3675
|
}
|
|
3369
3676
|
function projectLexicons(projectDir) {
|
|
3370
3677
|
try {
|
|
3371
|
-
const src =
|
|
3678
|
+
const src = readFileSync8(join9(projectDir, "chant.config.ts"), "utf-8");
|
|
3372
3679
|
const m = src.match(/lexicons\s*:\s*\[([^\]]*)\]/);
|
|
3373
3680
|
if (!m) return [];
|
|
3374
3681
|
return [...m[1].matchAll(/["']([^"']+)["']/g)].map((x) => x[1]);
|
|
@@ -3418,7 +3725,7 @@ async function detectSubstrates(projectDir, preview = false, boundContext) {
|
|
|
3418
3725
|
["forgejo", "Forgejo", ".forgejo", "test/forgejo-runtime-e2e.sh"]
|
|
3419
3726
|
];
|
|
3420
3727
|
for (const [name, label, marker, script] of forges) {
|
|
3421
|
-
if (!existsSync7(
|
|
3728
|
+
if (!existsSync7(join9(projectDir, marker))) continue;
|
|
3422
3729
|
const c = docker ? await dockerRunning(name) : [];
|
|
3423
3730
|
const d = dep(c.length > 0, "container up", "on-demand (pipeline run)");
|
|
3424
3731
|
subs.push({
|
|
@@ -3448,7 +3755,7 @@ async function detectSubstrates(projectDir, preview = false, boundContext) {
|
|
|
3448
3755
|
detail: endpoint ? `targeting ${endpoint}` : "real Fly (FLY_FLAPS_BASE_URL unset)"
|
|
3449
3756
|
});
|
|
3450
3757
|
}
|
|
3451
|
-
if (existsSync7(
|
|
3758
|
+
if (existsSync7(join9(projectDir, ".github", "workflows"))) {
|
|
3452
3759
|
const gh = await probe("gh", ["auth", "status"]);
|
|
3453
3760
|
const ready = gh.code === 0;
|
|
3454
3761
|
subs.push({
|
|
@@ -3461,7 +3768,7 @@ async function detectSubstrates(projectDir, preview = false, boundContext) {
|
|
|
3461
3768
|
if (lexicons.includes("temporal")) {
|
|
3462
3769
|
let hasProfiles = false;
|
|
3463
3770
|
try {
|
|
3464
|
-
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"));
|
|
3465
3772
|
} catch {
|
|
3466
3773
|
}
|
|
3467
3774
|
subs.push({
|
|
@@ -3578,6 +3885,13 @@ var firstLine = (e) => {
|
|
|
3578
3885
|
const m = msg.match(/exited \d+:\s*([\s\S]*)/);
|
|
3579
3886
|
return (m ? m[1] : msg).split("\n")[0].trim().slice(0, 160);
|
|
3580
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
|
+
}
|
|
3581
3895
|
async function composeEstateOverlay(projectDirs, opts, classify) {
|
|
3582
3896
|
const names = shortStackNames(projectDirs);
|
|
3583
3897
|
const unobserved = [];
|
|
@@ -3587,7 +3901,7 @@ async function composeEstateOverlay(projectDirs, opts, classify) {
|
|
|
3587
3901
|
projectDirs.map(async (dir, i) => {
|
|
3588
3902
|
const name = names[i];
|
|
3589
3903
|
try {
|
|
3590
|
-
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 }))) };
|
|
3591
3905
|
} catch (err) {
|
|
3592
3906
|
const reason = firstLine(err);
|
|
3593
3907
|
try {
|
|
@@ -3614,7 +3928,7 @@ async function composeEstateOverlay(projectDirs, opts, classify) {
|
|
|
3614
3928
|
|
|
3615
3929
|
// src/events.ts
|
|
3616
3930
|
import { watch, existsSync as existsSync8 } from "node:fs";
|
|
3617
|
-
import { join as
|
|
3931
|
+
import { join as join10 } from "node:path";
|
|
3618
3932
|
var Broadcaster = class {
|
|
3619
3933
|
listeners = /* @__PURE__ */ new Set();
|
|
3620
3934
|
subscribe(fn) {
|
|
@@ -3632,7 +3946,7 @@ var Broadcaster = class {
|
|
|
3632
3946
|
};
|
|
3633
3947
|
var IGNORE = /(^|[\\/])(node_modules|dist|\.git)([\\/]|$)/;
|
|
3634
3948
|
function watchSource(projectDir, onChange, debounceMs = 200) {
|
|
3635
|
-
const dir = existsSync8(
|
|
3949
|
+
const dir = existsSync8(join10(projectDir, "src")) ? join10(projectDir, "src") : projectDir;
|
|
3636
3950
|
let timer;
|
|
3637
3951
|
const watcher = watch(dir, { recursive: true }, (_event, file) => {
|
|
3638
3952
|
const name = typeof file === "string" ? file : "";
|
|
@@ -3740,19 +4054,21 @@ import { renderMorphHtml, layoutIr as layoutIr2 } from "@intentius/pinhole";
|
|
|
3740
4054
|
function safeJson(value) {
|
|
3741
4055
|
return JSON.stringify(value).replace(/</g, "\\u003c");
|
|
3742
4056
|
}
|
|
4057
|
+
var LANES_MONO = `ui-monospace, "SF Mono", SFMono-Regular, "Cascadia Code", "JetBrains Mono", "IBM Plex Mono", Menlo, Consolas, monospace`;
|
|
3743
4058
|
var LANES_CSS = `
|
|
3744
|
-
body { background: var(--bg, #
|
|
3745
|
-
#behold-lanes { position: fixed; left: 0; right: 0; bottom: 0; background: var(--bg, #
|
|
3746
|
-
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); }
|
|
3747
4062
|
#behold-lanes .hd { display: flex; gap: 14px; align-items: baseline; margin-bottom: 4px; }
|
|
3748
|
-
#behold-lanes .hd .rt { color: var(--foreign, #
|
|
3749
|
-
#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; }
|
|
3750
4065
|
#behold-lanes canvas { display: block; width: 100%; cursor: pointer; }
|
|
3751
4066
|
#behold-diff { position: fixed; right: 12px; bottom: 156px; width: 260px; max-height: 40vh; overflow: auto;
|
|
3752
|
-
background: var(--panel, #
|
|
3753
|
-
color: var(--fg, #
|
|
3754
|
-
#behold-diff h4 { margin: 0 0 6px; font
|
|
3755
|
-
|
|
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); }`;
|
|
3756
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>`;
|
|
3757
4073
|
function laneStripScript(frames) {
|
|
3758
4074
|
return `<script>
|
|
@@ -3778,7 +4094,7 @@ const LF = ${safeJson(frames)};
|
|
|
3778
4094
|
const inNow = now !== undefined, inPrev = prev !== undefined;
|
|
3779
4095
|
return inNow !== inPrev || (inNow && inPrev && now !== prev);
|
|
3780
4096
|
}
|
|
3781
|
-
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");
|
|
3782
4098
|
|
|
3783
4099
|
function draw() {
|
|
3784
4100
|
const dpr = window.devicePixelRatio || 1;
|
|
@@ -3787,22 +4103,22 @@ const LF = ${safeJson(frames)};
|
|
|
3787
4103
|
c.clearRect(0, 0, host.clientWidth, H); c.font = "12px ui-sans-serif, system-ui, sans-serif";
|
|
3788
4104
|
subs.forEach((s, r) => {
|
|
3789
4105
|
const y = padT + r * rowH + rowH / 2;
|
|
3790
|
-
c.fillStyle = offset[s] ? css("--foreign", "#
|
|
3791
|
-
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();
|
|
3792
4108
|
LF.forEach((f, i) => {
|
|
3793
4109
|
if (!f.byLexicon[s]) return;
|
|
3794
4110
|
// dot per substrate; brighter when a node in this substrate changed at i
|
|
3795
4111
|
const changed = Object.keys(f.status).some(id => f.lexicon[id] === s && changedAt(id, i));
|
|
3796
4112
|
const hi = focus && f.lexicon[focus] === s && changedAt(focus, i);
|
|
3797
|
-
c.fillStyle = hi ? css("--fg", "#
|
|
4113
|
+
c.fillStyle = hi ? css("--fg", "#cdd6f4") : changed ? color(mode(f, s)) : css("--line", "#3b3c4a");
|
|
3798
4114
|
c.beginPath(); c.arc(xOf(i, s), y, i === cur ? 5 : hi ? 4.5 : 3.5, 0, 7); c.fill();
|
|
3799
4115
|
});
|
|
3800
4116
|
});
|
|
3801
|
-
const px = baseX(cur); c.strokeStyle = css("--pending", "#
|
|
4117
|
+
const px = baseX(cur); c.strokeStyle = css("--pending", "#89b4fa"); c.lineWidth = 1.5;
|
|
3802
4118
|
c.beginPath(); c.moveTo(px, padT - 2); c.lineTo(px, padT + subs.length * rowH); c.stroke();
|
|
3803
|
-
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]);
|
|
3804
4120
|
c.beginPath(); c.moveTo(qx, padT - 2); c.lineTo(qx, padT + subs.length * rowH); c.stroke(); c.setLineDash([]); }
|
|
3805
|
-
c.fillStyle = css("--fg", "#
|
|
4121
|
+
c.fillStyle = css("--fg", "#cdd6f4"); c.textAlign = "center"; c.fillText(LF[cur].name, px, padT + subs.length * rowH + 16);
|
|
3806
4122
|
document.getElementById("behold-lanes-meta").textContent =
|
|
3807
4123
|
LF.length + " frames \xB7 frame " + (cur + 1) + "/" + LF.length + (focus ? " \xB7 focus " + focus : "");
|
|
3808
4124
|
document.getElementById("behold-lanes-rt").style.display = anyOffset() ? "inline" : "none";
|
|
@@ -3908,7 +4224,7 @@ async function emulatorDown(projectDir) {
|
|
|
3908
4224
|
}
|
|
3909
4225
|
|
|
3910
4226
|
// src/server.ts
|
|
3911
|
-
var webRoot =
|
|
4227
|
+
var webRoot = join11(dirname4(fileURLToPath3(import.meta.url)), "..", "web");
|
|
3912
4228
|
var execFileP = async (cmd, args) => (await promisify4(execFile4)(cmd, args, { encoding: "utf8", timeout: 1e4 })).stdout;
|
|
3913
4229
|
function optsFromQuery(url, tierEnvVar, projectDir) {
|
|
3914
4230
|
const q = url.searchParams;
|
|
@@ -3957,8 +4273,8 @@ function tierFailure(tier, message) {
|
|
|
3957
4273
|
}
|
|
3958
4274
|
function beholdVersion() {
|
|
3959
4275
|
try {
|
|
3960
|
-
const here =
|
|
3961
|
-
return JSON.parse(
|
|
4276
|
+
const here = dirname4(fileURLToPath3(import.meta.url));
|
|
4277
|
+
return JSON.parse(readFileSync9(join11(here, "..", "package.json"), "utf8")).version ?? "unknown";
|
|
3962
4278
|
} catch {
|
|
3963
4279
|
return "unknown";
|
|
3964
4280
|
}
|
|
@@ -4019,13 +4335,14 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
|
|
|
4019
4335
|
return void 0;
|
|
4020
4336
|
}
|
|
4021
4337
|
};
|
|
4338
|
+
const estateSourceRoots = async (opts) => (await Promise.all((cfg.projectDirs ?? []).map(async (dir) => [await graphPath(dir, opts), dir]))).flat();
|
|
4022
4339
|
app.get("/healthz", (c) => c.json({ ok: true, projectDir: cfg.projectDir, env: cfg.env ?? null, frames: frames.size }));
|
|
4023
4340
|
app.get("/api/frames", (c) => c.json({ frames: frames.summaries() }));
|
|
4024
4341
|
app.get("/lanes", (c) => {
|
|
4025
4342
|
const all = frames.all();
|
|
4026
4343
|
if (all.length < 2) {
|
|
4027
4344
|
return c.html(
|
|
4028
|
-
`<!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>`
|
|
4029
4346
|
);
|
|
4030
4347
|
}
|
|
4031
4348
|
return c.html(renderLanes(all, frames.summaries()));
|
|
@@ -4091,8 +4408,8 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
|
|
|
4091
4408
|
return c.json({ started: true, name, ran: label });
|
|
4092
4409
|
});
|
|
4093
4410
|
app.post("/api/local/reset", (c) => {
|
|
4094
|
-
const down =
|
|
4095
|
-
const up =
|
|
4411
|
+
const down = join11(cfg.projectDir, "scripts/local/local-down.sh");
|
|
4412
|
+
const up = join11(cfg.projectDir, "scripts/local/local-up.sh");
|
|
4096
4413
|
if (!existsSync9(down) || !existsSync9(up)) {
|
|
4097
4414
|
return c.json({ error: "no local-down.sh / local-up.sh in scripts/local \u2014 reset is only for local emulator projects" }, 400);
|
|
4098
4415
|
}
|
|
@@ -4172,7 +4489,7 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
|
|
|
4172
4489
|
const body = await c.req.json().catch(() => ({}));
|
|
4173
4490
|
const dir = typeof body.dir === "string" && body.dir.trim() ? resolve2(body.dir.trim()) : "";
|
|
4174
4491
|
if (!dir || !existsSync9(dir)) return c.json({ error: `no such directory: ${dir || "(no dir given)"}` }, 400);
|
|
4175
|
-
if (!existsSync9(
|
|
4492
|
+
if (!existsSync9(join11(dir, "chant.config.ts"))) {
|
|
4176
4493
|
return c.json({ error: `${dir} doesn't look like a chant project \u2014 no chant.config.ts` }, 400);
|
|
4177
4494
|
}
|
|
4178
4495
|
addRecent(cfg.projectDir);
|
|
@@ -4244,10 +4561,31 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
|
|
|
4244
4561
|
let mode;
|
|
4245
4562
|
let metaEnv = cfg.env ?? null;
|
|
4246
4563
|
if (multi) {
|
|
4247
|
-
ir = await composeEstate(cfg.projectDirs, opts);
|
|
4564
|
+
ir = await composeEstate(cfg.projectDirs, logical ? { ...opts, detail: 3 } : opts);
|
|
4248
4565
|
ir = addValueMatchEdges(ir);
|
|
4249
4566
|
ir = addK8sDeclaredEdges(ir);
|
|
4250
|
-
|
|
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
|
+
}
|
|
4251
4589
|
} else if (components) {
|
|
4252
4590
|
ir = await componentGraphIr(cfg.projectDir, opts);
|
|
4253
4591
|
const env = opts.env ?? cfg.env;
|
|
@@ -4283,13 +4621,13 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
|
|
|
4283
4621
|
srcCompositeEdgesAttached = 0;
|
|
4284
4622
|
}
|
|
4285
4623
|
}
|
|
4286
|
-
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"))) {
|
|
4287
4625
|
return c.json(noProjectError(cfg.projectDir), 404);
|
|
4288
4626
|
}
|
|
4289
4627
|
const radial = new URL(c.req.url).searchParams.get("radial") === "1";
|
|
4290
4628
|
const { svg } = renderGraph(ir, multi ? { boxes: "byStack" } : { radial });
|
|
4291
4629
|
const srcZoom = components ? "components" : logical ? "logical" : opts.detail === 1 ? "composites" : opts.detail === 3 ? "attributes" : "resources";
|
|
4292
|
-
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;
|
|
4293
4631
|
const srcNote = multi ? estateLensNote : notesFor(srcZoom, ir, srcCompositeEdgesAttached);
|
|
4294
4632
|
return c.json({
|
|
4295
4633
|
ir,
|
|
@@ -4391,22 +4729,36 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
|
|
|
4391
4729
|
try {
|
|
4392
4730
|
if (cfg.projectDirs && cfg.projectDirs.length > 1) {
|
|
4393
4731
|
const runtime = new URL(c.req.url).searchParams.get("runtime") === "1";
|
|
4394
|
-
const
|
|
4732
|
+
const detail = logical ? 3 : query.detail;
|
|
4733
|
+
const est = await composeEstateOverlay(cfg.projectDirs, { ...tierTargetOpts(query), detail, env }, reclassifyOverlay);
|
|
4395
4734
|
if (est.dropped.length === est.total) {
|
|
4396
4735
|
return c.json({ error: `no project in the estate could be graphed \u2014 ${est.dropped.map((d) => `${d.name}: ${d.reason}`).join("; ")}` }, 500);
|
|
4397
4736
|
}
|
|
4398
|
-
|
|
4399
|
-
|
|
4737
|
+
const composed = est.ir;
|
|
4738
|
+
let ir2 = runtime ? attachRuntimeContainment(composed) : pruneRuntimeChildren(composed);
|
|
4739
|
+
if ((detail ?? 2) < 3) ir2 = pruneImports(ir2);
|
|
4400
4740
|
ir2 = addValueMatchEdges(ir2);
|
|
4401
4741
|
ir2 = addK8sDeclaredEdges(ir2);
|
|
4402
|
-
|
|
4403
|
-
|
|
4404
|
-
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);
|
|
4405
4744
|
const coverNote = est.unobserved.length || est.dropped.length ? `live observe covered ${est.observed} of ${est.total} projects \u2014 ` + [
|
|
4406
4745
|
...est.unobserved.map((u) => `${u.name}: ${u.reason} (painted unobserved)`),
|
|
4407
4746
|
...est.dropped.map((d) => `${d.name}: dropped (${d.reason})`)
|
|
4408
4747
|
].join("; ") : void 0;
|
|
4409
|
-
|
|
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 ");
|
|
4410
4762
|
return c.json({
|
|
4411
4763
|
ir: ir2,
|
|
4412
4764
|
svg: svg2,
|
|
@@ -4512,10 +4864,12 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
|
|
|
4512
4864
|
const nodes = {};
|
|
4513
4865
|
for (const id of ids) {
|
|
4514
4866
|
const observed = nodeObserved(parsed, id);
|
|
4867
|
+
const verdict = classifyObservedHealth(observed);
|
|
4515
4868
|
nodes[id] = {
|
|
4516
4869
|
observed,
|
|
4517
4870
|
diff: nodeDiff(parsed, id),
|
|
4518
|
-
health:
|
|
4871
|
+
health: verdict.health,
|
|
4872
|
+
...verdict.detail ? { healthDetail: verdict.detail } : {},
|
|
4519
4873
|
// Field-level (per-manager) drift (#87, chant#1181) — null when no
|
|
4520
4874
|
// lexicon in this diff carries a `deep` section at all.
|
|
4521
4875
|
fieldDrift: nodeFieldDrift(parsed, id)
|
|
@@ -4539,12 +4893,14 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
|
|
|
4539
4893
|
return c.json({ error: "diff output was not JSON \u2014 chant may predate --live --json (needs 0.18.7+)" }, 500);
|
|
4540
4894
|
}
|
|
4541
4895
|
const observed = nodeObserved(parsed, node);
|
|
4896
|
+
const verdict = classifyObservedHealth(observed);
|
|
4542
4897
|
return c.json({
|
|
4543
4898
|
node,
|
|
4544
4899
|
env,
|
|
4545
4900
|
diff: nodeDiff(parsed, node),
|
|
4546
4901
|
observed,
|
|
4547
|
-
health:
|
|
4902
|
+
health: verdict.health,
|
|
4903
|
+
...verdict.detail ? { healthDetail: verdict.detail } : {},
|
|
4548
4904
|
// Field-level (per-manager) drift (#87, chant#1181) — null when no
|
|
4549
4905
|
// lexicon in this diff carries a `deep` section at all.
|
|
4550
4906
|
fieldDrift: nodeFieldDrift(parsed, node)
|
|
@@ -4595,7 +4951,7 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
|
|
|
4595
4951
|
});
|
|
4596
4952
|
const rel = relative(process.cwd(), webRoot) || ".";
|
|
4597
4953
|
app.use("/*", serveStatic({ root: rel }));
|
|
4598
|
-
app.get("/", serveStatic({ path:
|
|
4954
|
+
app.get("/", serveStatic({ path: join11(rel, "index.html") }));
|
|
4599
4955
|
return app;
|
|
4600
4956
|
}
|
|
4601
4957
|
async function startServer(cfg) {
|
|
@@ -4624,7 +4980,7 @@ async function startServer(cfg) {
|
|
|
4624
4980
|
}
|
|
4625
4981
|
}
|
|
4626
4982
|
for (const dir of cfg.projectDirs ?? [cfg.projectDir]) {
|
|
4627
|
-
if (existsSync9(
|
|
4983
|
+
if (existsSync9(join11(dir, "chant.config.ts"))) addRecent(dir);
|
|
4628
4984
|
}
|
|
4629
4985
|
const broadcaster = new Broadcaster();
|
|
4630
4986
|
const frames = new FrameBuffer();
|
|
@@ -4730,19 +5086,19 @@ async function startServer(cfg) {
|
|
|
4730
5086
|
}
|
|
4731
5087
|
|
|
4732
5088
|
// src/demos.ts
|
|
4733
|
-
import { readFileSync as
|
|
4734
|
-
import { join as
|
|
5089
|
+
import { readFileSync as readFileSync10 } from "node:fs";
|
|
5090
|
+
import { join as join12 } from "node:path";
|
|
4735
5091
|
import { spawnSync } from "node:child_process";
|
|
4736
5092
|
function loadDemoRegistry(pkgRoot) {
|
|
4737
5093
|
let raw;
|
|
4738
5094
|
try {
|
|
4739
|
-
raw = JSON.parse(
|
|
5095
|
+
raw = JSON.parse(readFileSync10(join12(pkgRoot, "demos.json"), "utf8"));
|
|
4740
5096
|
} catch {
|
|
4741
5097
|
return [];
|
|
4742
5098
|
}
|
|
4743
|
-
const
|
|
4744
|
-
if (!Array.isArray(
|
|
4745
|
-
return
|
|
5099
|
+
const list3 = raw?.demos;
|
|
5100
|
+
if (!Array.isArray(list3)) return [];
|
|
5101
|
+
return list3.filter((e) => {
|
|
4746
5102
|
const d = e;
|
|
4747
5103
|
if (!d || typeof d.name !== "string" || !d.name || typeof d.description !== "string") return false;
|
|
4748
5104
|
if (d.source === "bundled") {
|
|
@@ -4766,9 +5122,9 @@ function missingRequirements(entry) {
|
|
|
4766
5122
|
}
|
|
4767
5123
|
|
|
4768
5124
|
// src/export.ts
|
|
4769
|
-
import { mkdirSync as mkdirSync2, writeFileSync as writeFileSync2, copyFileSync, readFileSync as
|
|
4770
|
-
import { join as
|
|
4771
|
-
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";
|
|
4772
5128
|
var LENS_PARAMS = ["components", "detail", "env", "logical", "radial", "tier"];
|
|
4773
5129
|
function canonicalKey(path, params) {
|
|
4774
5130
|
const flat = params.get("components") === "1" || params.get("logical") === "1";
|
|
@@ -4815,7 +5171,7 @@ function captureKeys(axes) {
|
|
|
4815
5171
|
return [...keys];
|
|
4816
5172
|
}
|
|
4817
5173
|
function webDir() {
|
|
4818
|
-
return
|
|
5174
|
+
return join13(dirname5(fileURLToPath4(import.meta.url)), "..", "web");
|
|
4819
5175
|
}
|
|
4820
5176
|
function workerName(project, override) {
|
|
4821
5177
|
const raw = override ?? `behold-${basename(project)}`;
|
|
@@ -4826,7 +5182,7 @@ async function runExport(cfg, outDir, opts = {}) {
|
|
|
4826
5182
|
const app = createApp(cfg);
|
|
4827
5183
|
const proj = await (await app.request("/api/project")).json();
|
|
4828
5184
|
const axes = { environments: proj.environments ?? [], tiers: proj.tiers ?? [] };
|
|
4829
|
-
const snapDir =
|
|
5185
|
+
const snapDir = join13(outDir, "snapshots");
|
|
4830
5186
|
mkdirSync2(snapDir, { recursive: true });
|
|
4831
5187
|
const keyToFile = {};
|
|
4832
5188
|
let ok = 0;
|
|
@@ -4835,7 +5191,7 @@ async function runExport(cfg, outDir, opts = {}) {
|
|
|
4835
5191
|
const res = await app.request(key);
|
|
4836
5192
|
const body = await res.text();
|
|
4837
5193
|
const file = slug(key);
|
|
4838
|
-
writeFileSync2(
|
|
5194
|
+
writeFileSync2(join13(snapDir, file), body);
|
|
4839
5195
|
keyToFile[key] = `snapshots/${file}`;
|
|
4840
5196
|
if (res.ok) ok++;
|
|
4841
5197
|
else failed++;
|
|
@@ -4847,21 +5203,21 @@ async function runExport(cfg, outDir, opts = {}) {
|
|
|
4847
5203
|
axes,
|
|
4848
5204
|
keyToFile
|
|
4849
5205
|
};
|
|
4850
|
-
writeFileSync2(
|
|
4851
|
-
const html =
|
|
5206
|
+
writeFileSync2(join13(outDir, "manifest.json"), JSON.stringify(manifest, null, 2));
|
|
5207
|
+
const html = readFileSync11(join13(webDir(), "index.html"), "utf8").replace(
|
|
4852
5208
|
/<\/head>/i,
|
|
4853
5209
|
` <script>window.__BEHOLD_STATIC__ = true;</script>
|
|
4854
5210
|
</head>`
|
|
4855
5211
|
);
|
|
4856
|
-
writeFileSync2(
|
|
5212
|
+
writeFileSync2(join13(outDir, "index.html"), html);
|
|
4857
5213
|
for (const f of readdirSync4(webDir())) {
|
|
4858
5214
|
if (f === "index.html") continue;
|
|
4859
|
-
copyFileSync(
|
|
5215
|
+
copyFileSync(join13(webDir(), f), join13(outDir, f));
|
|
4860
5216
|
}
|
|
4861
|
-
writeFileSync2(
|
|
5217
|
+
writeFileSync2(join13(outDir, "README.md"), BUNDLE_README);
|
|
4862
5218
|
const name = workerName(cfg.projectDir, opts.name);
|
|
4863
5219
|
writeFileSync2(
|
|
4864
|
-
|
|
5220
|
+
join13(outDir, "wrangler.jsonc"),
|
|
4865
5221
|
JSON.stringify(
|
|
4866
5222
|
{ $schema: "node_modules/wrangler/config-schema.json", name, compatibility_date: "2025-06-01", assets: { directory: "." } },
|
|
4867
5223
|
null,
|
|
@@ -4908,15 +5264,239 @@ It's just files \u2014 GitHub Pages, S3, nginx, or Cloudflare Pages
|
|
|
4908
5264
|
(\`wrangler pages deploy .\`) all work.
|
|
4909
5265
|
`;
|
|
4910
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
|
+
|
|
4911
5482
|
// src/cli.ts
|
|
4912
5483
|
var USAGE = `behold \u2014 a live control plane on chant (read-only core)
|
|
4913
5484
|
|
|
4914
5485
|
Usage:
|
|
4915
5486
|
behold demo [name] [target-dir] [--port <n>] [--list]
|
|
5487
|
+
behold doctor [project-dir] [--json]
|
|
4916
5488
|
behold preview [project-dir] [--port <n>] [--emulator]
|
|
4917
5489
|
behold export [project-dir] [--out <dir>] [--env <name>] [--name <worker>] [--emulator]
|
|
4918
5490
|
behold serve <project-dir\u2026> [--port <n>] [--env <name>] [--poll <secs>] [--local]
|
|
4919
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
|
+
|
|
4920
5500
|
demo The five-minute path from npm \u2014 no chant project needed. A catalog
|
|
4921
5501
|
of demo estates (behold demo --list): bundled ones copy out of the
|
|
4922
5502
|
package into a directory that's yours to edit; git ones shallow-
|
|
@@ -4977,6 +5557,8 @@ Options:
|
|
|
4977
5557
|
names, unlike --local; kept separate because Loom's own
|
|
4978
5558
|
\`scripts/local/local-up.sh\` Floci setup clashes on :4566
|
|
4979
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.
|
|
4980
5562
|
--out <dir> export only: output directory (default ./behold-export).
|
|
4981
5563
|
--name <worker> export only: Cloudflare Worker name in the generated
|
|
4982
5564
|
wrangler.jsonc.
|
|
@@ -4997,6 +5579,10 @@ async function run3(argv) {
|
|
|
4997
5579
|
await runDemo(rest);
|
|
4998
5580
|
return;
|
|
4999
5581
|
}
|
|
5582
|
+
if (cmd === "doctor") {
|
|
5583
|
+
await runDoctor(rest);
|
|
5584
|
+
return;
|
|
5585
|
+
}
|
|
5000
5586
|
if (cmd === "preview") {
|
|
5001
5587
|
await runPreview(rest);
|
|
5002
5588
|
return;
|
|
@@ -5060,7 +5646,7 @@ ${USAGE}`);
|
|
|
5060
5646
|
process.stderr.write("behold serve: --auto-sync needs --env and --poll (it acts on polled drift)\n");
|
|
5061
5647
|
process.exit(2);
|
|
5062
5648
|
}
|
|
5063
|
-
const dirs = projectDirs.map((d) =>
|
|
5649
|
+
const dirs = projectDirs.map((d) => resolve4(d));
|
|
5064
5650
|
for (const d of dirs) warnIfNotChantProject(d);
|
|
5065
5651
|
await startServer({
|
|
5066
5652
|
projectDir: dirs[0],
|
|
@@ -5074,15 +5660,47 @@ ${USAGE}`);
|
|
|
5074
5660
|
});
|
|
5075
5661
|
}
|
|
5076
5662
|
function warnIfNotChantProject(dir) {
|
|
5077
|
-
|
|
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
|
+
}
|
|
5078
5673
|
process.stderr.write(
|
|
5079
5674
|
`behold: warning \u2014 ${dir} has no chant.config.ts; this doesn't look like a chant project.
|
|
5080
|
-
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).
|
|
5081
5676
|
`
|
|
5082
5677
|
);
|
|
5083
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
|
+
}
|
|
5084
5702
|
async function runDemo(rest) {
|
|
5085
|
-
const pkgRoot =
|
|
5703
|
+
const pkgRoot = join14(dirname6(fileURLToPath5(import.meta.url)), "..");
|
|
5086
5704
|
const registry = loadDemoRegistry(pkgRoot);
|
|
5087
5705
|
let port = 4600;
|
|
5088
5706
|
let name;
|
|
@@ -5134,12 +5752,12 @@ async function runDemo(rest) {
|
|
|
5134
5752
|
`);
|
|
5135
5753
|
process.exit(2);
|
|
5136
5754
|
}
|
|
5137
|
-
const target =
|
|
5138
|
-
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))
|
|
5139
5757
|
);
|
|
5140
5758
|
if (!existsSync10(target)) {
|
|
5141
5759
|
if (entry.source === "bundled") {
|
|
5142
|
-
const bundled =
|
|
5760
|
+
const bundled = join14(pkgRoot, entry.dir);
|
|
5143
5761
|
if (!existsSync10(bundled)) {
|
|
5144
5762
|
process.stderr.write(`behold demo ${entry.name}: this install has no bundled ${entry.dir}
|
|
5145
5763
|
`);
|
|
@@ -5149,7 +5767,7 @@ async function runDemo(rest) {
|
|
|
5149
5767
|
`);
|
|
5150
5768
|
cpSync(bundled, target, {
|
|
5151
5769
|
recursive: true,
|
|
5152
|
-
filter: (src) => !
|
|
5770
|
+
filter: (src) => !relative3(bundled, src).split(sep).includes("node_modules")
|
|
5153
5771
|
});
|
|
5154
5772
|
} else {
|
|
5155
5773
|
process.stdout.write(`behold demo ${entry.name} \u2192 cloning ${entry.repo} to ${target}
|
|
@@ -5165,7 +5783,7 @@ async function runDemo(rest) {
|
|
|
5165
5783
|
process.stdout.write(`behold demo ${entry.name} \u2192 reusing ${target}
|
|
5166
5784
|
`);
|
|
5167
5785
|
}
|
|
5168
|
-
if (existsSync10(
|
|
5786
|
+
if (existsSync10(join14(target, "package.json")) && !existsSync10(join14(target, "node_modules"))) {
|
|
5169
5787
|
process.stdout.write(`behold demo ${entry.name} \u2192 npm install\u2026
|
|
5170
5788
|
`);
|
|
5171
5789
|
const r = spawnSync2("npm", ["install"], { cwd: target, stdio: "inherit", shell: process.platform === "win32" });
|
|
@@ -5187,7 +5805,7 @@ async function runDemo(rest) {
|
|
|
5187
5805
|
}
|
|
5188
5806
|
process.stdout.write(`behold demo ${entry.name} \u2192 serving. Blue = declared; Deploy turns it green.
|
|
5189
5807
|
`);
|
|
5190
|
-
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];
|
|
5191
5809
|
const serveArgs = ["serve", ...serveDirs, "--port", String(port)];
|
|
5192
5810
|
if (entry.serve.local) serveArgs.push("--local");
|
|
5193
5811
|
if (entry.serve.env) serveArgs.push("--env", entry.serve.env);
|
|
@@ -5215,7 +5833,7 @@ async function runPreview(rest) {
|
|
|
5215
5833
|
process.stderr.write("behold preview: --port must be a number\n");
|
|
5216
5834
|
process.exit(2);
|
|
5217
5835
|
}
|
|
5218
|
-
const projectDir =
|
|
5836
|
+
const projectDir = resolve4(dirArg ?? process.cwd());
|
|
5219
5837
|
if (!existsSync10(projectDir)) {
|
|
5220
5838
|
process.stderr.write(`behold preview: project not found at ${projectDir}
|
|
5221
5839
|
`);
|
|
@@ -5236,21 +5854,21 @@ async function runPreview(rest) {
|
|
|
5236
5854
|
await startServer({ projectDir, port, env: "local", previewMode: true });
|
|
5237
5855
|
}
|
|
5238
5856
|
async function runExportCmd(rest) {
|
|
5239
|
-
let outDir =
|
|
5857
|
+
let outDir = resolve4("behold-export");
|
|
5240
5858
|
let env;
|
|
5241
5859
|
let name;
|
|
5242
5860
|
let dirArg;
|
|
5243
5861
|
let emulator = false;
|
|
5244
5862
|
for (let i = 0; i < rest.length; i++) {
|
|
5245
5863
|
const a = rest[i];
|
|
5246
|
-
if (a === "--out") outDir =
|
|
5864
|
+
if (a === "--out") outDir = resolve4(rest[++i]);
|
|
5247
5865
|
else if (a === "--env") env = rest[++i];
|
|
5248
5866
|
else if (a === "--name") name = rest[++i];
|
|
5249
5867
|
else if (a === "--emulator") emulator = true;
|
|
5250
5868
|
else if (a === "-h" || a === "--help") return void process.stdout.write(USAGE);
|
|
5251
5869
|
else if (!a.startsWith("-")) dirArg = a;
|
|
5252
5870
|
}
|
|
5253
|
-
const projectDir =
|
|
5871
|
+
const projectDir = resolve4(dirArg ?? process.cwd());
|
|
5254
5872
|
if (emulator) {
|
|
5255
5873
|
injectEmulatorEnv(env);
|
|
5256
5874
|
env ??= "local";
|
|
@@ -5266,7 +5884,7 @@ function isMainModule() {
|
|
|
5266
5884
|
const entry = process.argv[1];
|
|
5267
5885
|
if (!entry) return false;
|
|
5268
5886
|
try {
|
|
5269
|
-
return realpathSync(entry) === realpathSync(
|
|
5887
|
+
return realpathSync(entry) === realpathSync(fileURLToPath5(import.meta.url));
|
|
5270
5888
|
} catch {
|
|
5271
5889
|
return false;
|
|
5272
5890
|
}
|