@intentius/behold 0.10.1 → 0.12.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 +76 -1
- package/dist/cli.js +2613 -593
- package/example-argo-estate/app-a/package.json +2 -2
- package/example-argo-estate/app-b/package.json +2 -2
- package/example-argo-estate/control-plane/package.json +2 -2
- package/example-argo-estate/package-lock.json +225 -706
- package/example-carve/README.md +33 -10
- package/example-carve/app/package-lock.json +118 -575
- package/example-carve/app/package.json +2 -2
- 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/package-lock.json +225 -706
- package/example-k8s/package-lock.json +234 -715
- package/example-k8s/package.json +3 -3
- package/example-writes/package-lock.json +147 -604
- package/example-writes/package.json +3 -3
- package/package.json +4 -4
- package/web/app.js +682 -77
- package/web/carve-steps.js +132 -2
- package/web/carve-steps.test.js +98 -0
- package/web/index.html +61 -0
- package/web/layout-store.js +4 -4
- package/web/operator.js +236 -0
- package/web/operator.test.js +158 -0
- package/web/run-playhead.js +194 -0
- package/web/run-playhead.test.js +134 -0
- package/web/theme.test.js +35 -0
package/dist/cli.js
CHANGED
|
@@ -716,11 +716,11 @@ async function createK8sClient(options = {}) {
|
|
|
716
716
|
}
|
|
717
717
|
return void 0;
|
|
718
718
|
}
|
|
719
|
-
async function
|
|
719
|
+
async function resolve9(selector, signal) {
|
|
720
720
|
return "apiVersion" in selector ? resolveByGvk(selector.apiVersion, selector.kind, signal) : resolveByResourceString(selector.resource, selector.group, signal);
|
|
721
721
|
}
|
|
722
722
|
async function resolveOrThrow(selector, signal) {
|
|
723
|
-
const info = await
|
|
723
|
+
const info = await resolve9(selector, signal);
|
|
724
724
|
if (!info) throw new UnknownResourceError(selectorText(selector));
|
|
725
725
|
return info;
|
|
726
726
|
}
|
|
@@ -732,7 +732,7 @@ async function createK8sClient(options = {}) {
|
|
|
732
732
|
return parts.join("/");
|
|
733
733
|
}
|
|
734
734
|
async function pathFor(ref, signal) {
|
|
735
|
-
const info = await
|
|
735
|
+
const info = await resolve9({ apiVersion: ref.apiVersion, kind: ref.kind }, signal);
|
|
736
736
|
if (!info) return void 0;
|
|
737
737
|
return objectPath(info, ref.name, ref.namespace);
|
|
738
738
|
}
|
|
@@ -847,7 +847,7 @@ async function createK8sClient(options = {}) {
|
|
|
847
847
|
return {
|
|
848
848
|
provenance,
|
|
849
849
|
defaultNamespace,
|
|
850
|
-
resolve:
|
|
850
|
+
resolve: resolve9,
|
|
851
851
|
read,
|
|
852
852
|
pathFor,
|
|
853
853
|
readIfPresent,
|
|
@@ -1038,8 +1038,8 @@ var init_src = __esm({
|
|
|
1038
1038
|
});
|
|
1039
1039
|
|
|
1040
1040
|
// src/cli.ts
|
|
1041
|
-
import { resolve as
|
|
1042
|
-
import { realpathSync, existsSync as existsSync14, readFileSync as
|
|
1041
|
+
import { resolve as resolve8, dirname as dirname7, join as join20 } from "node:path";
|
|
1042
|
+
import { realpathSync, existsSync as existsSync14, readFileSync as readFileSync16, rmSync as rmSync2 } from "node:fs";
|
|
1043
1043
|
import { fileURLToPath as fileURLToPath5 } from "node:url";
|
|
1044
1044
|
import { spawn as spawn6, spawnSync as spawnSync2 } from "node:child_process";
|
|
1045
1045
|
|
|
@@ -1120,8 +1120,8 @@ import { serve } from "@hono/node-server";
|
|
|
1120
1120
|
import { fileURLToPath as fileURLToPath3 } from "node:url";
|
|
1121
1121
|
import { execFile as execFile3 } from "node:child_process";
|
|
1122
1122
|
import { promisify as promisify3 } from "node:util";
|
|
1123
|
-
import { existsSync as existsSync13, readFileSync as
|
|
1124
|
-
import { dirname as
|
|
1123
|
+
import { existsSync as existsSync13, readFileSync as readFileSync14 } from "node:fs";
|
|
1124
|
+
import { basename, dirname as dirname5, join as join18, relative as relative4, resolve as resolve6, sep as sep4 } from "node:path";
|
|
1125
1125
|
|
|
1126
1126
|
// src/recents.ts
|
|
1127
1127
|
import { readFileSync, writeFileSync, mkdirSync, existsSync } from "node:fs";
|
|
@@ -1310,6 +1310,8 @@ function detectProjectShape(projectDir) {
|
|
|
1310
1310
|
}
|
|
1311
1311
|
|
|
1312
1312
|
// node_modules/@intentius/chant/src/yaml.ts
|
|
1313
|
+
var KEY_LINE = /^(\s*)([^\s:][^:]*?):(?=\s|$)\s*(.*)$/;
|
|
1314
|
+
var ITEM_KEY = /^([^\s:][^:]*?):(?=\s|$)\s*(.*)$/;
|
|
1313
1315
|
function parseYAML(content) {
|
|
1314
1316
|
try {
|
|
1315
1317
|
return JSON.parse(content);
|
|
@@ -1385,7 +1387,7 @@ function parseYAMLLines(lines, startIndex, baseIndent) {
|
|
|
1385
1387
|
const indent = line.search(/\S/);
|
|
1386
1388
|
if (indent < baseIndent) break;
|
|
1387
1389
|
if (indent > baseIndent && startIndex > 0) break;
|
|
1388
|
-
const keyMatch = line.match(
|
|
1390
|
+
const keyMatch = line.match(KEY_LINE);
|
|
1389
1391
|
if (keyMatch) {
|
|
1390
1392
|
const key = keyMatch[2].trim();
|
|
1391
1393
|
const inlineValue = keyMatch[3].trim();
|
|
@@ -1518,7 +1520,7 @@ function parseYAMLArray(lines, startIndex, baseIndent) {
|
|
|
1518
1520
|
if (itemMatch && indent === baseIndent) {
|
|
1519
1521
|
const itemValue = itemMatch[2].trim();
|
|
1520
1522
|
const isQuotedScalar = itemValue.startsWith('"') && itemValue.endsWith('"') || itemValue.startsWith("'") && itemValue.endsWith("'");
|
|
1521
|
-
const kvMatch = !isQuotedScalar && itemValue.match(
|
|
1523
|
+
const kvMatch = !isQuotedScalar && itemValue.match(ITEM_KEY);
|
|
1522
1524
|
if (kvMatch) {
|
|
1523
1525
|
const obj = {};
|
|
1524
1526
|
obj[kvMatch[1].trim()] = parseArrayItemValue(kvMatch[2].trim(), lines, i, indent + 2);
|
|
@@ -1534,7 +1536,7 @@ function parseYAMLArray(lines, startIndex, baseIndent) {
|
|
|
1534
1536
|
const ni = nextLine.search(/\S/);
|
|
1535
1537
|
if (ni < nextIndent) break;
|
|
1536
1538
|
if (ni > nextIndent) break;
|
|
1537
|
-
const nextKV = nextLine.match(
|
|
1539
|
+
const nextKV = nextLine.match(KEY_LINE);
|
|
1538
1540
|
if (nextKV) {
|
|
1539
1541
|
const nextVal = nextKV[3].trim();
|
|
1540
1542
|
obj[nextKV[2].trim()] = parseArrayItemValue(nextVal, lines, j, ni);
|
|
@@ -1552,8 +1554,15 @@ function parseYAMLArray(lines, startIndex, baseIndent) {
|
|
|
1552
1554
|
result.push(obj);
|
|
1553
1555
|
i = j;
|
|
1554
1556
|
} else {
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
+
const header = blockScalarHeader(itemValue);
|
|
1558
|
+
if (header) {
|
|
1559
|
+
const block = parseBlockScalar(lines, i + 1, indent, header);
|
|
1560
|
+
result.push(block.value);
|
|
1561
|
+
i = block.endIndex;
|
|
1562
|
+
} else {
|
|
1563
|
+
result.push(parseScalar(itemValue));
|
|
1564
|
+
i++;
|
|
1565
|
+
}
|
|
1557
1566
|
}
|
|
1558
1567
|
} else {
|
|
1559
1568
|
break;
|
|
@@ -1565,13 +1574,68 @@ function parseScalar(value) {
|
|
|
1565
1574
|
if (value === "" || value === "~" || value === "null") return null;
|
|
1566
1575
|
if (value === "true" || value === "yes") return true;
|
|
1567
1576
|
if (value === "false" || value === "no") return false;
|
|
1568
|
-
if (value.
|
|
1569
|
-
|
|
1577
|
+
if (value.length >= 2) {
|
|
1578
|
+
if (value.startsWith('"') && value.endsWith('"')) {
|
|
1579
|
+
const unescaped = unescapeDoubleQuoted(value.slice(1, -1));
|
|
1580
|
+
return unescaped ?? value.slice(1, -1);
|
|
1581
|
+
}
|
|
1582
|
+
if (value.startsWith("'") && value.endsWith("'")) {
|
|
1583
|
+
return value.slice(1, -1).replace(/''/g, "'");
|
|
1584
|
+
}
|
|
1570
1585
|
}
|
|
1571
1586
|
const num = Number(value);
|
|
1572
1587
|
if (!isNaN(num) && value !== "") return num;
|
|
1573
1588
|
return value;
|
|
1574
1589
|
}
|
|
1590
|
+
var DQ_ESCAPES = {
|
|
1591
|
+
"0": "\0",
|
|
1592
|
+
a: "\x07",
|
|
1593
|
+
b: "\b",
|
|
1594
|
+
t: " ",
|
|
1595
|
+
" ": " ",
|
|
1596
|
+
n: "\n",
|
|
1597
|
+
v: "\v",
|
|
1598
|
+
f: "\f",
|
|
1599
|
+
r: "\r",
|
|
1600
|
+
e: "\x1B",
|
|
1601
|
+
" ": " ",
|
|
1602
|
+
'"': '"',
|
|
1603
|
+
"/": "/",
|
|
1604
|
+
"\\": "\\",
|
|
1605
|
+
N: "\x85",
|
|
1606
|
+
_: "\xA0",
|
|
1607
|
+
L: "\u2028",
|
|
1608
|
+
P: "\u2029"
|
|
1609
|
+
};
|
|
1610
|
+
function unescapeDoubleQuoted(body) {
|
|
1611
|
+
let out = "";
|
|
1612
|
+
for (let i = 0; i < body.length; i++) {
|
|
1613
|
+
const ch = body[i];
|
|
1614
|
+
if (ch === '"') return null;
|
|
1615
|
+
if (ch !== "\\") {
|
|
1616
|
+
out += ch;
|
|
1617
|
+
continue;
|
|
1618
|
+
}
|
|
1619
|
+
const esc = body[++i];
|
|
1620
|
+
if (esc === void 0) return null;
|
|
1621
|
+
const width = esc === "x" ? 2 : esc === "u" ? 4 : esc === "U" ? 8 : 0;
|
|
1622
|
+
if (width > 0) {
|
|
1623
|
+
const hex = body.slice(i + 1, i + 1 + width);
|
|
1624
|
+
if (hex.length < width || !/^[0-9a-fA-F]+$/.test(hex)) return null;
|
|
1625
|
+
out += String.fromCodePoint(parseInt(hex, 16));
|
|
1626
|
+
i += width;
|
|
1627
|
+
continue;
|
|
1628
|
+
}
|
|
1629
|
+
if (esc === "\n") {
|
|
1630
|
+
while (body[i + 1] === " " || body[i + 1] === " ") i++;
|
|
1631
|
+
continue;
|
|
1632
|
+
}
|
|
1633
|
+
const mapped = DQ_ESCAPES[esc];
|
|
1634
|
+
if (mapped === void 0) return null;
|
|
1635
|
+
out += mapped;
|
|
1636
|
+
}
|
|
1637
|
+
return out;
|
|
1638
|
+
}
|
|
1575
1639
|
|
|
1576
1640
|
// src/chant.ts
|
|
1577
1641
|
function envOverridesFor(opts) {
|
|
@@ -1829,6 +1893,25 @@ function lifecycleDiffArgs(env) {
|
|
|
1829
1893
|
function lifecycleDiffLive(projectDir, env, opts = {}) {
|
|
1830
1894
|
return runChantJson(lifecycleDiffArgs(env), projectDir, envOverridesFor(opts));
|
|
1831
1895
|
}
|
|
1896
|
+
function helmRendersArgs() {
|
|
1897
|
+
return ["helm", "renders", "--json"];
|
|
1898
|
+
}
|
|
1899
|
+
function helmRenders(projectDir, opts = {}) {
|
|
1900
|
+
return runChantJson(helmRendersArgs(), projectDir, envOverridesFor(opts));
|
|
1901
|
+
}
|
|
1902
|
+
function helmRenderDiffArgs(contentDigest, env) {
|
|
1903
|
+
return ["helm", "diff", contentDigest, env, "--live", "--json"];
|
|
1904
|
+
}
|
|
1905
|
+
async function helmRenderDiffLive(projectDir, contentDigest, env, opts = {}) {
|
|
1906
|
+
const args = helmRenderDiffArgs(contentDigest, env);
|
|
1907
|
+
const { code, stdout } = await runChantRaw(args, projectDir, envOverridesFor(opts));
|
|
1908
|
+
if (code !== 0) return void 0;
|
|
1909
|
+
try {
|
|
1910
|
+
return JSON.parse(stdout);
|
|
1911
|
+
} catch {
|
|
1912
|
+
return void 0;
|
|
1913
|
+
}
|
|
1914
|
+
}
|
|
1832
1915
|
function applyArgs(target, env) {
|
|
1833
1916
|
return ["run", target, "--components", "--env", env, "--progress-json"];
|
|
1834
1917
|
}
|
|
@@ -2035,19 +2118,19 @@ function finishPipelineProgress(state, exitCode) {
|
|
|
2035
2118
|
}
|
|
2036
2119
|
|
|
2037
2120
|
// src/gh-run.ts
|
|
2038
|
-
var defaultGhExec = (args) => new Promise((
|
|
2121
|
+
var defaultGhExec = (args) => new Promise((resolve9) => {
|
|
2039
2122
|
let out = "";
|
|
2040
2123
|
let proc;
|
|
2041
2124
|
try {
|
|
2042
2125
|
proc = spawn2("gh", args, { stdio: ["ignore", "pipe", "pipe"] });
|
|
2043
2126
|
} catch {
|
|
2044
|
-
|
|
2127
|
+
resolve9({ code: 127, out: "" });
|
|
2045
2128
|
return;
|
|
2046
2129
|
}
|
|
2047
2130
|
proc.stdout.on("data", (d) => out += d);
|
|
2048
2131
|
proc.stderr.on("data", (d) => out += d);
|
|
2049
|
-
proc.on("error", () =>
|
|
2050
|
-
proc.on("close", (code) =>
|
|
2132
|
+
proc.on("error", () => resolve9({ code: 127, out }));
|
|
2133
|
+
proc.on("close", (code) => resolve9({ code: code ?? 1, out }));
|
|
2051
2134
|
});
|
|
2052
2135
|
async function ghReady(exec = defaultGhExec) {
|
|
2053
2136
|
const { code, out } = await exec(["auth", "status"]);
|
|
@@ -2591,127 +2674,548 @@ function applyHelmArtifacts(ir, observed) {
|
|
|
2591
2674
|
return { charts, installed };
|
|
2592
2675
|
}
|
|
2593
2676
|
|
|
2594
|
-
// src/
|
|
2595
|
-
|
|
2596
|
-
|
|
2597
|
-
|
|
2598
|
-
|
|
2599
|
-
|
|
2600
|
-
|
|
2601
|
-
|
|
2602
|
-
|
|
2603
|
-
|
|
2604
|
-
|
|
2677
|
+
// src/estate.ts
|
|
2678
|
+
import { statSync as statSync3 } from "node:fs";
|
|
2679
|
+
import { availableParallelism } from "node:os";
|
|
2680
|
+
import { join as join7, resolve as resolve3, sep } from "node:path";
|
|
2681
|
+
import { composeStacks, shortStackNames } from "@intentius/pinhole";
|
|
2682
|
+
|
|
2683
|
+
// src/member-ir.ts
|
|
2684
|
+
import { createHash } from "node:crypto";
|
|
2685
|
+
import { readdirSync as readdirSync3, statSync as statSync2 } from "node:fs";
|
|
2686
|
+
import { join as join6, relative, resolve as resolve2 } from "node:path";
|
|
2687
|
+
var SKIP = /* @__PURE__ */ new Set(["node_modules", "dist", ".git"]);
|
|
2688
|
+
function memberIrCacheSize(env = process.env) {
|
|
2689
|
+
const override = Number.parseInt(env.BEHOLD_ESTATE_IR_CACHE ?? "", 10);
|
|
2690
|
+
return Number.isInteger(override) && override >= 0 ? override : 64;
|
|
2691
|
+
}
|
|
2692
|
+
function memberSourceStamp(dir) {
|
|
2693
|
+
const root = resolve2(dir);
|
|
2694
|
+
const h = createHash("sha1");
|
|
2695
|
+
let any = false;
|
|
2696
|
+
const walk = (at) => {
|
|
2697
|
+
const entries = readdirSync3(at, { withFileTypes: true }).sort((a, b) => a.name < b.name ? -1 : 1);
|
|
2698
|
+
for (const e of entries) {
|
|
2699
|
+
if (SKIP.has(e.name)) continue;
|
|
2700
|
+
const path = join6(at, e.name);
|
|
2701
|
+
if (e.isDirectory()) {
|
|
2702
|
+
walk(path);
|
|
2703
|
+
continue;
|
|
2704
|
+
}
|
|
2705
|
+
if (!e.isFile()) continue;
|
|
2706
|
+
const st = statSync2(path);
|
|
2707
|
+
h.update(`${relative(root, path)}\0${st.mtimeMs}\0${st.size}
|
|
2708
|
+
`);
|
|
2709
|
+
any = true;
|
|
2710
|
+
}
|
|
2711
|
+
};
|
|
2712
|
+
try {
|
|
2713
|
+
walk(root);
|
|
2714
|
+
} catch {
|
|
2715
|
+
return void 0;
|
|
2716
|
+
}
|
|
2717
|
+
return any ? h.digest("hex") : void 0;
|
|
2605
2718
|
}
|
|
2606
|
-
function
|
|
2607
|
-
const
|
|
2608
|
-
|
|
2719
|
+
function cacheKey(dir, opts) {
|
|
2720
|
+
const chant = resolveChant(dir);
|
|
2721
|
+
const canonical = JSON.stringify(opts, Object.keys(opts).sort());
|
|
2722
|
+
return `${resolve2(dir)}\0${chant.bin}\0${chant.version ?? ""}\0${canonical}`;
|
|
2609
2723
|
}
|
|
2610
|
-
|
|
2611
|
-
|
|
2724
|
+
var cache = /* @__PURE__ */ new Map();
|
|
2725
|
+
var hits = 0;
|
|
2726
|
+
var misses = 0;
|
|
2727
|
+
function invalidateMember(dir) {
|
|
2728
|
+
if (dir === void 0) {
|
|
2729
|
+
cache.clear();
|
|
2730
|
+
return;
|
|
2731
|
+
}
|
|
2732
|
+
const root = resolve2(dir);
|
|
2733
|
+
for (const [key, entry] of cache) if (entry.dir === root) cache.delete(key);
|
|
2734
|
+
}
|
|
2735
|
+
async function memberIr(dir, opts = {}) {
|
|
2736
|
+
const size = memberIrCacheSize();
|
|
2737
|
+
if (opts.live || opts.overlay || size === 0) return graphIr(dir, opts);
|
|
2738
|
+
const stamp = memberSourceStamp(dir);
|
|
2739
|
+
const key = cacheKey(dir, opts);
|
|
2740
|
+
const hit = stamp !== void 0 ? cache.get(key) : void 0;
|
|
2741
|
+
if (hit && hit.stamp === stamp) {
|
|
2742
|
+
hits++;
|
|
2743
|
+
cache.delete(key);
|
|
2744
|
+
cache.set(key, hit);
|
|
2745
|
+
return structuredClone(hit.ir);
|
|
2746
|
+
}
|
|
2747
|
+
misses++;
|
|
2748
|
+
const ir = await graphIr(dir, opts);
|
|
2749
|
+
if (stamp === void 0 || memberSourceStamp(dir) !== stamp) return ir;
|
|
2750
|
+
cache.delete(key);
|
|
2751
|
+
cache.set(key, { dir: resolve2(dir), stamp, ir });
|
|
2752
|
+
for (const oldest of cache.keys()) {
|
|
2753
|
+
if (cache.size <= size) break;
|
|
2754
|
+
cache.delete(oldest);
|
|
2755
|
+
}
|
|
2756
|
+
return structuredClone(ir);
|
|
2612
2757
|
}
|
|
2613
|
-
|
|
2614
|
-
|
|
2758
|
+
|
|
2759
|
+
// src/zoom-notes.ts
|
|
2760
|
+
function zoomNote(zoom, ir, compositeEdgesAttached) {
|
|
2761
|
+
const empty = ir.nodes.length === 0;
|
|
2762
|
+
if (zoom === "components") {
|
|
2763
|
+
return empty ? "no components discovered \u2014 nothing declares one in this project" : void 0;
|
|
2764
|
+
}
|
|
2765
|
+
if (zoom === "logical") {
|
|
2766
|
+
return empty ? "logical is an AWS projection \u2014 no AWS resources in this estate" : void 0;
|
|
2767
|
+
}
|
|
2768
|
+
if (zoom === "composites") {
|
|
2769
|
+
return compositeEdgesAttached === 0 ? "no component ownership to join \u2014 showing the resource graph unchanged" : void 0;
|
|
2770
|
+
}
|
|
2771
|
+
if (zoom === "runtime") {
|
|
2772
|
+
const children = ir.nodes.filter((n) => n.runtimeOwner).length;
|
|
2773
|
+
return children === 0 ? "nothing below the declaration boundary \u2014 no owner-referenced children on this substrate" : void 0;
|
|
2774
|
+
}
|
|
2775
|
+
return void 0;
|
|
2615
2776
|
}
|
|
2616
|
-
function
|
|
2617
|
-
|
|
2618
|
-
|
|
2619
|
-
|
|
2620
|
-
|
|
2621
|
-
|
|
2777
|
+
function logicalKept(before, after) {
|
|
2778
|
+
if (after > 0 && after * 3 >= before) return void 0;
|
|
2779
|
+
return after === 0 ? `logical projected nothing from ${before} resources \u2014 it is a cloud-topology lens, and this estate declares none of the kinds it nests (behold#74)` : `logical kept ${after} of ${before} resources \u2014 it is a cloud-topology lens, and the rest are kinds it does not nest (behold#74)`;
|
|
2780
|
+
}
|
|
2781
|
+
function edgelessNote(zoom, ir, detail) {
|
|
2782
|
+
if (zoom === "components" || zoom === "logical") return void 0;
|
|
2783
|
+
if (ir.nodes.length === 0 || ir.edges.length > 0) return void 0;
|
|
2784
|
+
if (detail !== void 0 && detail < 3) {
|
|
2785
|
+
return "no edges at this detail \u2014 sourceRef/dependsOn and other attrs-derived references only appear at detail 3 (\u2318K \u2192 attributes, or add &detail=3)";
|
|
2622
2786
|
}
|
|
2623
|
-
return
|
|
2787
|
+
return "no edges \u2014 nothing in this estate references anything else";
|
|
2624
2788
|
}
|
|
2625
|
-
|
|
2626
|
-
|
|
2627
|
-
|
|
2628
|
-
|
|
2629
|
-
|
|
2630
|
-
|
|
2789
|
+
var CLUSTER_SCOPED = /* @__PURE__ */ new Set([
|
|
2790
|
+
"K8s::Core::Namespace",
|
|
2791
|
+
"K8s::Rbac::ClusterRole",
|
|
2792
|
+
"K8s::Rbac::ClusterRoleBinding",
|
|
2793
|
+
"K8s::Storage::StorageClass",
|
|
2794
|
+
"K8s::Apiextensions::CustomResourceDefinition"
|
|
2795
|
+
]);
|
|
2796
|
+
function namespaceMismatchNote(nodes) {
|
|
2797
|
+
const k8s = nodes.filter((n) => n?.lexicon === "k8s");
|
|
2798
|
+
if (k8s.length === 0) return void 0;
|
|
2799
|
+
const pending = k8s.filter((n) => n.attrs?._status === "accent" && !CLUSTER_SCOPED.has(n.kind ?? ""));
|
|
2800
|
+
if (pending.length < 2) return void 0;
|
|
2801
|
+
if (pending.some((n) => typeof n.attrs?.metadata?.namespace === "string" && n.attrs.metadata.namespace)) return void 0;
|
|
2802
|
+
if (!k8s.some((n) => n.attrs?._status === "good")) return void 0;
|
|
2803
|
+
return `${pending.length} pending k8s objects declare no metadata.namespace \u2014 if a controller stamps it at apply time (e.g. Flux's targetNamespace), the live read looked in "default", not where they run`;
|
|
2631
2804
|
}
|
|
2632
|
-
function
|
|
2633
|
-
|
|
2634
|
-
const
|
|
2635
|
-
|
|
2636
|
-
|
|
2637
|
-
|
|
2638
|
-
const
|
|
2639
|
-
|
|
2640
|
-
|
|
2641
|
-
|
|
2642
|
-
|
|
2805
|
+
function namespaceJoinNote(joined) {
|
|
2806
|
+
if (joined.length === 0) return void 0;
|
|
2807
|
+
const each = joined.map((j) => `${j.name} in "${j.namespace}"`).join(", ");
|
|
2808
|
+
return `read ${each} \u2014 the namespace the estate's own Kustomization targetNamespace binds, not the app project's own declaration`;
|
|
2809
|
+
}
|
|
2810
|
+
function notesFor(zoom, ir, compositeEdgesAttached, logicalBefore, detail) {
|
|
2811
|
+
const primary = zoom === "logical" && logicalBefore !== void 0 ? logicalKept(logicalBefore, ir.nodes.length) : zoomNote(zoom, ir, compositeEdgesAttached);
|
|
2812
|
+
const notes = [primary, edgelessNote(zoom, ir, detail)].filter((n) => n !== void 0);
|
|
2813
|
+
return notes.length ? notes.join(" \xB7 ") : void 0;
|
|
2814
|
+
}
|
|
2815
|
+
function tierMismatchNote(ir, tiers, currentTier) {
|
|
2816
|
+
const values = tiers?.values ?? [];
|
|
2817
|
+
if (values.length < 2) return void 0;
|
|
2818
|
+
const byLexicon = /* @__PURE__ */ new Map();
|
|
2819
|
+
for (const n of ir.nodes) {
|
|
2820
|
+
const status = n.attrs?._status;
|
|
2821
|
+
if (status !== "good" && status !== "accent") continue;
|
|
2822
|
+
const lexicon = n.lexicon ?? "?";
|
|
2823
|
+
const c = byLexicon.get(lexicon) ?? { good: 0, accent: 0 };
|
|
2824
|
+
c[status === "good" ? "good" : "accent"]++;
|
|
2825
|
+
byLexicon.set(lexicon, c);
|
|
2643
2826
|
}
|
|
2644
|
-
const
|
|
2645
|
-
|
|
2646
|
-
|
|
2647
|
-
const
|
|
2648
|
-
if (
|
|
2649
|
-
|
|
2650
|
-
|
|
2651
|
-
|
|
2652
|
-
|
|
2653
|
-
|
|
2654
|
-
|
|
2655
|
-
|
|
2827
|
+
for (const [lexicon, { good, accent }] of [...byLexicon].sort()) {
|
|
2828
|
+
if (accent < 2 || accent * 2 < good + accent) continue;
|
|
2829
|
+
const here = currentTier ? `tier "${currentTier}"` : "the default tier";
|
|
2830
|
+
const others = values.filter((v) => v !== currentTier).join(", ");
|
|
2831
|
+
return `${accent} of ${good + accent} ${lexicon} resources read "declared, not deployed" at ${here} \u2014 if the estate runs another declared tier (${others}), pick it (\u2318K \u2192 tier)`;
|
|
2832
|
+
}
|
|
2833
|
+
return void 0;
|
|
2834
|
+
}
|
|
2835
|
+
|
|
2836
|
+
// src/estate.ts
|
|
2837
|
+
function estateReadPool(members, env = process.env) {
|
|
2838
|
+
const override = Number.parseInt(env.BEHOLD_ESTATE_CONCURRENCY ?? "", 10);
|
|
2839
|
+
const cap = Number.isInteger(override) && override >= 1 ? override : Math.min(4, availableParallelism());
|
|
2840
|
+
return Math.max(1, Math.min(cap, members));
|
|
2841
|
+
}
|
|
2842
|
+
async function mapPool(items, width, fn) {
|
|
2843
|
+
const results = new Array(items.length);
|
|
2844
|
+
let next = 0;
|
|
2845
|
+
const worker = async () => {
|
|
2846
|
+
for (let i = next++; i < items.length; i = next++) {
|
|
2847
|
+
results[i] = await fn(items[i], i);
|
|
2656
2848
|
}
|
|
2657
|
-
|
|
2658
|
-
|
|
2659
|
-
|
|
2849
|
+
};
|
|
2850
|
+
await Promise.all(Array.from({ length: Math.max(1, Math.min(width, items.length)) }, () => worker()));
|
|
2851
|
+
return results;
|
|
2852
|
+
}
|
|
2853
|
+
async function composeEstate(projectDirs, opts = {}) {
|
|
2854
|
+
const names = shortStackNames(projectDirs);
|
|
2855
|
+
const stacks = await mapPool(projectDirs, estateReadPool(projectDirs.length), async (dir, i) => ({
|
|
2856
|
+
name: names[i],
|
|
2857
|
+
ir: await memberIr(dir, opts)
|
|
2858
|
+
}));
|
|
2859
|
+
return composeStacks(stacks);
|
|
2860
|
+
}
|
|
2861
|
+
var firstLine2 = (e) => {
|
|
2862
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
2863
|
+
const m = msg.match(/exited \d+:\s*([\s\S]*)/);
|
|
2864
|
+
return (m ? m[1] : msg).split("\n")[0].trim().slice(0, 160);
|
|
2865
|
+
};
|
|
2866
|
+
function namespaceRuntimeOwners(name, ir) {
|
|
2867
|
+
const own = new Set(ir.nodes.map((n) => n.id));
|
|
2868
|
+
for (const n of ir.nodes) {
|
|
2869
|
+
if (n.runtimeOwner && own.has(n.runtimeOwner)) n.runtimeOwner = `${name}/${n.runtimeOwner}`;
|
|
2660
2870
|
}
|
|
2661
2871
|
return ir;
|
|
2662
2872
|
}
|
|
2663
|
-
|
|
2664
|
-
|
|
2665
|
-
var
|
|
2666
|
-
|
|
2667
|
-
|
|
2668
|
-
|
|
2669
|
-
|
|
2670
|
-
|
|
2671
|
-
|
|
2672
|
-
|
|
2673
|
-
|
|
2674
|
-
"Microsoft.Network/loadBalancers",
|
|
2675
|
-
"Microsoft.Network/applicationGateways",
|
|
2676
|
-
"Microsoft.Network/natGateways",
|
|
2677
|
-
"Microsoft.Network/dnsZones",
|
|
2678
|
-
// data stores
|
|
2679
|
-
"Microsoft.Storage/storageAccounts",
|
|
2680
|
-
"Microsoft.Sql/servers",
|
|
2681
|
-
"Microsoft.DocumentDB/databaseAccounts",
|
|
2682
|
-
"Microsoft.Cache/redis",
|
|
2683
|
-
"Microsoft.DBforPostgreSQL/flexibleServers",
|
|
2684
|
-
// registry / identity edge
|
|
2685
|
-
"Microsoft.ContainerRegistry/registries"
|
|
2686
|
-
]);
|
|
2687
|
-
var AZURE_FABRIC_KINDS = /* @__PURE__ */ new Set([
|
|
2688
|
-
"Microsoft.Network/virtualNetworks",
|
|
2689
|
-
"Microsoft.Network/virtualNetworks_subnets",
|
|
2690
|
-
"Microsoft.Network/networkSecurityGroups",
|
|
2691
|
-
"Microsoft.Network/routeTables",
|
|
2692
|
-
"Microsoft.Network/networkInterfaces",
|
|
2693
|
-
// identity/authorization hubs — a role assignment or managed identity is
|
|
2694
|
-
// referenced by half the estate, so contracting through one invents edges
|
|
2695
|
-
// between unrelated resources.
|
|
2696
|
-
"Microsoft.Authorization/roleAssignments",
|
|
2697
|
-
"Microsoft.ManagedIdentity/userAssignedIdentities",
|
|
2698
|
-
"Microsoft.KeyVault/vaults"
|
|
2699
|
-
]);
|
|
2700
|
-
var VNET_KIND = "Microsoft.Network/virtualNetworks";
|
|
2701
|
-
var SUBNET_KIND = "Microsoft.Network/virtualNetworks_subnets";
|
|
2702
|
-
var NSG_KIND = "Microsoft.Network/networkSecurityGroups";
|
|
2703
|
-
var RESOURCE_ID_RE = /resourceId\(\s*([^)]*)\)/g;
|
|
2704
|
-
var QUOTED_RE = /'([^']*)'/g;
|
|
2705
|
-
function resourceIdNames(value) {
|
|
2706
|
-
if (typeof value !== "string" || !value.includes("resourceId(")) return [];
|
|
2873
|
+
var NAMESPACE_JOIN_FLOOR = "0.44.5";
|
|
2874
|
+
var rec2 = (v) => v && typeof v === "object" && !Array.isArray(v) ? v : void 0;
|
|
2875
|
+
var str2 = (v) => typeof v === "string" && v.length > 0 ? v : void 0;
|
|
2876
|
+
var realIsDir = (p) => {
|
|
2877
|
+
try {
|
|
2878
|
+
return statSync3(p).isDirectory();
|
|
2879
|
+
} catch {
|
|
2880
|
+
return false;
|
|
2881
|
+
}
|
|
2882
|
+
};
|
|
2883
|
+
function declaredNamespaceBindings(ir) {
|
|
2707
2884
|
const out = [];
|
|
2708
|
-
for (const
|
|
2709
|
-
|
|
2710
|
-
|
|
2885
|
+
for (const n of ir.nodes) {
|
|
2886
|
+
if (n.kind !== "K8s::Flux::Kustomization") continue;
|
|
2887
|
+
const spec = rec2(n.attrs?.spec);
|
|
2888
|
+
const namespace = str2(spec?.targetNamespace);
|
|
2889
|
+
const path = str2(spec?.path);
|
|
2890
|
+
if (namespace && path) out.push({ path, namespace });
|
|
2711
2891
|
}
|
|
2712
2892
|
return out;
|
|
2713
2893
|
}
|
|
2714
|
-
function
|
|
2894
|
+
function awaitsNamespaceBinding(ir) {
|
|
2895
|
+
const namespaced = ir.nodes.filter((n) => n.lexicon === "k8s" && !CLUSTER_SCOPED.has(n.kind ?? ""));
|
|
2896
|
+
if (namespaced.length === 0) return false;
|
|
2897
|
+
return namespaced.every((n) => !str2(rec2(n.attrs?.metadata)?.namespace));
|
|
2898
|
+
}
|
|
2899
|
+
function declaredSegments(path) {
|
|
2900
|
+
if (path.startsWith("/")) return void 0;
|
|
2901
|
+
const segs = path.split("/").filter((s) => s !== "" && s !== ".");
|
|
2902
|
+
if (segs.length === 0 || segs.includes("..")) return void 0;
|
|
2903
|
+
return segs;
|
|
2904
|
+
}
|
|
2905
|
+
function pathAlignment(declared, dir, isDir = realIsDir) {
|
|
2906
|
+
const want = declaredSegments(declared);
|
|
2907
|
+
if (!want) return 0;
|
|
2908
|
+
const root = resolve3(dir);
|
|
2909
|
+
const have = root.split(sep).filter(Boolean);
|
|
2910
|
+
for (let k = Math.min(want.length, have.length); k >= 1; k--) {
|
|
2911
|
+
if (!have.slice(-k).every((s, i) => s === want[i])) continue;
|
|
2912
|
+
if (!isDir(join7(root, ...want.slice(k)))) continue;
|
|
2913
|
+
return k;
|
|
2914
|
+
}
|
|
2915
|
+
return 0;
|
|
2916
|
+
}
|
|
2917
|
+
function joinNamespaceBindings(members, isDir = realIsDir) {
|
|
2918
|
+
const bindings = members.flatMap(
|
|
2919
|
+
(m) => m.ir ? declaredNamespaceBindings(m.ir).map((b) => ({ ...b, declaredBy: m.dir })) : []
|
|
2920
|
+
);
|
|
2921
|
+
if (bindings.length === 0) return [];
|
|
2922
|
+
const awaiting = members.filter((m) => m.ir && awaitsNamespaceBinding(m.ir));
|
|
2923
|
+
if (awaiting.length === 0) return [];
|
|
2924
|
+
const best = /* @__PURE__ */ new Map();
|
|
2925
|
+
for (const b of bindings) {
|
|
2926
|
+
let winner;
|
|
2927
|
+
let tied = false;
|
|
2928
|
+
for (const m of awaiting) {
|
|
2929
|
+
if (m.dir === b.declaredBy) continue;
|
|
2930
|
+
const score = pathAlignment(b.path, m.dir, isDir);
|
|
2931
|
+
if (score === 0) continue;
|
|
2932
|
+
if (!winner || score > winner.score) {
|
|
2933
|
+
winner = { dir: m.dir, score };
|
|
2934
|
+
tied = false;
|
|
2935
|
+
} else if (score === winner.score) {
|
|
2936
|
+
tied = true;
|
|
2937
|
+
}
|
|
2938
|
+
}
|
|
2939
|
+
if (!winner || tied) continue;
|
|
2940
|
+
const prev = best.get(winner.dir);
|
|
2941
|
+
if (prev === "ambiguous") continue;
|
|
2942
|
+
const join21 = { dir: winner.dir, namespace: b.namespace, path: b.path, declaredBy: b.declaredBy };
|
|
2943
|
+
if (!prev) best.set(winner.dir, { score: winner.score, join: join21 });
|
|
2944
|
+
else if (prev.join.namespace !== b.namespace) best.set(winner.dir, "ambiguous");
|
|
2945
|
+
else if (winner.score > prev.score) best.set(winner.dir, { score: winner.score, join: join21 });
|
|
2946
|
+
}
|
|
2947
|
+
return [...best.values()].filter((v) => v !== "ambiguous").map((v) => v.join);
|
|
2948
|
+
}
|
|
2949
|
+
async function estateNamespaceScopes(projectDirs, opts, isDir = realIsDir) {
|
|
2950
|
+
if (projectDirs.length < 2) return /* @__PURE__ */ new Map();
|
|
2951
|
+
const { live: _live, overlay: _overlay, namespace: _namespace, ...src } = opts;
|
|
2952
|
+
const irs = await mapPool(
|
|
2953
|
+
projectDirs,
|
|
2954
|
+
estateReadPool(projectDirs.length),
|
|
2955
|
+
(dir) => memberIr(dir, { ...src, detail: 3 }).catch(() => void 0)
|
|
2956
|
+
);
|
|
2957
|
+
const scopes = /* @__PURE__ */ new Map();
|
|
2958
|
+
for (const j of joinNamespaceBindings(projectDirs.map((dir, i) => ({ dir, ir: irs[i] })), isDir)) {
|
|
2959
|
+
if (!meetsFloor(resolveChant(j.dir).version, NAMESPACE_JOIN_FLOOR)) continue;
|
|
2960
|
+
scopes.set(j.dir, j.namespace);
|
|
2961
|
+
}
|
|
2962
|
+
return scopes;
|
|
2963
|
+
}
|
|
2964
|
+
function withoutJoinedMembers(nodes, joined) {
|
|
2965
|
+
if (joined.length === 0) return [...nodes];
|
|
2966
|
+
return nodes.filter((n) => !joined.some((j) => n.id.startsWith(`${j.name}/`)));
|
|
2967
|
+
}
|
|
2968
|
+
async function composeEstateOverlay(projectDirs, opts, classify) {
|
|
2969
|
+
const names = shortStackNames(projectDirs);
|
|
2970
|
+
const unobserved = [];
|
|
2971
|
+
const dropped = [];
|
|
2972
|
+
const joined = [];
|
|
2973
|
+
const stacks = new Array(projectDirs.length);
|
|
2974
|
+
const scopes = await estateNamespaceScopes(projectDirs, opts);
|
|
2975
|
+
await mapPool(projectDirs, estateReadPool(projectDirs.length), async (dir, i) => {
|
|
2976
|
+
const name = names[i];
|
|
2977
|
+
const namespace = scopes.get(dir);
|
|
2978
|
+
try {
|
|
2979
|
+
const live = { ...opts, live: true, overlay: true, ...namespace ? { namespace } : {} };
|
|
2980
|
+
stacks[i] = { name, ir: namespaceRuntimeOwners(name, classify(await graphIr(dir, live))) };
|
|
2981
|
+
if (namespace) joined.push({ name, namespace });
|
|
2982
|
+
} catch (err) {
|
|
2983
|
+
const reason = firstLine2(err);
|
|
2984
|
+
try {
|
|
2985
|
+
const { env: _env, live: _live, overlay: _overlay, ...srcOpts } = opts;
|
|
2986
|
+
const src = await memberIr(dir, srcOpts);
|
|
2987
|
+
for (const n of src.nodes) n.attrs = { ...n.attrs, _status: "neutral", _unobserved: reason };
|
|
2988
|
+
stacks[i] = { name, ir: src };
|
|
2989
|
+
unobserved.push({ name, reason });
|
|
2990
|
+
} catch (err2) {
|
|
2991
|
+
dropped.push({ name, reason: firstLine2(err2) });
|
|
2992
|
+
}
|
|
2993
|
+
}
|
|
2994
|
+
});
|
|
2995
|
+
const present = stacks.filter((s) => !!s);
|
|
2996
|
+
return {
|
|
2997
|
+
ir: composeStacks(present),
|
|
2998
|
+
observed: present.length - unobserved.length,
|
|
2999
|
+
total: projectDirs.length,
|
|
3000
|
+
unobserved,
|
|
3001
|
+
dropped,
|
|
3002
|
+
// Concurrent reads finish in whatever order the clusters answer; the note
|
|
3003
|
+
// this feeds should read the same twice.
|
|
3004
|
+
joined: joined.sort((a, b) => a.name.localeCompare(b.name))
|
|
3005
|
+
};
|
|
3006
|
+
}
|
|
3007
|
+
|
|
3008
|
+
// src/helm-drift.ts
|
|
3009
|
+
function countChanges(docs) {
|
|
3010
|
+
return docs.reduce((n, d) => n + d.changes.length, 0);
|
|
3011
|
+
}
|
|
3012
|
+
function provenanceOf(live) {
|
|
3013
|
+
const m = live.manifest;
|
|
3014
|
+
return {
|
|
3015
|
+
contentDigest: m.contentDigest,
|
|
3016
|
+
inputDigest: m.inputDigest,
|
|
3017
|
+
valuesDigest: m.valuesDigest,
|
|
3018
|
+
chart: m.chart,
|
|
3019
|
+
chartVersion: m.chartVersion,
|
|
3020
|
+
repo: m.repo,
|
|
3021
|
+
releaseName: m.releaseName,
|
|
3022
|
+
namespace: m.namespace,
|
|
3023
|
+
profile: m.capabilityProfile?.cluster ?? "",
|
|
3024
|
+
kubeVersion: m.capabilityProfile?.kubeVersion ?? "",
|
|
3025
|
+
renderedAt: m.renderedAt,
|
|
3026
|
+
helmVersion: m.helmVersion,
|
|
3027
|
+
chantVersion: m.chantVersion,
|
|
3028
|
+
sourceRef: m.sourceRef
|
|
3029
|
+
};
|
|
3030
|
+
}
|
|
3031
|
+
function helmRenderReport(record, live) {
|
|
3032
|
+
const base = { render: record.name, chart: record.chart };
|
|
3033
|
+
if (!live) {
|
|
3034
|
+
return { ...base, verdict: "unobserved", reason: record.contentDigest ? "no-stored-render" : "unpinned" };
|
|
3035
|
+
}
|
|
3036
|
+
const d = live.diff;
|
|
3037
|
+
const counts = {
|
|
3038
|
+
drifted: d.drifted.length,
|
|
3039
|
+
changes: countChanges(d.drifted),
|
|
3040
|
+
unchanged: d.unchanged.length,
|
|
3041
|
+
unobserved: d.unobserved.length,
|
|
3042
|
+
undeclared: d.undeclaredEntities.length
|
|
3043
|
+
};
|
|
3044
|
+
const provenance = provenanceOf(live);
|
|
3045
|
+
if (d.drifted.length > 0) {
|
|
3046
|
+
return { ...base, verdict: "drifted", counts, provenance, drifted: d.drifted };
|
|
3047
|
+
}
|
|
3048
|
+
if (d.drifted.length + d.accepted.length + d.unchanged.length === 0) {
|
|
3049
|
+
return { ...base, verdict: "unobserved", reason: "nothing-observed", counts, provenance };
|
|
3050
|
+
}
|
|
3051
|
+
if (d.unobserved.length > 0) {
|
|
3052
|
+
return { ...base, verdict: "unobserved", reason: "partly-observed", counts, provenance };
|
|
3053
|
+
}
|
|
3054
|
+
return { ...base, verdict: "in-sync", counts, provenance };
|
|
3055
|
+
}
|
|
3056
|
+
async function readHelmRenderDrift(projectDir, env, opts = {}) {
|
|
3057
|
+
const report = await helmRenders(projectDir, opts).catch(() => void 0);
|
|
3058
|
+
const records = report?.records ?? [];
|
|
3059
|
+
if (records.length === 0) return [];
|
|
3060
|
+
return mapPool(records, estateReadPool(records.length), async (record) => {
|
|
3061
|
+
if (!record.contentDigest) return helmRenderReport(record, void 0);
|
|
3062
|
+
const live = await helmRenderDiffLive(projectDir, record.contentDigest, env, opts).catch(() => void 0);
|
|
3063
|
+
return helmRenderReport(record, live);
|
|
3064
|
+
});
|
|
3065
|
+
}
|
|
3066
|
+
function chartNameOf2(node) {
|
|
3067
|
+
const name = node.attrs?.name;
|
|
3068
|
+
return typeof name === "string" && name.length > 0 ? name : void 0;
|
|
3069
|
+
}
|
|
3070
|
+
var SEVERITY = { drifted: 2, unobserved: 1, "in-sync": 0 };
|
|
3071
|
+
function applyHelmRenderDrift(ir, reports) {
|
|
3072
|
+
if (reports.length === 0) return { charts: 0, drifted: 0 };
|
|
3073
|
+
const byChart = /* @__PURE__ */ new Map();
|
|
3074
|
+
for (const r of reports) {
|
|
3075
|
+
const list3 = byChart.get(r.chart);
|
|
3076
|
+
if (list3) list3.push(r);
|
|
3077
|
+
else byChart.set(r.chart, [r]);
|
|
3078
|
+
}
|
|
3079
|
+
let charts = 0;
|
|
3080
|
+
let drifted = 0;
|
|
3081
|
+
for (const node of ir.nodes) {
|
|
3082
|
+
if (node.lexicon !== "helm" || node.kind !== "Helm::Chart") continue;
|
|
3083
|
+
const chartName = chartNameOf2(node);
|
|
3084
|
+
const matched = chartName ? byChart.get(chartName) : void 0;
|
|
3085
|
+
if (!matched || matched.length === 0) continue;
|
|
3086
|
+
const worst = matched.reduce((a, b) => SEVERITY[b.verdict] > SEVERITY[a.verdict] ? b : a);
|
|
3087
|
+
const attrs = node.attrs ??= {};
|
|
3088
|
+
attrs._renderDrift = matched.length === 1 ? worst : { ...worst, renders: matched };
|
|
3089
|
+
charts++;
|
|
3090
|
+
if (worst.verdict === "drifted") {
|
|
3091
|
+
attrs._status = "warn";
|
|
3092
|
+
drifted++;
|
|
3093
|
+
}
|
|
3094
|
+
}
|
|
3095
|
+
return { charts, drifted };
|
|
3096
|
+
}
|
|
3097
|
+
|
|
3098
|
+
// src/cluster-anchor.ts
|
|
3099
|
+
var MANAGED_CLUSTER_KINDS = /* @__PURE__ */ new Set([
|
|
3100
|
+
"AWS::EKS::Cluster",
|
|
3101
|
+
"GCP::Container::Cluster",
|
|
3102
|
+
"Microsoft.ContainerService/managedClusters",
|
|
3103
|
+
"K3d::Cluster"
|
|
3104
|
+
]);
|
|
3105
|
+
var ANCHOR_VIA = "runs-on";
|
|
3106
|
+
function metadata(node) {
|
|
3107
|
+
const m = node.attrs?.metadata;
|
|
3108
|
+
return m && typeof m === "object" && !Array.isArray(m) ? m : void 0;
|
|
3109
|
+
}
|
|
3110
|
+
function metaString(node, key) {
|
|
3111
|
+
const v = metadata(node)?.[key];
|
|
3112
|
+
return typeof v === "string" && v.length > 0 ? v : void 0;
|
|
3113
|
+
}
|
|
3114
|
+
function isK8s(node) {
|
|
3115
|
+
return node.lexicon === "k8s";
|
|
3116
|
+
}
|
|
3117
|
+
function isNamespace(node) {
|
|
3118
|
+
return node.kind === "K8s::Core::Namespace";
|
|
3119
|
+
}
|
|
3120
|
+
function managedClusterName(node) {
|
|
3121
|
+
const metaName = metaString(node, "name");
|
|
3122
|
+
if (metaName) return metaName;
|
|
3123
|
+
for (const key of ["name", "clusterName"]) {
|
|
3124
|
+
const v = node.attrs?.[key];
|
|
3125
|
+
if (typeof v === "string" && v.length > 0) return v;
|
|
3126
|
+
}
|
|
3127
|
+
return node.id;
|
|
3128
|
+
}
|
|
3129
|
+
function boundManagedCluster(nodes, boundContext) {
|
|
3130
|
+
const clusters = nodes.filter((n) => MANAGED_CLUSTER_KINDS.has(n.kind));
|
|
3131
|
+
if (clusters.length === 1) return clusters[0];
|
|
3132
|
+
if (clusters.length === 0 || !boundContext) return void 0;
|
|
3133
|
+
const bound = clusters.filter((n) => contextBindsCluster(boundContext, managedClusterName(n)));
|
|
3134
|
+
return bound.length === 1 ? bound[0] : void 0;
|
|
3135
|
+
}
|
|
3136
|
+
function addClusterAnchorEdges(ir, boundContext) {
|
|
3137
|
+
const nodes = ir.nodes;
|
|
3138
|
+
const cluster = boundManagedCluster(ir.nodes, boundContext);
|
|
3139
|
+
if (!cluster) return ir;
|
|
3140
|
+
const k8sNodes = nodes.filter(isK8s);
|
|
3141
|
+
if (k8sNodes.length === 0) return ir;
|
|
3142
|
+
const namespaceByName = /* @__PURE__ */ new Map();
|
|
3143
|
+
for (const n of k8sNodes) {
|
|
3144
|
+
if (!isNamespace(n)) continue;
|
|
3145
|
+
const name = metaString(n, "name");
|
|
3146
|
+
if (name && !namespaceByName.has(name)) namespaceByName.set(name, n.id);
|
|
3147
|
+
}
|
|
3148
|
+
const existing = new Set(ir.edges.map((e) => `${e.from}\0${e.to}`));
|
|
3149
|
+
const add = (from, to) => {
|
|
3150
|
+
if (from === to) return;
|
|
3151
|
+
const key = `${from}\0${to}`;
|
|
3152
|
+
if (existing.has(key)) return;
|
|
3153
|
+
existing.add(key);
|
|
3154
|
+
ir.edges.push({ from, to, kind: "ref", viaAttr: ANCHOR_VIA, inferred: true });
|
|
3155
|
+
};
|
|
3156
|
+
for (const n of k8sNodes) {
|
|
3157
|
+
if (isNamespace(n)) {
|
|
3158
|
+
add(cluster.id, n.id);
|
|
3159
|
+
continue;
|
|
3160
|
+
}
|
|
3161
|
+
const ns = metaString(n, "namespace");
|
|
3162
|
+
const nsNode = ns ? namespaceByName.get(ns) : void 0;
|
|
3163
|
+
add(nsNode ?? cluster.id, n.id);
|
|
3164
|
+
}
|
|
3165
|
+
return ir;
|
|
3166
|
+
}
|
|
3167
|
+
|
|
3168
|
+
// src/logical-azure.ts
|
|
3169
|
+
var AZURE_HEADLINE_KINDS = /* @__PURE__ */ new Set([
|
|
3170
|
+
// compute / workloads
|
|
3171
|
+
"Microsoft.Compute/virtualMachines",
|
|
3172
|
+
"Microsoft.Compute/virtualMachineScaleSets",
|
|
3173
|
+
"Microsoft.ContainerService/managedClusters",
|
|
3174
|
+
"Microsoft.ContainerInstance/containerGroups",
|
|
3175
|
+
"Microsoft.Web/sites",
|
|
3176
|
+
// network edge
|
|
3177
|
+
"Microsoft.Network/publicIPAddresses",
|
|
3178
|
+
"Microsoft.Network/loadBalancers",
|
|
3179
|
+
"Microsoft.Network/applicationGateways",
|
|
3180
|
+
"Microsoft.Network/natGateways",
|
|
3181
|
+
"Microsoft.Network/dnsZones",
|
|
3182
|
+
// data stores
|
|
3183
|
+
"Microsoft.Storage/storageAccounts",
|
|
3184
|
+
"Microsoft.Sql/servers",
|
|
3185
|
+
"Microsoft.DocumentDB/databaseAccounts",
|
|
3186
|
+
"Microsoft.Cache/redis",
|
|
3187
|
+
"Microsoft.DBforPostgreSQL/flexibleServers",
|
|
3188
|
+
// registry / identity edge
|
|
3189
|
+
"Microsoft.ContainerRegistry/registries"
|
|
3190
|
+
]);
|
|
3191
|
+
var AZURE_FABRIC_KINDS = /* @__PURE__ */ new Set([
|
|
3192
|
+
"Microsoft.Network/virtualNetworks",
|
|
3193
|
+
"Microsoft.Network/virtualNetworks_subnets",
|
|
3194
|
+
"Microsoft.Network/networkSecurityGroups",
|
|
3195
|
+
"Microsoft.Network/routeTables",
|
|
3196
|
+
"Microsoft.Network/networkInterfaces",
|
|
3197
|
+
// identity/authorization hubs — a role assignment or managed identity is
|
|
3198
|
+
// referenced by half the estate, so contracting through one invents edges
|
|
3199
|
+
// between unrelated resources.
|
|
3200
|
+
"Microsoft.Authorization/roleAssignments",
|
|
3201
|
+
"Microsoft.ManagedIdentity/userAssignedIdentities",
|
|
3202
|
+
"Microsoft.KeyVault/vaults"
|
|
3203
|
+
]);
|
|
3204
|
+
var VNET_KIND = "Microsoft.Network/virtualNetworks";
|
|
3205
|
+
var SUBNET_KIND = "Microsoft.Network/virtualNetworks_subnets";
|
|
3206
|
+
var NSG_KIND = "Microsoft.Network/networkSecurityGroups";
|
|
3207
|
+
var RESOURCE_ID_RE = /resourceId\(\s*([^)]*)\)/g;
|
|
3208
|
+
var QUOTED_RE = /'([^']*)'/g;
|
|
3209
|
+
function resourceIdNames(value) {
|
|
3210
|
+
if (typeof value !== "string" || !value.includes("resourceId(")) return [];
|
|
3211
|
+
const out = [];
|
|
3212
|
+
for (const call of value.matchAll(RESOURCE_ID_RE)) {
|
|
3213
|
+
const args = [...call[1].matchAll(QUOTED_RE)].map((m) => m[1]);
|
|
3214
|
+
if (args.length >= 2) out.push(args[args.length - 1]);
|
|
3215
|
+
}
|
|
3216
|
+
return out;
|
|
3217
|
+
}
|
|
3218
|
+
function collectAzureRefs(value, out) {
|
|
2715
3219
|
if (value === null || typeof value !== "object") {
|
|
2716
3220
|
for (const name of resourceIdNames(value)) out.add(name);
|
|
2717
3221
|
return;
|
|
@@ -3081,7 +3585,7 @@ function projectK8sLogical(ir, env, boundContext) {
|
|
|
3081
3585
|
function releaseBoxTitle(chartName) {
|
|
3082
3586
|
return `release ${chartName}`;
|
|
3083
3587
|
}
|
|
3084
|
-
function
|
|
3588
|
+
function chartNameOf3(node) {
|
|
3085
3589
|
const name = node.attrs?.name;
|
|
3086
3590
|
return typeof name === "string" && name.length > 0 ? name : node.id;
|
|
3087
3591
|
}
|
|
@@ -3102,7 +3606,7 @@ function projectHelmLogical(ir) {
|
|
|
3102
3606
|
};
|
|
3103
3607
|
const k8s = ir.nodes.filter((n) => n.lexicon === "k8s");
|
|
3104
3608
|
for (const chart of charts) {
|
|
3105
|
-
const box = releaseBoxTitle(
|
|
3609
|
+
const box = releaseBoxTitle(chartNameOf3(chart));
|
|
3106
3610
|
child(box, chart.id);
|
|
3107
3611
|
const chartDir = dirOf(chart);
|
|
3108
3612
|
if (chartDir === "") continue;
|
|
@@ -3119,7 +3623,7 @@ function projectHelmLogical(ir) {
|
|
|
3119
3623
|
|
|
3120
3624
|
// src/logical-kustomize.ts
|
|
3121
3625
|
import { existsSync as existsSync5 } from "node:fs";
|
|
3122
|
-
import { join as
|
|
3626
|
+
import { join as join8 } from "node:path";
|
|
3123
3627
|
function overlayBoxTitle(name) {
|
|
3124
3628
|
return `overlay ${name}`;
|
|
3125
3629
|
}
|
|
@@ -3139,7 +3643,7 @@ function kustomizationRoot(bases, dir, exists) {
|
|
|
3139
3643
|
let current = dir;
|
|
3140
3644
|
for (let i = 0; i < 4 && current; i++) {
|
|
3141
3645
|
for (const base of bases) {
|
|
3142
|
-
if (exists(
|
|
3646
|
+
if (exists(join8(base, current, "kustomization.yaml")) || exists(join8(base, current, "kustomization.yml"))) {
|
|
3143
3647
|
return current;
|
|
3144
3648
|
}
|
|
3145
3649
|
}
|
|
@@ -3715,93 +4219,16 @@ function addCompositeDepsCounted(ir, dag) {
|
|
|
3715
4219
|
return { ir: out, attached: out.edges.length - before };
|
|
3716
4220
|
}
|
|
3717
4221
|
|
|
3718
|
-
// src/
|
|
3719
|
-
|
|
3720
|
-
|
|
3721
|
-
|
|
3722
|
-
|
|
3723
|
-
|
|
3724
|
-
|
|
3725
|
-
|
|
3726
|
-
|
|
3727
|
-
|
|
3728
|
-
return compositeEdgesAttached === 0 ? "no component ownership to join \u2014 showing the resource graph unchanged" : void 0;
|
|
3729
|
-
}
|
|
3730
|
-
if (zoom === "runtime") {
|
|
3731
|
-
const children = ir.nodes.filter((n) => n.runtimeOwner).length;
|
|
3732
|
-
return children === 0 ? "nothing below the declaration boundary \u2014 no owner-referenced children on this substrate" : void 0;
|
|
3733
|
-
}
|
|
3734
|
-
return void 0;
|
|
3735
|
-
}
|
|
3736
|
-
function logicalKept(before, after) {
|
|
3737
|
-
if (after > 0 && after * 3 >= before) return void 0;
|
|
3738
|
-
return after === 0 ? `logical projected nothing from ${before} resources \u2014 it is a cloud-topology lens, and this estate declares none of the kinds it nests (behold#74)` : `logical kept ${after} of ${before} resources \u2014 it is a cloud-topology lens, and the rest are kinds it does not nest (behold#74)`;
|
|
3739
|
-
}
|
|
3740
|
-
function edgelessNote(zoom, ir, detail) {
|
|
3741
|
-
if (zoom === "components" || zoom === "logical") return void 0;
|
|
3742
|
-
if (ir.nodes.length === 0 || ir.edges.length > 0) return void 0;
|
|
3743
|
-
if (detail !== void 0 && detail < 3) {
|
|
3744
|
-
return "no edges at this detail \u2014 sourceRef/dependsOn and other attrs-derived references only appear at detail 3 (\u2318K \u2192 attributes, or add &detail=3)";
|
|
3745
|
-
}
|
|
3746
|
-
return "no edges \u2014 nothing in this estate references anything else";
|
|
3747
|
-
}
|
|
3748
|
-
var CLUSTER_SCOPED = /* @__PURE__ */ new Set([
|
|
3749
|
-
"K8s::Core::Namespace",
|
|
3750
|
-
"K8s::Rbac::ClusterRole",
|
|
3751
|
-
"K8s::Rbac::ClusterRoleBinding",
|
|
3752
|
-
"K8s::Storage::StorageClass",
|
|
3753
|
-
"K8s::Apiextensions::CustomResourceDefinition"
|
|
3754
|
-
]);
|
|
3755
|
-
function namespaceMismatchNote(nodes) {
|
|
3756
|
-
const k8s = nodes.filter((n) => n?.lexicon === "k8s");
|
|
3757
|
-
if (k8s.length === 0) return void 0;
|
|
3758
|
-
const pending = k8s.filter((n) => n.attrs?._status === "accent" && !CLUSTER_SCOPED.has(n.kind ?? ""));
|
|
3759
|
-
if (pending.length < 2) return void 0;
|
|
3760
|
-
if (pending.some((n) => typeof n.attrs?.metadata?.namespace === "string" && n.attrs.metadata.namespace)) return void 0;
|
|
3761
|
-
if (!k8s.some((n) => n.attrs?._status === "good")) return void 0;
|
|
3762
|
-
return `${pending.length} pending k8s objects declare no metadata.namespace \u2014 if a controller stamps it at apply time (e.g. Flux's targetNamespace), the live read looked in "default", not where they run`;
|
|
3763
|
-
}
|
|
3764
|
-
function namespaceJoinNote(joined) {
|
|
3765
|
-
if (joined.length === 0) return void 0;
|
|
3766
|
-
const each = joined.map((j) => `${j.name} in "${j.namespace}"`).join(", ");
|
|
3767
|
-
return `read ${each} \u2014 the namespace the estate's own Kustomization targetNamespace binds, not the app project's own declaration`;
|
|
3768
|
-
}
|
|
3769
|
-
function notesFor(zoom, ir, compositeEdgesAttached, logicalBefore, detail) {
|
|
3770
|
-
const primary = zoom === "logical" && logicalBefore !== void 0 ? logicalKept(logicalBefore, ir.nodes.length) : zoomNote(zoom, ir, compositeEdgesAttached);
|
|
3771
|
-
const notes = [primary, edgelessNote(zoom, ir, detail)].filter((n) => n !== void 0);
|
|
3772
|
-
return notes.length ? notes.join(" \xB7 ") : void 0;
|
|
3773
|
-
}
|
|
3774
|
-
function tierMismatchNote(ir, tiers, currentTier) {
|
|
3775
|
-
const values = tiers?.values ?? [];
|
|
3776
|
-
if (values.length < 2) return void 0;
|
|
3777
|
-
const byLexicon = /* @__PURE__ */ new Map();
|
|
3778
|
-
for (const n of ir.nodes) {
|
|
3779
|
-
const status = n.attrs?._status;
|
|
3780
|
-
if (status !== "good" && status !== "accent") continue;
|
|
3781
|
-
const lexicon = n.lexicon ?? "?";
|
|
3782
|
-
const c = byLexicon.get(lexicon) ?? { good: 0, accent: 0 };
|
|
3783
|
-
c[status === "good" ? "good" : "accent"]++;
|
|
3784
|
-
byLexicon.set(lexicon, c);
|
|
3785
|
-
}
|
|
3786
|
-
for (const [lexicon, { good, accent }] of [...byLexicon].sort()) {
|
|
3787
|
-
if (accent < 2 || accent * 2 < good + accent) continue;
|
|
3788
|
-
const here = currentTier ? `tier "${currentTier}"` : "the default tier";
|
|
3789
|
-
const others = values.filter((v) => v !== currentTier).join(", ");
|
|
3790
|
-
return `${accent} of ${good + accent} ${lexicon} resources read "declared, not deployed" at ${here} \u2014 if the estate runs another declared tier (${others}), pick it (\u2318K \u2192 tier)`;
|
|
3791
|
-
}
|
|
3792
|
-
return void 0;
|
|
3793
|
-
}
|
|
3794
|
-
|
|
3795
|
-
// src/resources.ts
|
|
3796
|
-
var NON_RESOURCE_KINDS = /* @__PURE__ */ new Set(["AWS::CloudFormation::Parameter", "chant:output"]);
|
|
3797
|
-
function nonResourceEntities(ir) {
|
|
3798
|
-
const out = /* @__PURE__ */ new Set();
|
|
3799
|
-
for (const n of ir.nodes) if (NON_RESOURCE_KINDS.has(n.kind)) out.add(n.id);
|
|
3800
|
-
return out;
|
|
3801
|
-
}
|
|
3802
|
-
function componentSegment(parts, known) {
|
|
3803
|
-
if (known) {
|
|
3804
|
-
return parts.slice(0, -1).find((p) => known.has(p));
|
|
4222
|
+
// src/resources.ts
|
|
4223
|
+
var NON_RESOURCE_KINDS = /* @__PURE__ */ new Set(["AWS::CloudFormation::Parameter", "chant:output"]);
|
|
4224
|
+
function nonResourceEntities(ir) {
|
|
4225
|
+
const out = /* @__PURE__ */ new Set();
|
|
4226
|
+
for (const n of ir.nodes) if (NON_RESOURCE_KINDS.has(n.kind)) out.add(n.id);
|
|
4227
|
+
return out;
|
|
4228
|
+
}
|
|
4229
|
+
function componentSegment(parts, known) {
|
|
4230
|
+
if (known) {
|
|
4231
|
+
return parts.slice(0, -1).find((p) => known.has(p));
|
|
3805
4232
|
}
|
|
3806
4233
|
return parts[0] === "src" && parts.length >= 3 ? parts[1] : void 0;
|
|
3807
4234
|
}
|
|
@@ -3823,6 +4250,19 @@ function resourcesByComponent(ir, known) {
|
|
|
3823
4250
|
}
|
|
3824
4251
|
|
|
3825
4252
|
// src/reconcile.ts
|
|
4253
|
+
var DISRUPTION_SEVERITY = {
|
|
4254
|
+
"in-place": 0,
|
|
4255
|
+
rolling: 1,
|
|
4256
|
+
unknown: 1,
|
|
4257
|
+
replace: 2,
|
|
4258
|
+
destroy: 3
|
|
4259
|
+
};
|
|
4260
|
+
var DISRUPTION_LEVELS = ["in-place", "rolling", "replace", "destroy", "unknown"];
|
|
4261
|
+
function worstDisruption(a, b) {
|
|
4262
|
+
if (!a) return b;
|
|
4263
|
+
if (!b) return a;
|
|
4264
|
+
return DISRUPTION_SEVERITY[b] > DISRUPTION_SEVERITY[a] ? b : a;
|
|
4265
|
+
}
|
|
3826
4266
|
function summarizePlan(plan, byComponent, nonResource) {
|
|
3827
4267
|
const componentByEntity = /* @__PURE__ */ new Map();
|
|
3828
4268
|
for (const [component, resources] of Object.entries(byComponent)) {
|
|
@@ -3831,6 +4271,9 @@ function summarizePlan(plan, byComponent, nonResource) {
|
|
|
3831
4271
|
const counts = {};
|
|
3832
4272
|
const unobservedCounts = {};
|
|
3833
4273
|
const runtimeCounts = {};
|
|
4274
|
+
const disruption = Object.fromEntries(DISRUPTION_LEVELS.map((l) => [l, 0]));
|
|
4275
|
+
const disruptionByComponent = {};
|
|
4276
|
+
let disruptionUncorrelated;
|
|
3834
4277
|
let uncorrelated = 0;
|
|
3835
4278
|
let unobservedUncorrelated = 0;
|
|
3836
4279
|
let runtimeUncorrelated = 0;
|
|
@@ -3856,6 +4299,12 @@ function summarizePlan(plan, byComponent, nonResource) {
|
|
|
3856
4299
|
total++;
|
|
3857
4300
|
if (component) counts[component] = (counts[component] ?? 0) + 1;
|
|
3858
4301
|
else uncorrelated++;
|
|
4302
|
+
const level = entry.disruption;
|
|
4303
|
+
if (!level) continue;
|
|
4304
|
+
if (!DISRUPTION_LEVELS.includes(level)) continue;
|
|
4305
|
+
disruption[level]++;
|
|
4306
|
+
if (component) disruptionByComponent[component] = worstDisruption(disruptionByComponent[component], level);
|
|
4307
|
+
else disruptionUncorrelated = worstDisruption(disruptionUncorrelated, level);
|
|
3859
4308
|
}
|
|
3860
4309
|
return {
|
|
3861
4310
|
env: plan.env,
|
|
@@ -3867,7 +4316,10 @@ function summarizePlan(plan, byComponent, nonResource) {
|
|
|
3867
4316
|
unobservedUncorrelated,
|
|
3868
4317
|
runtime,
|
|
3869
4318
|
runtimeByComponent: runtimeCounts,
|
|
3870
|
-
runtimeUncorrelated
|
|
4319
|
+
runtimeUncorrelated,
|
|
4320
|
+
disruption,
|
|
4321
|
+
disruptionByComponent,
|
|
4322
|
+
...disruptionUncorrelated ? { disruptionUncorrelated } : {}
|
|
3871
4323
|
};
|
|
3872
4324
|
}
|
|
3873
4325
|
|
|
@@ -3883,10 +4335,10 @@ import {
|
|
|
3883
4335
|
|
|
3884
4336
|
// src/icon-packs.ts
|
|
3885
4337
|
import { readFileSync as readFileSync6 } from "node:fs";
|
|
3886
|
-
import { dirname as dirname3, join as
|
|
4338
|
+
import { dirname as dirname3, join as join9 } from "node:path";
|
|
3887
4339
|
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
3888
|
-
var ICON_ROOT =
|
|
3889
|
-
var
|
|
4340
|
+
var ICON_ROOT = join9(dirname3(fileURLToPath2(import.meta.url)), "..", "web", "icons");
|
|
4341
|
+
var cache2 = /* @__PURE__ */ new Map();
|
|
3890
4342
|
function unprefix(body) {
|
|
3891
4343
|
return body.replace(/\s(?:inkscape|sodipodi):[\w-]+\s*=\s*(["'])[\s\S]*?\1/g, "").replace(/\sxlink:href\s*=/g, " href=");
|
|
3892
4344
|
}
|
|
@@ -3905,9 +4357,9 @@ function plate(glyph) {
|
|
|
3905
4357
|
return { ...glyph, body: `${ground}<g transform="${inset}">${glyph.body}</g>` };
|
|
3906
4358
|
}
|
|
3907
4359
|
function loadIcon(rel) {
|
|
3908
|
-
const hit =
|
|
4360
|
+
const hit = cache2.get(rel);
|
|
3909
4361
|
if (hit) return hit;
|
|
3910
|
-
const raw = readFileSync6(
|
|
4362
|
+
const raw = readFileSync6(join9(ICON_ROOT, `${rel}.svg`), "utf8");
|
|
3911
4363
|
const open = raw.indexOf("<svg");
|
|
3912
4364
|
const openEnd = raw.indexOf(">", open);
|
|
3913
4365
|
const close = raw.lastIndexOf("</svg");
|
|
@@ -3916,7 +4368,7 @@ function loadIcon(rel) {
|
|
|
3916
4368
|
if (!viewBox) throw new Error(`vendored icon has no viewBox: ${rel}.svg`);
|
|
3917
4369
|
const bare = { body: unprefix(raw.slice(openEnd + 1, close).trim()), colored: true, viewBox };
|
|
3918
4370
|
const glyph = PLATED.has(rel) ? plate(bare) : bare;
|
|
3919
|
-
|
|
4371
|
+
cache2.set(rel, glyph);
|
|
3920
4372
|
return glyph;
|
|
3921
4373
|
}
|
|
3922
4374
|
var K8S_ICON_BY_KIND = {
|
|
@@ -4161,10 +4613,646 @@ function carveNote(report, ir) {
|
|
|
4161
4613
|
return `${head}. This report carries per-resource boundary COUNTS but no edge lists, so no boundary edges are drawn (chant#1636). Read-only: nothing is emitted, patched, or applied.`;
|
|
4162
4614
|
}
|
|
4163
4615
|
|
|
4616
|
+
// src/carve-manifest.ts
|
|
4617
|
+
import { readdirSync as readdirSync4, readFileSync as readFileSync7, statSync as statSync4 } from "node:fs";
|
|
4618
|
+
import { dirname as dirname4, join as join10 } from "node:path";
|
|
4619
|
+
var CARVE_MANIFEST_SUFFIX = ".carve.json";
|
|
4620
|
+
var APPLY_IS_HUMAN = "`chant carve apply` graduates ownership \u2014 it resolves the marker that makes chant the owner of a live resource. behold shows what the manifest records and echoes the command; it has no apply endpoint and no apply button, by design. Read the graduation plan, then run it yourself.";
|
|
4621
|
+
var isRecord2 = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
4622
|
+
var strings = (v) => Array.isArray(v) ? v.filter((x) => typeof x === "string") : [];
|
|
4623
|
+
function parseCarveManifest(value) {
|
|
4624
|
+
if (!isRecord2(value)) return null;
|
|
4625
|
+
if (value.version !== 1) return null;
|
|
4626
|
+
if (typeof value.target !== "string" || !value.target) return null;
|
|
4627
|
+
return value;
|
|
4628
|
+
}
|
|
4629
|
+
function readCarveManifest(path, readFile = (p) => readFileSync7(p, "utf8")) {
|
|
4630
|
+
let text;
|
|
4631
|
+
try {
|
|
4632
|
+
text = readFile(path);
|
|
4633
|
+
} catch {
|
|
4634
|
+
return null;
|
|
4635
|
+
}
|
|
4636
|
+
try {
|
|
4637
|
+
return parseCarveManifest(JSON.parse(text));
|
|
4638
|
+
} catch {
|
|
4639
|
+
return null;
|
|
4640
|
+
}
|
|
4641
|
+
}
|
|
4642
|
+
function carveStageOf(m) {
|
|
4643
|
+
if (m.apply) return "applied";
|
|
4644
|
+
if (m.bridge) return "bridged";
|
|
4645
|
+
return "emitted";
|
|
4646
|
+
}
|
|
4647
|
+
function carveWordFor(stage) {
|
|
4648
|
+
switch (stage) {
|
|
4649
|
+
case "applied":
|
|
4650
|
+
return "carved \u2192 chant";
|
|
4651
|
+
case "bridged":
|
|
4652
|
+
return "bridged \u2014 apply is yours";
|
|
4653
|
+
default:
|
|
4654
|
+
return "emitted \u2014 not bridged";
|
|
4655
|
+
}
|
|
4656
|
+
}
|
|
4657
|
+
function carveToneFor(stage) {
|
|
4658
|
+
return stage === "applied" ? "good" : "accent";
|
|
4659
|
+
}
|
|
4660
|
+
function carveStageNote(state) {
|
|
4661
|
+
switch (state.stage) {
|
|
4662
|
+
case "applied":
|
|
4663
|
+
return `graduated \u2014 \`carve apply\` recorded the ownership marker` + (state.ownership?.stack ? ` (stack ${state.ownership.stack}${state.ownership.env ? `, env ${state.ownership.env}` : ""})` : "") + ". chant owns it.";
|
|
4664
|
+
case "bridged":
|
|
4665
|
+
return "emitted and bridged \u2014 the survivor patch is proposed. Terraform still owns the resource until `carve apply`.";
|
|
4666
|
+
default:
|
|
4667
|
+
return "emitted \u2014 typed chant source exists, and the surviving Terraform still reads the resource directly. `carve bridge` is next.";
|
|
4668
|
+
}
|
|
4669
|
+
}
|
|
4670
|
+
function carveStateOf(m, path) {
|
|
4671
|
+
const stage = carveStageOf(m);
|
|
4672
|
+
const deferred = Object.entries(m.emit?.params ?? {}).map(([name, p]) => `${name} \u2190 ${p?.survivor ?? "a survivor"}`);
|
|
4673
|
+
return {
|
|
4674
|
+
target: m.target,
|
|
4675
|
+
...m.tfType ? { tfType: m.tfType } : {},
|
|
4676
|
+
stage,
|
|
4677
|
+
graduated: stage === "applied",
|
|
4678
|
+
// The last stamped step wins — that is the age of the state, not the age
|
|
4679
|
+
// of the carve.
|
|
4680
|
+
...m.apply?.at || m.bridge?.at || m.emit?.at ? { at: m.apply?.at ?? m.bridge?.at ?? m.emit?.at } : {},
|
|
4681
|
+
files: strings(m.emit?.files),
|
|
4682
|
+
excised: strings(m.bridge?.excised),
|
|
4683
|
+
deferredInputs: deferred,
|
|
4684
|
+
...m.apply ? {
|
|
4685
|
+
ownership: {
|
|
4686
|
+
...m.apply.marker?.stack ? { stack: m.apply.marker.stack } : {},
|
|
4687
|
+
...m.apply.marker?.env ? { env: m.apply.marker.env } : {},
|
|
4688
|
+
...m.apply.ownershipTags ? { tags: m.apply.ownershipTags } : {}
|
|
4689
|
+
}
|
|
4690
|
+
} : {},
|
|
4691
|
+
...m.from ? { from: m.from } : {},
|
|
4692
|
+
outDir: dirname4(path),
|
|
4693
|
+
path
|
|
4694
|
+
};
|
|
4695
|
+
}
|
|
4696
|
+
var nodeCarveIo = {
|
|
4697
|
+
readdir: (dir) => readdirSync4(dir),
|
|
4698
|
+
isDirectory: (p) => {
|
|
4699
|
+
try {
|
|
4700
|
+
return statSync4(p).isDirectory();
|
|
4701
|
+
} catch {
|
|
4702
|
+
return false;
|
|
4703
|
+
}
|
|
4704
|
+
},
|
|
4705
|
+
readFile: (p) => readFileSync7(p, "utf8")
|
|
4706
|
+
};
|
|
4707
|
+
var MANIFEST_SCAN_DEPTH = 2;
|
|
4708
|
+
var SKIP_DIRS = /* @__PURE__ */ new Set(["node_modules", "dist", "cdk.out", ".terraform", ".git"]);
|
|
4709
|
+
function listCarveManifests(dir, io = nodeCarveIo, depth = MANIFEST_SCAN_DEPTH) {
|
|
4710
|
+
const out = [];
|
|
4711
|
+
const walk = (d, level) => {
|
|
4712
|
+
let entries;
|
|
4713
|
+
try {
|
|
4714
|
+
entries = io.readdir(d).sort();
|
|
4715
|
+
} catch {
|
|
4716
|
+
return;
|
|
4717
|
+
}
|
|
4718
|
+
for (const name of entries) {
|
|
4719
|
+
const full = join10(d, name);
|
|
4720
|
+
if (name.endsWith(CARVE_MANIFEST_SUFFIX)) {
|
|
4721
|
+
out.push(full);
|
|
4722
|
+
continue;
|
|
4723
|
+
}
|
|
4724
|
+
if (level >= depth) continue;
|
|
4725
|
+
if (name.startsWith(".") || SKIP_DIRS.has(name)) continue;
|
|
4726
|
+
if (io.isDirectory(full)) walk(full, level + 1);
|
|
4727
|
+
}
|
|
4728
|
+
};
|
|
4729
|
+
walk(dir, 0);
|
|
4730
|
+
return out;
|
|
4731
|
+
}
|
|
4732
|
+
function readCarveStates(dirs, io = nodeCarveIo, depth = MANIFEST_SCAN_DEPTH) {
|
|
4733
|
+
const rank = { emitted: 0, bridged: 1, applied: 2 };
|
|
4734
|
+
const byTarget = /* @__PURE__ */ new Map();
|
|
4735
|
+
for (const dir of dirs) {
|
|
4736
|
+
for (const path of listCarveManifests(dir, io, depth)) {
|
|
4737
|
+
const manifest = readCarveManifest(path, io.readFile);
|
|
4738
|
+
if (!manifest) continue;
|
|
4739
|
+
const state = carveStateOf(manifest, path);
|
|
4740
|
+
const held = byTarget.get(state.target);
|
|
4741
|
+
if (!held || rank[state.stage] > rank[held.stage]) byTarget.set(state.target, state);
|
|
4742
|
+
}
|
|
4743
|
+
}
|
|
4744
|
+
return byTarget;
|
|
4745
|
+
}
|
|
4746
|
+
function carveProgress(states, total) {
|
|
4747
|
+
let applied = 0;
|
|
4748
|
+
let bridged = 0;
|
|
4749
|
+
let emitted = 0;
|
|
4750
|
+
for (const s of states) {
|
|
4751
|
+
if (s.stage === "applied") applied++;
|
|
4752
|
+
else if (s.stage === "bridged") bridged++;
|
|
4753
|
+
else emitted++;
|
|
4754
|
+
}
|
|
4755
|
+
const inFlight = bridged + emitted;
|
|
4756
|
+
const parts = [];
|
|
4757
|
+
if (emitted) parts.push(`${emitted} emitted`);
|
|
4758
|
+
if (bridged) parts.push(`${bridged} bridged`);
|
|
4759
|
+
return {
|
|
4760
|
+
applied,
|
|
4761
|
+
bridged,
|
|
4762
|
+
emitted,
|
|
4763
|
+
inFlight,
|
|
4764
|
+
total: typeof total === "number" ? total : null,
|
|
4765
|
+
label: typeof total === "number" ? `${applied} of ${total} carved` : `${applied} carved`,
|
|
4766
|
+
detail: parts.length ? `${parts.join(", ")} \u2014 apply is yours` : ""
|
|
4767
|
+
};
|
|
4768
|
+
}
|
|
4769
|
+
function applyCommandFor(state, labels = {}) {
|
|
4770
|
+
const env = state.ownership?.env ?? "<env>";
|
|
4771
|
+
const stack = state.ownership?.stack ?? "<stack>";
|
|
4772
|
+
const from = labels.from ?? state.from ?? "<terraform-dir>";
|
|
4773
|
+
const out = labels.out ?? state.outDir;
|
|
4774
|
+
return `chant carve apply --from ${from} --output ${out} --env ${env} --stack ${stack} --write-source`;
|
|
4775
|
+
}
|
|
4776
|
+
function carveStatePayload(states, total, labels = () => ({})) {
|
|
4777
|
+
const list3 = [...states.values()].sort((a, b) => a.target.localeCompare(b.target));
|
|
4778
|
+
return {
|
|
4779
|
+
manifests: list3.length,
|
|
4780
|
+
progress: carveProgress(list3, total),
|
|
4781
|
+
states: list3.map((s) => ({ ...s, note: carveStageNote(s), applyCommand: applyCommandFor(s, labels(s)) })),
|
|
4782
|
+
apply: { human: true, note: APPLY_IS_HUMAN }
|
|
4783
|
+
};
|
|
4784
|
+
}
|
|
4785
|
+
function splitCarveState(tfIr, states, opts = {}) {
|
|
4786
|
+
if (states.size === 0) return { tf: tfIr, graduated: [] };
|
|
4787
|
+
const shorten = opts.shorten ?? ((p) => p);
|
|
4788
|
+
const source = (files) => files.map(shorten).join(", ");
|
|
4789
|
+
const gone = /* @__PURE__ */ new Set();
|
|
4790
|
+
const graduated = [];
|
|
4791
|
+
const nodes = [];
|
|
4792
|
+
for (const node of tfIr.nodes) {
|
|
4793
|
+
const state = states.get(node.id);
|
|
4794
|
+
if (!state) {
|
|
4795
|
+
nodes.push(node);
|
|
4796
|
+
continue;
|
|
4797
|
+
}
|
|
4798
|
+
if (state.graduated) {
|
|
4799
|
+
gone.add(node.id);
|
|
4800
|
+
graduated.push({
|
|
4801
|
+
...node,
|
|
4802
|
+
attrs: {
|
|
4803
|
+
...node.attrs,
|
|
4804
|
+
_status: "good",
|
|
4805
|
+
carve: carveWordFor(state.stage),
|
|
4806
|
+
carveState: state.stage,
|
|
4807
|
+
...state.ownership?.stack ? { ownedStack: state.ownership.stack } : {},
|
|
4808
|
+
...state.ownership?.env ? { ownedEnv: state.ownership.env } : {},
|
|
4809
|
+
...state.at ? { carvedAt: state.at } : {},
|
|
4810
|
+
...state.files.length ? { chantSource: source(state.files) } : {}
|
|
4811
|
+
}
|
|
4812
|
+
});
|
|
4813
|
+
continue;
|
|
4814
|
+
}
|
|
4815
|
+
nodes.push({
|
|
4816
|
+
...node,
|
|
4817
|
+
attrs: {
|
|
4818
|
+
...node.attrs,
|
|
4819
|
+
_status: carveToneFor(state.stage),
|
|
4820
|
+
carve: carveWordFor(state.stage),
|
|
4821
|
+
carveState: state.stage,
|
|
4822
|
+
...state.at ? { carvedAt: state.at } : {},
|
|
4823
|
+
...state.files.length ? { chantSource: source(state.files) } : {},
|
|
4824
|
+
...state.excised.length ? { excisesOnApply: state.excised.join(", ") } : {},
|
|
4825
|
+
...state.deferredInputs.length ? { deferredInputs: state.deferredInputs.join(", ") } : {}
|
|
4826
|
+
}
|
|
4827
|
+
});
|
|
4828
|
+
}
|
|
4829
|
+
const byStack = {};
|
|
4830
|
+
for (const [band, members] of Object.entries(tfIr.groups?.byStack ?? {})) {
|
|
4831
|
+
const left = members.filter((id) => !gone.has(id));
|
|
4832
|
+
if (left.length) byStack[band] = left;
|
|
4833
|
+
}
|
|
4834
|
+
return {
|
|
4835
|
+
tf: {
|
|
4836
|
+
nodes,
|
|
4837
|
+
edges: tfIr.edges.filter((e) => !gone.has(e.from) && !gone.has(e.to)),
|
|
4838
|
+
groups: { ...tfIr.groups, byStack }
|
|
4839
|
+
},
|
|
4840
|
+
graduated
|
|
4841
|
+
};
|
|
4842
|
+
}
|
|
4843
|
+
var GRADUATED_BAND = "carved \u2192 chant";
|
|
4844
|
+
function bandGraduated(tf, graduated) {
|
|
4845
|
+
if (graduated.length === 0) return tf;
|
|
4846
|
+
return {
|
|
4847
|
+
nodes: [...graduated, ...tf.nodes],
|
|
4848
|
+
edges: tf.edges,
|
|
4849
|
+
groups: {
|
|
4850
|
+
...tf.groups,
|
|
4851
|
+
byStack: {
|
|
4852
|
+
[GRADUATED_BAND]: graduated.map((n) => n.id),
|
|
4853
|
+
...tf.groups?.byStack ?? {}
|
|
4854
|
+
}
|
|
4855
|
+
}
|
|
4856
|
+
};
|
|
4857
|
+
}
|
|
4858
|
+
function carveStateNote(progress, states) {
|
|
4859
|
+
if (states.size === 0) return "";
|
|
4860
|
+
const head = ` Carve state (from ${states.size} manifest${states.size === 1 ? "" : "s"}): ${progress.label}`;
|
|
4861
|
+
return progress.detail ? `${head}, ${progress.detail}.` : `${head}.`;
|
|
4862
|
+
}
|
|
4863
|
+
|
|
4864
|
+
// src/ops-lens.ts
|
|
4865
|
+
var SUPPORTED_MAJOR2 = 1;
|
|
4866
|
+
var refuse2 = (error, remedy) => ({ ok: false, refusal: { error, code: "op-ir", remedy } });
|
|
4867
|
+
var HOW_TO_EMIT = "Build the project with a chant >= 0.50 (`chant build --lexicon temporal`), which writes `dist/ops/<name>/op.json` beside each Op's generated workflow.";
|
|
4868
|
+
var isRecord3 = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
4869
|
+
function parseOpIr(value) {
|
|
4870
|
+
if (!isRecord3(value)) {
|
|
4871
|
+
return refuse2("That file isn't an op.json \u2014 its top level isn't a JSON object.", HOW_TO_EMIT);
|
|
4872
|
+
}
|
|
4873
|
+
if (typeof value.name !== "string" || !Array.isArray(value.phases)) {
|
|
4874
|
+
return refuse2(
|
|
4875
|
+
"That JSON doesn't look like an op.json \u2014 it needs a string `name` and a `phases` array.",
|
|
4876
|
+
HOW_TO_EMIT
|
|
4877
|
+
);
|
|
4878
|
+
}
|
|
4879
|
+
const bad = value.phases.findIndex((p) => !isRecord3(p) || typeof p.name !== "string" || !Array.isArray(p.steps));
|
|
4880
|
+
if (bad >= 0) {
|
|
4881
|
+
return refuse2(
|
|
4882
|
+
`That JSON has a \`phases\` array, but entry ${bad} isn't a phase (needs a string \`name\` and a \`steps\` array).`,
|
|
4883
|
+
HOW_TO_EMIT
|
|
4884
|
+
);
|
|
4885
|
+
}
|
|
4886
|
+
const version = value.formatVersion;
|
|
4887
|
+
if (version !== void 0) {
|
|
4888
|
+
const major = Number(String(version).split(".")[0]);
|
|
4889
|
+
if (!Number.isFinite(major) || major !== SUPPORTED_MAJOR2) {
|
|
4890
|
+
return refuse2(
|
|
4891
|
+
`This op.json declares format version ${String(version)}; behold reads version ${SUPPORTED_MAJOR2}.`,
|
|
4892
|
+
"Upgrade behold, or render the Op with a behold that speaks its version."
|
|
4893
|
+
);
|
|
4894
|
+
}
|
|
4895
|
+
}
|
|
4896
|
+
return { ok: true, ir: value };
|
|
4897
|
+
}
|
|
4898
|
+
function readOpIr(path, readFile) {
|
|
4899
|
+
let text;
|
|
4900
|
+
try {
|
|
4901
|
+
text = readFile(path);
|
|
4902
|
+
} catch (err) {
|
|
4903
|
+
return refuse2(`Couldn't read ${path}: ${err instanceof Error ? err.message : String(err)}`, HOW_TO_EMIT);
|
|
4904
|
+
}
|
|
4905
|
+
let json;
|
|
4906
|
+
try {
|
|
4907
|
+
json = JSON.parse(text);
|
|
4908
|
+
} catch (err) {
|
|
4909
|
+
return refuse2(`${path} isn't valid JSON: ${err instanceof Error ? err.message : String(err)}`, HOW_TO_EMIT);
|
|
4910
|
+
}
|
|
4911
|
+
return parseOpIr(json);
|
|
4912
|
+
}
|
|
4913
|
+
function receiptName(step) {
|
|
4914
|
+
const r = step.receipt;
|
|
4915
|
+
if (typeof r === "string") return r;
|
|
4916
|
+
return r && typeof r.name === "string" && r.name || "effect";
|
|
4917
|
+
}
|
|
4918
|
+
function stepLabel(step) {
|
|
4919
|
+
if (step.kind === "gate") return `gate:${step.signalName}`;
|
|
4920
|
+
if (step.kind === "effect") return `effect:${receiptName(step)}`;
|
|
4921
|
+
return step.fn;
|
|
4922
|
+
}
|
|
4923
|
+
function stepKind(step) {
|
|
4924
|
+
if (step.kind === "gate") return "Gate";
|
|
4925
|
+
if (step.kind === "effect") return "Effect";
|
|
4926
|
+
return step.fn;
|
|
4927
|
+
}
|
|
4928
|
+
function stepStatus(step) {
|
|
4929
|
+
return step.kind === "gate" ? "warn" : step.kind === "effect" ? "accent" : "neutral";
|
|
4930
|
+
}
|
|
4931
|
+
function argsLine(args) {
|
|
4932
|
+
const entries = Object.entries(args ?? {});
|
|
4933
|
+
if (!entries.length) return void 0;
|
|
4934
|
+
return entries.map(([k, v]) => `${k}=${typeof v === "object" && v !== null ? JSON.stringify(v) : String(v)}`).join(", ");
|
|
4935
|
+
}
|
|
4936
|
+
function profileLine(p) {
|
|
4937
|
+
if (!p) return void 0;
|
|
4938
|
+
const parts = [];
|
|
4939
|
+
if (p.startToCloseTimeout) parts.push(`${p.startToCloseTimeout} start-to-close`);
|
|
4940
|
+
if (p.heartbeatTimeout) parts.push(`${p.heartbeatTimeout} heartbeat`);
|
|
4941
|
+
if (p.retry?.maximumAttempts) parts.push(`${p.retry.maximumAttempts} attempts`);
|
|
4942
|
+
if (p.retry?.initialInterval) parts.push(`from ${p.retry.initialInterval}`);
|
|
4943
|
+
return parts.length ? parts.join(", ") : void 0;
|
|
4944
|
+
}
|
|
4945
|
+
function placeSteps(op) {
|
|
4946
|
+
const out = [];
|
|
4947
|
+
const lists = [
|
|
4948
|
+
[op.phases ?? [], "phases"],
|
|
4949
|
+
[op.onFailure ?? [], "onFailure"]
|
|
4950
|
+
];
|
|
4951
|
+
const used = /* @__PURE__ */ new Set();
|
|
4952
|
+
for (const [phases, when] of lists) {
|
|
4953
|
+
for (const phase of phases) {
|
|
4954
|
+
const box = when === "onFailure" ? `${op.name} \xB7 onFailure: ${phase.name}` : `${op.name} \xB7 ${phase.name}`;
|
|
4955
|
+
for (const step of phase.steps ?? []) {
|
|
4956
|
+
let id = `${op.name}/${phase.name}/${stepLabel(step)}`;
|
|
4957
|
+
for (let n = 2; used.has(id); n += 1) id = `${op.name}/${phase.name}/${stepLabel(step)}#${n}`;
|
|
4958
|
+
used.add(id);
|
|
4959
|
+
out.push({ id, step, phase, when, box });
|
|
4960
|
+
}
|
|
4961
|
+
}
|
|
4962
|
+
}
|
|
4963
|
+
return out;
|
|
4964
|
+
}
|
|
4965
|
+
function opPlacedSteps(op) {
|
|
4966
|
+
return placeSteps(op).map((p) => ({
|
|
4967
|
+
id: p.id,
|
|
4968
|
+
step: p.step,
|
|
4969
|
+
phase: p.phase.name,
|
|
4970
|
+
parallel: Boolean(p.phase.parallel),
|
|
4971
|
+
when: p.when
|
|
4972
|
+
}));
|
|
4973
|
+
}
|
|
4974
|
+
var RULE_BOX = (opName) => `${opName} \xB7 rule table`;
|
|
4975
|
+
var ruleId = (opName, rule) => `${opName}/rules/${rule.id}`;
|
|
4976
|
+
var isPredicate = (v) => isRecord3(v) && typeof v.kind === "string";
|
|
4977
|
+
function isConvergeRule(v) {
|
|
4978
|
+
if (!isRecord3(v)) return false;
|
|
4979
|
+
if (typeof v.id !== "string" || !v.id) return false;
|
|
4980
|
+
if (typeof v.why !== "string" || !v.why) return false;
|
|
4981
|
+
if (!isPredicate(v.when)) return false;
|
|
4982
|
+
const then = v.then;
|
|
4983
|
+
if (!isRecord3(then)) return false;
|
|
4984
|
+
return then.kind === "run" && typeof then.op === "string" || then.kind === "report" && typeof then.reason === "string";
|
|
4985
|
+
}
|
|
4986
|
+
function convergeTable(op) {
|
|
4987
|
+
for (const placed of placeSteps(op)) {
|
|
4988
|
+
const { step } = placed;
|
|
4989
|
+
if (step.kind !== "activity" || step.fn !== "convergeTick") continue;
|
|
4990
|
+
const rules = step.args?.rules;
|
|
4991
|
+
if (!Array.isArray(rules)) return void 0;
|
|
4992
|
+
const kept = rules.filter(isConvergeRule);
|
|
4993
|
+
if (!kept.length) return void 0;
|
|
4994
|
+
return {
|
|
4995
|
+
tickStepId: placed.id,
|
|
4996
|
+
phase: placed.phase.name,
|
|
4997
|
+
rules: kept,
|
|
4998
|
+
...typeof step.args?.dial === "string" ? { dial: step.args.dial } : {},
|
|
4999
|
+
...typeof step.args?.budget === "number" ? { budget: step.args.budget } : {},
|
|
5000
|
+
...typeof step.args?.env === "string" ? { env: step.args.env } : {}
|
|
5001
|
+
};
|
|
5002
|
+
}
|
|
5003
|
+
return void 0;
|
|
5004
|
+
}
|
|
5005
|
+
var COMPARISON_TEXT = {
|
|
5006
|
+
eq: "=",
|
|
5007
|
+
neq: "!=",
|
|
5008
|
+
gt: ">",
|
|
5009
|
+
gte: ">=",
|
|
5010
|
+
lt: "<",
|
|
5011
|
+
lte: "<="
|
|
5012
|
+
};
|
|
5013
|
+
function predicateText(predicate) {
|
|
5014
|
+
switch (predicate.kind) {
|
|
5015
|
+
case "field-comparison": {
|
|
5016
|
+
const op = COMPARISON_TEXT[predicate.op];
|
|
5017
|
+
if (!op) return `(unreadable comparison on ${predicate.field})`;
|
|
5018
|
+
const value = typeof predicate.value === "string" ? JSON.stringify(predicate.value) : String(predicate.value);
|
|
5019
|
+
return `${predicate.field} ${op} ${value}`;
|
|
5020
|
+
}
|
|
5021
|
+
case "field-truthiness":
|
|
5022
|
+
return `${predicate.field} is ${predicate.op}`;
|
|
5023
|
+
case "all-of":
|
|
5024
|
+
case "any-of": {
|
|
5025
|
+
const joiner = predicate.kind === "all-of" ? " and " : " or ";
|
|
5026
|
+
const parts = (predicate.predicates ?? []).filter(isPredicate);
|
|
5027
|
+
if (!parts.length) return predicate.kind === "all-of" ? "(always)" : "(never)";
|
|
5028
|
+
return parts.map((p) => p.kind === "all-of" || p.kind === "any-of" ? `(${predicateText(p)})` : predicateText(p)).join(joiner);
|
|
5029
|
+
}
|
|
5030
|
+
default:
|
|
5031
|
+
return "(predicate outside the kinds behold reads)";
|
|
5032
|
+
}
|
|
5033
|
+
}
|
|
5034
|
+
function actionText(action) {
|
|
5035
|
+
return action.kind === "run" ? `run(${action.op})` : `report: ${action.reason}`;
|
|
5036
|
+
}
|
|
5037
|
+
function ruleStatus(rule, declared) {
|
|
5038
|
+
if (rule.then.kind !== "run") return "neutral";
|
|
5039
|
+
return declared.has(rule.then.op) ? "accent" : "warn";
|
|
5040
|
+
}
|
|
5041
|
+
function ruleNode(op, table, rule, declared) {
|
|
5042
|
+
const dispatches = rule.then.kind === "run" && declared.has(rule.then.op) ? rule.then.op : void 0;
|
|
5043
|
+
const dangling = rule.then.kind === "run" && !declared.has(rule.then.op) ? rule.then.op : void 0;
|
|
5044
|
+
return {
|
|
5045
|
+
id: ruleId(op.name, rule),
|
|
5046
|
+
// The card's kind, as an activity's is its `fn`: a converge rule is what
|
|
5047
|
+
// this is, and the whole table reads as one kind of thing in the track.
|
|
5048
|
+
kind: "ConvergeRule",
|
|
5049
|
+
lexicon: "op",
|
|
5050
|
+
attrs: {
|
|
5051
|
+
_status: ruleStatus(rule, declared),
|
|
5052
|
+
_step: "rule",
|
|
5053
|
+
rule: rule.id,
|
|
5054
|
+
op: op.name,
|
|
5055
|
+
phase: table.phase,
|
|
5056
|
+
// The condition, the action, and the rationale — the three columns a rule
|
|
5057
|
+
// table has. `why` is chant's required field, verbatim: the accessible-ops
|
|
5058
|
+
// promise is that the answer to "why did it do that" is already written
|
|
5059
|
+
// down, so behold's job is to show it, not to summarise it.
|
|
5060
|
+
when: predicateText(rule.when),
|
|
5061
|
+
then: actionText(rule.then),
|
|
5062
|
+
why: rule.why,
|
|
5063
|
+
...dispatches ? { dispatches } : {},
|
|
5064
|
+
// A `then` naming an Op that isn't in this set: stated, not guessed at.
|
|
5065
|
+
// chant's TMP014 refuses a run() naming an Op the PROJECT doesn't declare,
|
|
5066
|
+
// so this is behold's own composition boundary showing through, and saying
|
|
5067
|
+
// which one it is beats an edge into nowhere.
|
|
5068
|
+
...dangling ? { dangling: `${dangling} \u2014 not among the Ops rendered here, so no edge is drawn` } : {},
|
|
5069
|
+
// `flapThreshold` absent means chant's own default applies. Named the way
|
|
5070
|
+
// a gate's unset timeout is, rather than restating a number chant owns.
|
|
5071
|
+
flap: rule.flapThreshold !== void 0 ? `${rule.flapThreshold} consecutive ticks` : "(chant default)",
|
|
5072
|
+
...table.dial ? { dial: table.dial } : {}
|
|
5073
|
+
}
|
|
5074
|
+
};
|
|
5075
|
+
}
|
|
5076
|
+
function nodeFor(op, placed, guards) {
|
|
5077
|
+
const { step, phase, when } = placed;
|
|
5078
|
+
const common = {
|
|
5079
|
+
op: op.name,
|
|
5080
|
+
phase: phase.name,
|
|
5081
|
+
...phase.parallel ? { parallel: true } : {},
|
|
5082
|
+
...when === "onFailure" ? { when: "onFailure" } : {}
|
|
5083
|
+
};
|
|
5084
|
+
if (step.kind === "gate") {
|
|
5085
|
+
return {
|
|
5086
|
+
id: placed.id,
|
|
5087
|
+
kind: stepKind(step),
|
|
5088
|
+
lexicon: "op",
|
|
5089
|
+
attrs: {
|
|
5090
|
+
_status: stepStatus(step),
|
|
5091
|
+
_step: "gate",
|
|
5092
|
+
gate: step.signalName,
|
|
5093
|
+
timeout: step.timeout ?? "(chant default)",
|
|
5094
|
+
...common,
|
|
5095
|
+
...step.description ? { description: step.description } : {},
|
|
5096
|
+
// "gates drawn as their own marks on the step they guard" (#284): the
|
|
5097
|
+
// gate names what it holds back, so the pause reads from the gate card
|
|
5098
|
+
// without hunting for the next one in the track.
|
|
5099
|
+
...guards ? { guards } : {}
|
|
5100
|
+
}
|
|
5101
|
+
};
|
|
5102
|
+
}
|
|
5103
|
+
if (step.kind === "effect") {
|
|
5104
|
+
return {
|
|
5105
|
+
id: placed.id,
|
|
5106
|
+
kind: stepKind(step),
|
|
5107
|
+
lexicon: "op",
|
|
5108
|
+
attrs: {
|
|
5109
|
+
_status: stepStatus(step),
|
|
5110
|
+
_step: "effect",
|
|
5111
|
+
effect: receiptName(step),
|
|
5112
|
+
...common,
|
|
5113
|
+
...step.expectation !== void 0 ? { expectation: String(step.expectation) } : {},
|
|
5114
|
+
...step.description ? { description: step.description } : {},
|
|
5115
|
+
// The nested steps, named but not drawn — see OpIrEffectStep's doc.
|
|
5116
|
+
...step.steps?.length ? { runs: step.steps.map(stepLabel).join(" \u2192 ") } : {}
|
|
5117
|
+
}
|
|
5118
|
+
};
|
|
5119
|
+
}
|
|
5120
|
+
const profile = step.profile;
|
|
5121
|
+
const policy = profileLine(profile ? op.activityProfiles?.[profile] : void 0);
|
|
5122
|
+
const args = argsLine(step.args);
|
|
5123
|
+
return {
|
|
5124
|
+
id: placed.id,
|
|
5125
|
+
kind: stepKind(step),
|
|
5126
|
+
lexicon: "op",
|
|
5127
|
+
attrs: {
|
|
5128
|
+
_status: stepStatus(step),
|
|
5129
|
+
_step: "activity",
|
|
5130
|
+
fn: step.fn,
|
|
5131
|
+
...common,
|
|
5132
|
+
...profile ? { profile } : {},
|
|
5133
|
+
...policy ? { policy } : {},
|
|
5134
|
+
// The resolved args, verbatim from the IR. They carry the Op's SCOPE
|
|
5135
|
+
// (env, stack, template path) and never an estate entity id — see the
|
|
5136
|
+
// module doc on why no edge is drawn from them.
|
|
5137
|
+
...args ? { args } : {},
|
|
5138
|
+
...step.outcomeAttribute ? { outcome: step.outcomeAttribute.from ? `${step.outcomeAttribute.name} \u2190 ${step.outcomeAttribute.from}` : step.outcomeAttribute.name } : {},
|
|
5139
|
+
...op.activityContracts?.[step.fn] ? { contract: "declared (args/returns JSON Schema in op.json)" } : {}
|
|
5140
|
+
}
|
|
5141
|
+
};
|
|
5142
|
+
}
|
|
5143
|
+
function chainPhases(placed, when) {
|
|
5144
|
+
const byPhase = [];
|
|
5145
|
+
for (const p of placed) {
|
|
5146
|
+
if (p.when !== when) continue;
|
|
5147
|
+
const last = byPhase[byPhase.length - 1];
|
|
5148
|
+
if (last && last.phase === p.phase) last.ids.push(p.id);
|
|
5149
|
+
else byPhase.push({ phase: p.phase, ids: [p.id] });
|
|
5150
|
+
}
|
|
5151
|
+
return byPhase.map(
|
|
5152
|
+
({ phase, ids }) => phase.parallel ? { heads: ids, tails: ids } : { heads: ids.slice(0, 1), tails: ids.slice(-1) }
|
|
5153
|
+
);
|
|
5154
|
+
}
|
|
5155
|
+
function opsToIr(ops) {
|
|
5156
|
+
const nodes = [];
|
|
5157
|
+
const edges = [];
|
|
5158
|
+
const byStack = {};
|
|
5159
|
+
const endpoints = /* @__PURE__ */ new Map();
|
|
5160
|
+
const declared = new Set(ops.map((o) => o.name));
|
|
5161
|
+
const dispatches = [];
|
|
5162
|
+
for (const op of ops) {
|
|
5163
|
+
const placed = placeSteps(op);
|
|
5164
|
+
for (let i = 0; i < placed.length; i += 1) {
|
|
5165
|
+
const p = placed[i];
|
|
5166
|
+
const next = placed[i + 1];
|
|
5167
|
+
const guards = p.step.kind === "gate" && next && next.when === p.when ? stepLabel(next.step) : void 0;
|
|
5168
|
+
nodes.push(nodeFor(op, p, guards));
|
|
5169
|
+
(byStack[p.box] ??= []).push(p.id);
|
|
5170
|
+
}
|
|
5171
|
+
for (const when of ["phases", "onFailure"]) {
|
|
5172
|
+
const track = chainPhases(placed, when);
|
|
5173
|
+
for (let i = 0; i + 1 < track.length; i += 1) {
|
|
5174
|
+
for (const from of track[i].tails) {
|
|
5175
|
+
for (const to of track[i + 1].heads) edges.push({ from, to, kind: "ref", viaAttr: "then" });
|
|
5176
|
+
}
|
|
5177
|
+
}
|
|
5178
|
+
if (when === "phases" && track.length) {
|
|
5179
|
+
endpoints.set(op.name, { first: track[0].heads, last: track[track.length - 1].tails });
|
|
5180
|
+
}
|
|
5181
|
+
}
|
|
5182
|
+
const table = convergeTable(op);
|
|
5183
|
+
if (!table) continue;
|
|
5184
|
+
for (const rule of table.rules) {
|
|
5185
|
+
const node = ruleNode(op, table, rule, declared);
|
|
5186
|
+
nodes.push(node);
|
|
5187
|
+
(byStack[RULE_BOX(op.name)] ??= []).push(node.id);
|
|
5188
|
+
edges.push({ from: table.tickStepId, to: node.id, kind: "ref", viaAttr: "rule" });
|
|
5189
|
+
if (rule.then.kind === "run") dispatches.push({ from: node.id, op: rule.then.op });
|
|
5190
|
+
}
|
|
5191
|
+
}
|
|
5192
|
+
for (const op of ops) {
|
|
5193
|
+
const mine = endpoints.get(op.name);
|
|
5194
|
+
if (!mine) continue;
|
|
5195
|
+
for (const dep of op.depends ?? []) {
|
|
5196
|
+
const theirs = endpoints.get(dep);
|
|
5197
|
+
if (!theirs) continue;
|
|
5198
|
+
for (const from of theirs.last) {
|
|
5199
|
+
for (const to of mine.first) edges.push({ from, to, kind: "ref", viaAttr: "depends" });
|
|
5200
|
+
}
|
|
5201
|
+
}
|
|
5202
|
+
}
|
|
5203
|
+
for (const { from, op } of dispatches) {
|
|
5204
|
+
const target = endpoints.get(op);
|
|
5205
|
+
if (!target) continue;
|
|
5206
|
+
for (const to of target.first) edges.push({ from, to, kind: "ref", viaAttr: "run" });
|
|
5207
|
+
}
|
|
5208
|
+
return { nodes, edges, groups: { byStack } };
|
|
5209
|
+
}
|
|
5210
|
+
function opCardFields(node) {
|
|
5211
|
+
const step = node.attrs._step;
|
|
5212
|
+
const phase = typeof node.attrs.phase === "string" ? node.attrs.phase : void 0;
|
|
5213
|
+
const declared = step === "gate" ? [
|
|
5214
|
+
{ label: "gate", value: String(node.attrs.gate ?? "") },
|
|
5215
|
+
{ label: "timeout", value: String(node.attrs.timeout ?? "") }
|
|
5216
|
+
] : step === "effect" ? [
|
|
5217
|
+
{ label: "effect", value: String(node.attrs.effect ?? "") },
|
|
5218
|
+
...phase ? [{ label: "phase", value: phase }] : []
|
|
5219
|
+
] : step === "activity" ? [
|
|
5220
|
+
...phase ? [{ label: "phase", value: phase }] : [],
|
|
5221
|
+
...typeof node.attrs.profile === "string" ? [{ label: "profile", value: node.attrs.profile }] : []
|
|
5222
|
+
] : (
|
|
5223
|
+
// A converge rule card leads with the two columns that make it a
|
|
5224
|
+
// rule — the condition and what it does about it. `why` is the long
|
|
5225
|
+
// one and belongs in the inspect pane, where it reads in full
|
|
5226
|
+
// rather than being cut to fit a card.
|
|
5227
|
+
step === "rule" ? [
|
|
5228
|
+
{ label: "when", value: String(node.attrs.when ?? "") },
|
|
5229
|
+
{ label: "then", value: String(node.attrs.then ?? "") }
|
|
5230
|
+
] : void 0
|
|
5231
|
+
);
|
|
5232
|
+
if (!declared) return void 0;
|
|
5233
|
+
const run3 = typeof node.attrs.run === "string" ? node.attrs.run : void 0;
|
|
5234
|
+
return run3 ? [{ label: "run", value: run3 }, ...declared.slice(0, 1)] : declared;
|
|
5235
|
+
}
|
|
5236
|
+
function opsNote(ops, ir) {
|
|
5237
|
+
const gates = ir.nodes.filter((n) => n.attrs._step === "gate").length;
|
|
5238
|
+
const rules = ir.nodes.filter((n) => n.attrs._step === "rule");
|
|
5239
|
+
const steps = ir.nodes.length - rules.length;
|
|
5240
|
+
const dangling = rules.filter((n) => n.attrs.dangling !== void 0).length;
|
|
5241
|
+
const counts = [
|
|
5242
|
+
`${ops.length} declared Op${ops.length === 1 ? "" : "s"}`,
|
|
5243
|
+
`${steps} step${steps === 1 ? "" : "s"}`,
|
|
5244
|
+
...gates ? [`${gates} gate${gates === 1 ? "" : "s"}`] : [],
|
|
5245
|
+
...rules.length ? [`${rules.length} converge rule${rules.length === 1 ? "" : "s"}`] : []
|
|
5246
|
+
].join(" \xB7 ");
|
|
5247
|
+
const ruleNote = rules.length ? ` Each converge rule carries the why chant requires of it, verbatim, and a rule whose then is run(<op>) is drawn as an edge to that Op's first step.` + (dangling ? ` ${dangling} rule${dangling === 1 ? " names an Op" : "s name Ops"} not in this set \u2014 stated on the card, not drawn.` : "") : "";
|
|
5248
|
+
return `${counts}, from each Op's emitted dist/ops/<name>/op.json.${ruleNote} No estate cross-links: op.json names an Op's env and stack but never an estate entity id, so nothing here is joined to the graph's nodes (chant ask). Read-only: this renders the declared Op \u2014 nothing is started, signalled, or applied.`;
|
|
5249
|
+
}
|
|
5250
|
+
|
|
4164
5251
|
// src/render.ts
|
|
4165
5252
|
registerPack({ lexicon: "k8s", iconFor: k8sIconFor });
|
|
4166
5253
|
registerPack({ lexicon: "helm", iconFor: helmIconFor });
|
|
4167
5254
|
registerPack({ lexicon: "terraform", iconFor: () => void 0, fields: carveCardFields });
|
|
5255
|
+
registerPack({ lexicon: "op", iconFor: () => void 0, fields: opCardFields });
|
|
4168
5256
|
function renderArchitecture(ir, byContainer, opts = {}) {
|
|
4169
5257
|
const spread = Math.min(1.5, ir.edges.length / Math.max(ir.nodes.length, 1));
|
|
4170
5258
|
const layout = layoutArchitecture(ir, byContainer, {
|
|
@@ -4187,6 +5275,7 @@ function renderGraph(ir, opts = {}) {
|
|
|
4187
5275
|
const layout = layoutIr(ir, { fit: true, ...boxes ? { groups: boxes } : {} });
|
|
4188
5276
|
if (opts.radial && !boxes) radializeLayout(layout, groupKeyByNode(ir), footprints(ir));
|
|
4189
5277
|
else if (!boxes) packComponents(layout, ir);
|
|
5278
|
+
else if (boxKey === "byStack" && boxes) packMemberBoxes(layout, ir, boxes);
|
|
4190
5279
|
const svg = renderSvg(ir, layout, {
|
|
4191
5280
|
fit: true,
|
|
4192
5281
|
hideTitle: true,
|
|
@@ -4253,7 +5342,8 @@ function bandedPlan(ir, bands) {
|
|
|
4253
5342
|
return { placed, boxes, width: panelW, height: Math.max(1, top - BAND_GAP) };
|
|
4254
5343
|
}
|
|
4255
5344
|
function renderCarveEstate(tfIr, appIr, opts) {
|
|
4256
|
-
const
|
|
5345
|
+
const staged = withCarveState(tfIr, appIr, opts);
|
|
5346
|
+
const comp = composeCarveEstate(staged.tf, staged.appNs, { ...opts, carvedTitle: staged.carvedTitle });
|
|
4257
5347
|
const svg = renderSvg(comp.ir, comp.layout, {
|
|
4258
5348
|
fit: true,
|
|
4259
5349
|
hideTitle: true,
|
|
@@ -4262,6 +5352,17 @@ function renderCarveEstate(tfIr, appIr, opts) {
|
|
|
4262
5352
|
});
|
|
4263
5353
|
return { svg, ir: comp.ir };
|
|
4264
5354
|
}
|
|
5355
|
+
function withCarveState(tfIr, appIr, opts) {
|
|
5356
|
+
const states = opts.carved ?? /* @__PURE__ */ new Map();
|
|
5357
|
+
const { tf, graduated } = splitCarveState(tfIr, states, { ...opts.shorten ? { shorten: opts.shorten } : {} });
|
|
5358
|
+
const ns = namespaceAppIr(appIr, opts.appTitle);
|
|
5359
|
+
const progress = carveProgress(states.values(), tfIr.nodes.length);
|
|
5360
|
+
return {
|
|
5361
|
+
tf,
|
|
5362
|
+
appNs: { ...ns, nodes: [...ns.nodes, ...graduated] },
|
|
5363
|
+
carvedTitle: states.size ? `carved so far \u2014 ${progress.label}` : "carved so far"
|
|
5364
|
+
};
|
|
5365
|
+
}
|
|
4265
5366
|
function namespaceAppIr(appIr, appTitle) {
|
|
4266
5367
|
const prefix = `${appTitle.split(" ")[0]}/`;
|
|
4267
5368
|
const appId = (id) => `${prefix}${id}`;
|
|
@@ -4274,7 +5375,7 @@ function namespaceAppIr(appIr, appTitle) {
|
|
|
4274
5375
|
}
|
|
4275
5376
|
function composeCarveEstate(tfIr, appNs, opts) {
|
|
4276
5377
|
const tfPlan = bandedPlan(tfIr, tfIr.groups.byStack ?? {});
|
|
4277
|
-
const appPlan = bandedPlan(appNs, { "carved so far": appNs.nodes.map((n) => n.id) });
|
|
5378
|
+
const appPlan = bandedPlan(appNs, { [opts.carvedTitle ?? "carved so far"]: appNs.nodes.map((n) => n.id) });
|
|
4278
5379
|
const memberH = (plan) => TITLE + PAD + plan.height + PAD;
|
|
4279
5380
|
const memberW = (plan) => plan.width + PAD * 2;
|
|
4280
5381
|
const appX0 = memberW(tfPlan) + MEMBER_GAP;
|
|
@@ -4313,23 +5414,25 @@ function composeCarveEstate(tfIr, appNs, opts) {
|
|
|
4313
5414
|
};
|
|
4314
5415
|
}
|
|
4315
5416
|
function renderCarveMorph(tfIr, appIr, carved, opts) {
|
|
4316
|
-
const
|
|
4317
|
-
const
|
|
5417
|
+
const staged = withCarveState(tfIr, appIr, opts);
|
|
5418
|
+
const appNs = staged.appNs;
|
|
5419
|
+
const morphOpts = { ...opts, carvedTitle: "carved so far" };
|
|
5420
|
+
const before = composeCarveEstate(staged.tf, appNs, morphOpts);
|
|
4318
5421
|
const gone = new Set(carved);
|
|
4319
|
-
const bands =
|
|
5422
|
+
const bands = staged.tf.groups.byStack ?? {};
|
|
4320
5423
|
const bandsAfter = {};
|
|
4321
5424
|
for (const [band, members] of Object.entries(bands)) {
|
|
4322
5425
|
const left = members.filter((id) => !gone.has(id));
|
|
4323
5426
|
if (left.length) bandsAfter[band] = left;
|
|
4324
5427
|
}
|
|
4325
5428
|
const tfAfter = {
|
|
4326
|
-
nodes:
|
|
4327
|
-
edges:
|
|
5429
|
+
nodes: staged.tf.nodes.filter((n) => !gone.has(n.id)),
|
|
5430
|
+
edges: staged.tf.edges.filter((e) => !gone.has(e.from) && !gone.has(e.to)),
|
|
4328
5431
|
groups: { byStack: bandsAfter }
|
|
4329
5432
|
};
|
|
4330
|
-
const carvedNodes =
|
|
5433
|
+
const carvedNodes = staged.tf.nodes.filter((n) => gone.has(n.id)).map((n) => ({ ...n, attrs: { ...n.attrs, _status: "good", carve: "carved \u2192 chant" } }));
|
|
4331
5434
|
const appAfter = { ...appNs, nodes: [...appNs.nodes, ...carvedNodes] };
|
|
4332
|
-
const after = composeCarveEstate(tfAfter, appAfter,
|
|
5435
|
+
const after = composeCarveEstate(tfAfter, appAfter, morphOpts);
|
|
4333
5436
|
const view = (name, comp) => ({
|
|
4334
5437
|
name,
|
|
4335
5438
|
ir: comp.ir,
|
|
@@ -4409,6 +5512,92 @@ function packComponents(layout, ir) {
|
|
|
4409
5512
|
layout.width = Math.max(...nodes.map((n) => n.x + halfW(n))) + pad;
|
|
4410
5513
|
layout.height = Math.max(...nodes.map((n) => n.y + halfH(n))) + pad;
|
|
4411
5514
|
}
|
|
5515
|
+
function packMemberBoxes(layout, ir, members) {
|
|
5516
|
+
const boxes = layout.groups ?? [];
|
|
5517
|
+
if (boxes.length < 4) return;
|
|
5518
|
+
const nodes = layout.nodes;
|
|
5519
|
+
const size = footprints(ir);
|
|
5520
|
+
const halfW = (n) => (size.get(n.id)?.w ?? NODE_W) / 2;
|
|
5521
|
+
const halfH = (n) => (size.get(n.id)?.h ?? NODE_H) / 2;
|
|
5522
|
+
for (const n of nodes) n.y = layout.height - n.y;
|
|
5523
|
+
for (const b of boxes) b.y = layout.height - b.y;
|
|
5524
|
+
const byId = new Map(nodes.map((n) => [n.id, n]));
|
|
5525
|
+
const claimed = /* @__PURE__ */ new Set();
|
|
5526
|
+
const blocks = boxes.map((box) => {
|
|
5527
|
+
const ns = (members[box.id ?? box.title] ?? members[box.title] ?? []).flatMap((id) => byId.get(id) ?? []);
|
|
5528
|
+
for (const n of ns) claimed.add(n.id);
|
|
5529
|
+
let minX = box.x - box.w / 2;
|
|
5530
|
+
let maxX2 = box.x + box.w / 2;
|
|
5531
|
+
let minY = box.y - box.h / 2;
|
|
5532
|
+
let maxY2 = box.y + box.h / 2;
|
|
5533
|
+
for (const n of ns) {
|
|
5534
|
+
minX = Math.min(minX, n.x - halfW(n));
|
|
5535
|
+
maxX2 = Math.max(maxX2, n.x + halfW(n));
|
|
5536
|
+
minY = Math.min(minY, n.y - halfH(n));
|
|
5537
|
+
maxY2 = Math.max(maxY2, n.y + halfH(n));
|
|
5538
|
+
}
|
|
5539
|
+
return { ns, box, minX, minY, w: maxX2 - minX, h: maxY2 - minY };
|
|
5540
|
+
});
|
|
5541
|
+
const rest = nodes.filter((n) => !claimed.has(n.id));
|
|
5542
|
+
if (rest.length) {
|
|
5543
|
+
const idxOf = new Map(rest.map((n, i) => [n.id, i]));
|
|
5544
|
+
const parent = rest.map((_, i) => i);
|
|
5545
|
+
const find = (x) => {
|
|
5546
|
+
while (parent[x] !== x) x = parent[x] = parent[parent[x]];
|
|
5547
|
+
return x;
|
|
5548
|
+
};
|
|
5549
|
+
for (const e of ir.edges) {
|
|
5550
|
+
const a = idxOf.get(e.from);
|
|
5551
|
+
const b = idxOf.get(e.to);
|
|
5552
|
+
if (a != null && b != null) parent[find(a)] = find(b);
|
|
5553
|
+
}
|
|
5554
|
+
const comps = /* @__PURE__ */ new Map();
|
|
5555
|
+
rest.forEach((n, i) => (comps.get(find(i)) ?? comps.set(find(i), []).get(find(i))).push(n));
|
|
5556
|
+
for (const ns of comps.values()) {
|
|
5557
|
+
const minX = Math.min(...ns.map((n) => n.x - halfW(n)));
|
|
5558
|
+
const minY = Math.min(...ns.map((n) => n.y - halfH(n)));
|
|
5559
|
+
blocks.push({
|
|
5560
|
+
ns,
|
|
5561
|
+
minX,
|
|
5562
|
+
minY,
|
|
5563
|
+
w: Math.max(...ns.map((n) => n.x + halfW(n))) - minX,
|
|
5564
|
+
h: Math.max(...ns.map((n) => n.y + halfH(n))) - minY
|
|
5565
|
+
});
|
|
5566
|
+
}
|
|
5567
|
+
}
|
|
5568
|
+
const totalArea = blocks.reduce((s, b) => s + (b.w + MEMBER_GAP) * (b.h + MEMBER_GAP), 0);
|
|
5569
|
+
const targetW = Math.max(Math.max(...blocks.map((b) => b.w)), Math.sqrt(totalArea) * 1.3);
|
|
5570
|
+
let shelfX = 0;
|
|
5571
|
+
let shelfY = 0;
|
|
5572
|
+
let shelfH = 0;
|
|
5573
|
+
let maxX = 0;
|
|
5574
|
+
let maxY = 0;
|
|
5575
|
+
for (const b of blocks) {
|
|
5576
|
+
if (shelfX > 0 && shelfX + b.w > targetW) {
|
|
5577
|
+
shelfX = 0;
|
|
5578
|
+
shelfY += shelfH + MEMBER_GAP;
|
|
5579
|
+
shelfH = 0;
|
|
5580
|
+
}
|
|
5581
|
+
const dx = shelfX - b.minX;
|
|
5582
|
+
const dy = shelfY - b.minY;
|
|
5583
|
+
for (const n of b.ns) {
|
|
5584
|
+
n.x += dx;
|
|
5585
|
+
n.y += dy;
|
|
5586
|
+
}
|
|
5587
|
+
if (b.box) {
|
|
5588
|
+
b.box.x += dx;
|
|
5589
|
+
b.box.y += dy;
|
|
5590
|
+
}
|
|
5591
|
+
maxX = Math.max(maxX, shelfX + b.w);
|
|
5592
|
+
maxY = Math.max(maxY, shelfY + b.h);
|
|
5593
|
+
shelfX += b.w + MEMBER_GAP;
|
|
5594
|
+
shelfH = Math.max(shelfH, b.h);
|
|
5595
|
+
}
|
|
5596
|
+
layout.width = maxX;
|
|
5597
|
+
layout.height = maxY;
|
|
5598
|
+
for (const n of nodes) n.y = maxY - n.y;
|
|
5599
|
+
for (const b of boxes) b.y = maxY - b.y;
|
|
5600
|
+
}
|
|
4412
5601
|
function groupKeyByNode(ir) {
|
|
4413
5602
|
const out = /* @__PURE__ */ new Map();
|
|
4414
5603
|
for (const n of ir.nodes) {
|
|
@@ -4505,12 +5694,12 @@ function radializeLayout(layout, groupOf, size = /* @__PURE__ */ new Map()) {
|
|
|
4505
5694
|
}
|
|
4506
5695
|
|
|
4507
5696
|
// src/carve-actions.ts
|
|
4508
|
-
import { existsSync as existsSync8, mkdirSync as mkdirSync3, readFileSync as
|
|
4509
|
-
import { isAbsolute, join as
|
|
5697
|
+
import { existsSync as existsSync8, mkdirSync as mkdirSync3, readFileSync as readFileSync10, readdirSync as readdirSync5, statSync as statSync5 } from "node:fs";
|
|
5698
|
+
import { isAbsolute, join as join13, relative as relative2, resolve as resolve4, sep as sep2 } from "node:path";
|
|
4510
5699
|
|
|
4511
5700
|
// src/layout.ts
|
|
4512
|
-
import { existsSync as existsSync6, mkdirSync as mkdirSync2, readFileSync as
|
|
4513
|
-
import { join as
|
|
5701
|
+
import { existsSync as existsSync6, mkdirSync as mkdirSync2, readFileSync as readFileSync8, renameSync, rmSync, writeFileSync as writeFileSync2, accessSync, constants } from "node:fs";
|
|
5702
|
+
import { join as join11 } from "node:path";
|
|
4514
5703
|
var LAYOUT_DIR = ".behold";
|
|
4515
5704
|
var LAYOUT_FILE = "layout.json";
|
|
4516
5705
|
var MAX_BODY_BYTES = 64 * 1024;
|
|
@@ -4551,7 +5740,7 @@ function lensFromQuery(params) {
|
|
|
4551
5740
|
return [slug(zoom), params.get("radial") === "1" ? "radial" : null, stack ? slug(`stack-${stack}`) : null].filter(Boolean).join("+");
|
|
4552
5741
|
}
|
|
4553
5742
|
function layoutPath(projectDir) {
|
|
4554
|
-
return
|
|
5743
|
+
return join11(projectDir, LAYOUT_DIR, LAYOUT_FILE);
|
|
4555
5744
|
}
|
|
4556
5745
|
function unwritableReason(projectDir) {
|
|
4557
5746
|
if (!projectDir || !existsSync6(projectDir)) return `no such project directory: ${projectDir || "(none)"}`;
|
|
@@ -4567,7 +5756,7 @@ function readLayoutFile(projectDir) {
|
|
|
4567
5756
|
const file = layoutPath(projectDir);
|
|
4568
5757
|
try {
|
|
4569
5758
|
if (!existsSync6(file)) return empty;
|
|
4570
|
-
const raw =
|
|
5759
|
+
const raw = readFileSync8(file, "utf8");
|
|
4571
5760
|
if (raw.length > MAX_FILE_BYTES) return empty;
|
|
4572
5761
|
const parsed = JSON.parse(raw);
|
|
4573
5762
|
if (!parsed || typeof parsed !== "object" || !parsed.lenses || typeof parsed.lenses !== "object") return empty;
|
|
@@ -4607,7 +5796,7 @@ function writeLens(projectDir, lens, deltas) {
|
|
|
4607
5796
|
}
|
|
4608
5797
|
const body = JSON.stringify({ version: 1, lenses }, null, 2) + "\n";
|
|
4609
5798
|
if (body.length > MAX_FILE_BYTES) throw new LayoutTooLarge(`a layout sidecar is capped at ${MAX_FILE_BYTES} bytes`);
|
|
4610
|
-
mkdirSync2(
|
|
5799
|
+
mkdirSync2(join11(projectDir, LAYOUT_DIR), { recursive: true });
|
|
4611
5800
|
const tmp = `${file}.tmp`;
|
|
4612
5801
|
writeFileSync2(tmp, body, "utf8");
|
|
4613
5802
|
renameSync(tmp, file);
|
|
@@ -4692,8 +5881,8 @@ function reanchorEdges(svg, moved) {
|
|
|
4692
5881
|
|
|
4693
5882
|
// src/carve-live.ts
|
|
4694
5883
|
import { spawn as spawn3 } from "node:child_process";
|
|
4695
|
-
import { readFileSync as
|
|
4696
|
-
import { join as
|
|
5884
|
+
import { readFileSync as readFileSync9, writeFileSync as writeFileSync3, existsSync as existsSync7 } from "node:fs";
|
|
5885
|
+
import { join as join12 } from "node:path";
|
|
4697
5886
|
var LIVE_CONTAINER = "behold-carve-floci";
|
|
4698
5887
|
var LIVE_PORT = 4602;
|
|
4699
5888
|
var LIVE_IMAGE = "floci/floci:1.5.34";
|
|
@@ -4752,9 +5941,9 @@ async function teardownScratchFloci() {
|
|
|
4752
5941
|
await capture("docker", ["rm", "-f", LIVE_CONTAINER], process.cwd()).catch(() => void 0);
|
|
4753
5942
|
}
|
|
4754
5943
|
async function applyIntoFloci(fromDir, step) {
|
|
4755
|
-
const template =
|
|
5944
|
+
const template = join12(fromDir, "floci-override.tf.disabled");
|
|
4756
5945
|
if (!existsSync7(template)) return `no floci-override.tf.disabled in ${fromDir} \u2014 this estate wasn't authored for the live tier`;
|
|
4757
|
-
writeFileSync3(
|
|
5946
|
+
writeFileSync3(join12(fromDir, "floci_override.tf"), armOverride(readFileSync9(template, "utf8"), LIVE_PORT));
|
|
4758
5947
|
const init = await step("terraform", ["init", "-input=false", "-no-color"], fromDir);
|
|
4759
5948
|
if (init !== 0) return `terraform init exited ${init} (provider downloads need network)`;
|
|
4760
5949
|
const apply = await step(
|
|
@@ -4778,7 +5967,7 @@ ${tail}`, exitCode: r.code };
|
|
|
4778
5967
|
// src/carve-actions.ts
|
|
4779
5968
|
async function runCarvePlan(demo) {
|
|
4780
5969
|
if (!demo?.live) {
|
|
4781
|
-
return
|
|
5970
|
+
return refuse3(
|
|
4782
5971
|
"carve-action",
|
|
4783
5972
|
"terraform plan is the live tier's beat, and this server isn't serving one",
|
|
4784
5973
|
"run `behold demo carve --live` (needs docker + terraform)"
|
|
@@ -4786,7 +5975,7 @@ async function runCarvePlan(demo) {
|
|
|
4786
5975
|
}
|
|
4787
5976
|
const r = await runLivePlan(demo.from);
|
|
4788
5977
|
if ("error" in r) {
|
|
4789
|
-
return
|
|
5978
|
+
return refuse3("carve-action", r.error, "Is the scratch Floci still up? `docker ps` should list " + demo.live.container + ".");
|
|
4790
5979
|
}
|
|
4791
5980
|
return {
|
|
4792
5981
|
ok: true,
|
|
@@ -4796,16 +5985,85 @@ async function runCarvePlan(demo) {
|
|
|
4796
5985
|
noDestroy: r.noDestroy
|
|
4797
5986
|
};
|
|
4798
5987
|
}
|
|
5988
|
+
async function runCarveObserve(demo, select) {
|
|
5989
|
+
if (!demo.live) {
|
|
5990
|
+
return refuse3(
|
|
5991
|
+
"carve-action",
|
|
5992
|
+
"the observe beat reads the scratch Floci, and this server isn't serving one",
|
|
5993
|
+
"run `behold demo carve --live` (needs docker + terraform)"
|
|
5994
|
+
);
|
|
5995
|
+
}
|
|
5996
|
+
if (!existsSync8(join13(demo.out, "src"))) {
|
|
5997
|
+
return refuse3("carve-action", "nothing has been emitted yet", "run Emit first \u2014 observe reads the carveout emit writes");
|
|
5998
|
+
}
|
|
5999
|
+
const args = ["lifecycle", "diff", "prod", "--live", "--json"];
|
|
6000
|
+
const run3 = await runChantRaw(args, demo.out, { AWS_ENDPOINT_URL: demo.live.endpoint }).catch((err) => ({
|
|
6001
|
+
code: 127,
|
|
6002
|
+
stdout: "",
|
|
6003
|
+
stderr: err instanceof Error ? err.message : String(err)
|
|
6004
|
+
}));
|
|
6005
|
+
if (run3.code !== 0) {
|
|
6006
|
+
return refuse3(
|
|
6007
|
+
"carve-action",
|
|
6008
|
+
`chant lifecycle diff exited ${run3.code}: ${merge(run3, demo.root) || "(no output)"}`,
|
|
6009
|
+
"Is the scratch Floci still up? `docker ps` should list " + demo.live.container + "."
|
|
6010
|
+
);
|
|
6011
|
+
}
|
|
6012
|
+
let parsed;
|
|
6013
|
+
try {
|
|
6014
|
+
parsed = JSON.parse(run3.stdout);
|
|
6015
|
+
} catch {
|
|
6016
|
+
return refuse3("carve-action", "chant lifecycle diff answered something that isn't JSON", "re-run \u2014 a partial read is not a verdict");
|
|
6017
|
+
}
|
|
6018
|
+
if (parsed.stacks && !parsed.lexicons) {
|
|
6019
|
+
return refuse3(
|
|
6020
|
+
"carve-action",
|
|
6021
|
+
"chant lifecycle diff answered a stacked shape ({stacks}) \u2014 observe only reads the stack-less {lexicons} shape",
|
|
6022
|
+
"point the target at a stack-less carveout, or file a shape-read gap against observe for the stacked case"
|
|
6023
|
+
);
|
|
6024
|
+
}
|
|
6025
|
+
const aws = parsed.lexicons?.aws ?? {};
|
|
6026
|
+
const entity = select.split(".").pop() ?? select;
|
|
6027
|
+
const command = `AWS_ENDPOINT_URL=${demo.live.endpoint} chant ${args.join(" ")} # in ${shortenIn(demo.out, demo.root)}`;
|
|
6028
|
+
const queried = aws.resources?.queried?.[entity];
|
|
6029
|
+
const meta = aws.observed?.[entity];
|
|
6030
|
+
if (meta) {
|
|
6031
|
+
return {
|
|
6032
|
+
ok: true,
|
|
6033
|
+
select,
|
|
6034
|
+
command,
|
|
6035
|
+
entity,
|
|
6036
|
+
verdict: "observed",
|
|
6037
|
+
...meta.status ? { status: meta.status } : {},
|
|
6038
|
+
...meta.ownership ? { ownership: meta.ownership } : {},
|
|
6039
|
+
...meta.physicalId ? { physicalId: meta.physicalId } : {},
|
|
6040
|
+
...queried ? { queried } : {}
|
|
6041
|
+
};
|
|
6042
|
+
}
|
|
6043
|
+
const hole = aws.resources?.unobserved?.find((u) => u.name === entity);
|
|
6044
|
+
if (hole) {
|
|
6045
|
+
return {
|
|
6046
|
+
ok: true,
|
|
6047
|
+
select,
|
|
6048
|
+
command,
|
|
6049
|
+
entity,
|
|
6050
|
+
verdict: "unobserved",
|
|
6051
|
+
...hole.detail || hole.reason ? { detail: [hole.reason, hole.detail].filter(Boolean).join(": ") } : {},
|
|
6052
|
+
...queried ? { queried } : {}
|
|
6053
|
+
};
|
|
6054
|
+
}
|
|
6055
|
+
return { ok: true, select, command, entity, verdict: "missing", ...queried ? { queried } : {} };
|
|
6056
|
+
}
|
|
4799
6057
|
var MAX_ARTIFACT_BYTES = 64 * 1024;
|
|
4800
6058
|
var MAX_ARTIFACTS = 24;
|
|
4801
6059
|
var MAX_OUTPUT_BYTES = 32 * 1024;
|
|
4802
|
-
var
|
|
6060
|
+
var refuse3 = (code, error, remedy) => ({
|
|
4803
6061
|
ok: false,
|
|
4804
6062
|
refusal: { error, code, remedy }
|
|
4805
6063
|
});
|
|
4806
6064
|
var BUILD_CAVEAT = "The gate shown is `chant lint`, not `chant build`. chant#1637's fold is applied now: the bucket's versioning and public-access-block sub-resources come out as native props, not just folded-in mentions. `chant build` still fails, but on one rule only \u2014 WAW042, a TLS-deny bucket policy the source Terraform never declared. That's real drift the estate inherited, not a gap in the carve. example-carve/README.md, \"Why build still fails\", has the full statement.";
|
|
4807
6065
|
function insideDemo(root, p) {
|
|
4808
|
-
const rel =
|
|
6066
|
+
const rel = relative2(resolve4(root), resolve4(p));
|
|
4809
6067
|
return rel !== "" && !rel.startsWith("..") && !isAbsolute(rel);
|
|
4810
6068
|
}
|
|
4811
6069
|
function selectFromReport(report, raw) {
|
|
@@ -4826,17 +6084,17 @@ function readArtifacts(demo, dir, filter) {
|
|
|
4826
6084
|
if (out.length >= MAX_ARTIFACTS || depth > 2) return;
|
|
4827
6085
|
let entries;
|
|
4828
6086
|
try {
|
|
4829
|
-
entries =
|
|
6087
|
+
entries = readdirSync5(d).sort();
|
|
4830
6088
|
} catch {
|
|
4831
6089
|
return;
|
|
4832
6090
|
}
|
|
4833
6091
|
for (const name of entries) {
|
|
4834
6092
|
if (out.length >= MAX_ARTIFACTS) return;
|
|
4835
6093
|
if (name === "node_modules" || name.startsWith(".")) continue;
|
|
4836
|
-
const full =
|
|
6094
|
+
const full = join13(d, name);
|
|
4837
6095
|
let st;
|
|
4838
6096
|
try {
|
|
4839
|
-
st =
|
|
6097
|
+
st = statSync5(full);
|
|
4840
6098
|
} catch {
|
|
4841
6099
|
continue;
|
|
4842
6100
|
}
|
|
@@ -4844,11 +6102,11 @@ function readArtifacts(demo, dir, filter) {
|
|
|
4844
6102
|
walk(full, depth + 1);
|
|
4845
6103
|
continue;
|
|
4846
6104
|
}
|
|
4847
|
-
const rel =
|
|
6105
|
+
const rel = relative2(demo.root, full).split(sep2).join("/");
|
|
4848
6106
|
if (!filter(rel)) continue;
|
|
4849
6107
|
let text = "";
|
|
4850
6108
|
try {
|
|
4851
|
-
text =
|
|
6109
|
+
text = readFileSync10(full, "utf8");
|
|
4852
6110
|
} catch {
|
|
4853
6111
|
continue;
|
|
4854
6112
|
}
|
|
@@ -4870,17 +6128,17 @@ function carveSlug(select) {
|
|
|
4870
6128
|
}
|
|
4871
6129
|
var clip = (s) => s.length > MAX_OUTPUT_BYTES ? s.slice(0, MAX_OUTPUT_BYTES) + "\n\u2026 (truncated)" : s;
|
|
4872
6130
|
function shortenIn(arg, root) {
|
|
4873
|
-
return arg.startsWith(root +
|
|
6131
|
+
return arg.startsWith(root + sep2) ? arg.slice(root.length + 1) : arg === root ? "." : arg;
|
|
4874
6132
|
}
|
|
4875
6133
|
function merge(r, root) {
|
|
4876
6134
|
const joined = stripAnsi([r.stdout, r.stderr].filter((s) => s.trim()).join("\n").trim());
|
|
4877
|
-
return clip(joined.split(root +
|
|
6135
|
+
return clip(joined.split(root + sep2).join("").split(root).join("."));
|
|
4878
6136
|
}
|
|
4879
6137
|
async function runCarveEmit(demo, select) {
|
|
4880
6138
|
const block = carveWriteBlock(demo);
|
|
4881
|
-
if (block) return
|
|
6139
|
+
if (block) return refuse3("read-only", block, "Start the walkthrough with `behold demo carve`.");
|
|
4882
6140
|
mkdirSync3(demo.out, { recursive: true });
|
|
4883
|
-
const reportFile =
|
|
6141
|
+
const reportFile = join13(demo.out, `${carveSlug(select)}-boundary.json`);
|
|
4884
6142
|
const args = [
|
|
4885
6143
|
"carve",
|
|
4886
6144
|
"emit",
|
|
@@ -4901,7 +6159,7 @@ async function runCarveEmit(demo, select) {
|
|
|
4901
6159
|
}));
|
|
4902
6160
|
const output = merge(run3, demo.root);
|
|
4903
6161
|
if (run3.code !== 0) {
|
|
4904
|
-
return
|
|
6162
|
+
return refuse3(
|
|
4905
6163
|
"carve-action",
|
|
4906
6164
|
`chant carve emit exited ${run3.code}: ${output || "(no output)"}`,
|
|
4907
6165
|
"The offline emit needs `@cdktf/hcl2json` in the demo copy and the demo's own chant install \u2014 `npm install` in the copy's `app/` and re-run `behold demo carve`."
|
|
@@ -4909,10 +6167,10 @@ async function runCarveEmit(demo, select) {
|
|
|
4909
6167
|
}
|
|
4910
6168
|
let boundary = null;
|
|
4911
6169
|
try {
|
|
4912
|
-
boundary = JSON.parse(
|
|
6170
|
+
boundary = JSON.parse(readFileSync10(reportFile, "utf8"));
|
|
4913
6171
|
} catch {
|
|
4914
6172
|
}
|
|
4915
|
-
const lintPath =
|
|
6173
|
+
const lintPath = relative2(demo.project, join13(demo.out, "src")).split(sep2).join("/");
|
|
4916
6174
|
const lintArgs = ["lint", lintPath];
|
|
4917
6175
|
const lint = await runChantRaw(lintArgs, demo.project).catch((err) => ({
|
|
4918
6176
|
code: 127,
|
|
@@ -4936,7 +6194,7 @@ async function runCarveEmit(demo, select) {
|
|
|
4936
6194
|
}
|
|
4937
6195
|
async function runCarveBridge(demo, select) {
|
|
4938
6196
|
const block = carveWriteBlock(demo);
|
|
4939
|
-
if (block) return
|
|
6197
|
+
if (block) return refuse3("read-only", block, "Start the walkthrough with `behold demo carve`.");
|
|
4940
6198
|
mkdirSync3(demo.out, { recursive: true });
|
|
4941
6199
|
const args = [
|
|
4942
6200
|
"carve",
|
|
@@ -4956,7 +6214,7 @@ async function runCarveBridge(demo, select) {
|
|
|
4956
6214
|
}));
|
|
4957
6215
|
const output = merge(run3, demo.root);
|
|
4958
6216
|
if (run3.code !== 0) {
|
|
4959
|
-
return
|
|
6217
|
+
return refuse3(
|
|
4960
6218
|
"carve-action",
|
|
4961
6219
|
`chant carve bridge exited ${run3.code}: ${output || "(no output)"}`,
|
|
4962
6220
|
"Run the Emit step first \u2014 bridge reads the carve manifest emit leaves in the output directory."
|
|
@@ -4975,8 +6233,8 @@ async function runCarveBridge(demo, select) {
|
|
|
4975
6233
|
}
|
|
4976
6234
|
|
|
4977
6235
|
// src/ops.ts
|
|
4978
|
-
import { readdirSync as
|
|
4979
|
-
import { join as
|
|
6236
|
+
import { readdirSync as readdirSync6, readFileSync as readFileSync11, existsSync as existsSync9 } from "node:fs";
|
|
6237
|
+
import { join as join14 } from "node:path";
|
|
4980
6238
|
var APPLY_TARGET_LEXICON = {
|
|
4981
6239
|
cloudformation: "aws",
|
|
4982
6240
|
kubectl: "k8s",
|
|
@@ -4994,45 +6252,623 @@ function kindOf(content) {
|
|
|
4994
6252
|
if (/AuditOp\b/.test(content)) return "audit";
|
|
4995
6253
|
return "op";
|
|
4996
6254
|
}
|
|
4997
|
-
function discoverOps(projectDir) {
|
|
4998
|
-
const seen = /* @__PURE__ */ new Set();
|
|
6255
|
+
function discoverOps(projectDir) {
|
|
6256
|
+
const seen = /* @__PURE__ */ new Set();
|
|
6257
|
+
const out = [];
|
|
6258
|
+
for (const sub of ["ops", "src", "."]) {
|
|
6259
|
+
const dir = join14(projectDir, sub);
|
|
6260
|
+
if (!existsSync9(dir)) continue;
|
|
6261
|
+
for (const f of readdirSync6(dir)) {
|
|
6262
|
+
if (!f.endsWith(".op.ts")) continue;
|
|
6263
|
+
const content = readFileSync11(join14(dir, f), "utf8");
|
|
6264
|
+
const name = content.match(/name:\s*["'`]([^"'`]+)["'`]/)?.[1];
|
|
6265
|
+
if (!name || seen.has(name)) continue;
|
|
6266
|
+
seen.add(name);
|
|
6267
|
+
const gate = content.match(/signalName:\s*["'`]([^"'`]+)["'`]/)?.[1];
|
|
6268
|
+
const env = content.match(/\benv:\s*["'`]([^"'`]+)["'`]/)?.[1];
|
|
6269
|
+
const target = content.match(/\btarget:\s*["'`]([^"'`]+)["'`]/)?.[1];
|
|
6270
|
+
const substrate = target ? APPLY_TARGET_LEXICON[target] : void 0;
|
|
6271
|
+
const irPath = opIrPath(projectDir, name);
|
|
6272
|
+
out.push({
|
|
6273
|
+
name,
|
|
6274
|
+
kind: kindOf(content),
|
|
6275
|
+
dir: projectDir,
|
|
6276
|
+
...gate ? { gate } : {},
|
|
6277
|
+
...env ? { env } : {},
|
|
6278
|
+
...substrate ? { substrate } : {},
|
|
6279
|
+
...existsSync9(irPath) ? { irPath } : {}
|
|
6280
|
+
});
|
|
6281
|
+
}
|
|
6282
|
+
}
|
|
6283
|
+
return out.sort((a, b) => a.name.localeCompare(b.name));
|
|
6284
|
+
}
|
|
6285
|
+
function opIrPath(projectDir, name) {
|
|
6286
|
+
return join14(projectDir, "dist", "ops", name, "op.json");
|
|
6287
|
+
}
|
|
6288
|
+
function discoverOpIrs(projectDirs) {
|
|
6289
|
+
const seen = /* @__PURE__ */ new Set();
|
|
6290
|
+
const out = [];
|
|
6291
|
+
for (const dir of projectDirs) {
|
|
6292
|
+
const root = join14(dir, "dist", "ops");
|
|
6293
|
+
if (!existsSync9(root)) continue;
|
|
6294
|
+
for (const name of readdirSync6(root)) {
|
|
6295
|
+
if (seen.has(name)) continue;
|
|
6296
|
+
const path = opIrPath(dir, name);
|
|
6297
|
+
if (!existsSync9(path)) continue;
|
|
6298
|
+
seen.add(name);
|
|
6299
|
+
out.push({ name, dir, path });
|
|
6300
|
+
}
|
|
6301
|
+
}
|
|
6302
|
+
return out.sort((a, b) => a.name.localeCompare(b.name));
|
|
6303
|
+
}
|
|
6304
|
+
function discoverEstateOps(projectDirs) {
|
|
6305
|
+
const seen = /* @__PURE__ */ new Set();
|
|
6306
|
+
const out = [];
|
|
6307
|
+
for (const dir of projectDirs) {
|
|
6308
|
+
for (const op of discoverOps(dir)) {
|
|
6309
|
+
if (seen.has(op.name)) continue;
|
|
6310
|
+
seen.add(op.name);
|
|
6311
|
+
out.push(op);
|
|
6312
|
+
}
|
|
6313
|
+
}
|
|
6314
|
+
return out.sort((a, b) => a.name.localeCompare(b.name));
|
|
6315
|
+
}
|
|
6316
|
+
|
|
6317
|
+
// src/run-playhead.ts
|
|
6318
|
+
var RECEIPT_READ_FN = "receiptRead";
|
|
6319
|
+
var RECEIPT_WRITE_FN = "receiptWrite";
|
|
6320
|
+
var STEP_STATUSES = /* @__PURE__ */ new Set(["ok", "fail", "skipped"]);
|
|
6321
|
+
var isRecord4 = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
6322
|
+
function asStepRecord(value) {
|
|
6323
|
+
if (!isRecord4(value)) return null;
|
|
6324
|
+
if (typeof value.phase !== "string" || typeof value.fn !== "string") return null;
|
|
6325
|
+
if (typeof value.status !== "string" || !STEP_STATUSES.has(value.status)) return null;
|
|
6326
|
+
if (typeof value.durationMs !== "number") return null;
|
|
6327
|
+
return value;
|
|
6328
|
+
}
|
|
6329
|
+
function parseRunProgressLine(line) {
|
|
6330
|
+
let parsed;
|
|
6331
|
+
try {
|
|
6332
|
+
parsed = JSON.parse(line);
|
|
6333
|
+
} catch {
|
|
6334
|
+
return null;
|
|
6335
|
+
}
|
|
6336
|
+
const one = asStepRecord(parsed);
|
|
6337
|
+
if (one) return [one];
|
|
6338
|
+
if (isRecord4(parsed) && typeof parsed.op === "string" && Array.isArray(parsed.records)) {
|
|
6339
|
+
const records = parsed.records.map(asStepRecord).filter((r) => r !== null);
|
|
6340
|
+
return records.length ? records : null;
|
|
6341
|
+
}
|
|
6342
|
+
return null;
|
|
6343
|
+
}
|
|
6344
|
+
function recordLine(record) {
|
|
6345
|
+
const mark = record.status === "ok" ? "\u2713" : record.status === "fail" ? "\u2717" : "\u2022";
|
|
6346
|
+
const took = record.status === "skipped" ? "skipped" : record.durationMs < 1e3 ? `${record.durationMs}ms` : `${(record.durationMs / 1e3).toFixed(1)}s`;
|
|
6347
|
+
return `${mark} ${record.phase} \xB7 ${record.fn} \u2014 ${took}${record.error ? ` \u2014 ${record.error}` : ""}`;
|
|
6348
|
+
}
|
|
6349
|
+
var ANSI = /\x1b\[[0-9;]*m/g;
|
|
6350
|
+
var strip = (s) => s.replace(ANSI, "");
|
|
6351
|
+
function statusFields(stdout) {
|
|
6352
|
+
const out = /* @__PURE__ */ new Map();
|
|
6353
|
+
for (const raw of strip(stdout).split("\n")) {
|
|
6354
|
+
const m = /^\s{2,}([A-Za-z][A-Za-z ]*?)\s*:\s*(.*)$/.exec(raw);
|
|
6355
|
+
if (m) out.set(m[1].trim(), m[2].trim());
|
|
6356
|
+
}
|
|
6357
|
+
return out;
|
|
6358
|
+
}
|
|
6359
|
+
function parseGateLine(line) {
|
|
6360
|
+
const m = /^(.*?)(?: — (.*?))? \(waiting since (.+)\)$/.exec(line.trim());
|
|
6361
|
+
if (!m) return null;
|
|
6362
|
+
const signalName = m[1].trim();
|
|
6363
|
+
if (!signalName) return null;
|
|
6364
|
+
return { signalName, ...m[2] ? { description: m[2].trim() } : {}, since: m[3].trim() };
|
|
6365
|
+
}
|
|
6366
|
+
function readRunStatus(op, run3) {
|
|
6367
|
+
if (run3.code !== 0) {
|
|
6368
|
+
const message = strip(run3.stderr).trim() || strip(run3.stdout).trim() || `chant run status exited ${run3.code}`;
|
|
6369
|
+
if (/not available in local mode|@temporalio\/client is not installed/.test(message)) {
|
|
6370
|
+
return {
|
|
6371
|
+
ok: false,
|
|
6372
|
+
refusal: {
|
|
6373
|
+
error: `No Temporal run state for "${op}" \u2014 ${message.split("\n")[0].replace(/^error:\s*/, "")}`,
|
|
6374
|
+
code: "no-temporal",
|
|
6375
|
+
remedy: "Gate state and per-step run records come from a durable run. Configure a Temporal profile in chant.config.ts (and `npm install @temporalio/client`), then run the Op with --temporal."
|
|
6376
|
+
}
|
|
6377
|
+
};
|
|
6378
|
+
}
|
|
6379
|
+
if (/not found|NotFound/.test(message)) {
|
|
6380
|
+
return {
|
|
6381
|
+
ok: false,
|
|
6382
|
+
refusal: {
|
|
6383
|
+
error: `No run of "${op}" to report \u2014 ${message.split("\n")[0].replace(/^error:\s*/, "")}`,
|
|
6384
|
+
code: "no-run",
|
|
6385
|
+
remedy: "Start the Op (Run) and the playhead fills in as each step settles."
|
|
6386
|
+
}
|
|
6387
|
+
};
|
|
6388
|
+
}
|
|
6389
|
+
return {
|
|
6390
|
+
ok: false,
|
|
6391
|
+
refusal: {
|
|
6392
|
+
error: `chant run status ${op} failed: ${message.split("\n")[0].replace(/^error:\s*/, "")}`,
|
|
6393
|
+
code: "run-status",
|
|
6394
|
+
remedy: "Run `chant run status " + op + " --temporal` in the project to see the whole error."
|
|
6395
|
+
}
|
|
6396
|
+
};
|
|
6397
|
+
}
|
|
6398
|
+
const f = statusFields(run3.stdout);
|
|
6399
|
+
const activities = /^(\d+)\/(\d+)/.exec(f.get("Activities") ?? "");
|
|
6400
|
+
const gateLine = f.get("Gate");
|
|
6401
|
+
return {
|
|
6402
|
+
ok: true,
|
|
6403
|
+
read: {
|
|
6404
|
+
op,
|
|
6405
|
+
...f.get("Workflow ID") ? { workflowId: f.get("Workflow ID") } : {},
|
|
6406
|
+
...f.get("Run ID") ? { runId: f.get("Run ID") } : {},
|
|
6407
|
+
...f.get("Status") ? { status: f.get("Status") } : {},
|
|
6408
|
+
...f.get("Started") ? { startedAt: f.get("Started") } : {},
|
|
6409
|
+
...f.get("Closed") ? { closedAt: f.get("Closed") } : {},
|
|
6410
|
+
...activities ? { activities: { completed: Number(activities[1]), scheduled: Number(activities[2]) } } : {},
|
|
6411
|
+
// No Gate line means chant printed none, which it does both when the
|
|
6412
|
+
// query says "nothing pending" and when the Op registers no handler.
|
|
6413
|
+
// Either way: no gate is waiting on a human right now.
|
|
6414
|
+
gate: gateLine ? parseGateLine(gateLine) : null
|
|
6415
|
+
}
|
|
6416
|
+
};
|
|
6417
|
+
}
|
|
6418
|
+
var initialRunState = {
|
|
6419
|
+
op: null,
|
|
6420
|
+
mode: null,
|
|
6421
|
+
status: "idle",
|
|
6422
|
+
records: [],
|
|
6423
|
+
gate: null,
|
|
6424
|
+
gateNote: null,
|
|
6425
|
+
startedAt: null,
|
|
6426
|
+
endedAt: null,
|
|
6427
|
+
lost: null
|
|
6428
|
+
};
|
|
6429
|
+
function runStarted(op, mode, at) {
|
|
6430
|
+
return { ...initialRunState, op, mode, status: "running", startedAt: at };
|
|
6431
|
+
}
|
|
6432
|
+
function runRecord(state, record) {
|
|
6433
|
+
return { ...state, records: [...state.records, record] };
|
|
6434
|
+
}
|
|
6435
|
+
function runGate(state, result) {
|
|
6436
|
+
return result.ok ? { ...state, gate: result.read.gate, gateNote: null } : { ...state, gate: null, gateNote: result.refusal.error };
|
|
6437
|
+
}
|
|
6438
|
+
function runEnded(state, exitCode, at) {
|
|
6439
|
+
if (state.status !== "running") return state;
|
|
6440
|
+
if (exitCode === 0) return { ...state, status: "ok", endedAt: at };
|
|
6441
|
+
if (state.records.some((r) => r.status === "fail")) return { ...state, status: "failed", endedAt: at };
|
|
6442
|
+
return {
|
|
6443
|
+
...state,
|
|
6444
|
+
status: "lost",
|
|
6445
|
+
endedAt: at,
|
|
6446
|
+
lost: `the progress stream ended (exit ${exitCode}) after ${state.records.length} settled step${state.records.length === 1 ? "" : "s"} without a failed step \u2014 behold does not know whether the Op failed or the stream broke, so the playhead stays at the last settled step.`
|
|
6447
|
+
};
|
|
6448
|
+
}
|
|
6449
|
+
function slotsFor(placed) {
|
|
6450
|
+
const out = [];
|
|
6451
|
+
for (const p of placed) {
|
|
6452
|
+
if (p.step.kind === "activity") {
|
|
6453
|
+
out.push({ id: p.id, phase: p.phase, fn: p.step.fn });
|
|
6454
|
+
} else if (p.step.kind === "effect") {
|
|
6455
|
+
out.push({ id: p.id, phase: p.phase, fn: RECEIPT_READ_FN });
|
|
6456
|
+
for (const nested of p.step.steps ?? []) {
|
|
6457
|
+
if (nested.kind === "activity") out.push({ id: p.id, phase: p.phase, fn: nested.fn });
|
|
6458
|
+
}
|
|
6459
|
+
out.push({ id: p.id, phase: p.phase, fn: RECEIPT_WRITE_FN });
|
|
6460
|
+
}
|
|
6461
|
+
}
|
|
6462
|
+
return out;
|
|
6463
|
+
}
|
|
6464
|
+
function mergeRun(prev, record) {
|
|
6465
|
+
const rank = { fail: 3, ok: 2, skipped: 1 };
|
|
6466
|
+
const keepPrev = prev && rank[prev.status] >= rank[record.status];
|
|
6467
|
+
return {
|
|
6468
|
+
status: keepPrev ? prev.status : record.status,
|
|
6469
|
+
durationMs: (prev?.durationMs ?? 0) + record.durationMs,
|
|
6470
|
+
...keepPrev && prev.error ? { error: prev.error } : record.error ? { error: record.error } : {},
|
|
6471
|
+
...keepPrev && prev.outcome ? { outcome: prev.outcome } : record.outcome ? { outcome: `${record.outcome.name}=${String(record.outcome.value)}` } : {},
|
|
6472
|
+
records: (prev?.records ?? 0) + 1
|
|
6473
|
+
};
|
|
6474
|
+
}
|
|
6475
|
+
function joinRun(op, records, gate) {
|
|
6476
|
+
const placed = opPlacedSteps(op);
|
|
6477
|
+
const slots = slotsFor(placed);
|
|
6478
|
+
const byKey = /* @__PURE__ */ new Map();
|
|
6479
|
+
for (const s of slots) {
|
|
6480
|
+
const key = `${s.phase}\0${s.fn}`;
|
|
6481
|
+
if (!byKey.has(key)) byKey.set(key, []);
|
|
6482
|
+
byKey.get(key).push({ id: s.id });
|
|
6483
|
+
}
|
|
6484
|
+
const cursor = /* @__PURE__ */ new Map();
|
|
6485
|
+
const byNode = /* @__PURE__ */ new Map();
|
|
6486
|
+
const unplaced = [];
|
|
6487
|
+
let lastSettledIndex = -1;
|
|
6488
|
+
const indexOf = new Map(placed.map((p, i) => [p.id, i]));
|
|
6489
|
+
for (const record of records) {
|
|
6490
|
+
const key = `${record.phase}\0${record.fn}`;
|
|
6491
|
+
const list3 = byKey.get(key);
|
|
6492
|
+
const i = cursor.get(key) ?? 0;
|
|
6493
|
+
const slot = list3?.[i];
|
|
6494
|
+
if (!slot) {
|
|
6495
|
+
unplaced.push(record);
|
|
6496
|
+
continue;
|
|
6497
|
+
}
|
|
6498
|
+
cursor.set(key, i + 1);
|
|
6499
|
+
byNode.set(slot.id, mergeRun(byNode.get(slot.id), record));
|
|
6500
|
+
if (record.status !== "skipped") {
|
|
6501
|
+
lastSettledIndex = Math.max(lastSettledIndex, indexOf.get(slot.id) ?? -1);
|
|
6502
|
+
}
|
|
6503
|
+
}
|
|
6504
|
+
if (gate) {
|
|
6505
|
+
const gateNode = placed.find((p) => p.step.kind === "gate" && p.step.signalName === gate.signalName);
|
|
6506
|
+
if (gateNode) {
|
|
6507
|
+
return {
|
|
6508
|
+
byNode,
|
|
6509
|
+
at: gateNode.id,
|
|
6510
|
+
atReason: `waiting for ${gate.signalName} since ${gate.since}`,
|
|
6511
|
+
unplaced
|
|
6512
|
+
};
|
|
6513
|
+
}
|
|
6514
|
+
}
|
|
6515
|
+
const next = placed.find((p, i) => i > lastSettledIndex && p.when === "phases");
|
|
6516
|
+
return {
|
|
6517
|
+
byNode,
|
|
6518
|
+
at: lastSettledIndex >= 0 && next ? next.id : null,
|
|
6519
|
+
atReason: lastSettledIndex >= 0 && next ? "reached \u2014 not settled yet" : null,
|
|
6520
|
+
unplaced
|
|
6521
|
+
};
|
|
6522
|
+
}
|
|
6523
|
+
var TONE = {
|
|
6524
|
+
ok: "good",
|
|
6525
|
+
fail: "warn",
|
|
6526
|
+
skipped: "neutral"
|
|
6527
|
+
};
|
|
6528
|
+
var RAN = { ok: "ok", fail: "failed", skipped: "skipped" };
|
|
6529
|
+
function tookLabel(ms) {
|
|
6530
|
+
return ms < 1e3 ? `${ms}ms` : `${(ms / 1e3).toFixed(1)}s`;
|
|
6531
|
+
}
|
|
6532
|
+
function paintPlayhead(ir, join21, gate) {
|
|
6533
|
+
if (!join21.byNode.size && !join21.at) return ir;
|
|
6534
|
+
const nodes = ir.nodes.map((node) => {
|
|
6535
|
+
const run3 = join21.byNode.get(node.id);
|
|
6536
|
+
const here = join21.at === node.id;
|
|
6537
|
+
if (!run3 && !here) return node;
|
|
6538
|
+
const isGate = node.attrs?._step === "gate";
|
|
6539
|
+
const attrs = { ...node.attrs };
|
|
6540
|
+
if (run3) {
|
|
6541
|
+
attrs._status = TONE[run3.status];
|
|
6542
|
+
attrs.run = run3.status === "skipped" ? "skipped" : `${RAN[run3.status]} \xB7 ${tookLabel(run3.durationMs)}`;
|
|
6543
|
+
attrs.ran = tookLabel(run3.durationMs);
|
|
6544
|
+
if (run3.error) attrs.error = run3.error;
|
|
6545
|
+
if (run3.outcome) attrs.outcome = run3.outcome;
|
|
6546
|
+
if (run3.records > 1) attrs.runRecords = run3.records;
|
|
6547
|
+
}
|
|
6548
|
+
if (here) {
|
|
6549
|
+
attrs._playhead = true;
|
|
6550
|
+
attrs._status = isGate && gate ? "warn" : "accent";
|
|
6551
|
+
attrs.run = isGate && gate ? `waiting for approval since ${gate.since}` : join21.atReason ?? "here";
|
|
6552
|
+
if (isGate && gate?.description) attrs.gateDescription = gate.description;
|
|
6553
|
+
}
|
|
6554
|
+
return { ...node, attrs };
|
|
6555
|
+
});
|
|
6556
|
+
return { ...ir, nodes };
|
|
6557
|
+
}
|
|
6558
|
+
function pendingGateCard(op, gate, ir) {
|
|
6559
|
+
const node = ir.nodes.find((n) => n.attrs?._step === "gate" && n.attrs?.gate === gate.signalName && n.attrs?.op === op);
|
|
6560
|
+
return {
|
|
6561
|
+
op,
|
|
6562
|
+
signalName: gate.signalName,
|
|
6563
|
+
...gate.description ? { description: gate.description } : {},
|
|
6564
|
+
since: gate.since,
|
|
6565
|
+
...node ? { node: node.id } : {},
|
|
6566
|
+
...node && typeof node.attrs?.guards === "string" ? { guards: node.attrs.guards } : {},
|
|
6567
|
+
approve: {
|
|
6568
|
+
method: "POST",
|
|
6569
|
+
path: `/api/ops/${encodeURIComponent(op)}/signal/${encodeURIComponent(gate.signalName)}`
|
|
6570
|
+
}
|
|
6571
|
+
};
|
|
6572
|
+
}
|
|
6573
|
+
function playheadNote(state, join21) {
|
|
6574
|
+
if (!state.op) {
|
|
6575
|
+
return "No run state: nothing has been run from this behold session, so every step paints as declared.";
|
|
6576
|
+
}
|
|
6577
|
+
const settled = join21 ? join21.byNode.size : state.records.length;
|
|
6578
|
+
const bits = [];
|
|
6579
|
+
const head = state.status === "idle" ? `Run state for ${state.op}: no per-step records \u2014 this behold session didn't start the run, so only the gate query answers` : `Run: ${state.op} (${state.mode === "temporal" ? "durable" : "local executor"}) \u2014 ${settled} step${settled === 1 ? "" : "s"} settled`;
|
|
6580
|
+
if (state.status === "running") {
|
|
6581
|
+
bits.push(
|
|
6582
|
+
state.mode === "local" ? "still running; the local executor reports every step at once when it ends, so the track fills in on completion" : "still running; only settled steps are painted"
|
|
6583
|
+
);
|
|
6584
|
+
}
|
|
6585
|
+
if (state.status === "ok") bits.push("the run exited cleanly");
|
|
6586
|
+
if (state.status === "failed") bits.push("a step failed");
|
|
6587
|
+
if (state.status === "lost") bits.push(`stream lost \u2014 ${state.lost ?? "the progress stream ended without a verdict"}`);
|
|
6588
|
+
if (state.gate) {
|
|
6589
|
+
bits.push(`waiting on gate ${state.gate.signalName} since ${state.gate.since} \u2014 approve is a delegated signal, behold never signals on its own`);
|
|
6590
|
+
} else if (state.gateNote) {
|
|
6591
|
+
bits.push(`gate state unavailable \u2014 ${state.gateNote}`);
|
|
6592
|
+
}
|
|
6593
|
+
if (join21?.unplaced.length) {
|
|
6594
|
+
bits.push(
|
|
6595
|
+
`${join21.unplaced.length} record${join21.unplaced.length === 1 ? "" : "s"} matched no declared step and ${join21.unplaced.length === 1 ? "is" : "are"} not painted`
|
|
6596
|
+
);
|
|
6597
|
+
}
|
|
6598
|
+
if (!bits.length) return `${head}.`;
|
|
6599
|
+
const rest = bits.join("; ");
|
|
6600
|
+
return `${head} \u2014 ${rest}.`;
|
|
6601
|
+
}
|
|
6602
|
+
|
|
6603
|
+
// src/operator.ts
|
|
6604
|
+
var ANSI2 = /\x1b\[[0-9;]*m/g;
|
|
6605
|
+
var strip2 = (s) => s.replace(ANSI2, "");
|
|
6606
|
+
var isRecord5 = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
6607
|
+
var HOW_TO_DECLARE = "An operating loop is a ConvergeOp: declare one in a `*.op.ts` (`ConvergeOp({ name, env, dial, rules })` from @intentius/chant-lexicon-temporal), build the project, and tick it with `chant operator` (or a cron / CronJob invoking it).";
|
|
6608
|
+
function readRow(value) {
|
|
6609
|
+
if (!isRecord5(value)) return null;
|
|
6610
|
+
if (typeof value.op !== "string" || typeof value.env !== "string") return null;
|
|
6611
|
+
if (!Array.isArray(value.pendingGates)) return null;
|
|
6612
|
+
const gates = [];
|
|
6613
|
+
for (const g of value.pendingGates) {
|
|
6614
|
+
if (!isRecord5(g) || typeof g.rule !== "string" || typeof g.gate !== "string") return null;
|
|
6615
|
+
gates.push({ rule: g.rule, gate: g.gate, ...typeof g.op === "string" ? { op: g.op } : {} });
|
|
6616
|
+
}
|
|
6617
|
+
const tick = isRecord5(value.lastTick) ? value.lastTick : void 0;
|
|
6618
|
+
const usable = tick && typeof tick.timestamp === "string" && typeof tick.log === "string";
|
|
6619
|
+
const lease = isRecord5(value.lease) ? value.lease : void 0;
|
|
6620
|
+
return {
|
|
6621
|
+
op: value.op,
|
|
6622
|
+
env: value.env,
|
|
6623
|
+
...usable ? { lastTick: tick } : {},
|
|
6624
|
+
pendingGates: gates,
|
|
6625
|
+
...lease && typeof lease.holder === "string" && typeof lease.expiresAt === "string" ? { lease: { holder: lease.holder, expiresAt: lease.expiresAt } } : {}
|
|
6626
|
+
};
|
|
6627
|
+
}
|
|
6628
|
+
function readOperatorStatus(run3) {
|
|
6629
|
+
const err = strip2(run3.stderr);
|
|
6630
|
+
const out = strip2(run3.stdout).trim();
|
|
6631
|
+
if (run3.code !== 0) {
|
|
6632
|
+
const message = err.trim() || out || `chant operator status exited ${run3.code}`;
|
|
6633
|
+
const first = message.split("\n")[0].replace(/^error:\s*/i, "");
|
|
6634
|
+
if (/Unknown command|Unknown operator subcommand/i.test(message)) {
|
|
6635
|
+
return {
|
|
6636
|
+
ok: false,
|
|
6637
|
+
refusal: {
|
|
6638
|
+
error: `This chant has no \`operator status\` \u2014 ${first}`,
|
|
6639
|
+
code: "no-operator-cli",
|
|
6640
|
+
remedy: "The operating loop's status surface landed in chant 0.52 (chant#1485). Upgrade chant to read it."
|
|
6641
|
+
}
|
|
6642
|
+
};
|
|
6643
|
+
}
|
|
6644
|
+
return {
|
|
6645
|
+
ok: false,
|
|
6646
|
+
refusal: {
|
|
6647
|
+
error: `chant operator status failed: ${first}`,
|
|
6648
|
+
code: "operator-status",
|
|
6649
|
+
remedy: "Run `chant operator status --json` in the project to see the whole error."
|
|
6650
|
+
}
|
|
6651
|
+
};
|
|
6652
|
+
}
|
|
6653
|
+
if (!out) {
|
|
6654
|
+
return {
|
|
6655
|
+
ok: false,
|
|
6656
|
+
refusal: {
|
|
6657
|
+
error: "This project declares no operating loop \u2014 chant found no ConvergeOp.",
|
|
6658
|
+
code: "no-operator",
|
|
6659
|
+
remedy: HOW_TO_DECLARE
|
|
6660
|
+
}
|
|
6661
|
+
};
|
|
6662
|
+
}
|
|
6663
|
+
let parsed;
|
|
6664
|
+
try {
|
|
6665
|
+
parsed = JSON.parse(out);
|
|
6666
|
+
} catch (e) {
|
|
6667
|
+
return {
|
|
6668
|
+
ok: false,
|
|
6669
|
+
refusal: {
|
|
6670
|
+
error: `chant operator status --json didn't print JSON: ${e instanceof Error ? e.message : String(e)}`,
|
|
6671
|
+
code: "operator-status",
|
|
6672
|
+
remedy: "Run `chant operator status --json` in the project and check what it printed."
|
|
6673
|
+
}
|
|
6674
|
+
};
|
|
6675
|
+
}
|
|
6676
|
+
if (!Array.isArray(parsed)) {
|
|
6677
|
+
return {
|
|
6678
|
+
ok: false,
|
|
6679
|
+
refusal: {
|
|
6680
|
+
error: "chant operator status --json answered with something that isn't an array of ConvergeOp rows.",
|
|
6681
|
+
code: "operator-status",
|
|
6682
|
+
remedy: "behold reads one row per ConvergeOp: `{op, env, lastTick?, pendingGates[], lease?}`."
|
|
6683
|
+
}
|
|
6684
|
+
};
|
|
6685
|
+
}
|
|
6686
|
+
const rows = [];
|
|
6687
|
+
for (const value of parsed) {
|
|
6688
|
+
const row = readRow(value);
|
|
6689
|
+
if (!row) {
|
|
6690
|
+
return {
|
|
6691
|
+
ok: false,
|
|
6692
|
+
refusal: {
|
|
6693
|
+
error: "chant operator status --json answered with a row behold couldn't read (needs `op`, `env`, `pendingGates[]`).",
|
|
6694
|
+
code: "operator-status",
|
|
6695
|
+
remedy: "Check `chant operator status --json` in the project \u2014 and file the shape change if chant's is the newer one."
|
|
6696
|
+
}
|
|
6697
|
+
};
|
|
6698
|
+
}
|
|
6699
|
+
rows.push(row);
|
|
6700
|
+
}
|
|
6701
|
+
return { ok: true, rows };
|
|
6702
|
+
}
|
|
6703
|
+
function declaredConvergeOps(ops) {
|
|
6704
|
+
return ops.filter((o) => o.searchAttributes?.Converge === "true").map((o) => ({
|
|
6705
|
+
name: o.name,
|
|
6706
|
+
...o.searchAttributes?.Env ? { env: o.searchAttributes.Env } : {},
|
|
6707
|
+
...o.searchAttributes?.Dial ? { dial: o.searchAttributes.Dial } : {}
|
|
6708
|
+
}));
|
|
6709
|
+
}
|
|
6710
|
+
var MANAGED_BY = "app.kubernetes.io/managed-by";
|
|
6711
|
+
var COMPONENT = "app.kubernetes.io/component";
|
|
6712
|
+
var NAME_LABEL = "app.kubernetes.io/name";
|
|
6713
|
+
var CHANT = "chant";
|
|
6714
|
+
var CONVERGE_TICK = "converge-tick";
|
|
6715
|
+
var CRONJOB_KIND = "K8s::Batch::CronJob";
|
|
6716
|
+
var NAMESPACE_KIND = "K8s::Core::Namespace";
|
|
6717
|
+
var rec3 = (v) => isRecord5(v) ? v : void 0;
|
|
6718
|
+
var str3 = (v) => typeof v === "string" && v ? v : void 0;
|
|
6719
|
+
var labelsOf = (n) => rec3(rec3(n.attrs?.metadata)?.labels) ?? {};
|
|
6720
|
+
function operatorHomes(ir) {
|
|
6721
|
+
const byNamespace = /* @__PURE__ */ new Map();
|
|
6722
|
+
const conflicted = /* @__PURE__ */ new Set();
|
|
6723
|
+
for (const n of ir.nodes) {
|
|
6724
|
+
if (n.kind !== CRONJOB_KIND) continue;
|
|
6725
|
+
const labels = labelsOf(n);
|
|
6726
|
+
if (labels[MANAGED_BY] !== CHANT || labels[COMPONENT] !== CONVERGE_TICK) continue;
|
|
6727
|
+
const meta = rec3(n.attrs?.metadata);
|
|
6728
|
+
const ns = str3(meta?.namespace);
|
|
6729
|
+
const op = str3(meta?.name);
|
|
6730
|
+
if (!ns || !op) continue;
|
|
6731
|
+
const home = byNamespace.get(ns) ?? { namespace: ns, ticks: [] };
|
|
6732
|
+
const stack = str3(labels[NAME_LABEL]);
|
|
6733
|
+
if (stack && !conflicted.has(ns)) {
|
|
6734
|
+
if (home.stack === void 0) home.stack = stack;
|
|
6735
|
+
else if (home.stack !== stack) {
|
|
6736
|
+
conflicted.add(ns);
|
|
6737
|
+
delete home.stack;
|
|
6738
|
+
}
|
|
6739
|
+
}
|
|
6740
|
+
const schedule = str3(rec3(n.attrs?.spec)?.schedule);
|
|
6741
|
+
home.ticks.push({ node: n.id, op, ...schedule ? { schedule } : {} });
|
|
6742
|
+
byNamespace.set(ns, home);
|
|
6743
|
+
}
|
|
6744
|
+
if (!byNamespace.size) return [];
|
|
6745
|
+
for (const n of ir.nodes) {
|
|
6746
|
+
if (n.kind !== NAMESPACE_KIND) continue;
|
|
6747
|
+
const name = str3(rec3(n.attrs?.metadata)?.name);
|
|
6748
|
+
const home = name ? byNamespace.get(name) : void 0;
|
|
6749
|
+
if (home && !home.node) home.node = n.id;
|
|
6750
|
+
}
|
|
6751
|
+
for (const home of byNamespace.values()) home.ticks.sort((a, b) => a.op.localeCompare(b.op));
|
|
6752
|
+
return [...byNamespace.values()].sort((a, b) => a.namespace.localeCompare(b.namespace));
|
|
6753
|
+
}
|
|
6754
|
+
function markOperatorHome(ir) {
|
|
6755
|
+
const homes = operatorHomes(ir);
|
|
6756
|
+
if (!homes.length) return ir;
|
|
6757
|
+
const marks = /* @__PURE__ */ new Map();
|
|
6758
|
+
for (const home of homes) {
|
|
6759
|
+
if (home.node) {
|
|
6760
|
+
marks.set(home.node, {
|
|
6761
|
+
role: "home",
|
|
6762
|
+
namespace: home.namespace,
|
|
6763
|
+
...home.stack ? { stack: home.stack } : {},
|
|
6764
|
+
ticks: home.ticks.map((t) => t.op)
|
|
6765
|
+
});
|
|
6766
|
+
}
|
|
6767
|
+
for (const tick of home.ticks) {
|
|
6768
|
+
marks.set(tick.node, {
|
|
6769
|
+
role: "tick",
|
|
6770
|
+
namespace: home.namespace,
|
|
6771
|
+
...home.stack ? { stack: home.stack } : {},
|
|
6772
|
+
op: tick.op,
|
|
6773
|
+
...tick.schedule ? { schedule: tick.schedule } : {}
|
|
6774
|
+
});
|
|
6775
|
+
}
|
|
6776
|
+
}
|
|
6777
|
+
return {
|
|
6778
|
+
...ir,
|
|
6779
|
+
nodes: ir.nodes.map((n) => {
|
|
6780
|
+
const mark = marks.get(n.id);
|
|
6781
|
+
return mark ? { ...n, attrs: { ...n.attrs, _operator: mark } } : n;
|
|
6782
|
+
})
|
|
6783
|
+
};
|
|
6784
|
+
}
|
|
6785
|
+
function operatorStrip(rows, now = Date.now()) {
|
|
6786
|
+
const out = rows.map((r) => {
|
|
6787
|
+
const expiresAt = r.lease ? Date.parse(r.lease.expiresAt) : NaN;
|
|
6788
|
+
const lease = !r.lease ? "free" : Number.isFinite(expiresAt) && expiresAt <= now ? "expired" : "held";
|
|
6789
|
+
return {
|
|
6790
|
+
op: r.op,
|
|
6791
|
+
env: r.env,
|
|
6792
|
+
log: r.lastTick?.log ?? null,
|
|
6793
|
+
at: r.lastTick?.timestamp ?? null,
|
|
6794
|
+
lease,
|
|
6795
|
+
leaseHolder: r.lease?.holder ?? null,
|
|
6796
|
+
leaseExpiresAt: r.lease?.expiresAt ?? null,
|
|
6797
|
+
pendingGates: r.pendingGates.length
|
|
6798
|
+
};
|
|
6799
|
+
});
|
|
6800
|
+
return { rows: out, pendingGates: out.reduce((n, r) => n + r.pendingGates, 0) };
|
|
6801
|
+
}
|
|
6802
|
+
var CONVERGE_GATE_LOOP = "converge";
|
|
6803
|
+
var APPROVE_SEMANTICS = "Approving records a fact in the gate ledger; it does not unblock the dispatch. The next tick reads it.";
|
|
6804
|
+
function convergeGateCards(rows) {
|
|
4999
6805
|
const out = [];
|
|
5000
|
-
for (const
|
|
5001
|
-
const
|
|
5002
|
-
|
|
5003
|
-
for (const f of readdirSync4(dir)) {
|
|
5004
|
-
if (!f.endsWith(".op.ts")) continue;
|
|
5005
|
-
const content = readFileSync10(join11(dir, f), "utf8");
|
|
5006
|
-
const name = content.match(/name:\s*["'`]([^"'`]+)["'`]/)?.[1];
|
|
5007
|
-
if (!name || seen.has(name)) continue;
|
|
5008
|
-
seen.add(name);
|
|
5009
|
-
const gate = content.match(/signalName:\s*["'`]([^"'`]+)["'`]/)?.[1];
|
|
5010
|
-
const env = content.match(/\benv:\s*["'`]([^"'`]+)["'`]/)?.[1];
|
|
5011
|
-
const target = content.match(/\btarget:\s*["'`]([^"'`]+)["'`]/)?.[1];
|
|
5012
|
-
const substrate = target ? APPLY_TARGET_LEXICON[target] : void 0;
|
|
6806
|
+
for (const row of rows) {
|
|
6807
|
+
for (const g of row.pendingGates) {
|
|
6808
|
+
if (!g.op) continue;
|
|
5013
6809
|
out.push({
|
|
5014
|
-
|
|
5015
|
-
|
|
5016
|
-
|
|
5017
|
-
|
|
5018
|
-
|
|
5019
|
-
|
|
6810
|
+
loop: CONVERGE_GATE_LOOP,
|
|
6811
|
+
rule: g.rule,
|
|
6812
|
+
op: g.op,
|
|
6813
|
+
gate: g.gate,
|
|
6814
|
+
convergeOp: row.op,
|
|
6815
|
+
env: row.env,
|
|
6816
|
+
since: row.lastTick?.timestamp ?? null,
|
|
6817
|
+
semantics: APPROVE_SEMANTICS,
|
|
6818
|
+
approve: {
|
|
6819
|
+
method: "POST",
|
|
6820
|
+
path: `/api/operator/approve/${encodeURIComponent(g.op)}/${encodeURIComponent(g.gate)}`,
|
|
6821
|
+
command: `chant approve ${g.op} ${g.gate}`
|
|
6822
|
+
}
|
|
5020
6823
|
});
|
|
5021
6824
|
}
|
|
5022
6825
|
}
|
|
5023
|
-
return out
|
|
6826
|
+
return out;
|
|
5024
6827
|
}
|
|
5025
|
-
|
|
5026
|
-
|
|
5027
|
-
|
|
5028
|
-
|
|
5029
|
-
|
|
5030
|
-
|
|
5031
|
-
|
|
5032
|
-
|
|
5033
|
-
|
|
6828
|
+
var initialOperatorState = {
|
|
6829
|
+
declared: [],
|
|
6830
|
+
strip: null,
|
|
6831
|
+
gates: [],
|
|
6832
|
+
note: null,
|
|
6833
|
+
code: null,
|
|
6834
|
+
readAt: null
|
|
6835
|
+
};
|
|
6836
|
+
function operatorRead(state, result, at, now = Date.now()) {
|
|
6837
|
+
if (!result.ok) {
|
|
6838
|
+
return { ...state, strip: null, gates: [], note: result.refusal.error, code: result.refusal.code, readAt: at };
|
|
5034
6839
|
}
|
|
5035
|
-
return
|
|
6840
|
+
return {
|
|
6841
|
+
...state,
|
|
6842
|
+
strip: operatorStrip(result.rows, now),
|
|
6843
|
+
gates: convergeGateCards(result.rows),
|
|
6844
|
+
note: null,
|
|
6845
|
+
code: null,
|
|
6846
|
+
readAt: at
|
|
6847
|
+
};
|
|
6848
|
+
}
|
|
6849
|
+
function operatorNote(state) {
|
|
6850
|
+
if (!state.declared.length) {
|
|
6851
|
+
return "No operating loop: this project declares no ConvergeOp, so there is no operator strip.";
|
|
6852
|
+
}
|
|
6853
|
+
const loops = `${state.declared.length} ConvergeOp${state.declared.length === 1 ? "" : "s"}`;
|
|
6854
|
+
if (!state.strip) {
|
|
6855
|
+
return state.note ? `Operating loop: ${loops} declared \u2014 status unavailable: ${state.note}` : `Operating loop: ${loops} declared \u2014 not read yet.`;
|
|
6856
|
+
}
|
|
6857
|
+
const bits = [];
|
|
6858
|
+
const ticked = state.strip.rows.filter((r) => r.at).length;
|
|
6859
|
+
bits.push(
|
|
6860
|
+
ticked ? `last tick only \u2014 chant's status surface exposes one tick per loop, not the history (chant#2029), so this is a strip and not a timeline` : `no tick recorded yet on any loop`
|
|
6861
|
+
);
|
|
6862
|
+
const held = state.strip.rows.filter((r) => r.lease === "held").length;
|
|
6863
|
+
if (held) bits.push(`${held} lease${held === 1 ? "" : "s"} held`);
|
|
6864
|
+
const expired = state.strip.rows.filter((r) => r.lease === "expired").length;
|
|
6865
|
+
if (expired) bits.push(`${expired} lease${expired === 1 ? "" : "s"} expired \u2014 the next round reclaims it`);
|
|
6866
|
+
if (state.strip.pendingGates) {
|
|
6867
|
+
bits.push(
|
|
6868
|
+
`${state.strip.pendingGates} converge gate${state.strip.pendingGates === 1 ? "" : "s"} pending \u2014 approving one records a fact for the next tick, it does not release a workflow (that is the run gate, a different card), and a gate fact carries no URL yet (chant#2028)`
|
|
6869
|
+
);
|
|
6870
|
+
}
|
|
6871
|
+
return `Operating loop: ${loops} \u2014 ${bits.join("; ")}.`;
|
|
5036
6872
|
}
|
|
5037
6873
|
|
|
5038
6874
|
// src/adopt.ts
|
|
@@ -5115,16 +6951,21 @@ function classifyHealth(status) {
|
|
|
5115
6951
|
if (HEALTHY.test(text)) return "healthy";
|
|
5116
6952
|
return "unknown";
|
|
5117
6953
|
}
|
|
5118
|
-
function
|
|
6954
|
+
function rec4(v) {
|
|
5119
6955
|
return typeof v === "object" && v !== null && !Array.isArray(v) ? v : void 0;
|
|
5120
6956
|
}
|
|
5121
|
-
function
|
|
6957
|
+
function str4(v) {
|
|
5122
6958
|
return typeof v === "string" && v.length > 0 ? v : void 0;
|
|
5123
6959
|
}
|
|
5124
6960
|
function statusTree(o) {
|
|
5125
|
-
const attrs =
|
|
6961
|
+
const attrs = rec4(o.attributes);
|
|
6962
|
+
if (!attrs) return void 0;
|
|
6963
|
+
return rec4(attrs.status) ?? attrs;
|
|
6964
|
+
}
|
|
6965
|
+
function specTree(o) {
|
|
6966
|
+
const attrs = rec4(o.attributes);
|
|
5126
6967
|
if (!attrs) return void 0;
|
|
5127
|
-
return
|
|
6968
|
+
return rec4(attrs.spec) ?? attrs;
|
|
5128
6969
|
}
|
|
5129
6970
|
var ARGO_HEALTH = {
|
|
5130
6971
|
Healthy: "healthy",
|
|
@@ -5150,9 +6991,9 @@ function argoErrorMessage(tree) {
|
|
|
5150
6991
|
if (m) return m[2];
|
|
5151
6992
|
continue;
|
|
5152
6993
|
}
|
|
5153
|
-
const type =
|
|
6994
|
+
const type = str4(rec4(c)?.type);
|
|
5154
6995
|
if (!type || !ARGO_ERROR_CONDITION.test(type)) continue;
|
|
5155
|
-
const message =
|
|
6996
|
+
const message = str4(rec4(c)?.message);
|
|
5156
6997
|
if (message) return message;
|
|
5157
6998
|
}
|
|
5158
6999
|
return void 0;
|
|
@@ -5160,10 +7001,10 @@ function argoErrorMessage(tree) {
|
|
|
5160
7001
|
function argoVerdict(o) {
|
|
5161
7002
|
if (o.type !== "K8s::Argo::Application") return void 0;
|
|
5162
7003
|
const tree = statusTree(o);
|
|
5163
|
-
let health =
|
|
5164
|
-
let sync =
|
|
7004
|
+
let health = str4(rec4(tree?.health)?.status);
|
|
7005
|
+
let sync = str4(rec4(tree?.sync)?.status);
|
|
5165
7006
|
if (!health && !sync) {
|
|
5166
|
-
const word =
|
|
7007
|
+
const word = str4(o.status);
|
|
5167
7008
|
if (word === "READY") {
|
|
5168
7009
|
health = "Healthy";
|
|
5169
7010
|
sync = "Synced";
|
|
@@ -5189,16 +7030,16 @@ function readyFromConditions(tree) {
|
|
|
5189
7030
|
const conditions = tree?.conditions;
|
|
5190
7031
|
if (!Array.isArray(conditions)) return void 0;
|
|
5191
7032
|
for (const c of conditions) {
|
|
5192
|
-
const cond =
|
|
7033
|
+
const cond = rec4(c);
|
|
5193
7034
|
if (!cond || cond.type !== "Ready") continue;
|
|
5194
|
-
const status =
|
|
7035
|
+
const status = str4(cond.status);
|
|
5195
7036
|
if (status !== "True" && status !== "False" && status !== "Unknown") continue;
|
|
5196
|
-
return { status, reason:
|
|
7037
|
+
return { status, reason: str4(cond.reason) };
|
|
5197
7038
|
}
|
|
5198
7039
|
return void 0;
|
|
5199
7040
|
}
|
|
5200
7041
|
function readyFromChantConditions(o) {
|
|
5201
|
-
const conditions =
|
|
7042
|
+
const conditions = rec4(o.attributes)?.conditions;
|
|
5202
7043
|
if (!Array.isArray(conditions)) return void 0;
|
|
5203
7044
|
for (const c of conditions) {
|
|
5204
7045
|
if (typeof c !== "string") continue;
|
|
@@ -5210,7 +7051,7 @@ function readyFromChantConditions(o) {
|
|
|
5210
7051
|
return void 0;
|
|
5211
7052
|
}
|
|
5212
7053
|
function readyFromStatusWord(o) {
|
|
5213
|
-
const word =
|
|
7054
|
+
const word = str4(o.status);
|
|
5214
7055
|
if (!word || word === "PRESENT") return void 0;
|
|
5215
7056
|
if (word === "READY") return { status: "True" };
|
|
5216
7057
|
if (word === "NOT-READY") return { status: "False" };
|
|
@@ -5220,12 +7061,13 @@ function describeReady(ready) {
|
|
|
5220
7061
|
return `Ready=${ready.status}${ready.reason ? ` (${ready.reason})` : ""}`;
|
|
5221
7062
|
}
|
|
5222
7063
|
function revisionVerdict(tree) {
|
|
5223
|
-
const applied =
|
|
5224
|
-
const attempted =
|
|
7064
|
+
const applied = str4(tree?.lastAppliedRevision);
|
|
7065
|
+
const attempted = str4(tree?.lastAttemptedRevision);
|
|
5225
7066
|
if (!applied || !attempted || applied === attempted) return void 0;
|
|
5226
7067
|
return { health: "progressing", detail: `applied ${applied}, attempted ${attempted}` };
|
|
5227
7068
|
}
|
|
5228
7069
|
var FLUX_TYPE_PREFIX = "K8s::Flux::";
|
|
7070
|
+
var FLUX_HELMREPOSITORY_TYPE = "K8s::Flux::HelmRepository";
|
|
5229
7071
|
function fluxVerdict(o) {
|
|
5230
7072
|
if (!o.type?.startsWith(FLUX_TYPE_PREFIX)) return void 0;
|
|
5231
7073
|
const tree = statusTree(o);
|
|
@@ -5233,20 +7075,55 @@ function fluxVerdict(o) {
|
|
|
5233
7075
|
if (ready?.status === "False") return { health: "degraded", detail: describeReady(ready) };
|
|
5234
7076
|
if (ready?.status === "Unknown") return { health: "progressing", detail: describeReady(ready) };
|
|
5235
7077
|
if (ready?.status === "True") return revisionVerdict(tree) ?? { health: "healthy", detail: describeReady(ready) };
|
|
7078
|
+
if (o.type === FLUX_HELMREPOSITORY_TYPE && str4(specTree(o)?.type) === "oci") {
|
|
7079
|
+
return { health: "healthy", detail: "spec.type=oci (nothing to reconcile)" };
|
|
7080
|
+
}
|
|
5236
7081
|
const conditions = tree?.conditions;
|
|
5237
7082
|
const reconciling = Array.isArray(conditions) && conditions.some((c) => {
|
|
5238
|
-
const cond =
|
|
7083
|
+
const cond = rec4(c);
|
|
5239
7084
|
return cond?.type === "Reconciling" && cond.status === "True";
|
|
5240
7085
|
});
|
|
5241
7086
|
if (reconciling) return { health: "progressing", detail: "Reconciling=True" };
|
|
5242
|
-
if (!
|
|
7087
|
+
if (!str4(o.status) || o.status === "PRESENT") {
|
|
5243
7088
|
return { health: "progressing", detail: "no Ready condition yet" };
|
|
5244
7089
|
}
|
|
5245
7090
|
return void 0;
|
|
5246
7091
|
}
|
|
7092
|
+
var DEPLOYMENT_TYPE = "K8s::Apps::Deployment";
|
|
7093
|
+
function unhappyCondition(o, type) {
|
|
7094
|
+
const fromStrings = rec4(o.attributes)?.conditions;
|
|
7095
|
+
if (Array.isArray(fromStrings)) {
|
|
7096
|
+
for (const c of fromStrings) {
|
|
7097
|
+
if (typeof c === "string" && c.startsWith(type)) return c;
|
|
7098
|
+
}
|
|
7099
|
+
}
|
|
7100
|
+
const tree = statusTree(o);
|
|
7101
|
+
const raw = tree?.conditions;
|
|
7102
|
+
if (Array.isArray(raw)) {
|
|
7103
|
+
for (const c of raw) {
|
|
7104
|
+
const cond = rec4(c);
|
|
7105
|
+
if (!cond || cond.type !== type) continue;
|
|
7106
|
+
const status = str4(cond.status);
|
|
7107
|
+
const unhappy = type === "ReplicaFailure" ? status === "True" : status === "False";
|
|
7108
|
+
if (!unhappy) continue;
|
|
7109
|
+
const reason = str4(cond.reason);
|
|
7110
|
+
const message = str4(cond.message);
|
|
7111
|
+
return `${type}${reason ? `=${reason}` : ""}${message ? `: ${message}` : ""}`;
|
|
7112
|
+
}
|
|
7113
|
+
}
|
|
7114
|
+
return void 0;
|
|
7115
|
+
}
|
|
7116
|
+
function deploymentVerdict(o) {
|
|
7117
|
+
if (o.type !== DEPLOYMENT_TYPE) return void 0;
|
|
7118
|
+
const damage = unhappyCondition(o, "ReplicaFailure") ?? unhappyCondition(o, "Progressing");
|
|
7119
|
+
if (damage) return { health: "degraded", detail: damage };
|
|
7120
|
+
const available = unhappyCondition(o, "Available");
|
|
7121
|
+
if (available) return { health: "progressing", detail: available };
|
|
7122
|
+
return void 0;
|
|
7123
|
+
}
|
|
5247
7124
|
function classifyObservedHealth(observed) {
|
|
5248
7125
|
if (!observed) return { health: "unknown" };
|
|
5249
|
-
return argoVerdict(observed) ?? fluxVerdict(observed) ?? { health: classifyHealth(observed.status) };
|
|
7126
|
+
return argoVerdict(observed) ?? fluxVerdict(observed) ?? deploymentVerdict(observed) ?? { health: classifyHealth(observed.status) };
|
|
5250
7127
|
}
|
|
5251
7128
|
|
|
5252
7129
|
// src/apply.ts
|
|
@@ -5359,6 +7236,19 @@ var OpRunner = class {
|
|
|
5359
7236
|
* before the first event lands so a reload between trigger and first event
|
|
5360
7237
|
* doesn't show the PREVIOUS run's stale terminal state). */
|
|
5361
7238
|
lastApplyProgress = initialApplyProgress;
|
|
7239
|
+
/** The run playhead (#284 item 2): the settled StepRecords of the last Op
|
|
7240
|
+
* `trigger()` started, plus the last `gateState` answer. Kept after the run
|
|
7241
|
+
* ends and across renders for the same reason `lastApplyProgress` is — a
|
|
7242
|
+
* client opening the ops lens mid-run (or after a reload) hydrates the
|
|
7243
|
+
* playhead instead of starting blank. In-process only: a run started
|
|
7244
|
+
* elsewhere is invisible here, and `chant run status` (src/server.ts's
|
|
7245
|
+
* `/api/ops/:name/status`) is what answers for one. */
|
|
7246
|
+
lastRunState = initialRunState;
|
|
7247
|
+
/** The operator strip (#234 join 3): the last `chant operator status --json`
|
|
7248
|
+
* read, plus the ConvergeOps the built project declares. Held here for the
|
|
7249
|
+
* reason `lastRunState` is — it is a durable status read, not a run, and a
|
|
7250
|
+
* client opening the ops lens hydrates from it instead of starting blank. */
|
|
7251
|
+
lastOperatorState = initialOperatorState;
|
|
5362
7252
|
/** #195: re-point delegated writes at a switched project. The runner reads
|
|
5363
7253
|
* `deps.projectDir` at trigger time, so this is the whole retarget. */
|
|
5364
7254
|
retarget(projectDir) {
|
|
@@ -5373,13 +7263,113 @@ var OpRunner = class {
|
|
|
5373
7263
|
get applyProgress() {
|
|
5374
7264
|
return this.lastApplyProgress;
|
|
5375
7265
|
}
|
|
7266
|
+
/** The run playhead's model (#284 item 2) — `initialRunState` until an Op has
|
|
7267
|
+
* been triggered this session. */
|
|
7268
|
+
get runState() {
|
|
7269
|
+
return this.lastRunState;
|
|
7270
|
+
}
|
|
7271
|
+
/**
|
|
7272
|
+
* Fold a `chant run status <name>` read (src/run-playhead.ts's
|
|
7273
|
+
* `readRunStatus`) into the playhead, and broadcast it.
|
|
7274
|
+
*
|
|
7275
|
+
* The gate is a workflow QUERY, not part of the record stream, so it can only
|
|
7276
|
+
* arrive this way. A read for an Op other than the one being watched is
|
|
7277
|
+
* returned to the caller but not folded — one playhead, one Op, and silently
|
|
7278
|
+
* re-pointing it at whatever was last polled would make the picture lie about
|
|
7279
|
+
* which run it shows. A read arriving before anything has run this session
|
|
7280
|
+
* adopts the Op: a gate pending on a run behold didn't start is exactly the
|
|
7281
|
+
* case the pending card exists for.
|
|
7282
|
+
*/
|
|
7283
|
+
noteGate(op, result) {
|
|
7284
|
+
if (this.lastRunState.op !== null && this.lastRunState.op !== op) return this.lastRunState;
|
|
7285
|
+
const base = this.lastRunState.op === op ? this.lastRunState : { ...initialRunState, op, mode: "temporal" };
|
|
7286
|
+
const next = runGate(base, result);
|
|
7287
|
+
const changed = JSON.stringify(next) !== JSON.stringify(this.lastRunState);
|
|
7288
|
+
this.lastRunState = next;
|
|
7289
|
+
if (changed) this.emitRun();
|
|
7290
|
+
return this.lastRunState;
|
|
7291
|
+
}
|
|
5376
7292
|
/**
|
|
5377
7293
|
* Start `chant run <name>` unless one is already running (the Sync/Adopt/auto-
|
|
5378
7294
|
* sync path). `cwd` is the Op's own project dir (#31 multi-estate); defaults to
|
|
5379
7295
|
* the primary. Returns true if it started, false if busy.
|
|
7296
|
+
*
|
|
7297
|
+
* `temporal` — pass `--temporal` so the run gets the durable runtime. Callers
|
|
7298
|
+
* set it from the Op's declared gate: chant refuses a gated Op outright in
|
|
7299
|
+
* local mode ("gates and schedules need a durable runtime"), so without the
|
|
7300
|
+
* flag a gated Op could never run from behold at all.
|
|
7301
|
+
*
|
|
7302
|
+
* Either way the run is asked for structured per-step records (#284 item 2,
|
|
7303
|
+
* chant#1676) — `--progress-json` streams one NDJSON `StepRecord` per settled
|
|
7304
|
+
* step on the durable path; `--json` prints the whole `OpRunResult` at the end
|
|
7305
|
+
* on the local one. Same record shape, which is what lets one playhead render
|
|
7306
|
+
* both. `--json` replaces chant's own human per-step summary on the local
|
|
7307
|
+
* path, so each parsed record is re-emitted to the `op` now-line
|
|
7308
|
+
* ({@link recordLine}) and the operator loses no narration.
|
|
7309
|
+
*/
|
|
7310
|
+
trigger(name, opEnv, cwd, temporal) {
|
|
7311
|
+
if (this.current) return false;
|
|
7312
|
+
const mode = temporal ? "temporal" : "local";
|
|
7313
|
+
const args = temporal ? ["run", name, "--temporal", "--progress-json"] : ["run", name, "--json"];
|
|
7314
|
+
this.lastRunState = runStarted(name, mode, (/* @__PURE__ */ new Date()).toISOString());
|
|
7315
|
+
this.emitRun();
|
|
7316
|
+
return this.start(
|
|
7317
|
+
args,
|
|
7318
|
+
name,
|
|
7319
|
+
opEnv,
|
|
7320
|
+
cwd,
|
|
7321
|
+
(line) => {
|
|
7322
|
+
const records = parseRunProgressLine(line);
|
|
7323
|
+
if (!records) return false;
|
|
7324
|
+
for (const record of records) {
|
|
7325
|
+
this.lastRunState = runRecord(this.lastRunState, record);
|
|
7326
|
+
this.deps.broadcaster.emit("op", recordLine(record));
|
|
7327
|
+
}
|
|
7328
|
+
this.emitRun();
|
|
7329
|
+
return true;
|
|
7330
|
+
},
|
|
7331
|
+
(code) => {
|
|
7332
|
+
this.lastRunState = runEnded(this.lastRunState, code, (/* @__PURE__ */ new Date()).toISOString());
|
|
7333
|
+
this.emitRun();
|
|
7334
|
+
}
|
|
7335
|
+
);
|
|
7336
|
+
}
|
|
7337
|
+
emitRun() {
|
|
7338
|
+
this.deps.broadcaster.emit("run", JSON.stringify(this.lastRunState));
|
|
7339
|
+
}
|
|
7340
|
+
/** The operator strip's model (#234 join 3) — `initialOperatorState` until the
|
|
7341
|
+
* ops lens has been opened once. */
|
|
7342
|
+
get operatorState() {
|
|
7343
|
+
return this.lastOperatorState;
|
|
7344
|
+
}
|
|
7345
|
+
/**
|
|
7346
|
+
* Record which ConvergeOps the built project declares, read from the emitted
|
|
7347
|
+
* op.json (src/operator.ts's `declaredConvergeOps`) — no subprocess, and the
|
|
7348
|
+
* thing that gates the strip. Broadcast on a change so a project rebuilt (or
|
|
7349
|
+
* switched, #195) under an open tab grows or loses its strip without a reload.
|
|
7350
|
+
*/
|
|
7351
|
+
noteDeclaredConvergeOps(declared) {
|
|
7352
|
+
return this.foldOperator({ ...this.lastOperatorState, declared });
|
|
7353
|
+
}
|
|
7354
|
+
/**
|
|
7355
|
+
* Fold a `chant operator status --json` read (src/operator.ts's
|
|
7356
|
+
* `readOperatorStatus`) into the strip, and broadcast it.
|
|
7357
|
+
*
|
|
7358
|
+
* Broadcast only a CHANGED answer, for `noteGate`'s reason: a client asks when
|
|
7359
|
+
* it opens the ops lens and re-pulls the lens on the broadcast, so an
|
|
7360
|
+
* unchanged answer that still broadcast would have the two chasing each other.
|
|
5380
7361
|
*/
|
|
5381
|
-
|
|
5382
|
-
return this.
|
|
7362
|
+
noteOperator(result, at = (/* @__PURE__ */ new Date()).toISOString()) {
|
|
7363
|
+
return this.foldOperator(operatorRead(this.lastOperatorState, result, at));
|
|
7364
|
+
}
|
|
7365
|
+
/** Compare-and-broadcast, ignoring `readAt` — the instant behold asked changes
|
|
7366
|
+
* on every poll and is not itself news; broadcasting on it would make the
|
|
7367
|
+
* interval a repaint loop. */
|
|
7368
|
+
foldOperator(next) {
|
|
7369
|
+
const same = JSON.stringify({ ...next, readAt: null }) === JSON.stringify({ ...this.lastOperatorState, readAt: null });
|
|
7370
|
+
this.lastOperatorState = next;
|
|
7371
|
+
if (!same) this.deps.broadcaster.emit("operator", JSON.stringify(next));
|
|
7372
|
+
return this.lastOperatorState;
|
|
5383
7373
|
}
|
|
5384
7374
|
/**
|
|
5385
7375
|
* Run an arbitrary `chant` invocation through the same guard/stream/PR/capture
|
|
@@ -5511,8 +7501,12 @@ var OpRunner = class {
|
|
|
5511
7501
|
* fully handled that line (apply()'s progress-JSON parsing) — `start()` then
|
|
5512
7502
|
* skips its own `op`/`pr` broadcast for that one line, leaving every other
|
|
5513
7503
|
* line's raw-log fallback untouched.
|
|
7504
|
+
*
|
|
7505
|
+
* `onExit` (the run playhead, #284 item 2) sees the exit code the moment the
|
|
7506
|
+
* process ends — the only signal that separates "the run finished" from "the
|
|
7507
|
+
* stream stopped", and the reason the playhead never has to infer completion.
|
|
5514
7508
|
*/
|
|
5515
|
-
start(args, label, opEnv, cwd, onLine) {
|
|
7509
|
+
start(args, label, opEnv, cwd, onLine, onExit) {
|
|
5516
7510
|
if (this.current) return false;
|
|
5517
7511
|
const { projectDir, broadcaster } = this.deps;
|
|
5518
7512
|
broadcaster.emit("op", `\u25B6 chant ${args.join(" ")}`);
|
|
@@ -5525,6 +7519,7 @@ var OpRunner = class {
|
|
|
5525
7519
|
this.current = label;
|
|
5526
7520
|
void op.done.then((code) => {
|
|
5527
7521
|
broadcaster.emit("op", `\u25A0 ${label} exited ${code}`);
|
|
7522
|
+
onExit?.(code);
|
|
5528
7523
|
this.current = null;
|
|
5529
7524
|
Promise.resolve(this.deps.onDone(opEnv)).then(() => broadcaster.emit("changed")).catch((err) => broadcaster.emit("op", `\u26A0 post-op capture: ${err instanceof Error ? err.message : String(err)}`));
|
|
5530
7525
|
});
|
|
@@ -5534,24 +7529,24 @@ var OpRunner = class {
|
|
|
5534
7529
|
|
|
5535
7530
|
// src/substrates.ts
|
|
5536
7531
|
import { spawn as spawn4 } from "node:child_process";
|
|
5537
|
-
import { existsSync as existsSync10, readFileSync as
|
|
5538
|
-
import { join as
|
|
7532
|
+
import { existsSync as existsSync10, readFileSync as readFileSync12 } from "node:fs";
|
|
7533
|
+
import { join as join15 } from "node:path";
|
|
5539
7534
|
import { platform } from "node:os";
|
|
5540
7535
|
var DOCKER_DOWN_DETAIL = "docker is down";
|
|
5541
7536
|
function probe(cmd, args) {
|
|
5542
|
-
return new Promise((
|
|
7537
|
+
return new Promise((resolve9) => {
|
|
5543
7538
|
let out = "";
|
|
5544
7539
|
let proc;
|
|
5545
7540
|
try {
|
|
5546
7541
|
proc = spawn4(cmd, args, { stdio: ["ignore", "pipe", "pipe"] });
|
|
5547
7542
|
} catch {
|
|
5548
|
-
|
|
7543
|
+
resolve9({ code: 127, out: "" });
|
|
5549
7544
|
return;
|
|
5550
7545
|
}
|
|
5551
7546
|
proc.stdout.on("data", (d) => out += d);
|
|
5552
7547
|
proc.stderr.on("data", (d) => out += d);
|
|
5553
|
-
proc.on("error", () =>
|
|
5554
|
-
proc.on("close", (code) =>
|
|
7548
|
+
proc.on("error", () => resolve9({ code: 127, out }));
|
|
7549
|
+
proc.on("close", (code) => resolve9({ code: code ?? 1, out }));
|
|
5555
7550
|
});
|
|
5556
7551
|
}
|
|
5557
7552
|
async function dockerAvailable(run3) {
|
|
@@ -5564,11 +7559,11 @@ async function dockerRunning(run3, nameFilter) {
|
|
|
5564
7559
|
return out.split(/\r?\n/).map((s) => s.trim()).filter(Boolean);
|
|
5565
7560
|
}
|
|
5566
7561
|
function scriptBringUp(projectDir, relPath, label) {
|
|
5567
|
-
return existsSync10(
|
|
7562
|
+
return existsSync10(join15(projectDir, relPath)) ? { label, cmd: "bash", args: [relPath] } : void 0;
|
|
5568
7563
|
}
|
|
5569
7564
|
function projectLexicons(projectDir) {
|
|
5570
7565
|
try {
|
|
5571
|
-
const src =
|
|
7566
|
+
const src = readFileSync12(join15(projectDir, "chant.config.ts"), "utf-8");
|
|
5572
7567
|
const m = src.match(/lexicons\s*:\s*\[([^\]]*)\]/);
|
|
5573
7568
|
if (!m) return [];
|
|
5574
7569
|
return [...m[1].matchAll(/["']([^"']+)["']/g)].map((x) => x[1]);
|
|
@@ -5620,7 +7615,7 @@ async function detectSubstrates(projectDir, preview = false, boundContext, probe
|
|
|
5620
7615
|
["forgejo", "Forgejo", ".forgejo", "test/forgejo-runtime-e2e.sh"]
|
|
5621
7616
|
];
|
|
5622
7617
|
for (const [name, label, marker, script] of forges) {
|
|
5623
|
-
if (!existsSync10(
|
|
7618
|
+
if (!existsSync10(join15(projectDir, marker))) continue;
|
|
5624
7619
|
const c = docker ? await dockerRunning(run3, name) : [];
|
|
5625
7620
|
const d = dep(c.length > 0, "container up", "on-demand (pipeline run)");
|
|
5626
7621
|
subs.push({
|
|
@@ -5654,7 +7649,7 @@ async function detectSubstrates(projectDir, preview = false, boundContext, probe
|
|
|
5654
7649
|
detail: endpoint ? `targeting ${endpoint}` : "real Fly (FLY_FLAPS_BASE_URL unset)"
|
|
5655
7650
|
});
|
|
5656
7651
|
}
|
|
5657
|
-
if (existsSync10(
|
|
7652
|
+
if (existsSync10(join15(projectDir, ".github", "workflows"))) {
|
|
5658
7653
|
const gh = await run3("gh", ["auth", "status"]);
|
|
5659
7654
|
const ready = gh.code === 0;
|
|
5660
7655
|
subs.push({
|
|
@@ -5667,7 +7662,7 @@ async function detectSubstrates(projectDir, preview = false, boundContext, probe
|
|
|
5667
7662
|
if (lexicons.includes("temporal")) {
|
|
5668
7663
|
let hasProfiles = false;
|
|
5669
7664
|
try {
|
|
5670
|
-
hasProfiles = /temporal\s*:\s*\{[\s\S]{0,400}?profiles\s*:/.test(
|
|
7665
|
+
hasProfiles = /temporal\s*:\s*\{[\s\S]{0,400}?profiles\s*:/.test(readFileSync12(join15(projectDir, "chant.config.ts"), "utf-8"));
|
|
5671
7666
|
} catch {
|
|
5672
7667
|
}
|
|
5673
7668
|
subs.push({
|
|
@@ -5770,165 +7765,9 @@ async function openRollbackBranches(projectDir, env) {
|
|
|
5770
7765
|
}
|
|
5771
7766
|
}
|
|
5772
7767
|
|
|
5773
|
-
// src/estate.ts
|
|
5774
|
-
import { statSync as statSync3 } from "node:fs";
|
|
5775
|
-
import { join as join13, resolve as resolve3, sep as sep2 } from "node:path";
|
|
5776
|
-
import { composeStacks, shortStackNames } from "@intentius/pinhole";
|
|
5777
|
-
async function composeEstate(projectDirs, opts = {}) {
|
|
5778
|
-
const names = shortStackNames(projectDirs);
|
|
5779
|
-
const stacks = await Promise.all(
|
|
5780
|
-
projectDirs.map(async (dir, i) => ({ name: names[i], ir: await graphIr(dir, opts) }))
|
|
5781
|
-
);
|
|
5782
|
-
return composeStacks(stacks);
|
|
5783
|
-
}
|
|
5784
|
-
var firstLine2 = (e) => {
|
|
5785
|
-
const msg = e instanceof Error ? e.message : String(e);
|
|
5786
|
-
const m = msg.match(/exited \d+:\s*([\s\S]*)/);
|
|
5787
|
-
return (m ? m[1] : msg).split("\n")[0].trim().slice(0, 160);
|
|
5788
|
-
};
|
|
5789
|
-
function namespaceRuntimeOwners(name, ir) {
|
|
5790
|
-
const own = new Set(ir.nodes.map((n) => n.id));
|
|
5791
|
-
for (const n of ir.nodes) {
|
|
5792
|
-
if (n.runtimeOwner && own.has(n.runtimeOwner)) n.runtimeOwner = `${name}/${n.runtimeOwner}`;
|
|
5793
|
-
}
|
|
5794
|
-
return ir;
|
|
5795
|
-
}
|
|
5796
|
-
var NAMESPACE_JOIN_FLOOR = "0.44.5";
|
|
5797
|
-
var rec3 = (v) => v && typeof v === "object" && !Array.isArray(v) ? v : void 0;
|
|
5798
|
-
var str3 = (v) => typeof v === "string" && v.length > 0 ? v : void 0;
|
|
5799
|
-
var realIsDir = (p) => {
|
|
5800
|
-
try {
|
|
5801
|
-
return statSync3(p).isDirectory();
|
|
5802
|
-
} catch {
|
|
5803
|
-
return false;
|
|
5804
|
-
}
|
|
5805
|
-
};
|
|
5806
|
-
function declaredNamespaceBindings(ir) {
|
|
5807
|
-
const out = [];
|
|
5808
|
-
for (const n of ir.nodes) {
|
|
5809
|
-
if (n.kind !== "K8s::Flux::Kustomization") continue;
|
|
5810
|
-
const spec = rec3(n.attrs?.spec);
|
|
5811
|
-
const namespace = str3(spec?.targetNamespace);
|
|
5812
|
-
const path = str3(spec?.path);
|
|
5813
|
-
if (namespace && path) out.push({ path, namespace });
|
|
5814
|
-
}
|
|
5815
|
-
return out;
|
|
5816
|
-
}
|
|
5817
|
-
function awaitsNamespaceBinding(ir) {
|
|
5818
|
-
const namespaced = ir.nodes.filter((n) => n.lexicon === "k8s" && !CLUSTER_SCOPED.has(n.kind ?? ""));
|
|
5819
|
-
if (namespaced.length === 0) return false;
|
|
5820
|
-
return namespaced.every((n) => !str3(rec3(n.attrs?.metadata)?.namespace));
|
|
5821
|
-
}
|
|
5822
|
-
function declaredSegments(path) {
|
|
5823
|
-
if (path.startsWith("/")) return void 0;
|
|
5824
|
-
const segs = path.split("/").filter((s) => s !== "" && s !== ".");
|
|
5825
|
-
if (segs.length === 0 || segs.includes("..")) return void 0;
|
|
5826
|
-
return segs;
|
|
5827
|
-
}
|
|
5828
|
-
function pathAlignment(declared, dir, isDir = realIsDir) {
|
|
5829
|
-
const want = declaredSegments(declared);
|
|
5830
|
-
if (!want) return 0;
|
|
5831
|
-
const root = resolve3(dir);
|
|
5832
|
-
const have = root.split(sep2).filter(Boolean);
|
|
5833
|
-
for (let k = Math.min(want.length, have.length); k >= 1; k--) {
|
|
5834
|
-
if (!have.slice(-k).every((s, i) => s === want[i])) continue;
|
|
5835
|
-
if (!isDir(join13(root, ...want.slice(k)))) continue;
|
|
5836
|
-
return k;
|
|
5837
|
-
}
|
|
5838
|
-
return 0;
|
|
5839
|
-
}
|
|
5840
|
-
function joinNamespaceBindings(members, isDir = realIsDir) {
|
|
5841
|
-
const bindings = members.flatMap(
|
|
5842
|
-
(m) => m.ir ? declaredNamespaceBindings(m.ir).map((b) => ({ ...b, declaredBy: m.dir })) : []
|
|
5843
|
-
);
|
|
5844
|
-
if (bindings.length === 0) return [];
|
|
5845
|
-
const awaiting = members.filter((m) => m.ir && awaitsNamespaceBinding(m.ir));
|
|
5846
|
-
if (awaiting.length === 0) return [];
|
|
5847
|
-
const best = /* @__PURE__ */ new Map();
|
|
5848
|
-
for (const b of bindings) {
|
|
5849
|
-
let winner;
|
|
5850
|
-
let tied = false;
|
|
5851
|
-
for (const m of awaiting) {
|
|
5852
|
-
if (m.dir === b.declaredBy) continue;
|
|
5853
|
-
const score = pathAlignment(b.path, m.dir, isDir);
|
|
5854
|
-
if (score === 0) continue;
|
|
5855
|
-
if (!winner || score > winner.score) {
|
|
5856
|
-
winner = { dir: m.dir, score };
|
|
5857
|
-
tied = false;
|
|
5858
|
-
} else if (score === winner.score) {
|
|
5859
|
-
tied = true;
|
|
5860
|
-
}
|
|
5861
|
-
}
|
|
5862
|
-
if (!winner || tied) continue;
|
|
5863
|
-
const prev = best.get(winner.dir);
|
|
5864
|
-
if (prev === "ambiguous") continue;
|
|
5865
|
-
const join19 = { dir: winner.dir, namespace: b.namespace, path: b.path, declaredBy: b.declaredBy };
|
|
5866
|
-
if (!prev) best.set(winner.dir, { score: winner.score, join: join19 });
|
|
5867
|
-
else if (prev.join.namespace !== b.namespace) best.set(winner.dir, "ambiguous");
|
|
5868
|
-
else if (winner.score > prev.score) best.set(winner.dir, { score: winner.score, join: join19 });
|
|
5869
|
-
}
|
|
5870
|
-
return [...best.values()].filter((v) => v !== "ambiguous").map((v) => v.join);
|
|
5871
|
-
}
|
|
5872
|
-
async function estateNamespaceScopes(projectDirs, opts, isDir = realIsDir) {
|
|
5873
|
-
if (projectDirs.length < 2) return /* @__PURE__ */ new Map();
|
|
5874
|
-
const { live: _live, overlay: _overlay, namespace: _namespace, ...src } = opts;
|
|
5875
|
-
const irs = await Promise.all(projectDirs.map((dir) => graphIr(dir, { ...src, detail: 3 }).catch(() => void 0)));
|
|
5876
|
-
const scopes = /* @__PURE__ */ new Map();
|
|
5877
|
-
for (const j of joinNamespaceBindings(projectDirs.map((dir, i) => ({ dir, ir: irs[i] })), isDir)) {
|
|
5878
|
-
if (!meetsFloor(resolveChant(j.dir).version, NAMESPACE_JOIN_FLOOR)) continue;
|
|
5879
|
-
scopes.set(j.dir, j.namespace);
|
|
5880
|
-
}
|
|
5881
|
-
return scopes;
|
|
5882
|
-
}
|
|
5883
|
-
function withoutJoinedMembers(nodes, joined) {
|
|
5884
|
-
if (joined.length === 0) return [...nodes];
|
|
5885
|
-
return nodes.filter((n) => !joined.some((j) => n.id.startsWith(`${j.name}/`)));
|
|
5886
|
-
}
|
|
5887
|
-
async function composeEstateOverlay(projectDirs, opts, classify) {
|
|
5888
|
-
const names = shortStackNames(projectDirs);
|
|
5889
|
-
const unobserved = [];
|
|
5890
|
-
const dropped = [];
|
|
5891
|
-
const joined = [];
|
|
5892
|
-
const stacks = new Array(projectDirs.length);
|
|
5893
|
-
const scopes = await estateNamespaceScopes(projectDirs, opts);
|
|
5894
|
-
await Promise.all(
|
|
5895
|
-
projectDirs.map(async (dir, i) => {
|
|
5896
|
-
const name = names[i];
|
|
5897
|
-
const namespace = scopes.get(dir);
|
|
5898
|
-
try {
|
|
5899
|
-
const live = { ...opts, live: true, overlay: true, ...namespace ? { namespace } : {} };
|
|
5900
|
-
stacks[i] = { name, ir: namespaceRuntimeOwners(name, classify(await graphIr(dir, live))) };
|
|
5901
|
-
if (namespace) joined.push({ name, namespace });
|
|
5902
|
-
} catch (err) {
|
|
5903
|
-
const reason = firstLine2(err);
|
|
5904
|
-
try {
|
|
5905
|
-
const { env: _env, live: _live, overlay: _overlay, ...srcOpts } = opts;
|
|
5906
|
-
const src = await graphIr(dir, srcOpts);
|
|
5907
|
-
for (const n of src.nodes) n.attrs = { ...n.attrs, _status: "neutral", _unobserved: reason };
|
|
5908
|
-
stacks[i] = { name, ir: src };
|
|
5909
|
-
unobserved.push({ name, reason });
|
|
5910
|
-
} catch (err2) {
|
|
5911
|
-
dropped.push({ name, reason: firstLine2(err2) });
|
|
5912
|
-
}
|
|
5913
|
-
}
|
|
5914
|
-
})
|
|
5915
|
-
);
|
|
5916
|
-
const present = stacks.filter((s) => !!s);
|
|
5917
|
-
return {
|
|
5918
|
-
ir: composeStacks(present),
|
|
5919
|
-
observed: present.length - unobserved.length,
|
|
5920
|
-
total: projectDirs.length,
|
|
5921
|
-
unobserved,
|
|
5922
|
-
dropped,
|
|
5923
|
-
// Concurrent reads finish in whatever order the clusters answer; the note
|
|
5924
|
-
// this feeds should read the same twice.
|
|
5925
|
-
joined: joined.sort((a, b) => a.name.localeCompare(b.name))
|
|
5926
|
-
};
|
|
5927
|
-
}
|
|
5928
|
-
|
|
5929
7768
|
// src/events.ts
|
|
5930
7769
|
import { watch, existsSync as existsSync11 } from "node:fs";
|
|
5931
|
-
import { join as
|
|
7770
|
+
import { join as join16 } from "node:path";
|
|
5932
7771
|
var Broadcaster = class {
|
|
5933
7772
|
listeners = /* @__PURE__ */ new Set();
|
|
5934
7773
|
subscribe(fn) {
|
|
@@ -5946,7 +7785,7 @@ var Broadcaster = class {
|
|
|
5946
7785
|
};
|
|
5947
7786
|
var IGNORE = /(^|[\\/])(node_modules|dist|\.git)([\\/]|$)/;
|
|
5948
7787
|
function watchSource(projectDir, onChange, debounceMs = 200) {
|
|
5949
|
-
const dir = existsSync11(
|
|
7788
|
+
const dir = existsSync11(join16(projectDir, "src")) ? join16(projectDir, "src") : projectDir;
|
|
5950
7789
|
let timer;
|
|
5951
7790
|
const watcher = watch(dir, { recursive: true }, (_event, file) => {
|
|
5952
7791
|
const name = typeof file === "string" ? file : "";
|
|
@@ -5959,6 +7798,12 @@ function watchSource(projectDir, onChange, debounceMs = 200) {
|
|
|
5959
7798
|
watcher.close();
|
|
5960
7799
|
};
|
|
5961
7800
|
}
|
|
7801
|
+
function watchSources(projectDirs, onChange, debounceMs = 200) {
|
|
7802
|
+
const stops = projectDirs.map((dir) => watchSource(dir, () => onChange(dir), debounceMs));
|
|
7803
|
+
return () => {
|
|
7804
|
+
for (const stop of stops) stop();
|
|
7805
|
+
};
|
|
7806
|
+
}
|
|
5962
7807
|
|
|
5963
7808
|
// src/poll.ts
|
|
5964
7809
|
function digestOf(nodes) {
|
|
@@ -5986,18 +7831,22 @@ function changedLexicons(prev, next) {
|
|
|
5986
7831
|
}
|
|
5987
7832
|
function startDriftPoll(opts) {
|
|
5988
7833
|
let stopped = false;
|
|
5989
|
-
|
|
7834
|
+
const last = /* @__PURE__ */ new Map();
|
|
5990
7835
|
let timer;
|
|
5991
7836
|
const tick = async () => {
|
|
5992
|
-
|
|
5993
|
-
|
|
5994
|
-
|
|
5995
|
-
const
|
|
5996
|
-
|
|
7837
|
+
for (const member of opts.members) {
|
|
7838
|
+
if (stopped) return;
|
|
7839
|
+
try {
|
|
7840
|
+
const digests = driftDigestsByLexicon(await member.query());
|
|
7841
|
+
const prev = last.get(member.dir);
|
|
7842
|
+
if (prev !== void 0) {
|
|
7843
|
+
const moved = changedLexicons(prev, digests);
|
|
7844
|
+
if (moved.length) opts.onChange(member.dir, moved);
|
|
7845
|
+
}
|
|
7846
|
+
last.set(member.dir, digests);
|
|
7847
|
+
} catch (err) {
|
|
7848
|
+
opts.onError?.(member.dir, err);
|
|
5997
7849
|
}
|
|
5998
|
-
last = digests;
|
|
5999
|
-
} catch (err) {
|
|
6000
|
-
opts.onError?.(err);
|
|
6001
7850
|
}
|
|
6002
7851
|
if (!stopped) timer = setTimeout(tick, opts.intervalMs);
|
|
6003
7852
|
};
|
|
@@ -6185,8 +8034,8 @@ function renderLanes(frames, summaries) {
|
|
|
6185
8034
|
status: Object.fromEntries(f.ir.nodes.map((n) => [n.id, n.attrs?._status ?? ""])),
|
|
6186
8035
|
lexicon: Object.fromEntries(f.ir.nodes.map((n) => [n.id, n.lexicon]))
|
|
6187
8036
|
}));
|
|
6188
|
-
const
|
|
6189
|
-
return doc.includes("</body>") ? doc.replace("</body>", `${
|
|
8037
|
+
const strip3 = `<style>${LANES_CSS}</style>` + THEME_BOOT + `<div id="behold-diff"></div><div id="behold-lanes"><div class="hd"><span>deployment lanes</span><span id="behold-lanes-meta"></span><span class="rt" id="behold-lanes-rt" style="display:none">offset \u2014 graph shows real time</span><a href="/">\u2190 graph</a><span id="behold-lanes-pickers"></span></div><canvas id="behold-lanes-canvas"></canvas></div>` + laneStripScript(laneFrames);
|
|
8038
|
+
return doc.includes("</body>") ? doc.replace("</body>", `${strip3}</body>`) : doc + strip3;
|
|
6190
8039
|
}
|
|
6191
8040
|
|
|
6192
8041
|
// src/emulator.ts
|
|
@@ -6224,13 +8073,13 @@ async function emulatorDown(projectDir) {
|
|
|
6224
8073
|
}
|
|
6225
8074
|
|
|
6226
8075
|
// src/demos.ts
|
|
6227
|
-
import { readFileSync as
|
|
6228
|
-
import { join as
|
|
8076
|
+
import { readFileSync as readFileSync13, existsSync as existsSync12, cpSync } from "node:fs";
|
|
8077
|
+
import { join as join17, relative as relative3, resolve as resolve5, sep as sep3 } from "node:path";
|
|
6229
8078
|
import { spawn as spawn5, spawnSync } from "node:child_process";
|
|
6230
8079
|
function loadDemoRegistry(pkgRoot2) {
|
|
6231
8080
|
let raw;
|
|
6232
8081
|
try {
|
|
6233
|
-
raw = JSON.parse(
|
|
8082
|
+
raw = JSON.parse(readFileSync13(join17(pkgRoot2, "demos.json"), "utf8"));
|
|
6234
8083
|
} catch {
|
|
6235
8084
|
return [];
|
|
6236
8085
|
}
|
|
@@ -6270,22 +8119,22 @@ function fetchesFromNetwork(entry) {
|
|
|
6270
8119
|
return entry.source === "git";
|
|
6271
8120
|
}
|
|
6272
8121
|
function demoTargetDir(entry, cwd = process.cwd()) {
|
|
6273
|
-
const legacy =
|
|
8122
|
+
const legacy = resolve5(cwd, "behold-demo");
|
|
6274
8123
|
if (entry.name === "writes" && existsSync12(legacy)) return legacy;
|
|
6275
|
-
return
|
|
8124
|
+
return resolve5(cwd, "behold-demos", entry.name);
|
|
6276
8125
|
}
|
|
6277
8126
|
async function loadDemo(entry, opts) {
|
|
6278
8127
|
const { pkgRoot: pkgRoot2, target } = opts;
|
|
6279
8128
|
const say = (line) => opts.log?.(`behold demo ${entry.name} \u2192 ${line}`);
|
|
6280
8129
|
if (!existsSync12(target)) {
|
|
6281
8130
|
if (entry.source === "bundled") {
|
|
6282
|
-
const bundled =
|
|
8131
|
+
const bundled = join17(pkgRoot2, entry.dir);
|
|
6283
8132
|
if (!existsSync12(bundled)) return { ok: false, error: `this install has no bundled ${entry.dir}` };
|
|
6284
8133
|
say(`copying to ${target} (it's yours \u2014 edit it)`);
|
|
6285
8134
|
try {
|
|
6286
8135
|
cpSync(bundled, target, {
|
|
6287
8136
|
recursive: true,
|
|
6288
|
-
filter: (src) => !
|
|
8137
|
+
filter: (src) => !relative3(bundled, src).split(sep3).includes("node_modules")
|
|
6289
8138
|
});
|
|
6290
8139
|
} catch (err) {
|
|
6291
8140
|
return { ok: false, error: `copy failed: ${err instanceof Error ? err.message : String(err)}` };
|
|
@@ -6299,7 +8148,7 @@ async function loadDemo(entry, opts) {
|
|
|
6299
8148
|
} else {
|
|
6300
8149
|
say(`reusing ${target}`);
|
|
6301
8150
|
}
|
|
6302
|
-
if (existsSync12(
|
|
8151
|
+
if (existsSync12(join17(target, "package.json")) && !existsSync12(join17(target, "node_modules"))) {
|
|
6303
8152
|
say("npm install\u2026");
|
|
6304
8153
|
if (await runStep("npm", ["install"], { cwd: target, shell: process.platform === "win32" }) !== 0) {
|
|
6305
8154
|
return { ok: false, error: `npm install failed in ${target}` };
|
|
@@ -6311,7 +8160,7 @@ async function loadDemo(entry, opts) {
|
|
|
6311
8160
|
return { ok: false, error: `setup failed (${entry.setup})` };
|
|
6312
8161
|
}
|
|
6313
8162
|
}
|
|
6314
|
-
return { ok: true, serveDirs: entry.serve.dirs?.length ? entry.serve.dirs.map((d) =>
|
|
8163
|
+
return { ok: true, serveDirs: entry.serve.dirs?.length ? entry.serve.dirs.map((d) => join17(target, d)) : [target] };
|
|
6315
8164
|
}
|
|
6316
8165
|
function runStep(cmd, args, opts = {}) {
|
|
6317
8166
|
return new Promise((res) => {
|
|
@@ -6322,8 +8171,8 @@ function runStep(cmd, args, opts = {}) {
|
|
|
6322
8171
|
}
|
|
6323
8172
|
|
|
6324
8173
|
// src/server.ts
|
|
6325
|
-
var pkgRoot =
|
|
6326
|
-
var webRoot =
|
|
8174
|
+
var pkgRoot = join18(dirname5(fileURLToPath3(import.meta.url)), "..");
|
|
8175
|
+
var webRoot = join18(pkgRoot, "web");
|
|
6327
8176
|
var execFileP = async (cmd, args) => (await promisify3(execFile3)(cmd, args, { encoding: "utf8", timeout: 1e4 })).stdout;
|
|
6328
8177
|
function optsFromQuery(url, tierEnvVar, projectDir) {
|
|
6329
8178
|
const q = url.searchParams;
|
|
@@ -6372,8 +8221,8 @@ function tierFailure(tier, message) {
|
|
|
6372
8221
|
}
|
|
6373
8222
|
function beholdVersion() {
|
|
6374
8223
|
try {
|
|
6375
|
-
const here =
|
|
6376
|
-
return JSON.parse(
|
|
8224
|
+
const here = dirname5(fileURLToPath3(import.meta.url));
|
|
8225
|
+
return JSON.parse(readFileSync14(join18(here, "..", "package.json"), "utf8")).version ?? "unknown";
|
|
6377
8226
|
} catch {
|
|
6378
8227
|
return "unknown";
|
|
6379
8228
|
}
|
|
@@ -6418,7 +8267,9 @@ async function captureFrame(projectDir, env, frames, broadcaster) {
|
|
|
6418
8267
|
}
|
|
6419
8268
|
}
|
|
6420
8269
|
function carveRoutes(app, reportPath, demo) {
|
|
6421
|
-
const load = () => readCarveReport(reportPath, (p) =>
|
|
8270
|
+
const load = () => readCarveReport(reportPath, (p) => readFileSync14(p, "utf8"));
|
|
8271
|
+
const carveDirs = [dirname5(reportPath), ...demo ? [demo.out] : []];
|
|
8272
|
+
const carveStates = () => readCarveStates(carveDirs, nodeCarveIo, 0);
|
|
6422
8273
|
const demoBlock = () => carveWriteBlock(demo);
|
|
6423
8274
|
const demoInfo = () => {
|
|
6424
8275
|
if (!demo) return null;
|
|
@@ -6430,8 +8281,8 @@ function carveRoutes(app, reportPath, demo) {
|
|
|
6430
8281
|
project: demo.project,
|
|
6431
8282
|
out: demo.out,
|
|
6432
8283
|
// Paths the UI can print without leaking the operator's whole home dir.
|
|
6433
|
-
outLabel:
|
|
6434
|
-
fromLabel:
|
|
8284
|
+
outLabel: relative4(demo.root, demo.out).split(sep4).join("/"),
|
|
8285
|
+
fromLabel: relative4(demo.root, demo.from).split(sep4).join("/"),
|
|
6435
8286
|
runnable: !block,
|
|
6436
8287
|
...block ? { reason: block } : {},
|
|
6437
8288
|
// The live tier, when this boot armed it: the scratch endpoint the
|
|
@@ -6448,7 +8299,7 @@ function carveRoutes(app, reportPath, demo) {
|
|
|
6448
8299
|
let appGraph;
|
|
6449
8300
|
const appGraphOnce = () => {
|
|
6450
8301
|
if (!demo) return Promise.resolve(null);
|
|
6451
|
-
appGraph ??= graphIr(demo.project).then((ir) => ir.nodes.length ? { ir, label:
|
|
8302
|
+
appGraph ??= graphIr(demo.project).then((ir) => ir.nodes.length ? { ir, label: relative4(demo.root, demo.project).split(sep4).join("/") } : null).catch(() => null);
|
|
6452
8303
|
return appGraph;
|
|
6453
8304
|
};
|
|
6454
8305
|
app.get("/api/graph", async (c) => {
|
|
@@ -6456,10 +8307,21 @@ function carveRoutes(app, reportPath, demo) {
|
|
|
6456
8307
|
if (!parsed.ok) return c.json(parsed.refusal, 422);
|
|
6457
8308
|
const tfIr = carveReportToIr(parsed.report);
|
|
6458
8309
|
const appSide = await appGraphOnce();
|
|
8310
|
+
const states = carveStates();
|
|
8311
|
+
const shorten = demo ? (p) => shortenIn(p, demo.root) : (p) => p;
|
|
8312
|
+
const single = () => {
|
|
8313
|
+
const split = splitCarveState(tfIr, states, { shorten });
|
|
8314
|
+
return bandGraduated(split.tf, split.graduated);
|
|
8315
|
+
};
|
|
6459
8316
|
const { svg, ir } = appSide ? renderCarveEstate(tfIr, appSide.ir, {
|
|
6460
|
-
tfTitle: `${
|
|
6461
|
-
appTitle: `${appSide.label} \u2014 chant
|
|
6462
|
-
|
|
8317
|
+
tfTitle: `${relative4(demo.root, demo.from).split(sep4).join("/")} \u2014 terraform`,
|
|
8318
|
+
appTitle: `${appSide.label} \u2014 chant`,
|
|
8319
|
+
carved: states,
|
|
8320
|
+
shorten
|
|
8321
|
+
}) : (() => {
|
|
8322
|
+
const banded = single();
|
|
8323
|
+
return { ...renderBanded(banded), ir: banded };
|
|
8324
|
+
})();
|
|
6463
8325
|
return c.json({
|
|
6464
8326
|
ir,
|
|
6465
8327
|
svg,
|
|
@@ -6471,7 +8333,7 @@ function carveRoutes(app, reportPath, demo) {
|
|
|
6471
8333
|
carve: true,
|
|
6472
8334
|
// A demo whose own advisor run failed says so on the statusbar, not
|
|
6473
8335
|
// only in the terminal the viewer isn't looking at.
|
|
6474
|
-
note: carveNote(parsed.report, tfIr) + (demo?.degraded ? ` Degraded: ${demo.degraded}` : "")
|
|
8336
|
+
note: carveNote(parsed.report, tfIr) + carveStateNote(carveProgress(states.values(), tfIr.nodes.length), states) + (demo?.degraded ? ` Degraded: ${demo.degraded}` : "")
|
|
6475
8337
|
}
|
|
6476
8338
|
});
|
|
6477
8339
|
});
|
|
@@ -6503,14 +8365,22 @@ function carveRoutes(app, reportPath, demo) {
|
|
|
6503
8365
|
}
|
|
6504
8366
|
const tfIr = carveReportToIr(parsed.report);
|
|
6505
8367
|
const html = renderCarveMorph(tfIr, appSide.ir, [select], {
|
|
6506
|
-
tfTitle: `${
|
|
8368
|
+
tfTitle: `${relative4(demo.root, demo.from).split(sep4).join("/")} \u2014 terraform`,
|
|
6507
8369
|
appTitle: `${appSide.label} \u2014 chant`,
|
|
6508
|
-
title: `carve \u2014 ${select}
|
|
8370
|
+
title: `carve \u2014 ${select}`,
|
|
8371
|
+
carved: carveStates(),
|
|
8372
|
+
shorten: (p) => shortenIn(p, demo.root)
|
|
6509
8373
|
});
|
|
6510
8374
|
return c.html(html);
|
|
6511
8375
|
});
|
|
6512
8376
|
app.get("/api/project", (c) => {
|
|
6513
8377
|
const parsed = load();
|
|
8378
|
+
const total = parsed.ok ? parsed.report.count ?? parsed.report.resources.length : null;
|
|
8379
|
+
const state = carveStatePayload(
|
|
8380
|
+
carveStates(),
|
|
8381
|
+
total,
|
|
8382
|
+
() => demo ? { from: shortenIn(demo.from, demo.root), out: shortenIn(demo.out, demo.root) } : {}
|
|
8383
|
+
);
|
|
6514
8384
|
return c.json({
|
|
6515
8385
|
projectDir: reportPath,
|
|
6516
8386
|
recents: [],
|
|
@@ -6524,11 +8394,12 @@ function carveRoutes(app, reportPath, demo) {
|
|
|
6524
8394
|
carve: parsed.ok ? {
|
|
6525
8395
|
report: reportPath,
|
|
6526
8396
|
from: parsed.report.from ?? null,
|
|
6527
|
-
count:
|
|
8397
|
+
count: total,
|
|
6528
8398
|
bands: parsed.report.bands ?? {},
|
|
6529
8399
|
advisory: parsed.report.advisory ?? null,
|
|
6530
|
-
demo: demoInfo()
|
|
6531
|
-
|
|
8400
|
+
demo: demoInfo(),
|
|
8401
|
+
state
|
|
8402
|
+
} : { report: reportPath, demo: demoInfo(), state }
|
|
6532
8403
|
});
|
|
6533
8404
|
});
|
|
6534
8405
|
const runStep2 = async (c, run3) => {
|
|
@@ -6571,6 +8442,7 @@ function carveRoutes(app, reportPath, demo) {
|
|
|
6571
8442
|
};
|
|
6572
8443
|
app.post("/api/carve/emit", (c) => runStep2(c, runCarveEmit));
|
|
6573
8444
|
app.post("/api/carve/bridge", (c) => runStep2(c, runCarveBridge));
|
|
8445
|
+
app.post("/api/carve/observe", (c) => runStep2(c, runCarveObserve));
|
|
6574
8446
|
app.post("/api/carve/plan", async (c) => {
|
|
6575
8447
|
const block = demoBlock();
|
|
6576
8448
|
if (block) {
|
|
@@ -6637,7 +8509,17 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
|
|
|
6637
8509
|
// of starting blank — the `apply` SSE event (below) carries every update
|
|
6638
8510
|
// after that. `status: "idle"` (initialApplyProgress) when nothing has
|
|
6639
8511
|
// applied yet this session.
|
|
6640
|
-
applyProgress: runner.applyProgress
|
|
8512
|
+
applyProgress: runner.applyProgress,
|
|
8513
|
+
// The run playhead (#284 item 2): the settled StepRecords of the Op this
|
|
8514
|
+
// session last triggered, so a client opening (or reloading) mid-run
|
|
8515
|
+
// hydrates the playhead instead of starting blank — the `run` SSE event
|
|
8516
|
+
// carries every update after that. `status: "idle"` when nothing has run.
|
|
8517
|
+
runState: runner.runState,
|
|
8518
|
+
// #234 join 3: the operator strip's last known model, so a client that
|
|
8519
|
+
// opens (or reloads) hydrates the strip instead of starting blank — same
|
|
8520
|
+
// reason `runState` is here. `declared: []` until the ops lens has been
|
|
8521
|
+
// opened once, since that is where the op.json read happens.
|
|
8522
|
+
operatorState: runner.operatorState
|
|
6641
8523
|
})
|
|
6642
8524
|
);
|
|
6643
8525
|
app.post("/api/ops/:name/run", (c) => {
|
|
@@ -6647,7 +8529,7 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
|
|
|
6647
8529
|
if (!info) {
|
|
6648
8530
|
return c.json({ error: `no Op named "${name}" in the estate` }, 404);
|
|
6649
8531
|
}
|
|
6650
|
-
if (!runner.trigger(name, info.env, info.dir)) {
|
|
8532
|
+
if (!runner.trigger(name, info.env, info.dir, Boolean(info.gate))) {
|
|
6651
8533
|
return c.json({ error: `an Op is already running (${runner.running})` }, 409);
|
|
6652
8534
|
}
|
|
6653
8535
|
return c.json({ started: true, name });
|
|
@@ -6678,8 +8560,8 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
|
|
|
6678
8560
|
return c.json({ started: true, name, ran: label });
|
|
6679
8561
|
});
|
|
6680
8562
|
app.post("/api/local/reset", (c) => {
|
|
6681
|
-
const down =
|
|
6682
|
-
const up =
|
|
8563
|
+
const down = join18(cfg.projectDir, "scripts/local/local-down.sh");
|
|
8564
|
+
const up = join18(cfg.projectDir, "scripts/local/local-up.sh");
|
|
6683
8565
|
if (!existsSync13(down) || !existsSync13(up)) {
|
|
6684
8566
|
return c.json({ error: "no local-down.sh / local-up.sh in scripts/local \u2014 reset is only for local emulator projects" }, 400);
|
|
6685
8567
|
}
|
|
@@ -6688,11 +8570,67 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
|
|
|
6688
8570
|
}
|
|
6689
8571
|
return c.json({ started: true, ran: "local-down + local-up" });
|
|
6690
8572
|
});
|
|
8573
|
+
app.get("/api/ops/:name/status", async (c) => {
|
|
8574
|
+
const name = c.req.param("name");
|
|
8575
|
+
const info = estateOps().find((o) => o.name === name);
|
|
8576
|
+
const run3 = await runChantRaw(["run", "status", name, "--temporal"], info?.dir ?? cfg.projectDir);
|
|
8577
|
+
const result = readRunStatus(name, run3);
|
|
8578
|
+
const runState = runner.noteGate(name, result);
|
|
8579
|
+
if (!result.ok) return c.json({ ...result.refusal, runState }, 422);
|
|
8580
|
+
return c.json({ ...result.read, runState });
|
|
8581
|
+
});
|
|
8582
|
+
const convergeDirs = () => {
|
|
8583
|
+
const dirs = [];
|
|
8584
|
+
const declared = [];
|
|
8585
|
+
for (const f of discoverOpIrs(estateDirs)) {
|
|
8586
|
+
const parsed = readOpIr(f.path, (p) => readFileSync14(p, "utf8"));
|
|
8587
|
+
if (!parsed.ok) continue;
|
|
8588
|
+
const mine = declaredConvergeOps([parsed.ir]);
|
|
8589
|
+
if (!mine.length) continue;
|
|
8590
|
+
declared.push(...mine);
|
|
8591
|
+
if (!dirs.includes(f.dir)) dirs.push(f.dir);
|
|
8592
|
+
}
|
|
8593
|
+
return { dirs, declared };
|
|
8594
|
+
};
|
|
8595
|
+
app.get("/api/operator/status", async (c) => {
|
|
8596
|
+
const { dirs, declared } = convergeDirs();
|
|
8597
|
+
runner.noteDeclaredConvergeOps(declared);
|
|
8598
|
+
if (!dirs.length) {
|
|
8599
|
+
const state2 = runner.noteOperator({
|
|
8600
|
+
ok: false,
|
|
8601
|
+
refusal: {
|
|
8602
|
+
error: "This project declares no operating loop \u2014 no emitted Op carries chant's `Converge` search attribute.",
|
|
8603
|
+
code: "no-operator",
|
|
8604
|
+
remedy: HOW_TO_DECLARE
|
|
8605
|
+
}
|
|
8606
|
+
});
|
|
8607
|
+
return c.json({ ...state2, operator: state2 }, 200);
|
|
8608
|
+
}
|
|
8609
|
+
const rows = [];
|
|
8610
|
+
for (const dir of dirs) {
|
|
8611
|
+
const run3 = await runChantRaw(["operator", "status", "--json"], dir);
|
|
8612
|
+
const result = readOperatorStatus(run3);
|
|
8613
|
+
if (!result.ok) return c.json({ ...result.refusal, operator: runner.noteOperator(result) }, 422);
|
|
8614
|
+
rows.push(...result.rows);
|
|
8615
|
+
}
|
|
8616
|
+
const state = runner.noteOperator({ ok: true, rows });
|
|
8617
|
+
return c.json({ ...state, operator: state });
|
|
8618
|
+
});
|
|
8619
|
+
app.post("/api/operator/approve/:op/:gate", async (c) => {
|
|
8620
|
+
const { op, gate } = c.req.param();
|
|
8621
|
+
const info = estateOps().find((o) => o.name === op);
|
|
8622
|
+
broadcaster.emit("op", `\u270E approve ${op} ${gate} \u2014 records a fact; the next tick acts on it`);
|
|
8623
|
+
const { code, stderr, stdout } = await runChantRaw(["approve", op, gate], info?.dir ?? cfg.projectDir);
|
|
8624
|
+
if (code !== 0) {
|
|
8625
|
+
return c.json({ error: stderr.trim() || stdout.trim() || `approve exited ${code}`, code: "approve" }, 500);
|
|
8626
|
+
}
|
|
8627
|
+
return c.json({ recorded: true, op, gate, semantics: APPROVE_SEMANTICS });
|
|
8628
|
+
});
|
|
6691
8629
|
app.post("/api/ops/:name/signal/:gate", async (c) => {
|
|
6692
8630
|
const { name, gate } = c.req.param();
|
|
6693
8631
|
const info = estateOps().find((o) => o.name === name);
|
|
6694
8632
|
broadcaster.emit("op", `\u270E signal ${name} ${gate}`);
|
|
6695
|
-
const { code, stderr } = await runChantRaw(["run", "signal", name, gate], info?.dir ?? cfg.projectDir);
|
|
8633
|
+
const { code, stderr } = await runChantRaw(["run", "signal", name, gate, "--temporal"], info?.dir ?? cfg.projectDir);
|
|
6696
8634
|
if (code !== 0) return c.json({ error: stderr.trim() || `signal exited ${code}` }, 500);
|
|
6697
8635
|
return c.json({ signalled: true });
|
|
6698
8636
|
});
|
|
@@ -6714,8 +8652,11 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
|
|
|
6714
8652
|
const { environments, lexicons, stacks, k8sProfiles } = await detectProject(cfg.projectDir);
|
|
6715
8653
|
const k8sTarget = lexicons.includes("k8s") ? resolveK8sTarget(k8sProfiles, cfg.env, await loadKubeconfig()) : void 0;
|
|
6716
8654
|
const axes = deployAxes(tierEnvVar, lexicons, k8sTarget);
|
|
8655
|
+
const emittedOps = discoverOpIrs(estateDirs).length;
|
|
8656
|
+
const carveState = carveStatePayload(readCarveStates(estateDirs));
|
|
6717
8657
|
return c.json({
|
|
6718
8658
|
projectDir: cfg.projectDir,
|
|
8659
|
+
...carveState.manifests ? { carve: { state: carveState } } : {},
|
|
6719
8660
|
// #195: the full estate composition (multi-project serves) and the
|
|
6720
8661
|
// switcher's recents, so the SPA's project section can show what's
|
|
6721
8662
|
// loaded and offer where to go. Recents exclude nothing here — the SPA
|
|
@@ -6746,6 +8687,12 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
|
|
|
6746
8687
|
// the names — `graphPath` (chant.ts) resolves a picked name to its `src`
|
|
6747
8688
|
// server-side; the SPA never needs the path.
|
|
6748
8689
|
...stacks?.length ? { stacks: stacks.map((s) => s.name) } : {},
|
|
8690
|
+
// The ops lens stop (#284), gated exactly like `tiers`/`stacks` above:
|
|
8691
|
+
// present only when the estate has actually emitted Ops, so a project on
|
|
8692
|
+
// a chant < 0.50, or one that has never been built, grows no dead zoom
|
|
8693
|
+
// stop and no dead ⌘K entry. The count, not a bare flag — the SPA shows
|
|
8694
|
+
// it, and "ops: 0" is not a thing this can ever say.
|
|
8695
|
+
...emittedOps ? { ops: emittedOps } : {},
|
|
6749
8696
|
targets: deployTargets(lexicons, k8sTarget),
|
|
6750
8697
|
// Where the k8s binding came from, so the SPA never implies behold chose
|
|
6751
8698
|
// it (#106). Absent for a project with no k8s lexicon or no resolvable
|
|
@@ -6757,9 +8704,9 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
|
|
|
6757
8704
|
app.post("/api/project/open", async (c) => {
|
|
6758
8705
|
if (cfg.previewMode) return c.json({ error: "switching projects is locked in preview mode" }, 403);
|
|
6759
8706
|
const body = await c.req.json().catch(() => ({}));
|
|
6760
|
-
const dir = typeof body.dir === "string" && body.dir.trim() ?
|
|
8707
|
+
const dir = typeof body.dir === "string" && body.dir.trim() ? resolve6(body.dir.trim()) : "";
|
|
6761
8708
|
if (!dir || !existsSync13(dir)) return c.json({ error: `no such directory: ${dir || "(no dir given)"}` }, 400);
|
|
6762
|
-
if (!existsSync13(
|
|
8709
|
+
if (!existsSync13(join18(dir, "chant.config.ts"))) {
|
|
6763
8710
|
return c.json({ error: `${dir} doesn't look like a chant project \u2014 no chant.config.ts` }, 400);
|
|
6764
8711
|
}
|
|
6765
8712
|
switchServedProject([dir]);
|
|
@@ -6779,7 +8726,7 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
|
|
|
6779
8726
|
};
|
|
6780
8727
|
app.post("/api/project/reveal", async (c) => {
|
|
6781
8728
|
const body = await c.req.json().catch(() => ({}));
|
|
6782
|
-
const dir = typeof body.dir === "string" && body.dir.trim() ?
|
|
8729
|
+
const dir = typeof body.dir === "string" && body.dir.trim() ? resolve6(body.dir.trim()) : cfg.projectDir;
|
|
6783
8730
|
const known = /* @__PURE__ */ new Set([cfg.projectDir, ...cfg.projectDirs ?? [], ...listRecents().map((r) => r.dir)]);
|
|
6784
8731
|
if (!known.has(dir)) return c.json({ error: "not a served or recent project directory" }, 400);
|
|
6785
8732
|
const opener = process.platform === "darwin" ? "open" : process.platform === "win32" ? "explorer" : "xdg-open";
|
|
@@ -6847,7 +8794,7 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
|
|
|
6847
8794
|
const loaded = await loadDemo(entry, { pkgRoot, target, log: (line) => process.stdout.write(line + "\n") });
|
|
6848
8795
|
if (!loaded.ok) return c.json({ error: loaded.error }, 500);
|
|
6849
8796
|
const dirs = loaded.serveDirs;
|
|
6850
|
-
if (!existsSync13(
|
|
8797
|
+
if (!existsSync13(join18(dirs[0], "chant.config.ts"))) {
|
|
6851
8798
|
return c.json({ error: `${entry.name} loaded to ${target} but ${dirs[0]} has no chant.config.ts` }, 500);
|
|
6852
8799
|
}
|
|
6853
8800
|
if (entry.serve.local) {
|
|
@@ -6945,7 +8892,7 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
|
|
|
6945
8892
|
path: "/api/demos/open",
|
|
6946
8893
|
desc: "load a demo and serve it: JSON body {name} (a catalog name, never a path) \u2014 copies/clones, installs, runs its setup, then switches (preview-locked)"
|
|
6947
8894
|
},
|
|
6948
|
-
{ method: "GET", path: "/api/graph", desc: "the graph {ir, svg, meta} \u2014 params: detail=0..3, components=1, logical=1, env, stack, tier, target, lens, up=1, down=1, radial=1, layout=1" },
|
|
8895
|
+
{ method: "GET", path: "/api/graph", desc: "the graph {ir, svg, meta} \u2014 params: detail=0..3, components=1, logical=1, ops=1, env, stack, tier, target, lens, up=1, down=1, radial=1, layout=1" },
|
|
6949
8896
|
...cfg.carveReport ? [
|
|
6950
8897
|
{ method: "GET", path: "/api/carve", desc: "carve mode: the raw `chant carve advise --json` peelability report this server is rendering" },
|
|
6951
8898
|
{
|
|
@@ -6967,14 +8914,17 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
|
|
|
6967
8914
|
{ method: "GET", path: "/api/resources", desc: "component \u2192 declared resources" },
|
|
6968
8915
|
{ method: "GET", path: "/api/ci", desc: "generated CI pipeline projection {stages, jobs, forge}" },
|
|
6969
8916
|
{ method: "GET", path: "/api/substrates", desc: "substrate readiness {substrates: [{name, label, status, detail, bringUp?}]}" },
|
|
6970
|
-
{ method: "GET", path: "/api/ops", desc: "committed Ops + adopt lexicons + apply progress" },
|
|
8917
|
+
{ method: "GET", path: "/api/ops", desc: "committed Ops + adopt lexicons + apply progress + run playhead" },
|
|
8918
|
+
{ method: "GET", path: "/api/ops/:name/status", desc: "an Op's durable run status + pending gate (chant run status)" },
|
|
8919
|
+
{ method: "GET", path: "/api/operator/status", desc: "the operating loop's strip: last tick per ConvergeOp + lease + pending converge gates" },
|
|
6971
8920
|
{ method: "GET", path: "/api/history", desc: "recent source commits (rollback targets)" },
|
|
6972
8921
|
{ method: "GET", path: "/api/frames", desc: "captured lanes frames" },
|
|
6973
|
-
{ method: "GET", path: "/api/events", desc: "SSE: changed / op / apply / pr" },
|
|
8922
|
+
{ method: "GET", path: "/api/events", desc: "SSE: changed / op / apply / run / pr" },
|
|
6974
8923
|
{ method: "POST", path: "/api/refresh", desc: "re-observe live now (?env=) \u2014 returns the fresh graph" },
|
|
6975
8924
|
{ method: "POST", path: "/api/apply", desc: "delegated apply: ?env=&component=<name|all> (guarded, preview-locked)" },
|
|
6976
8925
|
{ method: "POST", path: "/api/ops/:name/run", desc: "run a committed Op (delegated write)" },
|
|
6977
|
-
{ method: "POST", path: "/api/ops/:name/signal/:gate", desc: "approve an Op's gate" },
|
|
8926
|
+
{ method: "POST", path: "/api/ops/:name/signal/:gate", desc: "approve an Op's gate (releases the waiting run)" },
|
|
8927
|
+
{ method: "POST", path: "/api/operator/approve/:op/:gate", desc: "record a converge gate's resolution (chant approve \u2014 a fact for the next tick, not an unblock)" },
|
|
6978
8928
|
{ method: "POST", path: "/api/rollback", desc: "open a rollback PR: ?to=<sha>" },
|
|
6979
8929
|
{ method: "POST", path: "/api/substrates/:name/up", desc: "bring a substrate up" },
|
|
6980
8930
|
{ method: "POST", path: "/api/local/reset", desc: "reset the local emulator" },
|
|
@@ -6986,6 +8936,58 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
|
|
|
6986
8936
|
const url = new URL(c.req.url);
|
|
6987
8937
|
const opts = optsFromQuery(url, tierEnvVar, cfg.projectDir);
|
|
6988
8938
|
try {
|
|
8939
|
+
if (url.searchParams.get("ops") === "1") {
|
|
8940
|
+
const files = discoverOpIrs(estateDirs);
|
|
8941
|
+
if (!files.length) {
|
|
8942
|
+
return c.json(
|
|
8943
|
+
{
|
|
8944
|
+
error: "this project has no emitted Ops \u2014 nothing under dist/ops/<name>/op.json",
|
|
8945
|
+
code: "op-ir",
|
|
8946
|
+
remedy: HOW_TO_EMIT
|
|
8947
|
+
},
|
|
8948
|
+
422
|
|
8949
|
+
);
|
|
8950
|
+
}
|
|
8951
|
+
const parsedOps = [];
|
|
8952
|
+
for (const f of files) {
|
|
8953
|
+
const parsed = readOpIr(f.path, (p) => readFileSync14(p, "utf8"));
|
|
8954
|
+
if (!parsed.ok) return c.json(parsed.refusal, 422);
|
|
8955
|
+
parsedOps.push(parsed.ir);
|
|
8956
|
+
}
|
|
8957
|
+
const declaredIr = opsToIr(parsedOps);
|
|
8958
|
+
const runState = runner.runState;
|
|
8959
|
+
const ranOp = runState.op ? parsedOps.find((o) => o.name === runState.op) : void 0;
|
|
8960
|
+
const join21 = ranOp ? joinRun(ranOp, runState.records, runState.gate) : null;
|
|
8961
|
+
const opsIr = join21 ? paintPlayhead(declaredIr, join21, runState.gate) : declaredIr;
|
|
8962
|
+
const { svg: svg2 } = renderGraph(opsIr, { boxes: "byStack" });
|
|
8963
|
+
const operator = runner.noteDeclaredConvergeOps(declaredConvergeOps(parsedOps));
|
|
8964
|
+
return c.json({
|
|
8965
|
+
ir: opsIr,
|
|
8966
|
+
svg: svg2,
|
|
8967
|
+
meta: {
|
|
8968
|
+
projectDir: cfg.projectDir,
|
|
8969
|
+
env: cfg.env ?? null,
|
|
8970
|
+
tier: null,
|
|
8971
|
+
target: null,
|
|
8972
|
+
mode: "ops",
|
|
8973
|
+
ops: parsedOps.length,
|
|
8974
|
+
note: `${opsNote(parsedOps, opsIr)} ${playheadNote(runState, join21)} ${operatorNote(operator)}`,
|
|
8975
|
+
// The operator strip's own model: the ConvergeOps this project
|
|
8976
|
+
// declares, plus whatever the last status read said. `declared`
|
|
8977
|
+
// without a `strip` is the honest starting state — a loop exists and
|
|
8978
|
+
// behold hasn't asked about it yet.
|
|
8979
|
+
operator,
|
|
8980
|
+
// The playhead's own model, so the SPA renders the pending card and
|
|
8981
|
+
// the run legend from the same answer that painted the graph.
|
|
8982
|
+
run: runState,
|
|
8983
|
+
// #234's pending-approval card, whole: the gate, when it started
|
|
8984
|
+
// waiting, the step it holds back, and the delegated write its
|
|
8985
|
+
// approve button performs. Present only when `gateState` NAMED a
|
|
8986
|
+
// pending gate — never inferred from a stalled track.
|
|
8987
|
+
gate: runState.gate && runState.op ? pendingGateCard(runState.op, runState.gate, opsIr) : null
|
|
8988
|
+
}
|
|
8989
|
+
});
|
|
8990
|
+
}
|
|
6989
8991
|
const components = url.searchParams.get("components") === "1";
|
|
6990
8992
|
const logical = url.searchParams.get("logical") === "1";
|
|
6991
8993
|
const multi = cfg.projectDirs && cfg.projectDirs.length > 1;
|
|
@@ -6996,6 +8998,7 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
|
|
|
6996
8998
|
ir = await composeEstate(cfg.projectDirs, logical ? { ...opts, detail: 3 } : opts);
|
|
6997
8999
|
ir = addValueMatchEdges(ir);
|
|
6998
9000
|
ir = addK8sDeclaredEdges(ir);
|
|
9001
|
+
ir = markOperatorHome(ir);
|
|
6999
9002
|
const estateContext = await boundK8sContext(metaEnv ?? void 0);
|
|
7000
9003
|
ir = addClusterAnchorEdges(ir, estateContext);
|
|
7001
9004
|
if (logical) {
|
|
@@ -7040,6 +9043,7 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
|
|
|
7040
9043
|
if ((opts.detail ?? 2) < 3) ir = pruneImports(ir);
|
|
7041
9044
|
ir = addValueMatchEdges(ir);
|
|
7042
9045
|
ir = addK8sDeclaredEdges(ir);
|
|
9046
|
+
ir = markOperatorHome(ir);
|
|
7043
9047
|
ir = addClusterAnchorEdges(ir, await boundK8sContext(metaEnv ?? void 0));
|
|
7044
9048
|
}
|
|
7045
9049
|
let srcCompositeEdgesAttached;
|
|
@@ -7053,7 +9057,7 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
|
|
|
7053
9057
|
srcCompositeEdgesAttached = 0;
|
|
7054
9058
|
}
|
|
7055
9059
|
}
|
|
7056
|
-
if (!multi && !components && !logical && !opts.lens && ir.nodes.length === 0 && !existsSync13(
|
|
9060
|
+
if (!multi && !components && !logical && !opts.lens && ir.nodes.length === 0 && !existsSync13(join18(cfg.projectDir, "chant.config.ts"))) {
|
|
7057
9061
|
return c.json(noProjectError(cfg.projectDir), 404);
|
|
7058
9062
|
}
|
|
7059
9063
|
const radial = new URL(c.req.url).searchParams.get("radial") === "1";
|
|
@@ -7171,6 +9175,7 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
|
|
|
7171
9175
|
if ((detail ?? 2) < 3) ir2 = pruneImports(ir2);
|
|
7172
9176
|
ir2 = addValueMatchEdges(ir2);
|
|
7173
9177
|
ir2 = addK8sDeclaredEdges(ir2);
|
|
9178
|
+
ir2 = markOperatorHome(ir2);
|
|
7174
9179
|
const boundContext2 = await boundK8sContext(env);
|
|
7175
9180
|
ir2 = addClusterAnchorEdges(ir2, boundContext2);
|
|
7176
9181
|
const coverNote = est.unobserved.length || est.dropped.length ? `live observe covered ${est.observed} of ${est.total} projects \u2014 ` + [
|
|
@@ -7216,6 +9221,7 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
|
|
|
7216
9221
|
const observed = await lifecycleDiffLive(cfg.projectDir, env, query).then((d) => d.lexicons?.helm?.observedArtifacts).catch(() => void 0);
|
|
7217
9222
|
applyHelmArtifacts(ir, observed);
|
|
7218
9223
|
synthesizeHelmReleases(ir, observed, discoverReleaseUnits(cfg.projectDir));
|
|
9224
|
+
applyHelmRenderDrift(ir, await readHelmRenderDrift(cfg.projectDir, env, query));
|
|
7219
9225
|
}
|
|
7220
9226
|
ir = runtime ? attachRuntimeContainment(ir) : pruneRuntimeChildren(ir);
|
|
7221
9227
|
if (logical) {
|
|
@@ -7229,6 +9235,7 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
|
|
|
7229
9235
|
if ((query.detail ?? 2) < 3) ir = pruneImports(ir);
|
|
7230
9236
|
ir = addValueMatchEdges(ir);
|
|
7231
9237
|
ir = addK8sDeclaredEdges(ir);
|
|
9238
|
+
ir = markOperatorHome(ir);
|
|
7232
9239
|
ir = addClusterAnchorEdges(ir, boundContext);
|
|
7233
9240
|
let compositeEdgesAttached;
|
|
7234
9241
|
if (query.detail === 1) {
|
|
@@ -7264,6 +9271,7 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
|
|
|
7264
9271
|
const observed = await lifecycleDiffLive(cfg.projectDir, env).then((d) => d.lexicons?.helm?.observedArtifacts).catch(() => void 0);
|
|
7265
9272
|
applyHelmArtifacts(result.ir, observed);
|
|
7266
9273
|
synthesizeHelmReleases(result.ir, observed, discoverReleaseUnits(cfg.projectDir));
|
|
9274
|
+
applyHelmRenderDrift(result.ir, await readHelmRenderDrift(cfg.projectDir, env));
|
|
7267
9275
|
}
|
|
7268
9276
|
}
|
|
7269
9277
|
if (env) {
|
|
@@ -7391,9 +9399,9 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
|
|
|
7391
9399
|
}
|
|
7392
9400
|
return c.json({ started: true, component, env });
|
|
7393
9401
|
});
|
|
7394
|
-
const rel =
|
|
9402
|
+
const rel = relative4(process.cwd(), webRoot) || ".";
|
|
7395
9403
|
app.use("/*", serveStatic({ root: rel }));
|
|
7396
|
-
app.get("/", serveStatic({ path:
|
|
9404
|
+
app.get("/", serveStatic({ path: join18(rel, "index.html") }));
|
|
7397
9405
|
return app;
|
|
7398
9406
|
}
|
|
7399
9407
|
async function bootLocalEmulators(dir, who) {
|
|
@@ -7422,7 +9430,7 @@ async function bootLocalEmulators(dir, who) {
|
|
|
7422
9430
|
async function startServer(cfg) {
|
|
7423
9431
|
if (cfg.local) cfg.emulators = await bootLocalEmulators(cfg.projectDir, "behold serve --local");
|
|
7424
9432
|
for (const dir of cfg.projectDirs ?? [cfg.projectDir]) {
|
|
7425
|
-
if (existsSync13(
|
|
9433
|
+
if (existsSync13(join18(dir, "chant.config.ts"))) addRecent(dir);
|
|
7426
9434
|
}
|
|
7427
9435
|
const broadcaster = new Broadcaster();
|
|
7428
9436
|
const frames = new FrameBuffer();
|
|
@@ -7434,17 +9442,22 @@ async function startServer(cfg) {
|
|
|
7434
9442
|
const app = createApp(cfg, broadcaster, frames, runner);
|
|
7435
9443
|
const autoSync = cfg.autoSync ?? "off";
|
|
7436
9444
|
const capture2 = () => captureFrame(cfg.projectDir, cfg.env, frames, broadcaster);
|
|
7437
|
-
const onEstateChange = () => {
|
|
7438
|
-
broadcaster.emit("changed");
|
|
9445
|
+
const onEstateChange = (memberDir) => {
|
|
9446
|
+
broadcaster.emit("changed", memberDir ?? "");
|
|
7439
9447
|
void capture2();
|
|
7440
9448
|
};
|
|
7441
|
-
const
|
|
7442
|
-
|
|
9449
|
+
const onMemberSourceChange = (memberDir) => {
|
|
9450
|
+
invalidateMember(memberDir);
|
|
9451
|
+
onEstateChange(memberDir);
|
|
9452
|
+
};
|
|
9453
|
+
const memberTag = (dir) => (cfg.projectDirs?.length ?? 0) > 1 ? ` [${basename(dir)}]` : "";
|
|
9454
|
+
const onPollDrift = (dir, movedLexicons) => {
|
|
9455
|
+
onEstateChange(dir);
|
|
7443
9456
|
if (autoSync === "off") return;
|
|
7444
|
-
void routeAutoSync(movedLexicons);
|
|
9457
|
+
void routeAutoSync(dir, movedLexicons);
|
|
7445
9458
|
};
|
|
7446
|
-
const routeAutoSync = async (movedLexicons) => {
|
|
7447
|
-
const suspended = autoSync === "pull-request" ? suspendedByRollback(await openRollbackBranches(
|
|
9459
|
+
const routeAutoSync = async (dir, movedLexicons) => {
|
|
9460
|
+
const suspended = autoSync === "pull-request" ? suspendedByRollback(await openRollbackBranches(dir, cfg.env), movedLexicons) : /* @__PURE__ */ new Set();
|
|
7448
9461
|
const { picks, declined } = pickAutoSyncOps(
|
|
7449
9462
|
autoSync,
|
|
7450
9463
|
discoverEstateOps(cfg.projectDirs ?? [cfg.projectDir]),
|
|
@@ -7453,31 +9466,37 @@ async function startServer(cfg) {
|
|
|
7453
9466
|
suspended
|
|
7454
9467
|
);
|
|
7455
9468
|
for (const d of declined) {
|
|
7456
|
-
broadcaster.emit("op", `\u27F3 auto-sync (${autoSync}) declined ${d.lexicon}: ${d.reason}`);
|
|
9469
|
+
broadcaster.emit("op", `\u27F3 auto-sync (${autoSync})${memberTag(dir)} declined ${d.lexicon}: ${d.reason}`);
|
|
7457
9470
|
}
|
|
7458
9471
|
for (const { op, lexicons } of picks) {
|
|
7459
9472
|
const scope = lexicons.join("+");
|
|
7460
|
-
if (runner.trigger(op.name, op.env, op.dir)) {
|
|
7461
|
-
broadcaster.emit("op", `\u27F3 auto-sync (${autoSync}) ${scope} \u2192 ${op.name}`);
|
|
9473
|
+
if (runner.trigger(op.name, op.env, op.dir, Boolean(op.gate))) {
|
|
9474
|
+
broadcaster.emit("op", `\u27F3 auto-sync (${autoSync})${memberTag(dir)} ${scope} \u2192 ${op.name}`);
|
|
7462
9475
|
} else {
|
|
7463
|
-
broadcaster.emit("op", `\u27F3 auto-sync (${autoSync}) ${scope} \u2192 ${op.name} waiting \u2014 ${runner.running} is running`);
|
|
9476
|
+
broadcaster.emit("op", `\u27F3 auto-sync (${autoSync})${memberTag(dir)} ${scope} \u2192 ${op.name} waiting \u2014 ${runner.running} is running`);
|
|
7464
9477
|
}
|
|
7465
9478
|
}
|
|
7466
9479
|
};
|
|
7467
9480
|
const carve = !!cfg.carveReport;
|
|
7468
9481
|
let stopWatch = carve ? () => {
|
|
7469
|
-
} :
|
|
9482
|
+
} : watchSources(cfg.projectDirs ?? [cfg.projectDir], onMemberSourceChange);
|
|
7470
9483
|
let stopPoll = !carve && cfg.env && cfg.pollSecs ? startDriftPoll({
|
|
7471
9484
|
intervalMs: cfg.pollSecs * 1e3,
|
|
7472
|
-
|
|
9485
|
+
// Per-member queries, swept sequentially inside the poll (#297): a
|
|
9486
|
+
// member read can take seconds (#295), so an estate-wide tick must
|
|
9487
|
+
// stretch, not stampede N describes at once.
|
|
9488
|
+
members: (cfg.projectDirs ?? [cfg.projectDir]).map((dir) => ({
|
|
9489
|
+
dir,
|
|
9490
|
+
query: () => graphIr(dir, { live: true, overlay: true, env: cfg.env })
|
|
9491
|
+
})),
|
|
7473
9492
|
onChange: onPollDrift,
|
|
7474
|
-
onError: (err) => process.stderr.write(`poll: ${err instanceof Error ? err.message : String(err)}
|
|
9493
|
+
onError: (dir, err) => process.stderr.write(`poll${memberTag(dir)}: ${err instanceof Error ? err.message : String(err)}
|
|
7475
9494
|
`)
|
|
7476
9495
|
}) : () => {
|
|
7477
9496
|
};
|
|
7478
9497
|
cfg.onProjectSwitch = (dir) => {
|
|
7479
9498
|
stopWatch();
|
|
7480
|
-
stopWatch =
|
|
9499
|
+
stopWatch = watchSources(cfg.projectDirs ?? [dir], onMemberSourceChange);
|
|
7481
9500
|
stopPoll();
|
|
7482
9501
|
stopPoll = () => {
|
|
7483
9502
|
};
|
|
@@ -7550,12 +9569,12 @@ async function startServer(cfg) {
|
|
|
7550
9569
|
}
|
|
7551
9570
|
|
|
7552
9571
|
// src/export.ts
|
|
7553
|
-
import { mkdirSync as mkdirSync4, writeFileSync as writeFileSync4, copyFileSync, readFileSync as
|
|
7554
|
-
import { join as
|
|
9572
|
+
import { mkdirSync as mkdirSync4, writeFileSync as writeFileSync4, copyFileSync, readFileSync as readFileSync15, readdirSync as readdirSync7 } from "node:fs";
|
|
9573
|
+
import { join as join19, dirname as dirname6, basename as basename2 } from "node:path";
|
|
7555
9574
|
import { fileURLToPath as fileURLToPath4 } from "node:url";
|
|
7556
|
-
var LENS_PARAMS = ["components", "detail", "env", "logical", "radial", "tier"];
|
|
9575
|
+
var LENS_PARAMS = ["components", "detail", "env", "logical", "ops", "radial", "tier"];
|
|
7557
9576
|
function canonicalKey(path, params) {
|
|
7558
|
-
const flat = params.get("components") === "1" || params.get("logical") === "1";
|
|
9577
|
+
const flat = params.get("components") === "1" || params.get("logical") === "1" || params.get("ops") === "1";
|
|
7559
9578
|
const q = LENS_PARAMS.filter((k) => params.has(k) && !(flat && (k === "detail" || k === "radial"))).map((k) => `${k}=${params.get(k)}`).join("&");
|
|
7560
9579
|
return q ? `${path}?${q}` : path;
|
|
7561
9580
|
}
|
|
@@ -7569,6 +9588,7 @@ function captureKeys(axes) {
|
|
|
7569
9588
|
add("/api/project", {});
|
|
7570
9589
|
add("/api/substrates", {});
|
|
7571
9590
|
add("/api/ops", {});
|
|
9591
|
+
if (axes.ops) add("/api/graph", { ops: "1" });
|
|
7572
9592
|
const tiers = axes.tiers && axes.tiers.length ? axes.tiers : [""];
|
|
7573
9593
|
const envs = ["", ...axes.environments];
|
|
7574
9594
|
for (const env of envs) {
|
|
@@ -7599,18 +9619,18 @@ function captureKeys(axes) {
|
|
|
7599
9619
|
return [...keys];
|
|
7600
9620
|
}
|
|
7601
9621
|
function webDir() {
|
|
7602
|
-
return
|
|
9622
|
+
return join19(dirname6(fileURLToPath4(import.meta.url)), "..", "web");
|
|
7603
9623
|
}
|
|
7604
9624
|
function workerName(project, override) {
|
|
7605
|
-
const raw = override ?? `behold-${
|
|
9625
|
+
const raw = override ?? `behold-${basename2(project)}`;
|
|
7606
9626
|
const name = raw.toLowerCase().replace(/[^a-z0-9-]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 63);
|
|
7607
9627
|
return name || "behold-export";
|
|
7608
9628
|
}
|
|
7609
9629
|
async function runExport(cfg, outDir, opts = {}) {
|
|
7610
9630
|
const app = createApp({ ...cfg, layoutWrites: false });
|
|
7611
9631
|
const proj = await (await app.request("/api/project")).json();
|
|
7612
|
-
const axes = { environments: proj.environments ?? [], tiers: proj.tiers ?? [] };
|
|
7613
|
-
const snapDir =
|
|
9632
|
+
const axes = { environments: proj.environments ?? [], tiers: proj.tiers ?? [], ...proj.ops ? { ops: proj.ops } : {} };
|
|
9633
|
+
const snapDir = join19(outDir, "snapshots");
|
|
7614
9634
|
mkdirSync4(snapDir, { recursive: true });
|
|
7615
9635
|
const keyToFile = {};
|
|
7616
9636
|
let ok = 0;
|
|
@@ -7619,7 +9639,7 @@ async function runExport(cfg, outDir, opts = {}) {
|
|
|
7619
9639
|
const res = await app.request(`${key}${key.includes("?") ? "&" : "?"}layout=1`);
|
|
7620
9640
|
const body = await res.text();
|
|
7621
9641
|
const file = slug2(key);
|
|
7622
|
-
writeFileSync4(
|
|
9642
|
+
writeFileSync4(join19(snapDir, file), body);
|
|
7623
9643
|
keyToFile[key] = `snapshots/${file}`;
|
|
7624
9644
|
if (res.ok) ok++;
|
|
7625
9645
|
else failed++;
|
|
@@ -7631,21 +9651,21 @@ async function runExport(cfg, outDir, opts = {}) {
|
|
|
7631
9651
|
axes,
|
|
7632
9652
|
keyToFile
|
|
7633
9653
|
};
|
|
7634
|
-
writeFileSync4(
|
|
7635
|
-
const html =
|
|
9654
|
+
writeFileSync4(join19(outDir, "manifest.json"), JSON.stringify(manifest, null, 2));
|
|
9655
|
+
const html = readFileSync15(join19(webDir(), "index.html"), "utf8").replace(
|
|
7636
9656
|
/<\/head>/i,
|
|
7637
9657
|
` <script>window.__BEHOLD_STATIC__ = true;</script>
|
|
7638
9658
|
</head>`
|
|
7639
9659
|
);
|
|
7640
|
-
writeFileSync4(
|
|
7641
|
-
for (const f of
|
|
9660
|
+
writeFileSync4(join19(outDir, "index.html"), html);
|
|
9661
|
+
for (const f of readdirSync7(webDir())) {
|
|
7642
9662
|
if (f === "index.html") continue;
|
|
7643
|
-
copyFileSync(
|
|
9663
|
+
copyFileSync(join19(webDir(), f), join19(outDir, f));
|
|
7644
9664
|
}
|
|
7645
|
-
writeFileSync4(
|
|
9665
|
+
writeFileSync4(join19(outDir, "README.md"), BUNDLE_README);
|
|
7646
9666
|
const name = workerName(cfg.projectDir, opts.name);
|
|
7647
9667
|
writeFileSync4(
|
|
7648
|
-
|
|
9668
|
+
join19(outDir, "wrangler.jsonc"),
|
|
7649
9669
|
JSON.stringify(
|
|
7650
9670
|
{ $schema: "node_modules/wrangler/config-schema.json", name, compatibility_date: "2025-06-01", assets: { directory: "." } },
|
|
7651
9671
|
null,
|
|
@@ -7693,7 +9713,7 @@ It's just files \u2014 GitHub Pages, S3, nginx, or Cloudflare Pages
|
|
|
7693
9713
|
`;
|
|
7694
9714
|
|
|
7695
9715
|
// src/doctor.ts
|
|
7696
|
-
import { relative as
|
|
9716
|
+
import { relative as relative5, resolve as resolve7 } from "node:path";
|
|
7697
9717
|
var list2 = (xs) => xs.join(", ");
|
|
7698
9718
|
var EMPTY_KUBECONFIG = { contexts: /* @__PURE__ */ new Map(), servers: /* @__PURE__ */ new Map() };
|
|
7699
9719
|
function summarize(xs, max = 4) {
|
|
@@ -7701,7 +9721,7 @@ function summarize(xs, max = 4) {
|
|
|
7701
9721
|
}
|
|
7702
9722
|
function labelFor(root, target, estate) {
|
|
7703
9723
|
if (!estate) return "";
|
|
7704
|
-
const rel =
|
|
9724
|
+
const rel = relative5(root, target);
|
|
7705
9725
|
return `${rel || "."}: `;
|
|
7706
9726
|
}
|
|
7707
9727
|
function chantCheck(root, targets, estate) {
|
|
@@ -7714,7 +9734,7 @@ function chantCheck(root, targets, estate) {
|
|
|
7714
9734
|
name: "chant",
|
|
7715
9735
|
status: "fail",
|
|
7716
9736
|
detail: `${list2(resolutions.map(describe))} \u2014 behold shells the project's own chant`,
|
|
7717
|
-
fix: `Run \`npm install\` in ${list2(missing.map(({ target }) =>
|
|
9737
|
+
fix: `Run \`npm install\` in ${list2(missing.map(({ target }) => relative5(root, target) || target))}`
|
|
7718
9738
|
};
|
|
7719
9739
|
}
|
|
7720
9740
|
const stale = resolutions.filter(({ res }) => !meetsFloor(res.version, floor));
|
|
@@ -7842,7 +9862,7 @@ function opsCheck(root, ops, chantSource, estate) {
|
|
|
7842
9862
|
return { name: "ops", status: "pass", detail: `${ops.length} committed: ${list2(names)}; ${mcp}` };
|
|
7843
9863
|
}
|
|
7844
9864
|
async function diagnose(dir, probes = {}) {
|
|
7845
|
-
const root =
|
|
9865
|
+
const root = resolve7(dir);
|
|
7846
9866
|
const shape = detectProjectShape(root);
|
|
7847
9867
|
const behold = beholdVersion();
|
|
7848
9868
|
if (shape.kind === "none") {
|
|
@@ -7862,16 +9882,16 @@ async function diagnose(dir, probes = {}) {
|
|
|
7862
9882
|
};
|
|
7863
9883
|
}
|
|
7864
9884
|
const estate = shape.kind === "estate";
|
|
7865
|
-
const members = (shape.members ?? []).map((m) =>
|
|
9885
|
+
const members = (shape.members ?? []).map((m) => resolve7(root, m));
|
|
7866
9886
|
const targets = estate ? members : [root];
|
|
7867
9887
|
const primary = targets[0];
|
|
7868
9888
|
const projectCheck = estate ? {
|
|
7869
9889
|
name: "project",
|
|
7870
9890
|
status: "pass",
|
|
7871
9891
|
detail: `estate of ${members.length} projects (${shape.membersFrom === "behold-config" ? ".behold.json members" : "npm workspaces"}): ${list2(
|
|
7872
|
-
members.map((m) =>
|
|
9892
|
+
members.map((m) => relative5(root, m))
|
|
7873
9893
|
)}`
|
|
7874
|
-
} : { name: "project", status: "pass", detail: `chant project (${
|
|
9894
|
+
} : { name: "project", status: "pass", detail: `chant project (${relative5(root, shape.configFile)})` };
|
|
7875
9895
|
const infos = await Promise.all(targets.map(async (t) => ({ target: t, info: await detectProject(t) })));
|
|
7876
9896
|
const declared = new Map(infos.map(({ target, info }) => [target, info.lexicons]));
|
|
7877
9897
|
const lexicons = [...new Set(infos.flatMap(({ info }) => info.lexicons))];
|
|
@@ -8096,7 +10116,7 @@ ${USAGE}`);
|
|
|
8096
10116
|
process.stderr.write("behold serve: --auto-sync needs --env and --poll (it acts on polled drift)\n");
|
|
8097
10117
|
process.exit(2);
|
|
8098
10118
|
}
|
|
8099
|
-
const dirs = projectDirs.map((d) =>
|
|
10119
|
+
const dirs = projectDirs.map((d) => resolve8(d));
|
|
8100
10120
|
for (const d of dirs) warnIfNotChantProject(d);
|
|
8101
10121
|
await startServer({
|
|
8102
10122
|
projectDir: dirs[0],
|
|
@@ -8115,7 +10135,7 @@ function warnIfNotChantProject(dir) {
|
|
|
8115
10135
|
if (shape.kind === "estate") {
|
|
8116
10136
|
process.stderr.write(
|
|
8117
10137
|
`behold: warning \u2014 ${dir} is an estate root, not a chant project itself.
|
|
8118
|
-
Serve its members composed: behold serve ${shape.members.map((m) =>
|
|
10138
|
+
Serve its members composed: behold serve ${shape.members.map((m) => join20(dir, m)).join(" ")}
|
|
8119
10139
|
`
|
|
8120
10140
|
);
|
|
8121
10141
|
return;
|
|
@@ -8150,8 +10170,8 @@ async function runCarve(rest) {
|
|
|
8150
10170
|
);
|
|
8151
10171
|
process.exit(2);
|
|
8152
10172
|
}
|
|
8153
|
-
const reportPath =
|
|
8154
|
-
const parsed = readCarveReport(reportPath, (p) =>
|
|
10173
|
+
const reportPath = resolve8(fileArg);
|
|
10174
|
+
const parsed = readCarveReport(reportPath, (p) => readFileSync16(p, "utf8"));
|
|
8155
10175
|
if (!parsed.ok) {
|
|
8156
10176
|
process.stderr.write(`behold carve: ${parsed.refusal.error}
|
|
8157
10177
|
${parsed.refusal.remedy}
|
|
@@ -8162,7 +10182,7 @@ async function runCarve(rest) {
|
|
|
8162
10182
|
`behold carve \u2014 ${parsed.report.resources.length} resource(s)/module(s) ranked${parsed.report.from ? ` from ${parsed.report.from}` : ""}
|
|
8163
10183
|
`
|
|
8164
10184
|
);
|
|
8165
|
-
await startServer({ projectDir:
|
|
10185
|
+
await startServer({ projectDir: dirname7(reportPath), carveReport: reportPath, port });
|
|
8166
10186
|
}
|
|
8167
10187
|
async function runDoctor(rest) {
|
|
8168
10188
|
let json = false;
|
|
@@ -8178,8 +10198,8 @@ async function runDoctor(rest) {
|
|
|
8178
10198
|
}
|
|
8179
10199
|
}
|
|
8180
10200
|
const dir = dirArg ?? ".";
|
|
8181
|
-
if (!existsSync14(
|
|
8182
|
-
process.stderr.write(`behold doctor: no such directory: ${
|
|
10201
|
+
if (!existsSync14(resolve8(dir))) {
|
|
10202
|
+
process.stderr.write(`behold doctor: no such directory: ${resolve8(dir)}
|
|
8183
10203
|
`);
|
|
8184
10204
|
process.exit(2);
|
|
8185
10205
|
}
|
|
@@ -8188,7 +10208,7 @@ async function runDoctor(rest) {
|
|
|
8188
10208
|
if (!report.ok) process.exitCode = 1;
|
|
8189
10209
|
}
|
|
8190
10210
|
async function runDemo(rest) {
|
|
8191
|
-
const pkgRoot2 =
|
|
10211
|
+
const pkgRoot2 = join20(dirname7(fileURLToPath5(import.meta.url)), "..");
|
|
8192
10212
|
const registry = loadDemoRegistry(pkgRoot2);
|
|
8193
10213
|
let port = 4600;
|
|
8194
10214
|
let live = false;
|
|
@@ -8242,7 +10262,7 @@ async function runDemo(rest) {
|
|
|
8242
10262
|
`);
|
|
8243
10263
|
process.exit(2);
|
|
8244
10264
|
}
|
|
8245
|
-
const target = dirArg ?
|
|
10265
|
+
const target = dirArg ? resolve8(dirArg) : demoTargetDir(entry);
|
|
8246
10266
|
const loaded = await loadDemo(entry, { pkgRoot: pkgRoot2, target, log: (line) => process.stdout.write(line + "\n") });
|
|
8247
10267
|
if (!loaded.ok) {
|
|
8248
10268
|
process.stderr.write(`behold demo ${entry.name}: ${loaded.error}
|
|
@@ -8273,7 +10293,7 @@ function spawnStep(cmd, args, cwd) {
|
|
|
8273
10293
|
});
|
|
8274
10294
|
}
|
|
8275
10295
|
async function serveCarveDemo(target, carve, port, live = false) {
|
|
8276
|
-
const at = (rel) =>
|
|
10296
|
+
const at = (rel) => resolve8(target, rel);
|
|
8277
10297
|
const project = at(carve.project);
|
|
8278
10298
|
const from = at(carve.from);
|
|
8279
10299
|
const state = carve.state ? at(carve.state) : void 0;
|
|
@@ -8313,7 +10333,7 @@ async function serveCarveDemo(target, carve, port, live = false) {
|
|
|
8313
10333
|
};
|
|
8314
10334
|
process.stdout.write("behold demo carve --live \u2192 the tfstate is real now; the advisor reads what terraform wrote.\n");
|
|
8315
10335
|
}
|
|
8316
|
-
if (existsSync14(
|
|
10336
|
+
if (existsSync14(join20(project, "package.json")) && !existsSync14(join20(project, "node_modules"))) {
|
|
8317
10337
|
process.stdout.write(`behold demo carve \u2192 npm install in ${carve.project}/ (the chant this walkthrough shells)\u2026
|
|
8318
10338
|
`);
|
|
8319
10339
|
const code = await spawnStep("npm", ["install"], project);
|
|
@@ -8324,7 +10344,7 @@ async function serveCarveDemo(target, carve, port, live = false) {
|
|
|
8324
10344
|
}
|
|
8325
10345
|
}
|
|
8326
10346
|
let degraded;
|
|
8327
|
-
if (!existsSync14(
|
|
10347
|
+
if (!existsSync14(join20(target, "node_modules", "@cdktf", "hcl2json"))) {
|
|
8328
10348
|
process.stdout.write("behold demo carve \u2192 npm install @cdktf/hcl2json (chant's HCL parser, ~2MB, once)\u2026\n");
|
|
8329
10349
|
const code = await spawnStep("npm", ["install", "--no-save", "--no-package-lock", "@cdktf/hcl2json"], target);
|
|
8330
10350
|
if (code !== 0) degraded = "couldn't install @cdktf/hcl2json (chant's HCL parser) \u2014 no network?";
|
|
@@ -8393,7 +10413,7 @@ async function runPreview(rest) {
|
|
|
8393
10413
|
process.stderr.write("behold preview: --port must be a number\n");
|
|
8394
10414
|
process.exit(2);
|
|
8395
10415
|
}
|
|
8396
|
-
const projectDir =
|
|
10416
|
+
const projectDir = resolve8(dirArg ?? process.cwd());
|
|
8397
10417
|
if (!existsSync14(projectDir)) {
|
|
8398
10418
|
process.stderr.write(`behold preview: project not found at ${projectDir}
|
|
8399
10419
|
`);
|
|
@@ -8414,21 +10434,21 @@ async function runPreview(rest) {
|
|
|
8414
10434
|
await startServer({ projectDir, port, env: "local", previewMode: true });
|
|
8415
10435
|
}
|
|
8416
10436
|
async function runExportCmd(rest) {
|
|
8417
|
-
let outDir =
|
|
10437
|
+
let outDir = resolve8("behold-export");
|
|
8418
10438
|
let env;
|
|
8419
10439
|
let name;
|
|
8420
10440
|
let dirArg;
|
|
8421
10441
|
let emulator = false;
|
|
8422
10442
|
for (let i = 0; i < rest.length; i++) {
|
|
8423
10443
|
const a = rest[i];
|
|
8424
|
-
if (a === "--out") outDir =
|
|
10444
|
+
if (a === "--out") outDir = resolve8(rest[++i]);
|
|
8425
10445
|
else if (a === "--env") env = rest[++i];
|
|
8426
10446
|
else if (a === "--name") name = rest[++i];
|
|
8427
10447
|
else if (a === "--emulator") emulator = true;
|
|
8428
10448
|
else if (a === "-h" || a === "--help") return void process.stdout.write(USAGE);
|
|
8429
10449
|
else if (!a.startsWith("-")) dirArg = a;
|
|
8430
10450
|
}
|
|
8431
|
-
const projectDir =
|
|
10451
|
+
const projectDir = resolve8(dirArg ?? process.cwd());
|
|
8432
10452
|
if (emulator) {
|
|
8433
10453
|
injectEmulatorEnv(env);
|
|
8434
10454
|
env ??= "local";
|