@intentius/behold 0.15.0 → 0.17.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 +50 -5
- package/README.md +34 -2
- package/dist/cli.js +1214 -654
- 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 +64 -156
- package/example-carve/app/package-lock.json +9 -9
- 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 +64 -156
- package/example-k8s/package-lock.json +27 -27
- package/example-k8s/package.json +3 -3
- package/example-writes/package-lock.json +14 -14
- package/example-writes/package.json +3 -3
- package/package.json +3 -3
- package/web/app.js +137 -11
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 resolve12(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 resolve12(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 resolve12({ 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
|
}
|
|
@@ -872,7 +872,7 @@ async function createK8sClient(options = {}) {
|
|
|
872
872
|
return {
|
|
873
873
|
provenance,
|
|
874
874
|
defaultNamespace,
|
|
875
|
-
resolve:
|
|
875
|
+
resolve: resolve12,
|
|
876
876
|
read,
|
|
877
877
|
pathFor,
|
|
878
878
|
readIfPresent,
|
|
@@ -1065,8 +1065,8 @@ var init_src = __esm({
|
|
|
1065
1065
|
});
|
|
1066
1066
|
|
|
1067
1067
|
// src/cli.ts
|
|
1068
|
-
import { resolve as
|
|
1069
|
-
import { realpathSync, existsSync as
|
|
1068
|
+
import { resolve as resolve11, dirname as dirname7, join as join22 } from "node:path";
|
|
1069
|
+
import { realpathSync, existsSync as existsSync15, readFileSync as readFileSync17, rmSync as rmSync3 } from "node:fs";
|
|
1070
1070
|
import { fileURLToPath as fileURLToPath5 } from "node:url";
|
|
1071
1071
|
import { spawn as spawn6, spawnSync as spawnSync2 } from "node:child_process";
|
|
1072
1072
|
|
|
@@ -1147,8 +1147,8 @@ import { serve } from "@hono/node-server";
|
|
|
1147
1147
|
import { fileURLToPath as fileURLToPath3 } from "node:url";
|
|
1148
1148
|
import { execFile as execFile3 } from "node:child_process";
|
|
1149
1149
|
import { promisify as promisify3 } from "node:util";
|
|
1150
|
-
import { existsSync as
|
|
1151
|
-
import { basename, dirname as dirname5, join as
|
|
1150
|
+
import { existsSync as existsSync14, readFileSync as readFileSync15 } from "node:fs";
|
|
1151
|
+
import { basename, dirname as dirname5, join as join20, relative as relative5, resolve as resolve9, sep as sep6 } from "node:path";
|
|
1152
1152
|
|
|
1153
1153
|
// src/recents.ts
|
|
1154
1154
|
import { readFileSync, writeFileSync, mkdirSync, existsSync } from "node:fs";
|
|
@@ -1182,10 +1182,10 @@ function addRecent(dir, now = () => (/* @__PURE__ */ new Date()).toISOString())
|
|
|
1182
1182
|
|
|
1183
1183
|
// src/chant.ts
|
|
1184
1184
|
import { spawn } from "node:child_process";
|
|
1185
|
-
import { existsSync as existsSync3, readFileSync as
|
|
1185
|
+
import { existsSync as existsSync3, readFileSync as readFileSync4 } from "node:fs";
|
|
1186
1186
|
import { createRequire } from "node:module";
|
|
1187
1187
|
import { fileURLToPath } from "node:url";
|
|
1188
|
-
import { dirname as
|
|
1188
|
+
import { dirname as dirname3, join as join4, resolve as resolve3 } from "node:path";
|
|
1189
1189
|
|
|
1190
1190
|
// src/project.ts
|
|
1191
1191
|
import { readFileSync as readFileSync2, existsSync as existsSync2 } from "node:fs";
|
|
@@ -1305,6 +1305,30 @@ function readTiers(cfg) {
|
|
|
1305
1305
|
if (!values.length) return void 0;
|
|
1306
1306
|
return { envVar: tiers.envVar, values };
|
|
1307
1307
|
}
|
|
1308
|
+
var EXECUTOR_FORGES = ["github", "gitlab", "forgejo"];
|
|
1309
|
+
function readExecutor(cfg) {
|
|
1310
|
+
const raw = cfg?.executor;
|
|
1311
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return void 0;
|
|
1312
|
+
const out = {};
|
|
1313
|
+
for (const [env, value] of Object.entries(raw)) {
|
|
1314
|
+
if (!env) continue;
|
|
1315
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
1316
|
+
out[env] = { invalid: `executor.${env} must be {forge, workflow}, got ${JSON.stringify(value)}` };
|
|
1317
|
+
continue;
|
|
1318
|
+
}
|
|
1319
|
+
const v = value;
|
|
1320
|
+
if (typeof v.forge !== "string" || !EXECUTOR_FORGES.includes(v.forge)) {
|
|
1321
|
+
out[env] = { invalid: `executor.${env}.forge must be one of ${EXECUTOR_FORGES.join(", ")}, got ${JSON.stringify(v.forge)}` };
|
|
1322
|
+
continue;
|
|
1323
|
+
}
|
|
1324
|
+
if (typeof v.workflow !== "string" || !/^[^/\\]+\.ya?ml$/.test(v.workflow)) {
|
|
1325
|
+
out[env] = { invalid: `executor.${env}.workflow must name a file in .github/workflows (e.g. deploy-${env}.yml), got ${JSON.stringify(v.workflow)}` };
|
|
1326
|
+
continue;
|
|
1327
|
+
}
|
|
1328
|
+
out[env] = { forge: v.forge, workflow: v.workflow };
|
|
1329
|
+
}
|
|
1330
|
+
return Object.keys(out).length ? out : void 0;
|
|
1331
|
+
}
|
|
1308
1332
|
function loadBeholdConfig(projectDir) {
|
|
1309
1333
|
const path = join2(projectDir, BEHOLD_CONFIG_NAME);
|
|
1310
1334
|
if (!existsSync2(path)) return {};
|
|
@@ -1312,7 +1336,8 @@ function loadBeholdConfig(projectDir) {
|
|
|
1312
1336
|
const raw = JSON.parse(readFileSync2(path, "utf8"));
|
|
1313
1337
|
const tiers = readTiers(raw);
|
|
1314
1338
|
const members = Array.isArray(raw.members) ? raw.members.filter((m) => typeof m === "string" && !!m) : [];
|
|
1315
|
-
|
|
1339
|
+
const executor = readExecutor(raw);
|
|
1340
|
+
return { ...tiers ? { tiers } : {}, ...members.length ? { members } : {}, ...executor ? { executor } : {} };
|
|
1316
1341
|
} catch {
|
|
1317
1342
|
return {};
|
|
1318
1343
|
}
|
|
@@ -1664,6 +1689,367 @@ function unescapeDoubleQuoted(body) {
|
|
|
1664
1689
|
return out;
|
|
1665
1690
|
}
|
|
1666
1691
|
|
|
1692
|
+
// src/carve-manifest.ts
|
|
1693
|
+
import { readdirSync, readFileSync as readFileSync3, statSync } from "node:fs";
|
|
1694
|
+
import { dirname as dirname2, isAbsolute, join as join3, relative, resolve, sep } from "node:path";
|
|
1695
|
+
var CARVE_MANIFEST_SUFFIX = ".carve.json";
|
|
1696
|
+
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.";
|
|
1697
|
+
var isRecord = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
1698
|
+
var strings = (v) => Array.isArray(v) ? v.filter((x) => typeof x === "string") : [];
|
|
1699
|
+
function parseCarveManifest(value) {
|
|
1700
|
+
if (!isRecord(value)) return null;
|
|
1701
|
+
if (value.version !== 1 && value.version !== 2) return null;
|
|
1702
|
+
if (typeof value.target !== "string" || !value.target) return null;
|
|
1703
|
+
return value;
|
|
1704
|
+
}
|
|
1705
|
+
function readCarveManifest(path, readFile = (p) => readFileSync3(p, "utf8")) {
|
|
1706
|
+
let text;
|
|
1707
|
+
try {
|
|
1708
|
+
text = readFile(path);
|
|
1709
|
+
} catch {
|
|
1710
|
+
return null;
|
|
1711
|
+
}
|
|
1712
|
+
try {
|
|
1713
|
+
return parseCarveManifest(JSON.parse(text));
|
|
1714
|
+
} catch {
|
|
1715
|
+
return null;
|
|
1716
|
+
}
|
|
1717
|
+
}
|
|
1718
|
+
function carveStageOf(m) {
|
|
1719
|
+
if (m.apply) return "applied";
|
|
1720
|
+
if (m.bridge) return "bridged";
|
|
1721
|
+
return "emitted";
|
|
1722
|
+
}
|
|
1723
|
+
function carveWordFor(stage) {
|
|
1724
|
+
switch (stage) {
|
|
1725
|
+
case "applied":
|
|
1726
|
+
return "carved \u2192 chant";
|
|
1727
|
+
case "bridged":
|
|
1728
|
+
return "bridged \u2014 apply is yours";
|
|
1729
|
+
default:
|
|
1730
|
+
return "emitted \u2014 not bridged";
|
|
1731
|
+
}
|
|
1732
|
+
}
|
|
1733
|
+
function carveToneFor(stage) {
|
|
1734
|
+
return stage === "applied" ? "good" : "accent";
|
|
1735
|
+
}
|
|
1736
|
+
function carveStageNote(state) {
|
|
1737
|
+
switch (state.stage) {
|
|
1738
|
+
case "applied":
|
|
1739
|
+
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.";
|
|
1740
|
+
case "bridged":
|
|
1741
|
+
return "emitted and bridged \u2014 the survivor patch is proposed. Terraform still owns the resource until `carve apply`.";
|
|
1742
|
+
default:
|
|
1743
|
+
return "emitted \u2014 typed chant source exists, and the surviving Terraform still reads the resource directly. `carve bridge` is next.";
|
|
1744
|
+
}
|
|
1745
|
+
}
|
|
1746
|
+
function carveStateOf(m, path) {
|
|
1747
|
+
const stage = carveStageOf(m);
|
|
1748
|
+
const deferred = Object.entries(m.emit?.params ?? {}).map(([name, p]) => `${name} \u2190 ${p?.survivor ?? "a survivor"}`);
|
|
1749
|
+
return {
|
|
1750
|
+
target: m.target,
|
|
1751
|
+
...m.tfType ? { tfType: m.tfType } : {},
|
|
1752
|
+
stage,
|
|
1753
|
+
graduated: stage === "applied",
|
|
1754
|
+
// The last stamped step wins — that is the age of the state, not the age
|
|
1755
|
+
// of the carve.
|
|
1756
|
+
...m.apply?.at || m.bridge?.at || m.emit?.at ? { at: m.apply?.at ?? m.bridge?.at ?? m.emit?.at } : {},
|
|
1757
|
+
files: strings(m.emit?.files),
|
|
1758
|
+
sourceFiles: strings(m.emit?.files).map((f) => isAbsolute(f) ? f : resolve(dirname2(path), f)),
|
|
1759
|
+
excised: strings(m.bridge?.excised),
|
|
1760
|
+
deferredInputs: deferred,
|
|
1761
|
+
...m.apply ? {
|
|
1762
|
+
ownership: {
|
|
1763
|
+
...m.apply.marker?.stack ? { stack: m.apply.marker.stack } : {},
|
|
1764
|
+
...m.apply.marker?.env ? { env: m.apply.marker.env } : {},
|
|
1765
|
+
...m.apply.ownershipTags ? { tags: m.apply.ownershipTags } : {}
|
|
1766
|
+
}
|
|
1767
|
+
} : {},
|
|
1768
|
+
...m.from ? { from: m.from } : {},
|
|
1769
|
+
outDir: dirname2(path),
|
|
1770
|
+
path
|
|
1771
|
+
};
|
|
1772
|
+
}
|
|
1773
|
+
var nodeCarveIo = {
|
|
1774
|
+
readdir: (dir) => readdirSync(dir),
|
|
1775
|
+
isDirectory: (p) => {
|
|
1776
|
+
try {
|
|
1777
|
+
return statSync(p).isDirectory();
|
|
1778
|
+
} catch {
|
|
1779
|
+
return false;
|
|
1780
|
+
}
|
|
1781
|
+
},
|
|
1782
|
+
readFile: (p) => readFileSync3(p, "utf8")
|
|
1783
|
+
};
|
|
1784
|
+
var MANIFEST_SCAN_DEPTH = 2;
|
|
1785
|
+
var SKIP_DIRS = /* @__PURE__ */ new Set(["node_modules", "dist", "cdk.out", ".terraform", ".git"]);
|
|
1786
|
+
function listCarveManifests(dir, io = nodeCarveIo, depth = MANIFEST_SCAN_DEPTH) {
|
|
1787
|
+
const out = [];
|
|
1788
|
+
const walk = (d, level) => {
|
|
1789
|
+
let entries;
|
|
1790
|
+
try {
|
|
1791
|
+
entries = io.readdir(d).sort();
|
|
1792
|
+
} catch {
|
|
1793
|
+
return;
|
|
1794
|
+
}
|
|
1795
|
+
for (const name of entries) {
|
|
1796
|
+
const full = join3(d, name);
|
|
1797
|
+
if (name.endsWith(CARVE_MANIFEST_SUFFIX)) {
|
|
1798
|
+
out.push(full);
|
|
1799
|
+
continue;
|
|
1800
|
+
}
|
|
1801
|
+
if (level >= depth) continue;
|
|
1802
|
+
if (name.startsWith(".") || SKIP_DIRS.has(name)) continue;
|
|
1803
|
+
if (io.isDirectory(full)) walk(full, level + 1);
|
|
1804
|
+
}
|
|
1805
|
+
};
|
|
1806
|
+
walk(dir, 0);
|
|
1807
|
+
return out;
|
|
1808
|
+
}
|
|
1809
|
+
function readCarveStates(dirs, io = nodeCarveIo, depth = MANIFEST_SCAN_DEPTH) {
|
|
1810
|
+
const byTarget = /* @__PURE__ */ new Map();
|
|
1811
|
+
for (const dir of dirs) mergeCarveStates(byTarget, listCarveManifests(dir, io, depth), io);
|
|
1812
|
+
return byTarget;
|
|
1813
|
+
}
|
|
1814
|
+
function mergeCarveStates(byTarget, paths, io) {
|
|
1815
|
+
const rank = { emitted: 0, bridged: 1, applied: 2 };
|
|
1816
|
+
for (const path of paths) {
|
|
1817
|
+
const manifest = readCarveManifest(path, io.readFile);
|
|
1818
|
+
if (!manifest) continue;
|
|
1819
|
+
const state = carveStateOf(manifest, path);
|
|
1820
|
+
const held = byTarget.get(state.target);
|
|
1821
|
+
if (!held || rank[state.stage] > rank[held.stage]) byTarget.set(state.target, state);
|
|
1822
|
+
}
|
|
1823
|
+
}
|
|
1824
|
+
var CARVE_STATUS_FLOOR = "0.54.0";
|
|
1825
|
+
function carveStatusArgs(from) {
|
|
1826
|
+
return ["carve", "status", "--from", from, "--json"];
|
|
1827
|
+
}
|
|
1828
|
+
async function discoverCarveStates(dirs, read, io = nodeCarveIo, depth = MANIFEST_SCAN_DEPTH) {
|
|
1829
|
+
const byTarget = /* @__PURE__ */ new Map();
|
|
1830
|
+
for (const dir of dirs) {
|
|
1831
|
+
const status = await read(dir).catch(() => void 0);
|
|
1832
|
+
const paths = status?.carves ? status.carves.map((row) => resolve(status.from ?? dir, row.path)) : listCarveManifests(dir, io, depth);
|
|
1833
|
+
mergeCarveStates(byTarget, paths, io);
|
|
1834
|
+
}
|
|
1835
|
+
return byTarget;
|
|
1836
|
+
}
|
|
1837
|
+
function joinCarvedSources(ir, states, memberDir) {
|
|
1838
|
+
const root = resolve(memberDir);
|
|
1839
|
+
const candidates = [];
|
|
1840
|
+
for (const s of states) {
|
|
1841
|
+
for (const f of s.sourceFiles) {
|
|
1842
|
+
const abs = resolve(f);
|
|
1843
|
+
if (abs === root || abs.startsWith(root + sep)) candidates.push({ abs, state: s });
|
|
1844
|
+
}
|
|
1845
|
+
}
|
|
1846
|
+
if (candidates.length === 0) return { ir, joined: 0 };
|
|
1847
|
+
let joined = 0;
|
|
1848
|
+
const nodes = ir.nodes.map((n) => {
|
|
1849
|
+
const file = n.sourceLoc?.file;
|
|
1850
|
+
if (!file) return n;
|
|
1851
|
+
const hit = emittedFileFor(file, root, candidates);
|
|
1852
|
+
if (!hit) return n;
|
|
1853
|
+
joined++;
|
|
1854
|
+
const { state, abs } = hit;
|
|
1855
|
+
const carve = {
|
|
1856
|
+
target: state.target,
|
|
1857
|
+
...state.tfType ? { tfType: state.tfType } : {},
|
|
1858
|
+
stage: state.stage,
|
|
1859
|
+
graduated: state.graduated,
|
|
1860
|
+
...state.at ? { at: state.at } : {},
|
|
1861
|
+
...state.from ? { from: state.from } : {},
|
|
1862
|
+
file: relative(root, abs).split(sep).join("/")
|
|
1863
|
+
};
|
|
1864
|
+
return { ...n, attrs: { ...n.attrs, carved: state.target, _carve: carve } };
|
|
1865
|
+
});
|
|
1866
|
+
return { ir: joined ? { ...ir, nodes } : ir, joined };
|
|
1867
|
+
}
|
|
1868
|
+
function emittedFileFor(file, root, candidates) {
|
|
1869
|
+
const rel = file.split("\\").join("/").replace(/^\.\//, "");
|
|
1870
|
+
if (isAbsolute(rel)) {
|
|
1871
|
+
const abs = resolve(rel);
|
|
1872
|
+
return candidates.find((c) => c.abs === abs);
|
|
1873
|
+
}
|
|
1874
|
+
const exact = resolve(root, rel);
|
|
1875
|
+
const direct = candidates.find((c) => c.abs === exact);
|
|
1876
|
+
if (direct) return direct;
|
|
1877
|
+
const suffix = candidates.filter((c) => c.abs.split(sep).join("/").endsWith("/" + rel));
|
|
1878
|
+
return suffix.length === 1 ? suffix[0] : void 0;
|
|
1879
|
+
}
|
|
1880
|
+
function emittedNodesFor(ir, states, memberDir) {
|
|
1881
|
+
const out = /* @__PURE__ */ new Map();
|
|
1882
|
+
if (!ir) return out;
|
|
1883
|
+
for (const n of joinCarvedSources(ir, states, memberDir).ir.nodes) {
|
|
1884
|
+
const carve = n.attrs._carve;
|
|
1885
|
+
if (carve && !out.has(carve.target)) out.set(carve.target, n);
|
|
1886
|
+
}
|
|
1887
|
+
return out;
|
|
1888
|
+
}
|
|
1889
|
+
function carveProgress(states, total) {
|
|
1890
|
+
let applied = 0;
|
|
1891
|
+
let bridged = 0;
|
|
1892
|
+
let emitted = 0;
|
|
1893
|
+
for (const s of states) {
|
|
1894
|
+
if (s.stage === "applied") applied++;
|
|
1895
|
+
else if (s.stage === "bridged") bridged++;
|
|
1896
|
+
else emitted++;
|
|
1897
|
+
}
|
|
1898
|
+
const inFlight = bridged + emitted;
|
|
1899
|
+
const parts = [];
|
|
1900
|
+
if (emitted) parts.push(`${emitted} emitted`);
|
|
1901
|
+
if (bridged) parts.push(`${bridged} bridged`);
|
|
1902
|
+
return {
|
|
1903
|
+
applied,
|
|
1904
|
+
bridged,
|
|
1905
|
+
emitted,
|
|
1906
|
+
inFlight,
|
|
1907
|
+
total: typeof total === "number" ? total : null,
|
|
1908
|
+
label: typeof total === "number" ? `${applied} of ${total} carved` : `${applied} carved`,
|
|
1909
|
+
detail: parts.length ? `${parts.join(", ")} \u2014 apply is yours` : ""
|
|
1910
|
+
};
|
|
1911
|
+
}
|
|
1912
|
+
function applyCommandFor(state, labels = {}) {
|
|
1913
|
+
const env = state.ownership?.env ?? "<env>";
|
|
1914
|
+
const stack = state.ownership?.stack ?? "<stack>";
|
|
1915
|
+
const from = labels.from ?? state.from ?? "<terraform-dir>";
|
|
1916
|
+
const out = labels.out ?? state.outDir;
|
|
1917
|
+
return `chant carve apply --from ${from} --output ${out} --env ${env} --stack ${stack} --write-source`;
|
|
1918
|
+
}
|
|
1919
|
+
function carveStatePayload(states, total, labels = () => ({})) {
|
|
1920
|
+
const list3 = [...states.values()].sort((a, b) => a.target.localeCompare(b.target));
|
|
1921
|
+
return {
|
|
1922
|
+
manifests: list3.length,
|
|
1923
|
+
progress: carveProgress(list3, total),
|
|
1924
|
+
states: list3.map((s) => ({ ...s, note: carveStageNote(s), applyCommand: applyCommandFor(s, labels(s)) })),
|
|
1925
|
+
apply: { human: true, note: APPLY_IS_HUMAN }
|
|
1926
|
+
};
|
|
1927
|
+
}
|
|
1928
|
+
function splitCarveState(tfIr, states, opts = {}) {
|
|
1929
|
+
if (states.size === 0) return { tf: tfIr, graduated: [] };
|
|
1930
|
+
const shorten = opts.shorten ?? ((p) => p);
|
|
1931
|
+
const emitted = opts.emitted ?? /* @__PURE__ */ new Map();
|
|
1932
|
+
const declaredAttrs = (n) => Object.fromEntries(Object.entries(n.attrs ?? {}).filter(([k]) => !k.startsWith("_") && k !== "carved"));
|
|
1933
|
+
const source = (files) => files.map(shorten).join(", ");
|
|
1934
|
+
const gone = /* @__PURE__ */ new Set();
|
|
1935
|
+
const graduated = [];
|
|
1936
|
+
const nodes = [];
|
|
1937
|
+
for (const node of tfIr.nodes) {
|
|
1938
|
+
const state = states.get(node.id);
|
|
1939
|
+
if (!state) {
|
|
1940
|
+
nodes.push(node);
|
|
1941
|
+
continue;
|
|
1942
|
+
}
|
|
1943
|
+
const became = emitted.get(node.id);
|
|
1944
|
+
if (state.graduated) {
|
|
1945
|
+
gone.add(node.id);
|
|
1946
|
+
graduated.push({
|
|
1947
|
+
...node,
|
|
1948
|
+
...became ? { kind: became.kind, lexicon: became.lexicon, ...became.sourceLoc ? { sourceLoc: became.sourceLoc } : {} } : {},
|
|
1949
|
+
attrs: {
|
|
1950
|
+
...node.attrs,
|
|
1951
|
+
...became ? { ...declaredAttrs(became), chantEntity: `${became.kind} ${became.id}` } : {},
|
|
1952
|
+
_status: "good",
|
|
1953
|
+
carve: carveWordFor(state.stage),
|
|
1954
|
+
carveState: state.stage,
|
|
1955
|
+
...state.ownership?.stack ? { ownedStack: state.ownership.stack } : {},
|
|
1956
|
+
...state.ownership?.env ? { ownedEnv: state.ownership.env } : {},
|
|
1957
|
+
...state.at ? { carvedAt: state.at } : {},
|
|
1958
|
+
...state.files.length ? { chantSource: source(state.files) } : {}
|
|
1959
|
+
}
|
|
1960
|
+
});
|
|
1961
|
+
continue;
|
|
1962
|
+
}
|
|
1963
|
+
nodes.push({
|
|
1964
|
+
...node,
|
|
1965
|
+
attrs: {
|
|
1966
|
+
...node.attrs,
|
|
1967
|
+
_status: carveToneFor(state.stage),
|
|
1968
|
+
carve: carveWordFor(state.stage),
|
|
1969
|
+
carveState: state.stage,
|
|
1970
|
+
...became ? { chantEntity: `${became.kind} ${became.id}` } : {},
|
|
1971
|
+
...state.at ? { carvedAt: state.at } : {},
|
|
1972
|
+
...state.files.length ? { chantSource: source(state.files) } : {},
|
|
1973
|
+
...state.excised.length ? { excisesOnApply: state.excised.join(", ") } : {},
|
|
1974
|
+
...state.deferredInputs.length ? { deferredInputs: state.deferredInputs.join(", ") } : {}
|
|
1975
|
+
}
|
|
1976
|
+
});
|
|
1977
|
+
}
|
|
1978
|
+
const byStack = {};
|
|
1979
|
+
for (const [band, members] of Object.entries(tfIr.groups?.byStack ?? {})) {
|
|
1980
|
+
const left = members.filter((id) => !gone.has(id));
|
|
1981
|
+
if (left.length) byStack[band] = left;
|
|
1982
|
+
}
|
|
1983
|
+
return {
|
|
1984
|
+
tf: {
|
|
1985
|
+
nodes,
|
|
1986
|
+
edges: tfIr.edges.filter((e) => !gone.has(e.from) && !gone.has(e.to)),
|
|
1987
|
+
groups: { ...tfIr.groups, byStack }
|
|
1988
|
+
},
|
|
1989
|
+
graduated
|
|
1990
|
+
};
|
|
1991
|
+
}
|
|
1992
|
+
var GRADUATED_BAND = "carved \u2192 chant";
|
|
1993
|
+
function bandGraduated(tf, graduated) {
|
|
1994
|
+
if (graduated.length === 0) return tf;
|
|
1995
|
+
return {
|
|
1996
|
+
nodes: [...graduated, ...tf.nodes],
|
|
1997
|
+
edges: tf.edges,
|
|
1998
|
+
groups: {
|
|
1999
|
+
...tf.groups,
|
|
2000
|
+
byStack: {
|
|
2001
|
+
[GRADUATED_BAND]: graduated.map((n) => n.id),
|
|
2002
|
+
...tf.groups?.byStack ?? {}
|
|
2003
|
+
}
|
|
2004
|
+
}
|
|
2005
|
+
};
|
|
2006
|
+
}
|
|
2007
|
+
function carveStateNote(progress, states) {
|
|
2008
|
+
if (states.size === 0) return "";
|
|
2009
|
+
const head = ` Carve state (from ${states.size} manifest${states.size === 1 ? "" : "s"}): ${progress.label}`;
|
|
2010
|
+
return progress.detail ? `${head}, ${progress.detail}.` : `${head}.`;
|
|
2011
|
+
}
|
|
2012
|
+
|
|
2013
|
+
// src/foreign.ts
|
|
2014
|
+
import { isAbsolute as isAbsolute2, resolve as resolve2, sep as sep2 } from "node:path";
|
|
2015
|
+
function dropForeignDeclarations(ir, projectDir) {
|
|
2016
|
+
const root = resolve2(projectDir);
|
|
2017
|
+
const foreign = [];
|
|
2018
|
+
for (const n of ir.nodes) {
|
|
2019
|
+
const file = n.sourceLoc?.file;
|
|
2020
|
+
if (!file || !isAbsolute2(file)) continue;
|
|
2021
|
+
const abs = resolve2(file);
|
|
2022
|
+
if (abs === root || abs.startsWith(root + sep2)) continue;
|
|
2023
|
+
foreign.push({ id: n.id, kind: n.kind, file });
|
|
2024
|
+
}
|
|
2025
|
+
if (foreign.length === 0) return ir;
|
|
2026
|
+
const gone = new Set(foreign.map((f) => f.id));
|
|
2027
|
+
const groups = {};
|
|
2028
|
+
for (const [axis, members] of Object.entries(ir.groups ?? {})) {
|
|
2029
|
+
if (!members || typeof members !== "object") continue;
|
|
2030
|
+
const kept = {};
|
|
2031
|
+
for (const [box, ids] of Object.entries(members)) {
|
|
2032
|
+
const left = ids.filter((id) => !gone.has(id));
|
|
2033
|
+
if (left.length) kept[box] = left;
|
|
2034
|
+
}
|
|
2035
|
+
groups[axis] = kept;
|
|
2036
|
+
}
|
|
2037
|
+
return {
|
|
2038
|
+
...ir,
|
|
2039
|
+
nodes: ir.nodes.filter((n) => !gone.has(n.id)),
|
|
2040
|
+
edges: ir.edges.filter((e) => !gone.has(e.from) && !gone.has(e.to)),
|
|
2041
|
+
groups,
|
|
2042
|
+
foreign
|
|
2043
|
+
};
|
|
2044
|
+
}
|
|
2045
|
+
function foreignNote(ir) {
|
|
2046
|
+
const f = ir.foreign;
|
|
2047
|
+
if (!f || f.length === 0) return void 0;
|
|
2048
|
+
const named = f.slice(0, 3).map((d) => d.id).join(", ");
|
|
2049
|
+
const more = f.length > 3 ? ` +${f.length - 3}` : "";
|
|
2050
|
+
return `${f.length} declaration${f.length === 1 ? "" : "s"} outside this project dropped (${named}${more}) \u2014 chant graphs Ops from the git root (chant#2058)`;
|
|
2051
|
+
}
|
|
2052
|
+
|
|
1667
2053
|
// src/chant.ts
|
|
1668
2054
|
function envOverridesFor(opts) {
|
|
1669
2055
|
const overrides = {};
|
|
@@ -1694,14 +2080,14 @@ function packageManifestFrom(req, name) {
|
|
|
1694
2080
|
} catch {
|
|
1695
2081
|
return void 0;
|
|
1696
2082
|
}
|
|
1697
|
-
let dir =
|
|
2083
|
+
let dir = dirname3(entry);
|
|
1698
2084
|
for (; ; ) {
|
|
1699
2085
|
try {
|
|
1700
|
-
const pkg = createRequire(import.meta.url)(
|
|
2086
|
+
const pkg = createRequire(import.meta.url)(join4(dir, "package.json"));
|
|
1701
2087
|
if (pkg.name === name) return { root: dir, manifest: pkg };
|
|
1702
2088
|
} catch {
|
|
1703
2089
|
}
|
|
1704
|
-
const parent =
|
|
2090
|
+
const parent = dirname3(dir);
|
|
1705
2091
|
if (parent === dir) break;
|
|
1706
2092
|
dir = parent;
|
|
1707
2093
|
}
|
|
@@ -1711,10 +2097,10 @@ function chantPackageFrom(req) {
|
|
|
1711
2097
|
const found = packageManifestFrom(req, "@intentius/chant");
|
|
1712
2098
|
if (!found) return void 0;
|
|
1713
2099
|
const { root, manifest } = found;
|
|
1714
|
-
return { root, bin:
|
|
2100
|
+
return { root, bin: join4(root, manifest.bin?.chant ?? "bin/chant"), ...manifest.version ? { version: manifest.version } : {} };
|
|
1715
2101
|
}
|
|
1716
2102
|
function resolveLexicons(projectDir, lexicons) {
|
|
1717
|
-
const req = createRequire(
|
|
2103
|
+
const req = createRequire(join4(resolve3(projectDir), "noop.js"));
|
|
1718
2104
|
return lexicons.map((lexicon) => {
|
|
1719
2105
|
const pkg = `@intentius/chant-lexicon-${lexicon}`;
|
|
1720
2106
|
const found = packageManifestFrom(req, pkg);
|
|
@@ -1724,7 +2110,7 @@ function resolveLexicons(projectDir, lexicons) {
|
|
|
1724
2110
|
function resolveChant(projectDir) {
|
|
1725
2111
|
const own = chantPackageFrom(createRequire(import.meta.url));
|
|
1726
2112
|
if (projectDir) {
|
|
1727
|
-
const fromProject = chantPackageFrom(createRequire(
|
|
2113
|
+
const fromProject = chantPackageFrom(createRequire(join4(resolve3(projectDir), "noop.js")));
|
|
1728
2114
|
if (fromProject) return { source: fromProject.root === own?.root ? "behold" : "project", ...fromProject };
|
|
1729
2115
|
}
|
|
1730
2116
|
if (own) return { source: "behold", ...own };
|
|
@@ -1732,8 +2118,8 @@ function resolveChant(projectDir) {
|
|
|
1732
2118
|
}
|
|
1733
2119
|
function chantFloor() {
|
|
1734
2120
|
try {
|
|
1735
|
-
const here =
|
|
1736
|
-
const pkg = JSON.parse(
|
|
2121
|
+
const here = dirname3(fileURLToPath(import.meta.url));
|
|
2122
|
+
const pkg = JSON.parse(readFileSync4(join4(here, "..", "package.json"), "utf8"));
|
|
1737
2123
|
return pkg.dependencies?.["@intentius/chant"]?.replace(/^[^0-9]*/, "") || void 0;
|
|
1738
2124
|
} catch {
|
|
1739
2125
|
return void 0;
|
|
@@ -1839,7 +2225,7 @@ function runCommandStream(cmd, args, cwd, onLine) {
|
|
|
1839
2225
|
return { pid: proc.pid ?? -1, kill: () => proc.kill(), done };
|
|
1840
2226
|
}
|
|
1841
2227
|
function legacyGraphPath(projectDir) {
|
|
1842
|
-
const src =
|
|
2228
|
+
const src = join4(projectDir, "src");
|
|
1843
2229
|
return existsSync3(src) ? src : projectDir;
|
|
1844
2230
|
}
|
|
1845
2231
|
async function graphPath(projectDir, opts = {}) {
|
|
@@ -1851,9 +2237,9 @@ async function graphPath(projectDir, opts = {}) {
|
|
|
1851
2237
|
}
|
|
1852
2238
|
if (info.stacks?.length) {
|
|
1853
2239
|
const picked = opts.stack ? info.stacks.find((s) => s.name === opts.stack) : void 0;
|
|
1854
|
-
return
|
|
2240
|
+
return resolve3(projectDir, (picked ?? info.stacks[0]).src);
|
|
1855
2241
|
}
|
|
1856
|
-
if (info.sourceDir) return
|
|
2242
|
+
if (info.sourceDir) return resolve3(projectDir, info.sourceDir);
|
|
1857
2243
|
return legacyGraphPath(projectDir);
|
|
1858
2244
|
}
|
|
1859
2245
|
function graphArgs(src, format, opts, components) {
|
|
@@ -1861,10 +2247,11 @@ function graphArgs(src, format, opts, components) {
|
|
|
1861
2247
|
}
|
|
1862
2248
|
async function graphIr(projectDir, opts = {}) {
|
|
1863
2249
|
const src = await graphPath(projectDir, opts);
|
|
1864
|
-
|
|
2250
|
+
const ir = await runChantJson(graphArgs(src, "ir", opts, false), projectDir, envOverridesFor(opts));
|
|
2251
|
+
return dropForeignDeclarations(ir, projectDir);
|
|
1865
2252
|
}
|
|
1866
2253
|
async function clusterRootGraphIr(projectDir, opts = {}) {
|
|
1867
|
-
if (!existsSync3(
|
|
2254
|
+
if (!existsSync3(join4(projectDir, "cluster"))) return void 0;
|
|
1868
2255
|
const { live: _live, overlay: _overlay, env: _env, ...sourceOnly } = opts;
|
|
1869
2256
|
try {
|
|
1870
2257
|
return await runChantJson(graphArgs("cluster", "ir", sourceOnly, false), projectDir, envOverridesFor(sourceOnly));
|
|
@@ -1882,6 +2269,9 @@ function componentStatusArgs(env) {
|
|
|
1882
2269
|
function componentStatus(projectDir, env, opts = {}) {
|
|
1883
2270
|
return runChantJson(componentStatusArgs(env), projectDir, envOverridesFor(opts));
|
|
1884
2271
|
}
|
|
2272
|
+
function ciWorkflowName(env) {
|
|
2273
|
+
return `chant-components-${env}`;
|
|
2274
|
+
}
|
|
1885
2275
|
var CI_FORGES = ["gitlab", "github", "forgejo"];
|
|
1886
2276
|
function ciForgeFor(lexicons) {
|
|
1887
2277
|
return CI_FORGES.find((forge) => lexicons.includes(forge));
|
|
@@ -1899,7 +2289,21 @@ function parseCiPipeline(stdout) {
|
|
|
1899
2289
|
const script = Array.isArray(props?.script) ? props.script.map(String) : [];
|
|
1900
2290
|
return { ...j, script };
|
|
1901
2291
|
});
|
|
1902
|
-
|
|
2292
|
+
const fromJson = typeof parsed.env === "string" && parsed.env ? parsed.env : void 0;
|
|
2293
|
+
const env = fromJson ?? pipelineEnvFromYaml(doc);
|
|
2294
|
+
return { stages: parsed.stages, jobs, ...env ? { env } : {} };
|
|
2295
|
+
}
|
|
2296
|
+
function pipelineEnvFromYaml(doc) {
|
|
2297
|
+
const name = doc?.name;
|
|
2298
|
+
if (typeof name === "string") {
|
|
2299
|
+
const m = /^chant-components-(.+)$/.exec(name);
|
|
2300
|
+
if (m) return m[1];
|
|
2301
|
+
}
|
|
2302
|
+
const env = doc?.env;
|
|
2303
|
+
if (env && typeof env === "object" && typeof env.CHANT_ENV === "string") {
|
|
2304
|
+
return env.CHANT_ENV;
|
|
2305
|
+
}
|
|
2306
|
+
return void 0;
|
|
1903
2307
|
}
|
|
1904
2308
|
function ciPipeline(projectDir, opts = {}, forge = "gitlab") {
|
|
1905
2309
|
const args = ciPipelineArgs(opts, forge);
|
|
@@ -1920,6 +2324,9 @@ function lifecycleDiffArgs(env) {
|
|
|
1920
2324
|
function lifecycleDiffLive(projectDir, env, opts = {}) {
|
|
1921
2325
|
return runChantJson(lifecycleDiffArgs(env), projectDir, envOverridesFor(opts));
|
|
1922
2326
|
}
|
|
2327
|
+
function carveStatus(from, projectDir) {
|
|
2328
|
+
return runChantJson(carveStatusArgs(from), projectDir);
|
|
2329
|
+
}
|
|
1923
2330
|
function helmRendersArgs() {
|
|
1924
2331
|
return ["helm", "renders", "--json"];
|
|
1925
2332
|
}
|
|
@@ -1996,8 +2403,8 @@ function mergeClusterRoot(ir, clusterIr, running) {
|
|
|
1996
2403
|
}
|
|
1997
2404
|
|
|
1998
2405
|
// src/helm-releases.ts
|
|
1999
|
-
import { existsSync as existsSync4, readdirSync, readFileSync as
|
|
2000
|
-
import { join as
|
|
2406
|
+
import { existsSync as existsSync4, readdirSync as readdirSync2, readFileSync as readFileSync5, statSync as statSync2 } from "node:fs";
|
|
2407
|
+
import { join as join5 } from "node:path";
|
|
2001
2408
|
var RELEASE_KEY = /^release\/([^/]+)\/(.+)$/;
|
|
2002
2409
|
function declaredChartNames(ir) {
|
|
2003
2410
|
const names = /* @__PURE__ */ new Set();
|
|
@@ -2017,15 +2424,15 @@ function matchesDeclaredChart(chart, declared) {
|
|
|
2017
2424
|
}
|
|
2018
2425
|
function discoverReleaseUnits(projectDir) {
|
|
2019
2426
|
const owners = /* @__PURE__ */ new Map();
|
|
2020
|
-
const roots = [
|
|
2427
|
+
const roots = [join5(projectDir, "src"), join5(projectDir, "ops")];
|
|
2021
2428
|
const files = [];
|
|
2022
2429
|
const walk = (dir, depth) => {
|
|
2023
2430
|
if (depth > 4 || !existsSync4(dir)) return;
|
|
2024
|
-
for (const f of
|
|
2025
|
-
const p =
|
|
2431
|
+
for (const f of readdirSync2(dir)) {
|
|
2432
|
+
const p = join5(dir, f);
|
|
2026
2433
|
let s;
|
|
2027
2434
|
try {
|
|
2028
|
-
s =
|
|
2435
|
+
s = statSync2(p);
|
|
2029
2436
|
} catch {
|
|
2030
2437
|
continue;
|
|
2031
2438
|
}
|
|
@@ -2037,7 +2444,7 @@ function discoverReleaseUnits(projectDir) {
|
|
|
2037
2444
|
for (const file of files) {
|
|
2038
2445
|
let content;
|
|
2039
2446
|
try {
|
|
2040
|
-
content =
|
|
2447
|
+
content = readFileSync5(file, "utf8");
|
|
2041
2448
|
} catch {
|
|
2042
2449
|
continue;
|
|
2043
2450
|
}
|
|
@@ -2089,9 +2496,9 @@ function synthesizeHelmReleases(ir, observed, owners) {
|
|
|
2089
2496
|
}
|
|
2090
2497
|
|
|
2091
2498
|
// src/gh-run.ts
|
|
2092
|
-
import { spawn as spawn2 } from "node:child_process";
|
|
2093
|
-
import { readdirSync as
|
|
2094
|
-
import { join as
|
|
2499
|
+
import { execFileSync, spawn as spawn2 } from "node:child_process";
|
|
2500
|
+
import { readdirSync as readdirSync3, readFileSync as readFileSync6 } from "node:fs";
|
|
2501
|
+
import { join as join6 } from "node:path";
|
|
2095
2502
|
|
|
2096
2503
|
// src/ci-run.ts
|
|
2097
2504
|
function pipelineProgress(pipeline) {
|
|
@@ -2135,6 +2542,9 @@ function foldPipelineLine(state, line) {
|
|
|
2135
2542
|
}
|
|
2136
2543
|
return out;
|
|
2137
2544
|
}
|
|
2545
|
+
function losePipelineProgress(state) {
|
|
2546
|
+
return { ...state, status: "lost" };
|
|
2547
|
+
}
|
|
2138
2548
|
function finishPipelineProgress(state, exitCode) {
|
|
2139
2549
|
const terminal = exitCode === 0 ? "ok" : "failed";
|
|
2140
2550
|
let out = state;
|
|
@@ -2145,19 +2555,19 @@ function finishPipelineProgress(state, exitCode) {
|
|
|
2145
2555
|
}
|
|
2146
2556
|
|
|
2147
2557
|
// src/gh-run.ts
|
|
2148
|
-
var defaultGhExec = (args) => new Promise((
|
|
2558
|
+
var defaultGhExec = (args) => new Promise((resolve12) => {
|
|
2149
2559
|
let out = "";
|
|
2150
2560
|
let proc;
|
|
2151
2561
|
try {
|
|
2152
2562
|
proc = spawn2("gh", args, { stdio: ["ignore", "pipe", "pipe"] });
|
|
2153
2563
|
} catch {
|
|
2154
|
-
|
|
2564
|
+
resolve12({ code: 127, out: "" });
|
|
2155
2565
|
return;
|
|
2156
2566
|
}
|
|
2157
2567
|
proc.stdout.on("data", (d) => out += d);
|
|
2158
2568
|
proc.stderr.on("data", (d) => out += d);
|
|
2159
|
-
proc.on("error", () =>
|
|
2160
|
-
proc.on("close", (code) =>
|
|
2569
|
+
proc.on("error", () => resolve12({ code: 127, out }));
|
|
2570
|
+
proc.on("close", (code) => resolve12({ code: code ?? 1, out }));
|
|
2161
2571
|
});
|
|
2162
2572
|
async function ghReady(exec = defaultGhExec) {
|
|
2163
2573
|
const { code, out } = await exec(["auth", "status"]);
|
|
@@ -2176,31 +2586,64 @@ function parseWorkflow(file, text) {
|
|
|
2176
2586
|
const jobs = d.jobs && typeof d.jobs === "object" && !Array.isArray(d.jobs) ? Object.keys(d.jobs) : [];
|
|
2177
2587
|
const on = d.on ?? d[true];
|
|
2178
2588
|
const dispatchable = on === "workflow_dispatch" || Array.isArray(on) && on.includes("workflow_dispatch") || !!on && typeof on === "object" && "workflow_dispatch" in on || /^\s{2,}workflow_dispatch\s*:?\s*$/m.test(text);
|
|
2179
|
-
return { file, jobIds: jobs, dispatchable };
|
|
2589
|
+
return { file, jobIds: jobs, dispatchable, ...typeof d.name === "string" && d.name ? { name: d.name } : {} };
|
|
2590
|
+
}
|
|
2591
|
+
function workflowsDir(projectDir) {
|
|
2592
|
+
let root = projectDir;
|
|
2593
|
+
try {
|
|
2594
|
+
root = execFileSync("git", ["-C", projectDir, "rev-parse", "--show-toplevel"], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim() || projectDir;
|
|
2595
|
+
} catch {
|
|
2596
|
+
}
|
|
2597
|
+
return join6(root, ".github", "workflows");
|
|
2180
2598
|
}
|
|
2181
|
-
function
|
|
2182
|
-
const dir =
|
|
2599
|
+
function committedWorkflows(projectDir) {
|
|
2600
|
+
const dir = workflowsDir(projectDir);
|
|
2183
2601
|
let files;
|
|
2184
2602
|
try {
|
|
2185
|
-
files =
|
|
2603
|
+
files = readdirSync3(dir).filter((f) => f.endsWith(".yml") || f.endsWith(".yaml"));
|
|
2186
2604
|
} catch {
|
|
2187
|
-
return
|
|
2605
|
+
return [];
|
|
2188
2606
|
}
|
|
2189
|
-
const
|
|
2190
|
-
let best;
|
|
2607
|
+
const out = [];
|
|
2191
2608
|
for (const f of files) {
|
|
2192
|
-
let text;
|
|
2193
2609
|
try {
|
|
2194
|
-
|
|
2610
|
+
out.push(parseWorkflow(f, readFileSync6(join6(dir, f), "utf8")));
|
|
2195
2611
|
} catch {
|
|
2196
|
-
continue;
|
|
2197
2612
|
}
|
|
2198
|
-
const info = parseWorkflow(f, text);
|
|
2199
|
-
if (!info.dispatchable) continue;
|
|
2200
|
-
const overlap = info.jobIds.filter((id) => wanted.has(id)).length;
|
|
2201
|
-
if (overlap > 0 && (!best || overlap > best.overlap)) best = { info, overlap };
|
|
2202
2613
|
}
|
|
2203
|
-
return
|
|
2614
|
+
return out;
|
|
2615
|
+
}
|
|
2616
|
+
function resolveWorkflow(projectDir, pipeline, designated) {
|
|
2617
|
+
const all = committedWorkflows(projectDir);
|
|
2618
|
+
if (designated) {
|
|
2619
|
+
const hit = all.find((w) => w.file === designated);
|
|
2620
|
+
if (!hit) return { reason: `the designated workflow .github/workflows/${designated} is not committed \u2014 Deploy is disabled for this env until it is (behold never falls back to a guess, or to running it here)` };
|
|
2621
|
+
if (!hit.dispatchable) return { reason: `the designated workflow ${designated} declares no workflow_dispatch \u2014 add it to its \`on:\` block; until then Deploy is disabled for this env` };
|
|
2622
|
+
return { workflow: hit };
|
|
2623
|
+
}
|
|
2624
|
+
const dispatchable = all.filter((w) => w.dispatchable);
|
|
2625
|
+
if (pipeline.env) {
|
|
2626
|
+
const named = dispatchable.filter((w) => w.name === ciWorkflowName(pipeline.env));
|
|
2627
|
+
if (named.length === 1) return { workflow: named[0] };
|
|
2628
|
+
if (named.length > 1) return { reason: `${named.length} committed workflows are named ${ciWorkflowName(pipeline.env)} (${named.map((w) => w.file).join(", ")}) \u2014 designate one in .behold.json's executor block` };
|
|
2629
|
+
}
|
|
2630
|
+
const wanted = new Set(pipeline.jobs.map((j) => j.jobName));
|
|
2631
|
+
const otherEnv = (w) => {
|
|
2632
|
+
const m = w.name ? /^chant-components-(.+)$/.exec(w.name) : null;
|
|
2633
|
+
return !!m && !!pipeline.env && m[1] !== pipeline.env;
|
|
2634
|
+
};
|
|
2635
|
+
const scored = dispatchable.filter((w) => !otherEnv(w)).map((w) => ({ w, overlap: w.jobIds.filter((id) => wanted.has(id)).length })).filter((x) => x.overlap > 0).sort((a, b) => b.overlap - a.overlap);
|
|
2636
|
+
if (scored.length === 0) {
|
|
2637
|
+
const excluded = dispatchable.filter(otherEnv).map((w) => `${w.file} (${w.name})`);
|
|
2638
|
+
return {
|
|
2639
|
+
reason: "no committed workflow_dispatch workflow whose jobs match the generated pipeline \u2014 commit one (or add `workflow_dispatch:` to its `on:` block)" + (excluded.length ? `; ${excluded.join(", ")} ${excluded.length === 1 ? "is" : "are"} named for another env and never stand in` : "")
|
|
2640
|
+
};
|
|
2641
|
+
}
|
|
2642
|
+
if (scored.length > 1 && scored[1].overlap === scored[0].overlap) {
|
|
2643
|
+
const tied = scored.filter((x) => x.overlap === scored[0].overlap).map((x) => x.w.file);
|
|
2644
|
+
return { reason: `${tied.length} committed workflows match the pipeline equally (${tied.join(", ")}) \u2014 the generated job ids are the same for every env, so behold cannot tell which one deploys ${pipeline.env ?? "this env"}; designate it in .behold.json's executor block` };
|
|
2645
|
+
}
|
|
2646
|
+
return { workflow: scored[0].w };
|
|
2204
2647
|
}
|
|
2205
2648
|
async function latestRunId(workflowFile, exec = defaultGhExec) {
|
|
2206
2649
|
const { code, out } = await exec(["run", "list", "--workflow", workflowFile, "--limit", "1", "--json", "databaseId"]);
|
|
@@ -2234,11 +2677,13 @@ function runViewToProgress(pipeline, view) {
|
|
|
2234
2677
|
return { ...w, status };
|
|
2235
2678
|
})
|
|
2236
2679
|
};
|
|
2680
|
+
if (view.url) state = { ...state, url: view.url };
|
|
2237
2681
|
if (view.status === "completed") {
|
|
2238
2682
|
return { ...state, status: view.conclusion === "success" ? "ok" : "failed" };
|
|
2239
2683
|
}
|
|
2240
2684
|
return state;
|
|
2241
2685
|
}
|
|
2686
|
+
var LOST_EXIT = 75;
|
|
2242
2687
|
async function dispatchAndFollow(workflow, pipeline, ref, deps) {
|
|
2243
2688
|
const exec = deps.exec ?? defaultGhExec;
|
|
2244
2689
|
const pollMs = deps.pollMs ?? 5e3;
|
|
@@ -2267,9 +2712,28 @@ async function dispatchAndFollow(workflow, pipeline, ref, deps) {
|
|
|
2267
2712
|
return 1;
|
|
2268
2713
|
}
|
|
2269
2714
|
deps.onLine(`\u25CF run ${runId} started \u2014 following`);
|
|
2715
|
+
deps.onAdopted?.({ runId });
|
|
2716
|
+
return followRun(runId, pipeline, deps);
|
|
2717
|
+
}
|
|
2718
|
+
async function followRun(runId, pipeline, deps) {
|
|
2719
|
+
const exec = deps.exec ?? defaultGhExec;
|
|
2720
|
+
const pollMs = deps.pollMs ?? 5e3;
|
|
2721
|
+
const sleep = deps.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
|
|
2722
|
+
const pollFailBudget = deps.pollFailBudget ?? 24;
|
|
2723
|
+
const followDeadline = Date.now() + (deps.followTimeoutMs ?? 6 * 60 * 60 * 1e3);
|
|
2724
|
+
let urlSaid = false;
|
|
2725
|
+
let state = pipelineProgress(pipeline);
|
|
2726
|
+
let consecutiveFailures = 0;
|
|
2727
|
+
const lose = (why) => {
|
|
2728
|
+
deps.onLine(`\u2717 run ${runId}: ${why} \u2014 behold stopped following; the run itself may still be live${state.url ? ` at ${state.url}` : " on the forge"} (restart behold, or POST /api/ci/readopt, to re-adopt it)`);
|
|
2729
|
+
deps.onProgress(losePipelineProgress(state));
|
|
2730
|
+
deps.onConcluded?.({ verdict: "lost" });
|
|
2731
|
+
return LOST_EXIT;
|
|
2732
|
+
};
|
|
2270
2733
|
for (; ; ) {
|
|
2271
|
-
const { code, out } = await exec(["run", "view", String(runId), "--json", "status,conclusion,jobs"]);
|
|
2734
|
+
const { code, out } = await exec(["run", "view", String(runId), "--json", "status,conclusion,jobs,url"]);
|
|
2272
2735
|
if (code === 0) {
|
|
2736
|
+
consecutiveFailures = 0;
|
|
2273
2737
|
let view;
|
|
2274
2738
|
try {
|
|
2275
2739
|
view = JSON.parse(out);
|
|
@@ -2277,14 +2741,29 @@ async function dispatchAndFollow(workflow, pipeline, ref, deps) {
|
|
|
2277
2741
|
view = void 0;
|
|
2278
2742
|
}
|
|
2279
2743
|
if (view) {
|
|
2280
|
-
|
|
2744
|
+
if (view.url && !urlSaid) {
|
|
2745
|
+
urlSaid = true;
|
|
2746
|
+
deps.onAdopted?.({ runId, url: view.url });
|
|
2747
|
+
deps.onLine(`\u25CF ${view.url} \u2014 if the workflow's environment requires an approval, it is granted there, by a GitHub identity; behold holds none that could`);
|
|
2748
|
+
}
|
|
2749
|
+
state = runViewToProgress(pipeline, view);
|
|
2750
|
+
deps.onProgress(state);
|
|
2281
2751
|
if (view.status === "completed") {
|
|
2282
2752
|
deps.onLine(`\u25A0 run ${runId} ${view.conclusion ?? "completed"}`);
|
|
2283
|
-
|
|
2753
|
+
const ok = view.conclusion === "success";
|
|
2754
|
+
deps.onConcluded?.({ verdict: ok ? "ok" : "failed" });
|
|
2755
|
+
return ok ? 0 : 1;
|
|
2284
2756
|
}
|
|
2285
2757
|
}
|
|
2286
2758
|
} else {
|
|
2287
|
-
|
|
2759
|
+
consecutiveFailures++;
|
|
2760
|
+
deps.onLine(`\u26A0 gh run view ${runId} failed (exit ${code}) \u2014 retrying (${consecutiveFailures}/${pollFailBudget})`);
|
|
2761
|
+
if (consecutiveFailures >= pollFailBudget) {
|
|
2762
|
+
return lose(`${pollFailBudget} consecutive polls failed \u2014 the stream is dead, not flaky`);
|
|
2763
|
+
}
|
|
2764
|
+
}
|
|
2765
|
+
if (Date.now() >= followDeadline) {
|
|
2766
|
+
return lose("the follow deadline passed");
|
|
2288
2767
|
}
|
|
2289
2768
|
await sleep(pollMs);
|
|
2290
2769
|
}
|
|
@@ -2310,18 +2789,57 @@ function joinCiProgress(ir, state) {
|
|
|
2310
2789
|
return ir;
|
|
2311
2790
|
}
|
|
2312
2791
|
|
|
2792
|
+
// src/ci-run-store.ts
|
|
2793
|
+
import { existsSync as existsSync5, mkdirSync as mkdirSync2, readFileSync as readFileSync7, writeFileSync as writeFileSync2, rmSync } from "node:fs";
|
|
2794
|
+
import { homedir as homedir2 } from "node:os";
|
|
2795
|
+
import { join as join7, resolve as resolve4 } from "node:path";
|
|
2796
|
+
function slug(projectDir) {
|
|
2797
|
+
return resolve4(projectDir).replace(/[^A-Za-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(-80);
|
|
2798
|
+
}
|
|
2799
|
+
function defaultRoot() {
|
|
2800
|
+
return process.env.BEHOLD_CI_RUN_DIR ?? join7(homedir2(), ".behold", "ci-runs");
|
|
2801
|
+
}
|
|
2802
|
+
function dispatchedRunPath(projectDir, root = defaultRoot()) {
|
|
2803
|
+
return join7(root, `${slug(projectDir)}.json`);
|
|
2804
|
+
}
|
|
2805
|
+
function saveDispatchedRun(run3, root = defaultRoot()) {
|
|
2806
|
+
try {
|
|
2807
|
+
const path = dispatchedRunPath(run3.projectDir, root);
|
|
2808
|
+
mkdirSync2(join7(path, ".."), { recursive: true });
|
|
2809
|
+
writeFileSync2(path, JSON.stringify({ version: 1, ...run3 }, null, 2) + "\n");
|
|
2810
|
+
} catch {
|
|
2811
|
+
}
|
|
2812
|
+
}
|
|
2813
|
+
function readDispatchedRun(projectDir, root) {
|
|
2814
|
+
try {
|
|
2815
|
+
const path = dispatchedRunPath(projectDir, root);
|
|
2816
|
+
if (!existsSync5(path)) return void 0;
|
|
2817
|
+
const parsed = JSON.parse(readFileSync7(path, "utf8"));
|
|
2818
|
+
if (parsed?.version !== 1 || typeof parsed.runId !== "number") return void 0;
|
|
2819
|
+
if (resolve4(parsed.projectDir) !== resolve4(projectDir)) return void 0;
|
|
2820
|
+
return parsed;
|
|
2821
|
+
} catch {
|
|
2822
|
+
return void 0;
|
|
2823
|
+
}
|
|
2824
|
+
}
|
|
2825
|
+
function concludeDispatchedRun(projectDir, runId, verdict, root) {
|
|
2826
|
+
const current = readDispatchedRun(projectDir, root);
|
|
2827
|
+
if (!current || current.runId !== runId) return;
|
|
2828
|
+
saveDispatchedRun({ ...current, concluded: { verdict, at: (/* @__PURE__ */ new Date()).toISOString() } }, root);
|
|
2829
|
+
}
|
|
2830
|
+
|
|
2313
2831
|
// src/operator.ts
|
|
2314
2832
|
var ANSI = /\x1b\[[0-9;]*m/g;
|
|
2315
2833
|
var strip = (s) => s.replace(ANSI, "");
|
|
2316
|
-
var
|
|
2834
|
+
var isRecord2 = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
2317
2835
|
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).";
|
|
2318
2836
|
function readRow(value) {
|
|
2319
|
-
if (!
|
|
2837
|
+
if (!isRecord2(value)) return null;
|
|
2320
2838
|
if (typeof value.op !== "string" || typeof value.env !== "string") return null;
|
|
2321
2839
|
if (!Array.isArray(value.pendingGates)) return null;
|
|
2322
2840
|
const gates = [];
|
|
2323
2841
|
for (const g of value.pendingGates) {
|
|
2324
|
-
if (!
|
|
2842
|
+
if (!isRecord2(g) || typeof g.rule !== "string" || typeof g.gate !== "string") return null;
|
|
2325
2843
|
gates.push({
|
|
2326
2844
|
rule: g.rule,
|
|
2327
2845
|
gate: g.gate,
|
|
@@ -2333,9 +2851,9 @@ function readRow(value) {
|
|
|
2333
2851
|
...typeof g.url === "string" && g.url ? { url: g.url } : {}
|
|
2334
2852
|
});
|
|
2335
2853
|
}
|
|
2336
|
-
const tick =
|
|
2854
|
+
const tick = isRecord2(value.lastTick) ? value.lastTick : void 0;
|
|
2337
2855
|
const usable = tick && typeof tick.timestamp === "string" && typeof tick.log === "string";
|
|
2338
|
-
const lease =
|
|
2856
|
+
const lease = isRecord2(value.lease) ? value.lease : void 0;
|
|
2339
2857
|
return {
|
|
2340
2858
|
op: value.op,
|
|
2341
2859
|
env: value.env,
|
|
@@ -2433,7 +2951,7 @@ var CHANT = "chant";
|
|
|
2433
2951
|
var CONVERGE_TICK = "converge-tick";
|
|
2434
2952
|
var CRONJOB_KIND = "K8s::Batch::CronJob";
|
|
2435
2953
|
var NAMESPACE_KIND = "K8s::Core::Namespace";
|
|
2436
|
-
var rec = (v) =>
|
|
2954
|
+
var rec = (v) => isRecord2(v) ? v : void 0;
|
|
2437
2955
|
var str = (v) => typeof v === "string" && v ? v : void 0;
|
|
2438
2956
|
var labelsOf = (n) => rec(rec(n.attrs?.metadata)?.labels) ?? {};
|
|
2439
2957
|
function operatorHomes(ir) {
|
|
@@ -2541,10 +3059,10 @@ function tickFreshness(at, now = Date.now()) {
|
|
|
2541
3059
|
}
|
|
2542
3060
|
var RECONCILIATIONS = /* @__PURE__ */ new Set(["reconciled", "unrecorded", "stale", "drifted", "unknown"]);
|
|
2543
3061
|
function readVerdict(value) {
|
|
2544
|
-
if (!
|
|
3062
|
+
if (!isRecord2(value)) return null;
|
|
2545
3063
|
if (typeof value.component !== "string" || !value.component) return null;
|
|
2546
3064
|
if (typeof value.reconciliation !== "string" || !RECONCILIATIONS.has(value.reconciliation)) return null;
|
|
2547
|
-
const unobserved =
|
|
3065
|
+
const unobserved = isRecord2(value.unobserved) && typeof value.unobserved.reason === "string" ? value.unobserved : void 0;
|
|
2548
3066
|
return {
|
|
2549
3067
|
component: value.component,
|
|
2550
3068
|
reconciliation: value.reconciliation,
|
|
@@ -2655,12 +3173,12 @@ function operatorLogArgs(window) {
|
|
|
2655
3173
|
];
|
|
2656
3174
|
}
|
|
2657
3175
|
function readTickRecord(value) {
|
|
2658
|
-
if (!
|
|
3176
|
+
if (!isRecord2(value)) return null;
|
|
2659
3177
|
if (typeof value.op !== "string" || typeof value.timestamp !== "string" || typeof value.log !== "string") return null;
|
|
2660
3178
|
return value;
|
|
2661
3179
|
}
|
|
2662
3180
|
function readResolutionRecord(value) {
|
|
2663
|
-
if (!
|
|
3181
|
+
if (!isRecord2(value)) return null;
|
|
2664
3182
|
const needed = ["op", "gate", "resolvedBy", "timestamp"];
|
|
2665
3183
|
if (needed.some((k) => typeof value[k] !== "string")) return null;
|
|
2666
3184
|
return value;
|
|
@@ -2721,10 +3239,10 @@ function readOperatorLog(run3) {
|
|
|
2721
3239
|
remedy: "behold reads `{entries: [{kind, timestamp, record}], malformed: {converge, gates}}`."
|
|
2722
3240
|
}
|
|
2723
3241
|
});
|
|
2724
|
-
if (!
|
|
3242
|
+
if (!isRecord2(parsed) || !Array.isArray(parsed.entries)) return unreadable("no `entries` array");
|
|
2725
3243
|
const entries = [];
|
|
2726
3244
|
for (const value of parsed.entries) {
|
|
2727
|
-
if (!
|
|
3245
|
+
if (!isRecord2(value) || typeof value.timestamp !== "string") return unreadable("an entry with no timestamp");
|
|
2728
3246
|
if (value.kind === "tick") {
|
|
2729
3247
|
const record = readTickRecord(value.record);
|
|
2730
3248
|
if (!record) return unreadable("a tick entry with no `op`/`timestamp`/`log`");
|
|
@@ -2737,7 +3255,7 @@ function readOperatorLog(run3) {
|
|
|
2737
3255
|
return unreadable(`an entry of an unknown kind "${String(value.kind)}"`);
|
|
2738
3256
|
}
|
|
2739
3257
|
}
|
|
2740
|
-
const counts =
|
|
3258
|
+
const counts = isRecord2(parsed.malformed) ? parsed.malformed : {};
|
|
2741
3259
|
const count = (v) => typeof v === "number" && Number.isFinite(v) && v > 0 ? v : 0;
|
|
2742
3260
|
return { ok: true, entries, malformed: { converge: count(counts.converge), gates: count(counts.gates) } };
|
|
2743
3261
|
}
|
|
@@ -3369,7 +3887,9 @@ function applyHelmArtifacts(ir, observed) {
|
|
|
3369
3887
|
release: key.replace(/^release\//, ""),
|
|
3370
3888
|
...o.status ? { status: o.status } : {},
|
|
3371
3889
|
...typeof o.attributes?.revision === "string" ? { revision: o.attributes.revision } : {},
|
|
3372
|
-
...typeof o.attributes?.chart === "string" ? { chart: o.attributes.chart } : {}
|
|
3890
|
+
...typeof o.attributes?.chart === "string" ? { chart: o.attributes.chart } : {},
|
|
3891
|
+
...typeof o.attributes?.inputDigest === "string" ? { inputDigest: o.attributes.inputDigest } : {},
|
|
3892
|
+
...typeof o.attributes?.contentDigest === "string" ? { contentDigest: o.attributes.contentDigest } : {}
|
|
3373
3893
|
};
|
|
3374
3894
|
attrs._artifact = match;
|
|
3375
3895
|
attrs._status = o.status === "deployed" ? "good" : "warn";
|
|
@@ -3378,37 +3898,35 @@ function applyHelmArtifacts(ir, observed) {
|
|
|
3378
3898
|
return { charts, installed };
|
|
3379
3899
|
}
|
|
3380
3900
|
|
|
3381
|
-
// src/
|
|
3382
|
-
import { statSync as
|
|
3383
|
-
import {
|
|
3384
|
-
import { join as join7, resolve as resolve3, sep } from "node:path";
|
|
3385
|
-
import { composeStacks, shortStackNames } from "@intentius/pinhole";
|
|
3901
|
+
// src/carve-discovery.ts
|
|
3902
|
+
import { readdirSync as readdirSync5, statSync as statSync4 } from "node:fs";
|
|
3903
|
+
import { join as join9 } from "node:path";
|
|
3386
3904
|
|
|
3387
3905
|
// src/member-ir.ts
|
|
3388
3906
|
import { createHash } from "node:crypto";
|
|
3389
|
-
import { readdirSync as
|
|
3390
|
-
import { join as
|
|
3907
|
+
import { readdirSync as readdirSync4, statSync as statSync3 } from "node:fs";
|
|
3908
|
+
import { join as join8, relative as relative2, resolve as resolve5 } from "node:path";
|
|
3391
3909
|
var SKIP = /* @__PURE__ */ new Set(["node_modules", "dist", ".git"]);
|
|
3392
3910
|
function memberIrCacheSize(env = process.env) {
|
|
3393
3911
|
const override = Number.parseInt(env.BEHOLD_ESTATE_IR_CACHE ?? "", 10);
|
|
3394
3912
|
return Number.isInteger(override) && override >= 0 ? override : 64;
|
|
3395
3913
|
}
|
|
3396
3914
|
function memberSourceStamp(dir) {
|
|
3397
|
-
const root =
|
|
3915
|
+
const root = resolve5(dir);
|
|
3398
3916
|
const h = createHash("sha1");
|
|
3399
3917
|
let any = false;
|
|
3400
3918
|
const walk = (at) => {
|
|
3401
|
-
const entries =
|
|
3919
|
+
const entries = readdirSync4(at, { withFileTypes: true }).sort((a, b) => a.name < b.name ? -1 : 1);
|
|
3402
3920
|
for (const e of entries) {
|
|
3403
3921
|
if (SKIP.has(e.name)) continue;
|
|
3404
|
-
const path =
|
|
3922
|
+
const path = join8(at, e.name);
|
|
3405
3923
|
if (e.isDirectory()) {
|
|
3406
3924
|
walk(path);
|
|
3407
3925
|
continue;
|
|
3408
3926
|
}
|
|
3409
3927
|
if (!e.isFile()) continue;
|
|
3410
|
-
const st =
|
|
3411
|
-
h.update(`${
|
|
3928
|
+
const st = statSync3(path);
|
|
3929
|
+
h.update(`${relative2(root, path)}\0${st.mtimeMs}\0${st.size}
|
|
3412
3930
|
`);
|
|
3413
3931
|
any = true;
|
|
3414
3932
|
}
|
|
@@ -3423,7 +3941,7 @@ function memberSourceStamp(dir) {
|
|
|
3423
3941
|
function cacheKey(dir, opts) {
|
|
3424
3942
|
const chant = resolveChant(dir);
|
|
3425
3943
|
const canonical = JSON.stringify(opts, Object.keys(opts).sort());
|
|
3426
|
-
return `${
|
|
3944
|
+
return `${resolve5(dir)}\0${chant.bin}\0${chant.version ?? ""}\0${canonical}`;
|
|
3427
3945
|
}
|
|
3428
3946
|
var cache = /* @__PURE__ */ new Map();
|
|
3429
3947
|
var hits = 0;
|
|
@@ -3433,7 +3951,7 @@ function invalidateMember(dir) {
|
|
|
3433
3951
|
cache.clear();
|
|
3434
3952
|
return;
|
|
3435
3953
|
}
|
|
3436
|
-
const root =
|
|
3954
|
+
const root = resolve5(dir);
|
|
3437
3955
|
for (const [key, entry] of cache) if (entry.dir === root) cache.delete(key);
|
|
3438
3956
|
}
|
|
3439
3957
|
async function memberIr(dir, opts = {}) {
|
|
@@ -3448,18 +3966,82 @@ async function memberIr(dir, opts = {}) {
|
|
|
3448
3966
|
cache.set(key, hit);
|
|
3449
3967
|
return structuredClone(hit.ir);
|
|
3450
3968
|
}
|
|
3451
|
-
misses++;
|
|
3452
|
-
const ir = await graphIr(dir, opts);
|
|
3453
|
-
if (stamp === void 0 || memberSourceStamp(dir) !== stamp) return ir;
|
|
3454
|
-
cache.delete(key);
|
|
3455
|
-
cache.set(key, { dir:
|
|
3456
|
-
for (const oldest of cache.keys()) {
|
|
3457
|
-
if (cache.size <= size) break;
|
|
3458
|
-
cache.delete(oldest);
|
|
3969
|
+
misses++;
|
|
3970
|
+
const ir = await graphIr(dir, opts);
|
|
3971
|
+
if (stamp === void 0 || memberSourceStamp(dir) !== stamp) return ir;
|
|
3972
|
+
cache.delete(key);
|
|
3973
|
+
cache.set(key, { dir: resolve5(dir), stamp, ir });
|
|
3974
|
+
for (const oldest of cache.keys()) {
|
|
3975
|
+
if (cache.size <= size) break;
|
|
3976
|
+
cache.delete(oldest);
|
|
3977
|
+
}
|
|
3978
|
+
return structuredClone(ir);
|
|
3979
|
+
}
|
|
3980
|
+
|
|
3981
|
+
// src/carve-discovery.ts
|
|
3982
|
+
function carveStatusReader(projectDir) {
|
|
3983
|
+
return async (dir) => {
|
|
3984
|
+
const via = projectDir ?? dir;
|
|
3985
|
+
if (!meetsFloor(resolveChant(via).version, CARVE_STATUS_FLOOR)) return void 0;
|
|
3986
|
+
return carveStatus(dir, via).catch(() => void 0);
|
|
3987
|
+
};
|
|
3988
|
+
}
|
|
3989
|
+
var SKIP2 = /* @__PURE__ */ new Set(["node_modules", ".git", "dist", ".chant", ".terraform", "cdk.out"]);
|
|
3990
|
+
function hasAnyCarveManifest(dir, io = fsIo) {
|
|
3991
|
+
const walk = (d) => {
|
|
3992
|
+
let entries;
|
|
3993
|
+
try {
|
|
3994
|
+
entries = io.readdir(d);
|
|
3995
|
+
} catch {
|
|
3996
|
+
return false;
|
|
3997
|
+
}
|
|
3998
|
+
for (const name of entries) {
|
|
3999
|
+
if (name.endsWith(CARVE_MANIFEST_SUFFIX)) return true;
|
|
4000
|
+
if (name.startsWith(".") || SKIP2.has(name)) continue;
|
|
4001
|
+
const full = join9(d, name);
|
|
4002
|
+
if (io.isDirectory(full) && walk(full)) return true;
|
|
4003
|
+
}
|
|
4004
|
+
return false;
|
|
4005
|
+
};
|
|
4006
|
+
return walk(dir);
|
|
4007
|
+
}
|
|
4008
|
+
var fsIo = {
|
|
4009
|
+
readdir: (d) => readdirSync5(d),
|
|
4010
|
+
isDirectory: (p) => {
|
|
4011
|
+
try {
|
|
4012
|
+
return statSync4(p).isDirectory();
|
|
4013
|
+
} catch {
|
|
4014
|
+
return false;
|
|
4015
|
+
}
|
|
4016
|
+
}
|
|
4017
|
+
};
|
|
4018
|
+
var cache2 = /* @__PURE__ */ new Map();
|
|
4019
|
+
async function carveStatesFor(dir) {
|
|
4020
|
+
const stamp = memberSourceStamp(dir);
|
|
4021
|
+
const held = cache2.get(dir);
|
|
4022
|
+
if (held && stamp !== void 0 && held.stamp === stamp) return held.states;
|
|
4023
|
+
const states = hasAnyCarveManifest(dir) ? await discoverCarveStates([dir], carveStatusReader(), nodeCarveIo, MANIFEST_SCAN_DEPTH) : /* @__PURE__ */ new Map();
|
|
4024
|
+
if (stamp !== void 0) cache2.set(dir, { stamp, states });
|
|
4025
|
+
return states;
|
|
4026
|
+
}
|
|
4027
|
+
async function carveStatesUnder(dirs) {
|
|
4028
|
+
const rank = { emitted: 0, bridged: 1, applied: 2 };
|
|
4029
|
+
const out = /* @__PURE__ */ new Map();
|
|
4030
|
+
for (const dir of dirs) {
|
|
4031
|
+
for (const s of (await carveStatesFor(dir)).values()) {
|
|
4032
|
+
const held = out.get(s.target);
|
|
4033
|
+
if (!held || rank[s.stage] > rank[held.stage]) out.set(s.target, s);
|
|
4034
|
+
}
|
|
3459
4035
|
}
|
|
3460
|
-
return
|
|
4036
|
+
return out;
|
|
3461
4037
|
}
|
|
3462
4038
|
|
|
4039
|
+
// src/estate.ts
|
|
4040
|
+
import { statSync as statSync5 } from "node:fs";
|
|
4041
|
+
import { availableParallelism } from "node:os";
|
|
4042
|
+
import { join as join10, resolve as resolve6, sep as sep3 } from "node:path";
|
|
4043
|
+
import { composeStacks, shortStackNames } from "@intentius/pinhole";
|
|
4044
|
+
|
|
3463
4045
|
// src/zoom-notes.ts
|
|
3464
4046
|
function zoomNote(zoom, ir, compositeEdgesAttached) {
|
|
3465
4047
|
const empty = ir.nodes.length === 0;
|
|
@@ -3562,7 +4144,10 @@ async function composeEstate(projectDirs, opts = {}) {
|
|
|
3562
4144
|
const names = shortStackNames(projectDirs);
|
|
3563
4145
|
const stacks = await mapPool(projectDirs, estateReadPool(projectDirs.length), async (dir, i) => ({
|
|
3564
4146
|
name: names[i],
|
|
3565
|
-
|
|
4147
|
+
// chant#2040: the manifests a member holds name the files its carved
|
|
4148
|
+
// entities were emitted into — joined here, where the member's root is
|
|
4149
|
+
// known, onto a COPY (the member IR is the #312 cache's own object).
|
|
4150
|
+
ir: joinCarvedSources(await memberIr(dir, opts), (await carveStatesFor(dir)).values(), dir).ir
|
|
3566
4151
|
}));
|
|
3567
4152
|
return composeStacks(stacks);
|
|
3568
4153
|
}
|
|
@@ -3583,7 +4168,7 @@ var rec3 = (v) => v && typeof v === "object" && !Array.isArray(v) ? v : void 0;
|
|
|
3583
4168
|
var str3 = (v) => typeof v === "string" && v.length > 0 ? v : void 0;
|
|
3584
4169
|
var realIsDir = (p) => {
|
|
3585
4170
|
try {
|
|
3586
|
-
return
|
|
4171
|
+
return statSync5(p).isDirectory();
|
|
3587
4172
|
} catch {
|
|
3588
4173
|
return false;
|
|
3589
4174
|
}
|
|
@@ -3613,11 +4198,11 @@ function declaredSegments(path) {
|
|
|
3613
4198
|
function pathAlignment(declared, dir, isDir = realIsDir) {
|
|
3614
4199
|
const want = declaredSegments(declared);
|
|
3615
4200
|
if (!want) return 0;
|
|
3616
|
-
const root =
|
|
3617
|
-
const have = root.split(
|
|
4201
|
+
const root = resolve6(dir);
|
|
4202
|
+
const have = root.split(sep3).filter(Boolean);
|
|
3618
4203
|
for (let k = Math.min(want.length, have.length); k >= 1; k--) {
|
|
3619
4204
|
if (!have.slice(-k).every((s, i) => s === want[i])) continue;
|
|
3620
|
-
if (!isDir(
|
|
4205
|
+
if (!isDir(join10(root, ...want.slice(k)))) continue;
|
|
3621
4206
|
return k;
|
|
3622
4207
|
}
|
|
3623
4208
|
return 0;
|
|
@@ -3647,10 +4232,10 @@ function joinNamespaceBindings(members, isDir = realIsDir) {
|
|
|
3647
4232
|
if (!winner || tied) continue;
|
|
3648
4233
|
const prev = best.get(winner.dir);
|
|
3649
4234
|
if (prev === "ambiguous") continue;
|
|
3650
|
-
const
|
|
3651
|
-
if (!prev) best.set(winner.dir, { score: winner.score, join:
|
|
4235
|
+
const join23 = { dir: winner.dir, namespace: b.namespace, path: b.path, declaredBy: b.declaredBy };
|
|
4236
|
+
if (!prev) best.set(winner.dir, { score: winner.score, join: join23 });
|
|
3652
4237
|
else if (prev.join.namespace !== b.namespace) best.set(winner.dir, "ambiguous");
|
|
3653
|
-
else if (winner.score > prev.score) best.set(winner.dir, { score: winner.score, join:
|
|
4238
|
+
else if (winner.score > prev.score) best.set(winner.dir, { score: winner.score, join: join23 });
|
|
3654
4239
|
}
|
|
3655
4240
|
return [...best.values()].filter((v) => v !== "ambiguous").map((v) => v.join);
|
|
3656
4241
|
}
|
|
@@ -3691,7 +4276,7 @@ async function composeEstateOverlay(projectDirs, opts, classify) {
|
|
|
3691
4276
|
const reason = firstLine2(err);
|
|
3692
4277
|
try {
|
|
3693
4278
|
const { env: _env, live: _live, overlay: _overlay, ...srcOpts } = opts;
|
|
3694
|
-
const src = await memberIr(dir, srcOpts);
|
|
4279
|
+
const src = joinCarvedSources(await memberIr(dir, srcOpts), (await carveStatesFor(dir)).values(), dir).ir;
|
|
3695
4280
|
for (const n of src.nodes) n.attrs = { ...n.attrs, _status: "neutral", _unobserved: reason };
|
|
3696
4281
|
stacks[i] = { name, ir: src };
|
|
3697
4282
|
unobserved.push({ name, reason });
|
|
@@ -3714,6 +4299,36 @@ async function composeEstateOverlay(projectDirs, opts, classify) {
|
|
|
3714
4299
|
}
|
|
3715
4300
|
|
|
3716
4301
|
// src/helm-drift.ts
|
|
4302
|
+
function renderIdentity(record, observed) {
|
|
4303
|
+
const declared = {
|
|
4304
|
+
...record.inputDigest ? { inputDigest: record.inputDigest } : {},
|
|
4305
|
+
...record.contentDigest ? { contentDigest: record.contentDigest } : {}
|
|
4306
|
+
};
|
|
4307
|
+
const a = observed?.observation.attributes ?? {};
|
|
4308
|
+
const obs = {
|
|
4309
|
+
...observed ? { release: observed.key.replace(/^release\//, "") } : {},
|
|
4310
|
+
...typeof a.inputDigest === "string" ? { inputDigest: a.inputDigest } : {},
|
|
4311
|
+
...typeof a.contentDigest === "string" ? { contentDigest: a.contentDigest } : {}
|
|
4312
|
+
};
|
|
4313
|
+
const base = { declared, observed: obs };
|
|
4314
|
+
if (!declared.inputDigest && !declared.contentDigest) return { match: "unpinned", ...base };
|
|
4315
|
+
if (!obs.inputDigest && !obs.contentDigest) return { match: "unrecorded", ...base };
|
|
4316
|
+
if (declared.contentDigest && obs.contentDigest) {
|
|
4317
|
+
return { match: declared.contentDigest === obs.contentDigest ? "content" : "mismatch", ...base };
|
|
4318
|
+
}
|
|
4319
|
+
if (declared.inputDigest && obs.inputDigest) {
|
|
4320
|
+
return { match: declared.inputDigest === obs.inputDigest ? "input" : "mismatch", ...base };
|
|
4321
|
+
}
|
|
4322
|
+
return { match: "unrecorded", ...base };
|
|
4323
|
+
}
|
|
4324
|
+
function observedReleaseFor(record, observed, namespace) {
|
|
4325
|
+
if (!observed) return void 0;
|
|
4326
|
+
const candidates = Object.entries(observed).filter(([key2]) => key2.startsWith("release/") && key2.split("/").pop() === record.name);
|
|
4327
|
+
if (candidates.length === 0) return void 0;
|
|
4328
|
+
const scoped = namespace ? candidates.find(([key2]) => key2 === `release/${namespace}/${record.name}`) : void 0;
|
|
4329
|
+
const [key, observation] = scoped ?? candidates[0];
|
|
4330
|
+
return { key, observation };
|
|
4331
|
+
}
|
|
3717
4332
|
function countChanges(docs) {
|
|
3718
4333
|
return docs.reduce((n, d) => n + d.changes.length, 0);
|
|
3719
4334
|
}
|
|
@@ -3736,8 +4351,9 @@ function provenanceOf(live) {
|
|
|
3736
4351
|
sourceRef: m.sourceRef
|
|
3737
4352
|
};
|
|
3738
4353
|
}
|
|
3739
|
-
function helmRenderReport(record, live) {
|
|
3740
|
-
const
|
|
4354
|
+
function helmRenderReport(record, live, observed) {
|
|
4355
|
+
const identity = observed ? { identity: renderIdentity(record, observedReleaseFor(record, observed, live?.manifest.namespace)) } : {};
|
|
4356
|
+
const base = { render: record.name, chart: record.chart, ...identity };
|
|
3741
4357
|
if (!live) {
|
|
3742
4358
|
return { ...base, verdict: "unobserved", reason: record.contentDigest ? "no-stored-render" : "unpinned" };
|
|
3743
4359
|
}
|
|
@@ -3761,21 +4377,24 @@ function helmRenderReport(record, live) {
|
|
|
3761
4377
|
}
|
|
3762
4378
|
return { ...base, verdict: "in-sync", counts, provenance };
|
|
3763
4379
|
}
|
|
3764
|
-
async function readHelmRenderDrift(projectDir, env, opts = {}) {
|
|
4380
|
+
async function readHelmRenderDrift(projectDir, env, opts = {}, observed) {
|
|
3765
4381
|
const report = await helmRenders(projectDir, opts).catch(() => void 0);
|
|
3766
4382
|
const records = report?.records ?? [];
|
|
3767
4383
|
if (records.length === 0) return [];
|
|
3768
4384
|
return mapPool(records, estateReadPool(records.length), async (record) => {
|
|
3769
|
-
if (!record.contentDigest) return helmRenderReport(record, void 0);
|
|
4385
|
+
if (!record.contentDigest) return helmRenderReport(record, void 0, observed);
|
|
3770
4386
|
const live = await helmRenderDiffLive(projectDir, record.contentDigest, env, opts).catch(() => void 0);
|
|
3771
|
-
return helmRenderReport(record, live);
|
|
4387
|
+
return helmRenderReport(record, live, observed);
|
|
3772
4388
|
});
|
|
3773
4389
|
}
|
|
3774
4390
|
function chartNameOf2(node) {
|
|
3775
4391
|
const name = node.attrs?.name;
|
|
3776
4392
|
return typeof name === "string" && name.length > 0 ? name : void 0;
|
|
3777
4393
|
}
|
|
3778
|
-
var SEVERITY = { drifted:
|
|
4394
|
+
var SEVERITY = { drifted: 3, unobserved: 1, "in-sync": 0 };
|
|
4395
|
+
function severityOf(r) {
|
|
4396
|
+
return Math.max(SEVERITY[r.verdict], r.identity?.match === "mismatch" ? 2 : 0);
|
|
4397
|
+
}
|
|
3779
4398
|
function applyHelmRenderDrift(ir, reports) {
|
|
3780
4399
|
if (reports.length === 0) return { charts: 0, drifted: 0 };
|
|
3781
4400
|
const byChart = /* @__PURE__ */ new Map();
|
|
@@ -3791,11 +4410,11 @@ function applyHelmRenderDrift(ir, reports) {
|
|
|
3791
4410
|
const chartName = chartNameOf2(node);
|
|
3792
4411
|
const matched = chartName ? byChart.get(chartName) : void 0;
|
|
3793
4412
|
if (!matched || matched.length === 0) continue;
|
|
3794
|
-
const worst = matched.reduce((a, b) =>
|
|
4413
|
+
const worst = matched.reduce((a, b) => severityOf(b) > severityOf(a) ? b : a);
|
|
3795
4414
|
const attrs = node.attrs ??= {};
|
|
3796
4415
|
attrs._renderDrift = matched.length === 1 ? worst : { ...worst, renders: matched };
|
|
3797
4416
|
charts++;
|
|
3798
|
-
if (worst.verdict === "drifted") {
|
|
4417
|
+
if (worst.verdict === "drifted" || matched.some((r) => r.identity?.match === "mismatch")) {
|
|
3799
4418
|
attrs._status = "warn";
|
|
3800
4419
|
drifted++;
|
|
3801
4420
|
}
|
|
@@ -4334,8 +4953,8 @@ function projectHelmLogical(ir) {
|
|
|
4334
4953
|
}
|
|
4335
4954
|
|
|
4336
4955
|
// src/logical-kustomize.ts
|
|
4337
|
-
import { existsSync as
|
|
4338
|
-
import { join as
|
|
4956
|
+
import { existsSync as existsSync6 } from "node:fs";
|
|
4957
|
+
import { join as join11 } from "node:path";
|
|
4339
4958
|
function overlayBoxTitle(name) {
|
|
4340
4959
|
return `overlay ${name}`;
|
|
4341
4960
|
}
|
|
@@ -4355,7 +4974,7 @@ function kustomizationRoot(bases, dir, exists) {
|
|
|
4355
4974
|
let current = dir;
|
|
4356
4975
|
for (let i = 0; i < 4 && current; i++) {
|
|
4357
4976
|
for (const base of bases) {
|
|
4358
|
-
if (exists(
|
|
4977
|
+
if (exists(join11(base, current, "kustomization.yaml")) || exists(join11(base, current, "kustomization.yml"))) {
|
|
4359
4978
|
return current;
|
|
4360
4979
|
}
|
|
4361
4980
|
}
|
|
@@ -4364,7 +4983,7 @@ function kustomizationRoot(bases, dir, exists) {
|
|
|
4364
4983
|
}
|
|
4365
4984
|
return void 0;
|
|
4366
4985
|
}
|
|
4367
|
-
function projectKustomizeLogical(ir, sourceRoots, exists =
|
|
4986
|
+
function projectKustomizeLogical(ir, sourceRoots, exists = existsSync6) {
|
|
4368
4987
|
const bases = Array.isArray(sourceRoots) ? sourceRoots : [sourceRoots];
|
|
4369
4988
|
const k8s = ir.nodes.filter((n) => n.lexicon === "k8s");
|
|
4370
4989
|
if (k8s.length === 0) return { ir: { nodes: [], edges: [], groups: {} }, byContainer: {} };
|
|
@@ -5086,11 +5705,11 @@ import {
|
|
|
5086
5705
|
} from "@intentius/pinhole";
|
|
5087
5706
|
|
|
5088
5707
|
// src/icon-packs.ts
|
|
5089
|
-
import { readFileSync as
|
|
5090
|
-
import { dirname as
|
|
5708
|
+
import { readFileSync as readFileSync8 } from "node:fs";
|
|
5709
|
+
import { dirname as dirname4, join as join12 } from "node:path";
|
|
5091
5710
|
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
5092
|
-
var ICON_ROOT =
|
|
5093
|
-
var
|
|
5711
|
+
var ICON_ROOT = join12(dirname4(fileURLToPath2(import.meta.url)), "..", "web", "icons");
|
|
5712
|
+
var cache3 = /* @__PURE__ */ new Map();
|
|
5094
5713
|
function unprefix(body) {
|
|
5095
5714
|
return body.replace(/\s(?:inkscape|sodipodi):[\w-]+\s*=\s*(["'])[\s\S]*?\1/g, "").replace(/\sxlink:href\s*=/g, " href=");
|
|
5096
5715
|
}
|
|
@@ -5109,9 +5728,9 @@ function plate(glyph) {
|
|
|
5109
5728
|
return { ...glyph, body: `${ground}<g transform="${inset}">${glyph.body}</g>` };
|
|
5110
5729
|
}
|
|
5111
5730
|
function loadIcon(rel) {
|
|
5112
|
-
const hit =
|
|
5731
|
+
const hit = cache3.get(rel);
|
|
5113
5732
|
if (hit) return hit;
|
|
5114
|
-
const raw =
|
|
5733
|
+
const raw = readFileSync8(join12(ICON_ROOT, `${rel}.svg`), "utf8");
|
|
5115
5734
|
const open = raw.indexOf("<svg");
|
|
5116
5735
|
const openEnd = raw.indexOf(">", open);
|
|
5117
5736
|
const close = raw.lastIndexOf("</svg");
|
|
@@ -5120,7 +5739,7 @@ function loadIcon(rel) {
|
|
|
5120
5739
|
if (!viewBox) throw new Error(`vendored icon has no viewBox: ${rel}.svg`);
|
|
5121
5740
|
const bare = { body: unprefix(raw.slice(openEnd + 1, close).trim()), colored: true, viewBox };
|
|
5122
5741
|
const glyph = PLATED.has(rel) ? plate(bare) : bare;
|
|
5123
|
-
|
|
5742
|
+
cache3.set(rel, glyph);
|
|
5124
5743
|
return glyph;
|
|
5125
5744
|
}
|
|
5126
5745
|
var K8S_ICON_BY_KIND = {
|
|
@@ -5176,9 +5795,9 @@ function helmIconFor(kind) {
|
|
|
5176
5795
|
var SUPPORTED_MAJOR = 1;
|
|
5177
5796
|
var refuse = (error, remedy) => ({ ok: false, refusal: { error, code: "carve-report", remedy } });
|
|
5178
5797
|
var HOW_TO_GET_ONE = "Generate one with `chant carve advise --from <terraform-dir> --report report.json` (chant's read-only Terraform peelability advisor), then `behold carve report.json`.";
|
|
5179
|
-
var
|
|
5798
|
+
var isRecord3 = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
5180
5799
|
function parseCarveReport(value) {
|
|
5181
|
-
if (!
|
|
5800
|
+
if (!isRecord3(value)) {
|
|
5182
5801
|
return refuse("That file isn't a carve report \u2014 its top level isn't a JSON object.", HOW_TO_GET_ONE);
|
|
5183
5802
|
}
|
|
5184
5803
|
if (!Array.isArray(value.resources)) {
|
|
@@ -5188,7 +5807,7 @@ function parseCarveReport(value) {
|
|
|
5188
5807
|
);
|
|
5189
5808
|
}
|
|
5190
5809
|
const bad = value.resources.findIndex(
|
|
5191
|
-
(r) => !
|
|
5810
|
+
(r) => !isRecord3(r) || typeof r.address !== "string" || typeof r.score !== "number" || typeof r.band !== "string"
|
|
5192
5811
|
);
|
|
5193
5812
|
if (bad >= 0) {
|
|
5194
5813
|
return refuse(
|
|
@@ -5253,364 +5872,116 @@ var directionOf = (e) => e.direction ?? "inbound";
|
|
|
5253
5872
|
function boundaryEdgesOf(r) {
|
|
5254
5873
|
const b = r.boundary;
|
|
5255
5874
|
if (!b) return [];
|
|
5256
|
-
const list3 = Array.isArray(b) ? b : [...b.inbound ?? [], ...b.outbound ?? []];
|
|
5257
|
-
return list3.filter((e) =>
|
|
5258
|
-
}
|
|
5259
|
-
function scoreArithmetic(r) {
|
|
5260
|
-
const b = r.breakdown;
|
|
5261
|
-
if (!b || b.tier === null) return `${r.score} (no known native mapping \u2014 nothing to carve into)`;
|
|
5262
|
-
const p = b.penalties ?? {};
|
|
5263
|
-
const terms = [];
|
|
5264
|
-
if (p.inbound) terms.push(`- 12x${b.inbound ?? 0} inbound`);
|
|
5265
|
-
if (p.outbound) terms.push(`- 4x${b.outbound ?? 0} outbound`);
|
|
5266
|
-
if (p.outputs) terms.push(`- 4x${b.outputs ?? 0} output`);
|
|
5267
|
-
if (p.tier) terms.push(`- 15x${(b.tier ?? 1) - 1} tier${b.tier}`);
|
|
5268
|
-
if (p.dynamic) terms.push(`- 10 dynamic`);
|
|
5269
|
-
if (p.instances) terms.push(`- 3x${(b.instances ?? 1) - 1} instances`);
|
|
5270
|
-
if (!terms.length) return `100 (no penalties) = ${r.score}`;
|
|
5271
|
-
const raw = 100 + Object.values(p).reduce((s, n) => s + (n ?? 0), 0);
|
|
5272
|
-
const clamped = raw !== r.score ? ` (clamped from ${raw})` : "";
|
|
5273
|
-
return `100 ${terms.join(" ")} = ${r.score}${clamped}`;
|
|
5274
|
-
}
|
|
5275
|
-
function boundaryWorkOf(r) {
|
|
5276
|
-
const b = r.breakdown;
|
|
5277
|
-
if (!b || b.tier === null) return "no known native mapping (unsupported provider/type)";
|
|
5278
|
-
const parts = [];
|
|
5279
|
-
if (b.inbound) parts.push(`${b.inbound} inbound (a data-source patch each)`);
|
|
5280
|
-
if (b.outbound) parts.push(`${b.outbound} outbound (a deferred input each)`);
|
|
5281
|
-
if (b.outputs) parts.push(`${b.outputs} output block(s) reading it (a one-line rewrite each)`);
|
|
5282
|
-
if ((b.tier ?? 1) > 1) parts.push(`tier ${b.tier} map`);
|
|
5283
|
-
if (b.hasDynamic) parts.push("count/for_each/data present");
|
|
5284
|
-
if ((b.instances ?? 1) > 1) parts.push(`${b.instances} instances`);
|
|
5285
|
-
return parts.length ? parts.join(", ") : "clean 1:1 native map, no boundary edges";
|
|
5286
|
-
}
|
|
5287
|
-
function carveCardFields(node) {
|
|
5288
|
-
const score = node.attrs.score;
|
|
5289
|
-
const carve = node.attrs.carve;
|
|
5290
|
-
if (typeof score !== "number") return void 0;
|
|
5291
|
-
return [
|
|
5292
|
-
{ label: "score", value: String(score) },
|
|
5293
|
-
...typeof carve === "string" ? [{ label: "carve", value: carve }] : []
|
|
5294
|
-
];
|
|
5295
|
-
}
|
|
5296
|
-
function carveReportToIr(report) {
|
|
5297
|
-
const resources = report.resources ?? [];
|
|
5298
|
-
const known = new Set(resources.map((r) => r.address));
|
|
5299
|
-
const nodes = resources.map((r) => {
|
|
5300
|
-
const status = statusForBand(r.band, r.score);
|
|
5301
|
-
const b = r.breakdown ?? {};
|
|
5302
|
-
const edges2 = boundaryEdgesOf(r);
|
|
5303
|
-
const patches = edges2.filter((e) => e.carved === r.address && directionOf(e) === "inbound").map((e) => e.survivor);
|
|
5304
|
-
const inputs = edges2.filter((e) => e.carved === r.address && directionOf(e) === "outbound").map((e) => e.survivor);
|
|
5305
|
-
return {
|
|
5306
|
-
id: r.address,
|
|
5307
|
-
kind: tfTypeOf(r.address, r.kind),
|
|
5308
|
-
lexicon: "terraform",
|
|
5309
|
-
attrs: {
|
|
5310
|
-
// The drift palette's channel — `_`-prefixed, so it paints the card and
|
|
5311
|
-
// stays out of both the card's fields and the inspect pane's list.
|
|
5312
|
-
_status: status,
|
|
5313
|
-
score: r.score,
|
|
5314
|
-
carve: verdictForStatus(status),
|
|
5315
|
-
band: r.band,
|
|
5316
|
-
arithmetic: scoreArithmetic(r),
|
|
5317
|
-
boundaryWork: boundaryWorkOf(r),
|
|
5318
|
-
...r.mapsTo ? { mapsTo: r.mapsTo } : {},
|
|
5319
|
-
tier: b.tier === null || b.tier === void 0 ? "none" : b.tier,
|
|
5320
|
-
inbound: b.inbound ?? 0,
|
|
5321
|
-
outbound: b.outbound ?? 0,
|
|
5322
|
-
outputs: b.outputs ?? 0,
|
|
5323
|
-
instances: b.instances ?? 1,
|
|
5324
|
-
dynamic: b.hasDynamic ?? false,
|
|
5325
|
-
// The predicted diff, when the report carries the edge lists: who needs
|
|
5326
|
-
// a `data` source the moment this is carved, and what becomes a
|
|
5327
|
-
// deploy-time input. Absent (not empty) when the report has no lists —
|
|
5328
|
-
// "none" and "not reported" are different claims.
|
|
5329
|
-
...patches.length ? { patchOnCarve: patches.join(", ") } : {},
|
|
5330
|
-
...inputs.length ? { deferredInputs: inputs.join(", ") } : {}
|
|
5331
|
-
}
|
|
5332
|
-
};
|
|
5333
|
-
});
|
|
5334
|
-
const seen = /* @__PURE__ */ new Set();
|
|
5335
|
-
const edges = [];
|
|
5336
|
-
for (const r of resources) {
|
|
5337
|
-
for (const e of boundaryEdgesOf(r)) {
|
|
5338
|
-
const direction = directionOf(e);
|
|
5339
|
-
const from = direction === "inbound" ? e.survivor : e.carved;
|
|
5340
|
-
const to = direction === "inbound" ? e.carved : e.survivor;
|
|
5341
|
-
if (!known.has(from) || !known.has(to)) continue;
|
|
5342
|
-
const key = `${from}|${to}`;
|
|
5343
|
-
if (seen.has(key)) continue;
|
|
5344
|
-
seen.add(key);
|
|
5345
|
-
edges.push({
|
|
5346
|
-
from,
|
|
5347
|
-
to,
|
|
5348
|
-
kind: "ref",
|
|
5349
|
-
viaAttr: direction,
|
|
5350
|
-
...e.attrs?.length ? { toAttr: e.attrs.join(", ") } : {}
|
|
5351
|
-
});
|
|
5352
|
-
}
|
|
5353
|
-
}
|
|
5354
|
-
const byStack = {};
|
|
5355
|
-
for (const order of ["good", "warn", "neutral"]) {
|
|
5356
|
-
const members = resources.filter((r) => statusForBand(r.band, r.score) === order).map((r) => r.address);
|
|
5357
|
-
if (members.length) byStack[verdictForStatus(order)] = members;
|
|
5358
|
-
}
|
|
5359
|
-
return { nodes, edges, groups: { byStack } };
|
|
5360
|
-
}
|
|
5361
|
-
function carveNote(report, ir) {
|
|
5362
|
-
const counts = Object.entries(report.bands ?? {}).filter(([, n]) => n > 0).map(([band, n]) => `${n} ${band}`).join(" \xB7 ");
|
|
5363
|
-
const head = `chant carve advisory${report.from ? ` for ${report.from}` : ""}${counts ? ` \u2014 ${counts}` : ""}`;
|
|
5364
|
-
if (ir.edges.length) return `${head}. Read-only: nothing is emitted, patched, or applied.`;
|
|
5365
|
-
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.`;
|
|
5366
|
-
}
|
|
5367
|
-
|
|
5368
|
-
// src/carve-manifest.ts
|
|
5369
|
-
import { readdirSync as readdirSync4, readFileSync as readFileSync7, statSync as statSync4 } from "node:fs";
|
|
5370
|
-
import { dirname as dirname4, join as join10 } from "node:path";
|
|
5371
|
-
var CARVE_MANIFEST_SUFFIX = ".carve.json";
|
|
5372
|
-
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.";
|
|
5373
|
-
var isRecord3 = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
5374
|
-
var strings = (v) => Array.isArray(v) ? v.filter((x) => typeof x === "string") : [];
|
|
5375
|
-
function parseCarveManifest(value) {
|
|
5376
|
-
if (!isRecord3(value)) return null;
|
|
5377
|
-
if (value.version !== 1) return null;
|
|
5378
|
-
if (typeof value.target !== "string" || !value.target) return null;
|
|
5379
|
-
return value;
|
|
5380
|
-
}
|
|
5381
|
-
function readCarveManifest(path, readFile = (p) => readFileSync7(p, "utf8")) {
|
|
5382
|
-
let text;
|
|
5383
|
-
try {
|
|
5384
|
-
text = readFile(path);
|
|
5385
|
-
} catch {
|
|
5386
|
-
return null;
|
|
5387
|
-
}
|
|
5388
|
-
try {
|
|
5389
|
-
return parseCarveManifest(JSON.parse(text));
|
|
5390
|
-
} catch {
|
|
5391
|
-
return null;
|
|
5392
|
-
}
|
|
5393
|
-
}
|
|
5394
|
-
function carveStageOf(m) {
|
|
5395
|
-
if (m.apply) return "applied";
|
|
5396
|
-
if (m.bridge) return "bridged";
|
|
5397
|
-
return "emitted";
|
|
5398
|
-
}
|
|
5399
|
-
function carveWordFor(stage) {
|
|
5400
|
-
switch (stage) {
|
|
5401
|
-
case "applied":
|
|
5402
|
-
return "carved \u2192 chant";
|
|
5403
|
-
case "bridged":
|
|
5404
|
-
return "bridged \u2014 apply is yours";
|
|
5405
|
-
default:
|
|
5406
|
-
return "emitted \u2014 not bridged";
|
|
5407
|
-
}
|
|
5408
|
-
}
|
|
5409
|
-
function carveToneFor(stage) {
|
|
5410
|
-
return stage === "applied" ? "good" : "accent";
|
|
5411
|
-
}
|
|
5412
|
-
function carveStageNote(state) {
|
|
5413
|
-
switch (state.stage) {
|
|
5414
|
-
case "applied":
|
|
5415
|
-
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.";
|
|
5416
|
-
case "bridged":
|
|
5417
|
-
return "emitted and bridged \u2014 the survivor patch is proposed. Terraform still owns the resource until `carve apply`.";
|
|
5418
|
-
default:
|
|
5419
|
-
return "emitted \u2014 typed chant source exists, and the surviving Terraform still reads the resource directly. `carve bridge` is next.";
|
|
5420
|
-
}
|
|
5421
|
-
}
|
|
5422
|
-
function carveStateOf(m, path) {
|
|
5423
|
-
const stage = carveStageOf(m);
|
|
5424
|
-
const deferred = Object.entries(m.emit?.params ?? {}).map(([name, p]) => `${name} \u2190 ${p?.survivor ?? "a survivor"}`);
|
|
5425
|
-
return {
|
|
5426
|
-
target: m.target,
|
|
5427
|
-
...m.tfType ? { tfType: m.tfType } : {},
|
|
5428
|
-
stage,
|
|
5429
|
-
graduated: stage === "applied",
|
|
5430
|
-
// The last stamped step wins — that is the age of the state, not the age
|
|
5431
|
-
// of the carve.
|
|
5432
|
-
...m.apply?.at || m.bridge?.at || m.emit?.at ? { at: m.apply?.at ?? m.bridge?.at ?? m.emit?.at } : {},
|
|
5433
|
-
files: strings(m.emit?.files),
|
|
5434
|
-
excised: strings(m.bridge?.excised),
|
|
5435
|
-
deferredInputs: deferred,
|
|
5436
|
-
...m.apply ? {
|
|
5437
|
-
ownership: {
|
|
5438
|
-
...m.apply.marker?.stack ? { stack: m.apply.marker.stack } : {},
|
|
5439
|
-
...m.apply.marker?.env ? { env: m.apply.marker.env } : {},
|
|
5440
|
-
...m.apply.ownershipTags ? { tags: m.apply.ownershipTags } : {}
|
|
5441
|
-
}
|
|
5442
|
-
} : {},
|
|
5443
|
-
...m.from ? { from: m.from } : {},
|
|
5444
|
-
outDir: dirname4(path),
|
|
5445
|
-
path
|
|
5446
|
-
};
|
|
5447
|
-
}
|
|
5448
|
-
var nodeCarveIo = {
|
|
5449
|
-
readdir: (dir) => readdirSync4(dir),
|
|
5450
|
-
isDirectory: (p) => {
|
|
5451
|
-
try {
|
|
5452
|
-
return statSync4(p).isDirectory();
|
|
5453
|
-
} catch {
|
|
5454
|
-
return false;
|
|
5455
|
-
}
|
|
5456
|
-
},
|
|
5457
|
-
readFile: (p) => readFileSync7(p, "utf8")
|
|
5458
|
-
};
|
|
5459
|
-
var MANIFEST_SCAN_DEPTH = 2;
|
|
5460
|
-
var SKIP_DIRS = /* @__PURE__ */ new Set(["node_modules", "dist", "cdk.out", ".terraform", ".git"]);
|
|
5461
|
-
function listCarveManifests(dir, io = nodeCarveIo, depth = MANIFEST_SCAN_DEPTH) {
|
|
5462
|
-
const out = [];
|
|
5463
|
-
const walk = (d, level) => {
|
|
5464
|
-
let entries;
|
|
5465
|
-
try {
|
|
5466
|
-
entries = io.readdir(d).sort();
|
|
5467
|
-
} catch {
|
|
5468
|
-
return;
|
|
5469
|
-
}
|
|
5470
|
-
for (const name of entries) {
|
|
5471
|
-
const full = join10(d, name);
|
|
5472
|
-
if (name.endsWith(CARVE_MANIFEST_SUFFIX)) {
|
|
5473
|
-
out.push(full);
|
|
5474
|
-
continue;
|
|
5475
|
-
}
|
|
5476
|
-
if (level >= depth) continue;
|
|
5477
|
-
if (name.startsWith(".") || SKIP_DIRS.has(name)) continue;
|
|
5478
|
-
if (io.isDirectory(full)) walk(full, level + 1);
|
|
5479
|
-
}
|
|
5480
|
-
};
|
|
5481
|
-
walk(dir, 0);
|
|
5482
|
-
return out;
|
|
5875
|
+
const list3 = Array.isArray(b) ? b : [...b.inbound ?? [], ...b.outbound ?? []];
|
|
5876
|
+
return list3.filter((e) => isRecord3(e) && typeof e.survivor === "string" && typeof e.carved === "string");
|
|
5483
5877
|
}
|
|
5484
|
-
function
|
|
5485
|
-
const
|
|
5486
|
-
|
|
5487
|
-
|
|
5488
|
-
|
|
5489
|
-
|
|
5490
|
-
|
|
5491
|
-
|
|
5492
|
-
|
|
5493
|
-
|
|
5494
|
-
|
|
5495
|
-
}
|
|
5496
|
-
|
|
5878
|
+
function scoreArithmetic(r) {
|
|
5879
|
+
const b = r.breakdown;
|
|
5880
|
+
if (!b || b.tier === null) return `${r.score} (no known native mapping \u2014 nothing to carve into)`;
|
|
5881
|
+
const p = b.penalties ?? {};
|
|
5882
|
+
const terms = [];
|
|
5883
|
+
if (p.inbound) terms.push(`- 12x${b.inbound ?? 0} inbound`);
|
|
5884
|
+
if (p.outbound) terms.push(`- 4x${b.outbound ?? 0} outbound`);
|
|
5885
|
+
if (p.outputs) terms.push(`- 4x${b.outputs ?? 0} output`);
|
|
5886
|
+
if (p.tier) terms.push(`- 15x${(b.tier ?? 1) - 1} tier${b.tier}`);
|
|
5887
|
+
if (p.dynamic) terms.push(`- 10 dynamic`);
|
|
5888
|
+
if (p.instances) terms.push(`- 3x${(b.instances ?? 1) - 1} instances`);
|
|
5889
|
+
if (!terms.length) return `100 (no penalties) = ${r.score}`;
|
|
5890
|
+
const raw = 100 + Object.values(p).reduce((s, n) => s + (n ?? 0), 0);
|
|
5891
|
+
const clamped = raw !== r.score ? ` (clamped from ${raw})` : "";
|
|
5892
|
+
return `100 ${terms.join(" ")} = ${r.score}${clamped}`;
|
|
5497
5893
|
}
|
|
5498
|
-
function
|
|
5499
|
-
|
|
5500
|
-
|
|
5501
|
-
let emitted = 0;
|
|
5502
|
-
for (const s of states) {
|
|
5503
|
-
if (s.stage === "applied") applied++;
|
|
5504
|
-
else if (s.stage === "bridged") bridged++;
|
|
5505
|
-
else emitted++;
|
|
5506
|
-
}
|
|
5507
|
-
const inFlight = bridged + emitted;
|
|
5894
|
+
function boundaryWorkOf(r) {
|
|
5895
|
+
const b = r.breakdown;
|
|
5896
|
+
if (!b || b.tier === null) return "no known native mapping (unsupported provider/type)";
|
|
5508
5897
|
const parts = [];
|
|
5509
|
-
if (
|
|
5510
|
-
if (
|
|
5511
|
-
|
|
5512
|
-
|
|
5513
|
-
|
|
5514
|
-
|
|
5515
|
-
|
|
5516
|
-
total: typeof total === "number" ? total : null,
|
|
5517
|
-
label: typeof total === "number" ? `${applied} of ${total} carved` : `${applied} carved`,
|
|
5518
|
-
detail: parts.length ? `${parts.join(", ")} \u2014 apply is yours` : ""
|
|
5519
|
-
};
|
|
5520
|
-
}
|
|
5521
|
-
function applyCommandFor(state, labels = {}) {
|
|
5522
|
-
const env = state.ownership?.env ?? "<env>";
|
|
5523
|
-
const stack = state.ownership?.stack ?? "<stack>";
|
|
5524
|
-
const from = labels.from ?? state.from ?? "<terraform-dir>";
|
|
5525
|
-
const out = labels.out ?? state.outDir;
|
|
5526
|
-
return `chant carve apply --from ${from} --output ${out} --env ${env} --stack ${stack} --write-source`;
|
|
5898
|
+
if (b.inbound) parts.push(`${b.inbound} inbound (a data-source patch each)`);
|
|
5899
|
+
if (b.outbound) parts.push(`${b.outbound} outbound (a deferred input each)`);
|
|
5900
|
+
if (b.outputs) parts.push(`${b.outputs} output block(s) reading it (a one-line rewrite each)`);
|
|
5901
|
+
if ((b.tier ?? 1) > 1) parts.push(`tier ${b.tier} map`);
|
|
5902
|
+
if (b.hasDynamic) parts.push("count/for_each/data present");
|
|
5903
|
+
if ((b.instances ?? 1) > 1) parts.push(`${b.instances} instances`);
|
|
5904
|
+
return parts.length ? parts.join(", ") : "clean 1:1 native map, no boundary edges";
|
|
5527
5905
|
}
|
|
5528
|
-
function
|
|
5529
|
-
const
|
|
5530
|
-
|
|
5531
|
-
|
|
5532
|
-
|
|
5533
|
-
|
|
5534
|
-
|
|
5535
|
-
|
|
5906
|
+
function carveCardFields(node) {
|
|
5907
|
+
const score = node.attrs.score;
|
|
5908
|
+
const carve = node.attrs.carve;
|
|
5909
|
+
if (typeof score !== "number") return void 0;
|
|
5910
|
+
return [
|
|
5911
|
+
{ label: "score", value: String(score) },
|
|
5912
|
+
...typeof carve === "string" ? [{ label: "carve", value: carve }] : []
|
|
5913
|
+
];
|
|
5536
5914
|
}
|
|
5537
|
-
function
|
|
5538
|
-
|
|
5539
|
-
const
|
|
5540
|
-
const
|
|
5541
|
-
|
|
5542
|
-
|
|
5543
|
-
|
|
5544
|
-
|
|
5545
|
-
const
|
|
5546
|
-
|
|
5547
|
-
|
|
5548
|
-
|
|
5549
|
-
|
|
5550
|
-
if (state.graduated) {
|
|
5551
|
-
gone.add(node.id);
|
|
5552
|
-
graduated.push({
|
|
5553
|
-
...node,
|
|
5554
|
-
attrs: {
|
|
5555
|
-
...node.attrs,
|
|
5556
|
-
_status: "good",
|
|
5557
|
-
carve: carveWordFor(state.stage),
|
|
5558
|
-
carveState: state.stage,
|
|
5559
|
-
...state.ownership?.stack ? { ownedStack: state.ownership.stack } : {},
|
|
5560
|
-
...state.ownership?.env ? { ownedEnv: state.ownership.env } : {},
|
|
5561
|
-
...state.at ? { carvedAt: state.at } : {},
|
|
5562
|
-
...state.files.length ? { chantSource: source(state.files) } : {}
|
|
5563
|
-
}
|
|
5564
|
-
});
|
|
5565
|
-
continue;
|
|
5566
|
-
}
|
|
5567
|
-
nodes.push({
|
|
5568
|
-
...node,
|
|
5915
|
+
function carveReportToIr(report) {
|
|
5916
|
+
const resources = report.resources ?? [];
|
|
5917
|
+
const known = new Set(resources.map((r) => r.address));
|
|
5918
|
+
const nodes = resources.map((r) => {
|
|
5919
|
+
const status = statusForBand(r.band, r.score);
|
|
5920
|
+
const b = r.breakdown ?? {};
|
|
5921
|
+
const edges2 = boundaryEdgesOf(r);
|
|
5922
|
+
const patches = edges2.filter((e) => e.carved === r.address && directionOf(e) === "inbound").map((e) => e.survivor);
|
|
5923
|
+
const inputs = edges2.filter((e) => e.carved === r.address && directionOf(e) === "outbound").map((e) => e.survivor);
|
|
5924
|
+
return {
|
|
5925
|
+
id: r.address,
|
|
5926
|
+
kind: tfTypeOf(r.address, r.kind),
|
|
5927
|
+
lexicon: "terraform",
|
|
5569
5928
|
attrs: {
|
|
5570
|
-
|
|
5571
|
-
|
|
5572
|
-
|
|
5573
|
-
|
|
5574
|
-
|
|
5575
|
-
|
|
5576
|
-
|
|
5577
|
-
|
|
5929
|
+
// The drift palette's channel — `_`-prefixed, so it paints the card and
|
|
5930
|
+
// stays out of both the card's fields and the inspect pane's list.
|
|
5931
|
+
_status: status,
|
|
5932
|
+
score: r.score,
|
|
5933
|
+
carve: verdictForStatus(status),
|
|
5934
|
+
band: r.band,
|
|
5935
|
+
arithmetic: scoreArithmetic(r),
|
|
5936
|
+
boundaryWork: boundaryWorkOf(r),
|
|
5937
|
+
...r.mapsTo ? { mapsTo: r.mapsTo } : {},
|
|
5938
|
+
tier: b.tier === null || b.tier === void 0 ? "none" : b.tier,
|
|
5939
|
+
inbound: b.inbound ?? 0,
|
|
5940
|
+
outbound: b.outbound ?? 0,
|
|
5941
|
+
outputs: b.outputs ?? 0,
|
|
5942
|
+
instances: b.instances ?? 1,
|
|
5943
|
+
dynamic: b.hasDynamic ?? false,
|
|
5944
|
+
// The predicted diff, when the report carries the edge lists: who needs
|
|
5945
|
+
// a `data` source the moment this is carved, and what becomes a
|
|
5946
|
+
// deploy-time input. Absent (not empty) when the report has no lists —
|
|
5947
|
+
// "none" and "not reported" are different claims.
|
|
5948
|
+
...patches.length ? { patchOnCarve: patches.join(", ") } : {},
|
|
5949
|
+
...inputs.length ? { deferredInputs: inputs.join(", ") } : {}
|
|
5578
5950
|
}
|
|
5579
|
-
}
|
|
5951
|
+
};
|
|
5952
|
+
});
|
|
5953
|
+
const seen = /* @__PURE__ */ new Set();
|
|
5954
|
+
const edges = [];
|
|
5955
|
+
for (const r of resources) {
|
|
5956
|
+
for (const e of boundaryEdgesOf(r)) {
|
|
5957
|
+
const direction = directionOf(e);
|
|
5958
|
+
const from = direction === "inbound" ? e.survivor : e.carved;
|
|
5959
|
+
const to = direction === "inbound" ? e.carved : e.survivor;
|
|
5960
|
+
if (!known.has(from) || !known.has(to)) continue;
|
|
5961
|
+
const key = `${from}|${to}`;
|
|
5962
|
+
if (seen.has(key)) continue;
|
|
5963
|
+
seen.add(key);
|
|
5964
|
+
edges.push({
|
|
5965
|
+
from,
|
|
5966
|
+
to,
|
|
5967
|
+
kind: "ref",
|
|
5968
|
+
viaAttr: direction,
|
|
5969
|
+
...e.attrs?.length ? { toAttr: e.attrs.join(", ") } : {}
|
|
5970
|
+
});
|
|
5971
|
+
}
|
|
5580
5972
|
}
|
|
5581
5973
|
const byStack = {};
|
|
5582
|
-
for (const
|
|
5583
|
-
const
|
|
5584
|
-
if (
|
|
5974
|
+
for (const order of ["good", "warn", "neutral"]) {
|
|
5975
|
+
const members = resources.filter((r) => statusForBand(r.band, r.score) === order).map((r) => r.address);
|
|
5976
|
+
if (members.length) byStack[verdictForStatus(order)] = members;
|
|
5585
5977
|
}
|
|
5586
|
-
return {
|
|
5587
|
-
tf: {
|
|
5588
|
-
nodes,
|
|
5589
|
-
edges: tfIr.edges.filter((e) => !gone.has(e.from) && !gone.has(e.to)),
|
|
5590
|
-
groups: { ...tfIr.groups, byStack }
|
|
5591
|
-
},
|
|
5592
|
-
graduated
|
|
5593
|
-
};
|
|
5594
|
-
}
|
|
5595
|
-
var GRADUATED_BAND = "carved \u2192 chant";
|
|
5596
|
-
function bandGraduated(tf, graduated) {
|
|
5597
|
-
if (graduated.length === 0) return tf;
|
|
5598
|
-
return {
|
|
5599
|
-
nodes: [...graduated, ...tf.nodes],
|
|
5600
|
-
edges: tf.edges,
|
|
5601
|
-
groups: {
|
|
5602
|
-
...tf.groups,
|
|
5603
|
-
byStack: {
|
|
5604
|
-
[GRADUATED_BAND]: graduated.map((n) => n.id),
|
|
5605
|
-
...tf.groups?.byStack ?? {}
|
|
5606
|
-
}
|
|
5607
|
-
}
|
|
5608
|
-
};
|
|
5978
|
+
return { nodes, edges, groups: { byStack } };
|
|
5609
5979
|
}
|
|
5610
|
-
function
|
|
5611
|
-
|
|
5612
|
-
const head = `
|
|
5613
|
-
|
|
5980
|
+
function carveNote(report, ir) {
|
|
5981
|
+
const counts = Object.entries(report.bands ?? {}).filter(([, n]) => n > 0).map(([band, n]) => `${n} ${band}`).join(" \xB7 ");
|
|
5982
|
+
const head = `chant carve advisory${report.from ? ` for ${report.from}` : ""}${counts ? ` \u2014 ${counts}` : ""}`;
|
|
5983
|
+
if (ir.edges.length) return `${head}. Read-only: nothing is emitted, patched, or applied.`;
|
|
5984
|
+
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.`;
|
|
5614
5985
|
}
|
|
5615
5986
|
|
|
5616
5987
|
// src/ops-lens.ts
|
|
@@ -5884,9 +6255,12 @@ function nodeFor(op, placed, guards) {
|
|
|
5884
6255
|
...profile ? { profile } : {},
|
|
5885
6256
|
...policy ? { policy } : {},
|
|
5886
6257
|
// The resolved args, verbatim from the IR. They carry the Op's SCOPE
|
|
5887
|
-
// (env, stack, template path)
|
|
5888
|
-
//
|
|
6258
|
+
// (env, stack, template path); which of them identify an ENTITY is the
|
|
6259
|
+
// contract's to say, and 0.54's op.json says it in `entities` below.
|
|
5889
6260
|
...args ? { args } : {},
|
|
6261
|
+
// chant#2022: what the step touches, verbatim. `entities` prints on the
|
|
6262
|
+
// card; `_entities` is what `joinStepEntities` resolves.
|
|
6263
|
+
...step.entities?.length ? { entities: step.entities.join(", "), _entities: step.entities } : {},
|
|
5890
6264
|
...step.outcomeAttribute ? { outcome: step.outcomeAttribute.from ? `${step.outcomeAttribute.name} \u2190 ${step.outcomeAttribute.from}` : step.outcomeAttribute.name } : {},
|
|
5891
6265
|
...op.activityContracts?.[step.fn] ? { contract: "declared (args/returns JSON Schema in op.json)" } : {}
|
|
5892
6266
|
}
|
|
@@ -5988,17 +6362,83 @@ function opCardFields(node) {
|
|
|
5988
6362
|
function opsNote(ops, ir) {
|
|
5989
6363
|
const gates = ir.nodes.filter((n) => n.attrs._step === "gate").length;
|
|
5990
6364
|
const rules = ir.nodes.filter((n) => n.attrs._step === "rule");
|
|
5991
|
-
const
|
|
6365
|
+
const estate = ir.nodes.filter((n) => n.lexicon !== "op").length;
|
|
6366
|
+
const steps = ir.nodes.length - rules.length - estate;
|
|
5992
6367
|
const dangling = rules.filter((n) => n.attrs.dangling !== void 0).length;
|
|
5993
6368
|
const counts = [
|
|
5994
6369
|
`${ops.length} declared Op${ops.length === 1 ? "" : "s"}`,
|
|
5995
6370
|
`${steps} step${steps === 1 ? "" : "s"}`,
|
|
5996
6371
|
...gates ? [`${gates} gate${gates === 1 ? "" : "s"}`] : [],
|
|
5997
|
-
...rules.length ? [`${rules.length} converge rule${rules.length === 1 ? "" : "s"}`] : []
|
|
6372
|
+
...rules.length ? [`${rules.length} converge rule${rules.length === 1 ? "" : "s"}`] : [],
|
|
6373
|
+
...entityCountClause(ir)
|
|
5998
6374
|
].join(" \xB7 ");
|
|
5999
6375
|
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.` : "") : "";
|
|
6000
6376
|
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.`;
|
|
6001
6377
|
}
|
|
6378
|
+
var ESTATE_BOX = "estate";
|
|
6379
|
+
function joinStepEntities(ir, estate) {
|
|
6380
|
+
const steps = ir.nodes.filter((n) => Array.isArray(n.attrs._entities));
|
|
6381
|
+
if (steps.length === 0) return { refs: 0, resolved: 0 };
|
|
6382
|
+
const byId = /* @__PURE__ */ new Map();
|
|
6383
|
+
const byLeaf = /* @__PURE__ */ new Map();
|
|
6384
|
+
for (const n of estate?.nodes ?? []) {
|
|
6385
|
+
byId.set(n.id, n);
|
|
6386
|
+
for (const leaf of stringLeaves(n.attrs)) {
|
|
6387
|
+
const list3 = byLeaf.get(leaf.value);
|
|
6388
|
+
if (list3) {
|
|
6389
|
+
if (!list3.includes(n)) list3.push(n);
|
|
6390
|
+
} else byLeaf.set(leaf.value, [n]);
|
|
6391
|
+
}
|
|
6392
|
+
}
|
|
6393
|
+
const present = new Set(ir.nodes.map((n) => n.id));
|
|
6394
|
+
let refs = 0;
|
|
6395
|
+
let resolvedCount = 0;
|
|
6396
|
+
for (const step of steps) {
|
|
6397
|
+
const links = { resolved: [], unresolved: [] };
|
|
6398
|
+
for (const value of step.attrs._entities) {
|
|
6399
|
+
refs++;
|
|
6400
|
+
const targets = [];
|
|
6401
|
+
const idHit = byId.get(value);
|
|
6402
|
+
if (idHit) targets.push({ node: idHit, via: "id" });
|
|
6403
|
+
for (const n of byLeaf.get(value) ?? []) if (n !== idHit) targets.push({ node: n, via: "attr" });
|
|
6404
|
+
if (targets.length === 0) {
|
|
6405
|
+
links.unresolved.push(value);
|
|
6406
|
+
continue;
|
|
6407
|
+
}
|
|
6408
|
+
resolvedCount++;
|
|
6409
|
+
for (const { node, via } of targets) {
|
|
6410
|
+
links.resolved.push({ value, node: node.id, via });
|
|
6411
|
+
if (!present.has(node.id)) {
|
|
6412
|
+
present.add(node.id);
|
|
6413
|
+
ir.nodes.push({ ...node, attrs: { ...node.attrs, _touchedBy: [step.id] } });
|
|
6414
|
+
(ir.groups.byStack ??= {})[ESTATE_BOX] = [...ir.groups.byStack[ESTATE_BOX] ?? [], node.id];
|
|
6415
|
+
} else {
|
|
6416
|
+
const held = ir.nodes.find((n) => n.id === node.id);
|
|
6417
|
+
const touched = held.attrs._touchedBy ?? [];
|
|
6418
|
+
if (!touched.includes(step.id)) held.attrs._touchedBy = [...touched, step.id];
|
|
6419
|
+
}
|
|
6420
|
+
ir.edges.push({ from: step.id, to: node.id, kind: "ref", viaAttr: "entities" });
|
|
6421
|
+
}
|
|
6422
|
+
}
|
|
6423
|
+
step.attrs._entityLinks = links;
|
|
6424
|
+
}
|
|
6425
|
+
return { refs, resolved: resolvedCount };
|
|
6426
|
+
}
|
|
6427
|
+
function entityCountClause(ir) {
|
|
6428
|
+
let refs = 0;
|
|
6429
|
+
let resolved = 0;
|
|
6430
|
+
for (const n of ir.nodes) {
|
|
6431
|
+
const links = n.attrs._entityLinks;
|
|
6432
|
+
if (links) {
|
|
6433
|
+
refs += links.resolved.length + links.unresolved.length;
|
|
6434
|
+
resolved += links.resolved.length;
|
|
6435
|
+
} else if (Array.isArray(n.attrs._entities)) {
|
|
6436
|
+
refs += n.attrs._entities.length;
|
|
6437
|
+
}
|
|
6438
|
+
}
|
|
6439
|
+
if (refs === 0) return [];
|
|
6440
|
+
return [`${refs} entity ref${refs === 1 ? "" : "s"}, ${resolved} linked`];
|
|
6441
|
+
}
|
|
6002
6442
|
|
|
6003
6443
|
// src/render.ts
|
|
6004
6444
|
registerPack({ lexicon: "k8s", iconFor: k8sIconFor });
|
|
@@ -6112,7 +6552,10 @@ function renderCarveEstate(tfIr, appIr, opts) {
|
|
|
6112
6552
|
}
|
|
6113
6553
|
function withCarveState(tfIr, appIr, opts) {
|
|
6114
6554
|
const states = opts.carved ?? /* @__PURE__ */ new Map();
|
|
6115
|
-
const { tf, graduated } = splitCarveState(tfIr, states, {
|
|
6555
|
+
const { tf, graduated } = splitCarveState(tfIr, states, {
|
|
6556
|
+
...opts.shorten ? { shorten: opts.shorten } : {},
|
|
6557
|
+
...opts.emitted ? { emitted: opts.emitted } : {}
|
|
6558
|
+
});
|
|
6116
6559
|
const ns = namespaceAppIr(appIr, opts.appTitle);
|
|
6117
6560
|
const progress = carveProgress(states.values(), tfIr.nodes.length);
|
|
6118
6561
|
return {
|
|
@@ -6452,12 +6895,12 @@ function radializeLayout(layout, groupOf, size = /* @__PURE__ */ new Map()) {
|
|
|
6452
6895
|
}
|
|
6453
6896
|
|
|
6454
6897
|
// src/carve-actions.ts
|
|
6455
|
-
import { existsSync as
|
|
6456
|
-
import { isAbsolute, join as
|
|
6898
|
+
import { existsSync as existsSync9, mkdirSync as mkdirSync4, readFileSync as readFileSync11, readdirSync as readdirSync6, statSync as statSync6 } from "node:fs";
|
|
6899
|
+
import { isAbsolute as isAbsolute3, join as join15, relative as relative3, resolve as resolve7, sep as sep4 } from "node:path";
|
|
6457
6900
|
|
|
6458
6901
|
// src/layout.ts
|
|
6459
|
-
import { existsSync as
|
|
6460
|
-
import { join as
|
|
6902
|
+
import { existsSync as existsSync7, mkdirSync as mkdirSync3, readFileSync as readFileSync9, renameSync, rmSync as rmSync2, writeFileSync as writeFileSync3, accessSync, constants } from "node:fs";
|
|
6903
|
+
import { join as join13 } from "node:path";
|
|
6461
6904
|
var LAYOUT_DIR = ".behold";
|
|
6462
6905
|
var LAYOUT_FILE = "layout.json";
|
|
6463
6906
|
var MAX_BODY_BYTES = 64 * 1024;
|
|
@@ -6467,12 +6910,12 @@ var MAX_LENSES = 64;
|
|
|
6467
6910
|
var MAX_ID_LENGTH = 512;
|
|
6468
6911
|
var MAX_LENS_LENGTH = 128;
|
|
6469
6912
|
var NUM = ["dx", "dy", "dw", "dh"];
|
|
6470
|
-
function
|
|
6913
|
+
function slug2(s) {
|
|
6471
6914
|
return String(s ?? "").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "unknown";
|
|
6472
6915
|
}
|
|
6473
6916
|
function normalizeLens(raw) {
|
|
6474
6917
|
if (typeof raw !== "string" || !raw.trim() || raw.length > MAX_LENS_LENGTH) return null;
|
|
6475
|
-
const parts = raw.split("+").map((p) =>
|
|
6918
|
+
const parts = raw.split("+").map((p) => slug2(p)).filter((p) => p && p !== "unknown");
|
|
6476
6919
|
if (!parts.length) return null;
|
|
6477
6920
|
const key = parts.join("+");
|
|
6478
6921
|
return key === "__proto__" || key === "constructor" || key === "prototype" ? null : key;
|
|
@@ -6495,13 +6938,13 @@ function normalizeDeltas(raw) {
|
|
|
6495
6938
|
function lensFromQuery(params) {
|
|
6496
6939
|
const zoom = params.get("components") === "1" ? "components" : params.get("logical") === "1" ? "logical" : params.get("runtime") === "1" ? "runtime" : { 1: "composites", 2: "resources", 3: "attributes" }[Number(params.get("detail") ?? 2)] ?? "resources";
|
|
6497
6940
|
const stack = params.get("stack");
|
|
6498
|
-
return [
|
|
6941
|
+
return [slug2(zoom), params.get("radial") === "1" ? "radial" : null, stack ? slug2(`stack-${stack}`) : null].filter(Boolean).join("+");
|
|
6499
6942
|
}
|
|
6500
6943
|
function layoutPath(projectDir) {
|
|
6501
|
-
return
|
|
6944
|
+
return join13(projectDir, LAYOUT_DIR, LAYOUT_FILE);
|
|
6502
6945
|
}
|
|
6503
6946
|
function unwritableReason(projectDir) {
|
|
6504
|
-
if (!projectDir || !
|
|
6947
|
+
if (!projectDir || !existsSync7(projectDir)) return `no such project directory: ${projectDir || "(none)"}`;
|
|
6505
6948
|
try {
|
|
6506
6949
|
accessSync(projectDir, constants.W_OK);
|
|
6507
6950
|
} catch {
|
|
@@ -6513,8 +6956,8 @@ function readLayoutFile(projectDir) {
|
|
|
6513
6956
|
const empty = { version: 1, lenses: {} };
|
|
6514
6957
|
const file = layoutPath(projectDir);
|
|
6515
6958
|
try {
|
|
6516
|
-
if (!
|
|
6517
|
-
const raw =
|
|
6959
|
+
if (!existsSync7(file)) return empty;
|
|
6960
|
+
const raw = readFileSync9(file, "utf8");
|
|
6518
6961
|
if (raw.length > MAX_FILE_BYTES) return empty;
|
|
6519
6962
|
const parsed = JSON.parse(raw);
|
|
6520
6963
|
if (!parsed || typeof parsed !== "object" || !parsed.lenses || typeof parsed.lenses !== "object") return empty;
|
|
@@ -6549,14 +6992,14 @@ function writeLens(projectDir, lens, deltas) {
|
|
|
6549
6992
|
if (Object.keys(lenses).length > MAX_LENSES) throw new LayoutTooLarge(`a layout sidecar holds at most ${MAX_LENSES} lenses`);
|
|
6550
6993
|
const file = layoutPath(projectDir);
|
|
6551
6994
|
if (!Object.keys(lenses).length) {
|
|
6552
|
-
|
|
6995
|
+
rmSync2(file, { force: true });
|
|
6553
6996
|
return { lens: key, deltas: clean, bytes: 0 };
|
|
6554
6997
|
}
|
|
6555
6998
|
const body = JSON.stringify({ version: 1, lenses }, null, 2) + "\n";
|
|
6556
6999
|
if (body.length > MAX_FILE_BYTES) throw new LayoutTooLarge(`a layout sidecar is capped at ${MAX_FILE_BYTES} bytes`);
|
|
6557
|
-
|
|
7000
|
+
mkdirSync3(join13(projectDir, LAYOUT_DIR), { recursive: true });
|
|
6558
7001
|
const tmp = `${file}.tmp`;
|
|
6559
|
-
|
|
7002
|
+
writeFileSync3(tmp, body, "utf8");
|
|
6560
7003
|
renameSync(tmp, file);
|
|
6561
7004
|
return { lens: key, deltas: clean, bytes: body.length };
|
|
6562
7005
|
}
|
|
@@ -6639,8 +7082,8 @@ function reanchorEdges(svg, moved) {
|
|
|
6639
7082
|
|
|
6640
7083
|
// src/carve-live.ts
|
|
6641
7084
|
import { spawn as spawn3 } from "node:child_process";
|
|
6642
|
-
import { readFileSync as
|
|
6643
|
-
import { join as
|
|
7085
|
+
import { readFileSync as readFileSync10, writeFileSync as writeFileSync4, existsSync as existsSync8 } from "node:fs";
|
|
7086
|
+
import { join as join14 } from "node:path";
|
|
6644
7087
|
|
|
6645
7088
|
// src/scratch.ts
|
|
6646
7089
|
var PROTECTED_INFRA = [
|
|
@@ -6728,9 +7171,9 @@ async function teardownScratchFloci() {
|
|
|
6728
7171
|
await capture("docker", ["rm", "-f", LIVE_CONTAINER], process.cwd()).catch(() => void 0);
|
|
6729
7172
|
}
|
|
6730
7173
|
async function applyIntoFloci(fromDir, step) {
|
|
6731
|
-
const template =
|
|
6732
|
-
if (!
|
|
6733
|
-
|
|
7174
|
+
const template = join14(fromDir, "floci-override.tf.disabled");
|
|
7175
|
+
if (!existsSync8(template)) return `no floci-override.tf.disabled in ${fromDir} \u2014 this estate wasn't authored for the live tier`;
|
|
7176
|
+
writeFileSync4(join14(fromDir, "floci_override.tf"), armOverride(readFileSync10(template, "utf8"), LIVE_PORT));
|
|
6734
7177
|
const init = await step("terraform", ["init", "-input=false", "-no-color"], fromDir);
|
|
6735
7178
|
if (init !== 0) return `terraform init exited ${init} (provider downloads need network)`;
|
|
6736
7179
|
const apply = await step(
|
|
@@ -6780,7 +7223,7 @@ async function runCarveObserve(demo, select) {
|
|
|
6780
7223
|
"run `behold demo carve --live` (needs docker + terraform)"
|
|
6781
7224
|
);
|
|
6782
7225
|
}
|
|
6783
|
-
if (!
|
|
7226
|
+
if (!existsSync9(join15(demo.out, "src"))) {
|
|
6784
7227
|
return refuse3("carve-action", "nothing has been emitted yet", "run Emit first \u2014 observe reads the carveout emit writes");
|
|
6785
7228
|
}
|
|
6786
7229
|
const args = ["lifecycle", "diff", "prod", "--live", "--json"];
|
|
@@ -6850,8 +7293,8 @@ var refuse3 = (code, error, remedy) => ({
|
|
|
6850
7293
|
});
|
|
6851
7294
|
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.";
|
|
6852
7295
|
function insideDemo(root, p) {
|
|
6853
|
-
const rel =
|
|
6854
|
-
return rel !== "" && !rel.startsWith("..") && !
|
|
7296
|
+
const rel = relative3(resolve7(root), resolve7(p));
|
|
7297
|
+
return rel !== "" && !rel.startsWith("..") && !isAbsolute3(rel);
|
|
6855
7298
|
}
|
|
6856
7299
|
function selectFromReport(report, raw) {
|
|
6857
7300
|
if (typeof raw !== "string" || !raw || raw.length > 512) return null;
|
|
@@ -6861,7 +7304,7 @@ function carveWriteBlock(demo) {
|
|
|
6861
7304
|
if (!demo) {
|
|
6862
7305
|
return "this server isn't running a carve demo \u2014 the carve actions only exist inside a `behold demo carve` copy";
|
|
6863
7306
|
}
|
|
6864
|
-
if (!
|
|
7307
|
+
if (!existsSync9(demo.from)) return `the demo copy has no Terraform estate at ${demo.from}`;
|
|
6865
7308
|
if (!insideDemo(demo.root, demo.out)) return "the carve output directory is outside the demo copy";
|
|
6866
7309
|
return unwritableReason(demo.root);
|
|
6867
7310
|
}
|
|
@@ -6871,17 +7314,17 @@ function readArtifacts(demo, dir, filter) {
|
|
|
6871
7314
|
if (out.length >= MAX_ARTIFACTS || depth > 2) return;
|
|
6872
7315
|
let entries;
|
|
6873
7316
|
try {
|
|
6874
|
-
entries =
|
|
7317
|
+
entries = readdirSync6(d).sort();
|
|
6875
7318
|
} catch {
|
|
6876
7319
|
return;
|
|
6877
7320
|
}
|
|
6878
7321
|
for (const name of entries) {
|
|
6879
7322
|
if (out.length >= MAX_ARTIFACTS) return;
|
|
6880
7323
|
if (name === "node_modules" || name.startsWith(".")) continue;
|
|
6881
|
-
const full =
|
|
7324
|
+
const full = join15(d, name);
|
|
6882
7325
|
let st;
|
|
6883
7326
|
try {
|
|
6884
|
-
st =
|
|
7327
|
+
st = statSync6(full);
|
|
6885
7328
|
} catch {
|
|
6886
7329
|
continue;
|
|
6887
7330
|
}
|
|
@@ -6889,11 +7332,11 @@ function readArtifacts(demo, dir, filter) {
|
|
|
6889
7332
|
walk(full, depth + 1);
|
|
6890
7333
|
continue;
|
|
6891
7334
|
}
|
|
6892
|
-
const rel =
|
|
7335
|
+
const rel = relative3(demo.root, full).split(sep4).join("/");
|
|
6893
7336
|
if (!filter(rel)) continue;
|
|
6894
7337
|
let text = "";
|
|
6895
7338
|
try {
|
|
6896
|
-
text =
|
|
7339
|
+
text = readFileSync11(full, "utf8");
|
|
6897
7340
|
} catch {
|
|
6898
7341
|
continue;
|
|
6899
7342
|
}
|
|
@@ -6915,17 +7358,17 @@ function carveSlug(select) {
|
|
|
6915
7358
|
}
|
|
6916
7359
|
var clip = (s) => s.length > MAX_OUTPUT_BYTES ? s.slice(0, MAX_OUTPUT_BYTES) + "\n\u2026 (truncated)" : s;
|
|
6917
7360
|
function shortenIn(arg, root) {
|
|
6918
|
-
return arg.startsWith(root +
|
|
7361
|
+
return arg.startsWith(root + sep4) ? arg.slice(root.length + 1) : arg === root ? "." : arg;
|
|
6919
7362
|
}
|
|
6920
7363
|
function merge(r, root) {
|
|
6921
7364
|
const joined = stripAnsi([r.stdout, r.stderr].filter((s) => s.trim()).join("\n").trim());
|
|
6922
|
-
return clip(joined.split(root +
|
|
7365
|
+
return clip(joined.split(root + sep4).join("").split(root).join("."));
|
|
6923
7366
|
}
|
|
6924
7367
|
async function runCarveEmit(demo, select) {
|
|
6925
7368
|
const block = carveWriteBlock(demo);
|
|
6926
7369
|
if (block) return refuse3("read-only", block, "Start the walkthrough with `behold demo carve`.");
|
|
6927
|
-
|
|
6928
|
-
const reportFile =
|
|
7370
|
+
mkdirSync4(demo.out, { recursive: true });
|
|
7371
|
+
const reportFile = join15(demo.out, `${carveSlug(select)}-boundary.json`);
|
|
6929
7372
|
const args = [
|
|
6930
7373
|
"carve",
|
|
6931
7374
|
"emit",
|
|
@@ -6954,10 +7397,10 @@ async function runCarveEmit(demo, select) {
|
|
|
6954
7397
|
}
|
|
6955
7398
|
let boundary = null;
|
|
6956
7399
|
try {
|
|
6957
|
-
boundary = JSON.parse(
|
|
7400
|
+
boundary = JSON.parse(readFileSync11(reportFile, "utf8"));
|
|
6958
7401
|
} catch {
|
|
6959
7402
|
}
|
|
6960
|
-
const lintPath =
|
|
7403
|
+
const lintPath = relative3(demo.project, join15(demo.out, "src")).split(sep4).join("/");
|
|
6961
7404
|
const lintArgs = ["lint", lintPath];
|
|
6962
7405
|
const lint = await runChantRaw(lintArgs, demo.project).catch((err) => ({
|
|
6963
7406
|
code: 127,
|
|
@@ -6982,7 +7425,7 @@ async function runCarveEmit(demo, select) {
|
|
|
6982
7425
|
async function runCarveBridge(demo, select) {
|
|
6983
7426
|
const block = carveWriteBlock(demo);
|
|
6984
7427
|
if (block) return refuse3("read-only", block, "Start the walkthrough with `behold demo carve`.");
|
|
6985
|
-
|
|
7428
|
+
mkdirSync4(demo.out, { recursive: true });
|
|
6986
7429
|
const args = [
|
|
6987
7430
|
"carve",
|
|
6988
7431
|
"bridge",
|
|
@@ -7007,21 +7450,21 @@ async function runCarveBridge(demo, select) {
|
|
|
7007
7450
|
"Run the Emit step first \u2014 bridge reads the carve manifest emit leaves in the output directory."
|
|
7008
7451
|
);
|
|
7009
7452
|
}
|
|
7010
|
-
const
|
|
7453
|
+
const slug4 = carveSlug(select);
|
|
7011
7454
|
const all = readArtifacts(demo, demo.out, (rel) => /\.(tf|md|patch)$/.test(rel));
|
|
7012
7455
|
return {
|
|
7013
7456
|
ok: true,
|
|
7014
7457
|
select,
|
|
7015
7458
|
command: `chant ${args.map((a) => shortenIn(a, demo.root)).join(" ")}`,
|
|
7016
7459
|
output,
|
|
7017
|
-
runbook: all.find((a) => a.path.endsWith(`${
|
|
7018
|
-
proposals: all.filter((a) => !a.path.endsWith(`${
|
|
7460
|
+
runbook: all.find((a) => a.path.endsWith(`${slug4}-runbook.md`)) ?? null,
|
|
7461
|
+
proposals: all.filter((a) => !a.path.endsWith(`${slug4}-runbook.md`))
|
|
7019
7462
|
};
|
|
7020
7463
|
}
|
|
7021
7464
|
|
|
7022
7465
|
// src/ops.ts
|
|
7023
|
-
import { readdirSync as
|
|
7024
|
-
import { join as
|
|
7466
|
+
import { readdirSync as readdirSync7, readFileSync as readFileSync12, existsSync as existsSync10 } from "node:fs";
|
|
7467
|
+
import { join as join16 } from "node:path";
|
|
7025
7468
|
var APPLY_TARGET_LEXICON = {
|
|
7026
7469
|
cloudformation: "aws",
|
|
7027
7470
|
kubectl: "k8s",
|
|
@@ -7043,11 +7486,11 @@ function discoverOps(projectDir) {
|
|
|
7043
7486
|
const seen = /* @__PURE__ */ new Set();
|
|
7044
7487
|
const out = [];
|
|
7045
7488
|
for (const sub of ["ops", "src", "."]) {
|
|
7046
|
-
const dir =
|
|
7047
|
-
if (!
|
|
7048
|
-
for (const f of
|
|
7489
|
+
const dir = join16(projectDir, sub);
|
|
7490
|
+
if (!existsSync10(dir)) continue;
|
|
7491
|
+
for (const f of readdirSync7(dir)) {
|
|
7049
7492
|
if (!f.endsWith(".op.ts")) continue;
|
|
7050
|
-
const content =
|
|
7493
|
+
const content = readFileSync12(join16(dir, f), "utf8");
|
|
7051
7494
|
const name = content.match(/name:\s*["'`]([^"'`]+)["'`]/)?.[1];
|
|
7052
7495
|
if (!name || seen.has(name)) continue;
|
|
7053
7496
|
seen.add(name);
|
|
@@ -7063,25 +7506,25 @@ function discoverOps(projectDir) {
|
|
|
7063
7506
|
...gate ? { gate } : {},
|
|
7064
7507
|
...env ? { env } : {},
|
|
7065
7508
|
...substrate ? { substrate } : {},
|
|
7066
|
-
...
|
|
7509
|
+
...existsSync10(irPath) ? { irPath } : {}
|
|
7067
7510
|
});
|
|
7068
7511
|
}
|
|
7069
7512
|
}
|
|
7070
7513
|
return out.sort((a, b) => a.name.localeCompare(b.name));
|
|
7071
7514
|
}
|
|
7072
7515
|
function opIrPath(projectDir, name) {
|
|
7073
|
-
return
|
|
7516
|
+
return join16(projectDir, "dist", "ops", name, "op.json");
|
|
7074
7517
|
}
|
|
7075
7518
|
function discoverOpIrs(projectDirs) {
|
|
7076
7519
|
const seen = /* @__PURE__ */ new Set();
|
|
7077
7520
|
const out = [];
|
|
7078
7521
|
for (const dir of projectDirs) {
|
|
7079
|
-
const root =
|
|
7080
|
-
if (!
|
|
7081
|
-
for (const name of
|
|
7522
|
+
const root = join16(dir, "dist", "ops");
|
|
7523
|
+
if (!existsSync10(root)) continue;
|
|
7524
|
+
for (const name of readdirSync7(root)) {
|
|
7082
7525
|
if (seen.has(name)) continue;
|
|
7083
7526
|
const path = opIrPath(dir, name);
|
|
7084
|
-
if (!
|
|
7527
|
+
if (!existsSync10(path)) continue;
|
|
7085
7528
|
seen.add(name);
|
|
7086
7529
|
out.push({ name, dir, path });
|
|
7087
7530
|
}
|
|
@@ -7316,11 +7759,11 @@ var RAN = { ok: "ok", fail: "failed", skipped: "skipped" };
|
|
|
7316
7759
|
function tookLabel(ms) {
|
|
7317
7760
|
return ms < 1e3 ? `${ms}ms` : `${(ms / 1e3).toFixed(1)}s`;
|
|
7318
7761
|
}
|
|
7319
|
-
function paintPlayhead(ir,
|
|
7320
|
-
if (!
|
|
7762
|
+
function paintPlayhead(ir, join23, gate) {
|
|
7763
|
+
if (!join23.byNode.size && !join23.at) return ir;
|
|
7321
7764
|
const nodes = ir.nodes.map((node) => {
|
|
7322
|
-
const run3 =
|
|
7323
|
-
const here =
|
|
7765
|
+
const run3 = join23.byNode.get(node.id);
|
|
7766
|
+
const here = join23.at === node.id;
|
|
7324
7767
|
if (!run3 && !here) return node;
|
|
7325
7768
|
const isGate = node.attrs?._step === "gate";
|
|
7326
7769
|
const attrs = { ...node.attrs };
|
|
@@ -7335,7 +7778,7 @@ function paintPlayhead(ir, join21, gate) {
|
|
|
7335
7778
|
if (here) {
|
|
7336
7779
|
attrs._playhead = true;
|
|
7337
7780
|
attrs._status = isGate && gate ? "warn" : "accent";
|
|
7338
|
-
attrs.run = isGate && gate ? `waiting for approval since ${gate.since}` :
|
|
7781
|
+
attrs.run = isGate && gate ? `waiting for approval since ${gate.since}` : join23.atReason ?? "here";
|
|
7339
7782
|
if (isGate && gate?.description) attrs.gateDescription = gate.description;
|
|
7340
7783
|
}
|
|
7341
7784
|
return { ...node, attrs };
|
|
@@ -7357,11 +7800,11 @@ function pendingGateCard(op, gate, ir) {
|
|
|
7357
7800
|
}
|
|
7358
7801
|
};
|
|
7359
7802
|
}
|
|
7360
|
-
function playheadNote(state,
|
|
7803
|
+
function playheadNote(state, join23) {
|
|
7361
7804
|
if (!state.op) {
|
|
7362
7805
|
return "No run state: nothing has been run from this behold session, so every step paints as declared.";
|
|
7363
7806
|
}
|
|
7364
|
-
const settled =
|
|
7807
|
+
const settled = join23 ? join23.byNode.size : state.records.length;
|
|
7365
7808
|
const bits = [];
|
|
7366
7809
|
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`;
|
|
7367
7810
|
if (state.status === "running") {
|
|
@@ -7377,9 +7820,9 @@ function playheadNote(state, join21) {
|
|
|
7377
7820
|
} else if (state.gateNote) {
|
|
7378
7821
|
bits.push(`gate state unavailable \u2014 ${state.gateNote}`);
|
|
7379
7822
|
}
|
|
7380
|
-
if (
|
|
7823
|
+
if (join23?.unplaced.length) {
|
|
7381
7824
|
bits.push(
|
|
7382
|
-
`${
|
|
7825
|
+
`${join23.unplaced.length} record${join23.unplaced.length === 1 ? "" : "s"} matched no declared step and ${join23.unplaced.length === 1 ? "is" : "are"} not painted`
|
|
7383
7826
|
);
|
|
7384
7827
|
}
|
|
7385
7828
|
if (!bits.length) return `${head}.`;
|
|
@@ -8045,24 +8488,24 @@ var OpRunner = class {
|
|
|
8045
8488
|
|
|
8046
8489
|
// src/substrates.ts
|
|
8047
8490
|
import { spawn as spawn4 } from "node:child_process";
|
|
8048
|
-
import { existsSync as
|
|
8049
|
-
import { join as
|
|
8491
|
+
import { existsSync as existsSync11, readFileSync as readFileSync13 } from "node:fs";
|
|
8492
|
+
import { join as join17 } from "node:path";
|
|
8050
8493
|
import { platform } from "node:os";
|
|
8051
8494
|
var DOCKER_DOWN_DETAIL = "docker is down";
|
|
8052
8495
|
function probe(cmd, args) {
|
|
8053
|
-
return new Promise((
|
|
8496
|
+
return new Promise((resolve12) => {
|
|
8054
8497
|
let out = "";
|
|
8055
8498
|
let proc;
|
|
8056
8499
|
try {
|
|
8057
8500
|
proc = spawn4(cmd, args, { stdio: ["ignore", "pipe", "pipe"] });
|
|
8058
8501
|
} catch {
|
|
8059
|
-
|
|
8502
|
+
resolve12({ code: 127, out: "" });
|
|
8060
8503
|
return;
|
|
8061
8504
|
}
|
|
8062
8505
|
proc.stdout.on("data", (d) => out += d);
|
|
8063
8506
|
proc.stderr.on("data", (d) => out += d);
|
|
8064
|
-
proc.on("error", () =>
|
|
8065
|
-
proc.on("close", (code) =>
|
|
8507
|
+
proc.on("error", () => resolve12({ code: 127, out }));
|
|
8508
|
+
proc.on("close", (code) => resolve12({ code: code ?? 1, out }));
|
|
8066
8509
|
});
|
|
8067
8510
|
}
|
|
8068
8511
|
async function dockerAvailable(run3) {
|
|
@@ -8075,11 +8518,11 @@ async function dockerRunning(run3, nameFilter) {
|
|
|
8075
8518
|
return out.split(/\r?\n/).map((s) => s.trim()).filter(Boolean);
|
|
8076
8519
|
}
|
|
8077
8520
|
function scriptBringUp(projectDir, relPath, label) {
|
|
8078
|
-
return
|
|
8521
|
+
return existsSync11(join17(projectDir, relPath)) ? { label, cmd: "bash", args: [relPath] } : void 0;
|
|
8079
8522
|
}
|
|
8080
8523
|
function projectLexicons(projectDir) {
|
|
8081
8524
|
try {
|
|
8082
|
-
const src =
|
|
8525
|
+
const src = readFileSync13(join17(projectDir, "chant.config.ts"), "utf-8");
|
|
8083
8526
|
const m = src.match(/lexicons\s*:\s*\[([^\]]*)\]/);
|
|
8084
8527
|
if (!m) return [];
|
|
8085
8528
|
return [...m[1].matchAll(/["']([^"']+)["']/g)].map((x) => x[1]);
|
|
@@ -8131,7 +8574,7 @@ async function detectSubstrates(projectDir, preview = false, boundContext, probe
|
|
|
8131
8574
|
["forgejo", "Forgejo", ".forgejo", "test/forgejo-runtime-e2e.sh"]
|
|
8132
8575
|
];
|
|
8133
8576
|
for (const [name, label, marker, script] of forges) {
|
|
8134
|
-
if (!
|
|
8577
|
+
if (!existsSync11(join17(projectDir, marker))) continue;
|
|
8135
8578
|
const c = docker ? await dockerRunning(run3, name) : [];
|
|
8136
8579
|
const d = dep(c.length > 0, "container up", "on-demand (pipeline run)");
|
|
8137
8580
|
subs.push({
|
|
@@ -8165,7 +8608,7 @@ async function detectSubstrates(projectDir, preview = false, boundContext, probe
|
|
|
8165
8608
|
detail: endpoint ? `targeting ${endpoint}` : "real Fly (FLY_FLAPS_BASE_URL unset)"
|
|
8166
8609
|
});
|
|
8167
8610
|
}
|
|
8168
|
-
if (
|
|
8611
|
+
if (existsSync11(join17(projectDir, ".github", "workflows"))) {
|
|
8169
8612
|
const gh = await run3("gh", ["auth", "status"]);
|
|
8170
8613
|
const ready = gh.code === 0;
|
|
8171
8614
|
subs.push({
|
|
@@ -8178,7 +8621,7 @@ async function detectSubstrates(projectDir, preview = false, boundContext, probe
|
|
|
8178
8621
|
if (lexicons.includes("temporal")) {
|
|
8179
8622
|
let hasProfiles = false;
|
|
8180
8623
|
try {
|
|
8181
|
-
hasProfiles = /temporal\s*:\s*\{[\s\S]{0,400}?profiles\s*:/.test(
|
|
8624
|
+
hasProfiles = /temporal\s*:\s*\{[\s\S]{0,400}?profiles\s*:/.test(readFileSync13(join17(projectDir, "chant.config.ts"), "utf-8"));
|
|
8182
8625
|
} catch {
|
|
8183
8626
|
}
|
|
8184
8627
|
subs.push({
|
|
@@ -8282,12 +8725,12 @@ async function openRollbackBranches(projectDir, env) {
|
|
|
8282
8725
|
}
|
|
8283
8726
|
|
|
8284
8727
|
// src/estate-edges.ts
|
|
8285
|
-
import { statSync as
|
|
8728
|
+
import { statSync as statSync7 } from "node:fs";
|
|
8286
8729
|
var rec5 = (v) => v && typeof v === "object" && !Array.isArray(v) ? v : void 0;
|
|
8287
8730
|
var str5 = (v) => typeof v === "string" && v.length > 0 ? v : void 0;
|
|
8288
8731
|
var realIsDir2 = (p) => {
|
|
8289
8732
|
try {
|
|
8290
|
-
return
|
|
8733
|
+
return statSync7(p).isDirectory();
|
|
8291
8734
|
} catch {
|
|
8292
8735
|
return false;
|
|
8293
8736
|
}
|
|
@@ -8386,8 +8829,8 @@ function addEstateMemberEdges(ir, members, isDir = realIsDir2) {
|
|
|
8386
8829
|
}
|
|
8387
8830
|
|
|
8388
8831
|
// src/events.ts
|
|
8389
|
-
import { watch, existsSync as
|
|
8390
|
-
import { join as
|
|
8832
|
+
import { watch, existsSync as existsSync12 } from "node:fs";
|
|
8833
|
+
import { join as join18 } from "node:path";
|
|
8391
8834
|
var Broadcaster = class {
|
|
8392
8835
|
listeners = /* @__PURE__ */ new Set();
|
|
8393
8836
|
subscribe(fn) {
|
|
@@ -8405,7 +8848,7 @@ var Broadcaster = class {
|
|
|
8405
8848
|
};
|
|
8406
8849
|
var IGNORE = /(^|[\\/])(node_modules|dist|\.git)([\\/]|$)/;
|
|
8407
8850
|
function watchSource(projectDir, onChange, debounceMs = 200) {
|
|
8408
|
-
const dir =
|
|
8851
|
+
const dir = existsSync12(join18(projectDir, "src")) ? join18(projectDir, "src") : projectDir;
|
|
8409
8852
|
let timer;
|
|
8410
8853
|
const watcher = watch(dir, { recursive: true }, (_event, file) => {
|
|
8411
8854
|
const name = typeof file === "string" ? file : "";
|
|
@@ -8693,13 +9136,13 @@ async function emulatorDown(projectDir) {
|
|
|
8693
9136
|
}
|
|
8694
9137
|
|
|
8695
9138
|
// src/demos.ts
|
|
8696
|
-
import { readFileSync as
|
|
8697
|
-
import { join as
|
|
9139
|
+
import { readFileSync as readFileSync14, existsSync as existsSync13, cpSync } from "node:fs";
|
|
9140
|
+
import { join as join19, relative as relative4, resolve as resolve8, sep as sep5 } from "node:path";
|
|
8698
9141
|
import { spawn as spawn5, spawnSync } from "node:child_process";
|
|
8699
9142
|
function loadDemoRegistry(pkgRoot2) {
|
|
8700
9143
|
let raw;
|
|
8701
9144
|
try {
|
|
8702
|
-
raw = JSON.parse(
|
|
9145
|
+
raw = JSON.parse(readFileSync14(join19(pkgRoot2, "demos.json"), "utf8"));
|
|
8703
9146
|
} catch {
|
|
8704
9147
|
return [];
|
|
8705
9148
|
}
|
|
@@ -8739,22 +9182,22 @@ function fetchesFromNetwork(entry) {
|
|
|
8739
9182
|
return entry.source === "git";
|
|
8740
9183
|
}
|
|
8741
9184
|
function demoTargetDir(entry, cwd = process.cwd()) {
|
|
8742
|
-
const legacy =
|
|
8743
|
-
if (entry.name === "writes" &&
|
|
8744
|
-
return
|
|
9185
|
+
const legacy = resolve8(cwd, "behold-demo");
|
|
9186
|
+
if (entry.name === "writes" && existsSync13(legacy)) return legacy;
|
|
9187
|
+
return resolve8(cwd, "behold-demos", entry.name);
|
|
8745
9188
|
}
|
|
8746
9189
|
async function loadDemo(entry, opts) {
|
|
8747
9190
|
const { pkgRoot: pkgRoot2, target } = opts;
|
|
8748
9191
|
const say = (line) => opts.log?.(`behold demo ${entry.name} \u2192 ${line}`);
|
|
8749
|
-
if (!
|
|
9192
|
+
if (!existsSync13(target)) {
|
|
8750
9193
|
if (entry.source === "bundled") {
|
|
8751
|
-
const bundled =
|
|
8752
|
-
if (!
|
|
9194
|
+
const bundled = join19(pkgRoot2, entry.dir);
|
|
9195
|
+
if (!existsSync13(bundled)) return { ok: false, error: `this install has no bundled ${entry.dir}` };
|
|
8753
9196
|
say(`copying to ${target} (it's yours \u2014 edit it)`);
|
|
8754
9197
|
try {
|
|
8755
9198
|
cpSync(bundled, target, {
|
|
8756
9199
|
recursive: true,
|
|
8757
|
-
filter: (src) => !
|
|
9200
|
+
filter: (src) => !relative4(bundled, src).split(sep5).includes("node_modules")
|
|
8758
9201
|
});
|
|
8759
9202
|
} catch (err) {
|
|
8760
9203
|
return { ok: false, error: `copy failed: ${err instanceof Error ? err.message : String(err)}` };
|
|
@@ -8768,7 +9211,7 @@ async function loadDemo(entry, opts) {
|
|
|
8768
9211
|
} else {
|
|
8769
9212
|
say(`reusing ${target}`);
|
|
8770
9213
|
}
|
|
8771
|
-
if (
|
|
9214
|
+
if (existsSync13(join19(target, "package.json")) && !existsSync13(join19(target, "node_modules"))) {
|
|
8772
9215
|
say("npm install\u2026");
|
|
8773
9216
|
if (await runStep("npm", ["install"], { cwd: target, shell: process.platform === "win32" }) !== 0) {
|
|
8774
9217
|
return { ok: false, error: `npm install failed in ${target}` };
|
|
@@ -8780,7 +9223,7 @@ async function loadDemo(entry, opts) {
|
|
|
8780
9223
|
return { ok: false, error: `setup failed (${entry.setup})` };
|
|
8781
9224
|
}
|
|
8782
9225
|
}
|
|
8783
|
-
return { ok: true, serveDirs: entry.serve.dirs?.length ? entry.serve.dirs.map((d) =>
|
|
9226
|
+
return { ok: true, serveDirs: entry.serve.dirs?.length ? entry.serve.dirs.map((d) => join19(target, d)) : [target] };
|
|
8784
9227
|
}
|
|
8785
9228
|
function runStep(cmd, args, opts = {}) {
|
|
8786
9229
|
return new Promise((res) => {
|
|
@@ -8791,8 +9234,8 @@ function runStep(cmd, args, opts = {}) {
|
|
|
8791
9234
|
}
|
|
8792
9235
|
|
|
8793
9236
|
// src/server.ts
|
|
8794
|
-
var pkgRoot =
|
|
8795
|
-
var webRoot =
|
|
9237
|
+
var pkgRoot = join20(dirname5(fileURLToPath3(import.meta.url)), "..");
|
|
9238
|
+
var webRoot = join20(pkgRoot, "web");
|
|
8796
9239
|
var execFileP = async (cmd, args) => (await promisify3(execFile3)(cmd, args, { encoding: "utf8", timeout: 1e4 })).stdout;
|
|
8797
9240
|
function optsFromQuery(url, tierEnvVar, projectDir) {
|
|
8798
9241
|
const q = url.searchParams;
|
|
@@ -8842,7 +9285,7 @@ function tierFailure(tier, message) {
|
|
|
8842
9285
|
function beholdVersion() {
|
|
8843
9286
|
try {
|
|
8844
9287
|
const here = dirname5(fileURLToPath3(import.meta.url));
|
|
8845
|
-
return JSON.parse(
|
|
9288
|
+
return JSON.parse(readFileSync15(join20(here, "..", "package.json"), "utf8")).version ?? "unknown";
|
|
8846
9289
|
} catch {
|
|
8847
9290
|
return "unknown";
|
|
8848
9291
|
}
|
|
@@ -8887,9 +9330,14 @@ async function captureFrame(projectDir, env, frames, broadcaster) {
|
|
|
8887
9330
|
}
|
|
8888
9331
|
}
|
|
8889
9332
|
function carveRoutes(app, reportPath, demo) {
|
|
8890
|
-
const load = () => readCarveReport(reportPath, (p) =>
|
|
9333
|
+
const load = () => readCarveReport(reportPath, (p) => readFileSync15(p, "utf8"));
|
|
8891
9334
|
const carveDirs = [dirname5(reportPath), ...demo ? [demo.out] : []];
|
|
8892
9335
|
const carveStates = () => readCarveStates(carveDirs, nodeCarveIo, 0);
|
|
9336
|
+
const emittedNodes = async (states) => {
|
|
9337
|
+
if (!demo || states.size === 0) return /* @__PURE__ */ new Map();
|
|
9338
|
+
const outIr = await memberIr(demo.out).catch(() => void 0);
|
|
9339
|
+
return emittedNodesFor(outIr, states.values(), demo.out);
|
|
9340
|
+
};
|
|
8893
9341
|
const demoBlock = () => carveWriteBlock(demo);
|
|
8894
9342
|
const demoInfo = () => {
|
|
8895
9343
|
if (!demo) return null;
|
|
@@ -8901,8 +9349,8 @@ function carveRoutes(app, reportPath, demo) {
|
|
|
8901
9349
|
project: demo.project,
|
|
8902
9350
|
out: demo.out,
|
|
8903
9351
|
// Paths the UI can print without leaking the operator's whole home dir.
|
|
8904
|
-
outLabel:
|
|
8905
|
-
fromLabel:
|
|
9352
|
+
outLabel: relative5(demo.root, demo.out).split(sep6).join("/"),
|
|
9353
|
+
fromLabel: relative5(demo.root, demo.from).split(sep6).join("/"),
|
|
8906
9354
|
runnable: !block,
|
|
8907
9355
|
...block ? { reason: block } : {},
|
|
8908
9356
|
// The live tier, when this boot armed it: the scratch endpoint the
|
|
@@ -8919,7 +9367,7 @@ function carveRoutes(app, reportPath, demo) {
|
|
|
8919
9367
|
let appGraph;
|
|
8920
9368
|
const appGraphOnce = () => {
|
|
8921
9369
|
if (!demo) return Promise.resolve(null);
|
|
8922
|
-
appGraph ??= graphIr(demo.project).then((ir) => ir.nodes.length ? { ir, label:
|
|
9370
|
+
appGraph ??= graphIr(demo.project).then((ir) => ir.nodes.length ? { ir, label: relative5(demo.root, demo.project).split(sep6).join("/") } : null).catch(() => null);
|
|
8923
9371
|
return appGraph;
|
|
8924
9372
|
};
|
|
8925
9373
|
app.get("/api/graph", async (c) => {
|
|
@@ -8928,16 +9376,18 @@ function carveRoutes(app, reportPath, demo) {
|
|
|
8928
9376
|
const tfIr = carveReportToIr(parsed.report);
|
|
8929
9377
|
const appSide = await appGraphOnce();
|
|
8930
9378
|
const states = carveStates();
|
|
9379
|
+
const emitted = await emittedNodes(states);
|
|
8931
9380
|
const shorten = demo ? (p) => shortenIn(p, demo.root) : (p) => p;
|
|
8932
9381
|
const single = () => {
|
|
8933
|
-
const split = splitCarveState(tfIr, states, { shorten });
|
|
9382
|
+
const split = splitCarveState(tfIr, states, { shorten, emitted });
|
|
8934
9383
|
return bandGraduated(split.tf, split.graduated);
|
|
8935
9384
|
};
|
|
8936
9385
|
const { svg, ir } = appSide ? renderCarveEstate(tfIr, appSide.ir, {
|
|
8937
|
-
tfTitle: `${
|
|
9386
|
+
tfTitle: `${relative5(demo.root, demo.from).split(sep6).join("/")} \u2014 terraform`,
|
|
8938
9387
|
appTitle: `${appSide.label} \u2014 chant`,
|
|
8939
9388
|
carved: states,
|
|
8940
|
-
shorten
|
|
9389
|
+
shorten,
|
|
9390
|
+
emitted
|
|
8941
9391
|
}) : (() => {
|
|
8942
9392
|
const banded = single();
|
|
8943
9393
|
return { ...renderBanded(banded), ir: banded };
|
|
@@ -8953,7 +9403,9 @@ function carveRoutes(app, reportPath, demo) {
|
|
|
8953
9403
|
carve: true,
|
|
8954
9404
|
// A demo whose own advisor run failed says so on the statusbar, not
|
|
8955
9405
|
// only in the terminal the viewer isn't looking at.
|
|
8956
|
-
note: carveNote(parsed.report, tfIr) + carveStateNote(carveProgress(states.values(), tfIr.nodes.length), states) + (demo?.degraded ? ` Degraded: ${demo.degraded}` : "")
|
|
9406
|
+
note: carveNote(parsed.report, tfIr) + carveStateNote(carveProgress(states.values(), tfIr.nodes.length), states) + (demo?.degraded ? ` Degraded: ${demo.degraded}` : "") + // chant#2058: the app box shows only the app's own declarations;
|
|
9407
|
+
// what the git-root Op discovery added from elsewhere is named.
|
|
9408
|
+
(appSide && foreignNote(appSide.ir) ? ` ${foreignNote(appSide.ir)}.` : "")
|
|
8957
9409
|
}
|
|
8958
9410
|
});
|
|
8959
9411
|
});
|
|
@@ -8985,7 +9437,7 @@ function carveRoutes(app, reportPath, demo) {
|
|
|
8985
9437
|
}
|
|
8986
9438
|
const tfIr = carveReportToIr(parsed.report);
|
|
8987
9439
|
const html = renderCarveMorph(tfIr, appSide.ir, [select], {
|
|
8988
|
-
tfTitle: `${
|
|
9440
|
+
tfTitle: `${relative5(demo.root, demo.from).split(sep6).join("/")} \u2014 terraform`,
|
|
8989
9441
|
appTitle: `${appSide.label} \u2014 chant`,
|
|
8990
9442
|
title: `carve \u2014 ${select}`,
|
|
8991
9443
|
carved: carveStates(),
|
|
@@ -8993,7 +9445,7 @@ function carveRoutes(app, reportPath, demo) {
|
|
|
8993
9445
|
});
|
|
8994
9446
|
return c.html(html);
|
|
8995
9447
|
});
|
|
8996
|
-
app.get("/api/project", (c) => {
|
|
9448
|
+
app.get("/api/project", async (c) => {
|
|
8997
9449
|
const parsed = load();
|
|
8998
9450
|
const total = parsed.ok ? parsed.report.count ?? parsed.report.resources.length : null;
|
|
8999
9451
|
const state = carveStatePayload(
|
|
@@ -9079,6 +9531,36 @@ function carveRoutes(app, reportPath, demo) {
|
|
|
9079
9531
|
app.get("/api/resources", (c) => c.json({ byComponent: {} }));
|
|
9080
9532
|
app.get("/api/ci", (c) => c.json({ stages: [], jobs: [], forge: null }));
|
|
9081
9533
|
}
|
|
9534
|
+
function followLimits(env = process.env) {
|
|
9535
|
+
const timeout = Number.parseInt(env.BEHOLD_CI_FOLLOW_TIMEOUT_MS ?? "", 10);
|
|
9536
|
+
const budget = Number.parseInt(env.BEHOLD_CI_POLL_FAIL_BUDGET ?? "", 10);
|
|
9537
|
+
return {
|
|
9538
|
+
...Number.isInteger(timeout) && timeout >= 0 ? { followTimeoutMs: timeout } : {},
|
|
9539
|
+
...Number.isInteger(budget) && budget > 0 ? { pollFailBudget: budget } : {}
|
|
9540
|
+
};
|
|
9541
|
+
}
|
|
9542
|
+
async function readoptDispatchedRun(cfg, runner) {
|
|
9543
|
+
const run3 = readDispatchedRun(cfg.projectDir);
|
|
9544
|
+
if (!run3 || run3.concluded && run3.concluded.verdict !== "lost") return { outcome: "none" };
|
|
9545
|
+
const ready = await ghReady();
|
|
9546
|
+
if (!ready.ok) return { outcome: "refused", reason: ready.reason ?? "gh is not ready", run: run3 };
|
|
9547
|
+
const pipeline = await ciPipeline(cfg.projectDir, run3.env ? { env: run3.env } : {}, "github").catch(() => void 0);
|
|
9548
|
+
if (!pipeline || pipeline.jobs.length === 0) {
|
|
9549
|
+
return { outcome: "refused", reason: "no generated GitHub pipeline to paint the run onto \u2014 `chant build --components --generate github` first", run: run3 };
|
|
9550
|
+
}
|
|
9551
|
+
const started = runner.track(`GitHub Actions ${run3.workflow} (re-adopted run ${run3.runId})`, (io) => {
|
|
9552
|
+
io.line(`\u25CF re-adopting run ${run3.runId} (${run3.workflow}, dispatched ${run3.dispatchedAt}) \u2014 \`gh run view\` is the durable read`);
|
|
9553
|
+
return followRun(run3.runId, pipeline, {
|
|
9554
|
+
...followLimits(),
|
|
9555
|
+
onLine: io.line,
|
|
9556
|
+
onProgress: io.progress,
|
|
9557
|
+
onAdopted: ({ runId, url }) => saveDispatchedRun({ ...run3, runId, ...url ? { url } : {} }),
|
|
9558
|
+
onConcluded: ({ verdict }) => concludeDispatchedRun(cfg.projectDir, run3.runId, verdict)
|
|
9559
|
+
});
|
|
9560
|
+
});
|
|
9561
|
+
if (!started) return { outcome: "refused", reason: `busy \u2014 ${runner.running} is running`, run: run3 };
|
|
9562
|
+
return { outcome: "readopted", run: run3 };
|
|
9563
|
+
}
|
|
9082
9564
|
function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffer(), runner = new OpRunner({
|
|
9083
9565
|
projectDir: cfg.projectDir,
|
|
9084
9566
|
broadcaster,
|
|
@@ -9088,6 +9570,20 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
|
|
|
9088
9570
|
if (cfg.carveReport) carveRoutes(app, cfg.carveReport, cfg.carveDemo);
|
|
9089
9571
|
let beholdConfig = loadBeholdConfig(cfg.projectDir);
|
|
9090
9572
|
let tierEnvVar = beholdConfig.tiers?.envVar;
|
|
9573
|
+
const executorFor = (env) => env ? beholdConfig.executor?.[env] : void 0;
|
|
9574
|
+
const executorStatus = (env, d, pipeline) => {
|
|
9575
|
+
if ("invalid" in d) return { ok: false, reason: d.invalid };
|
|
9576
|
+
if (d.forge !== "github") {
|
|
9577
|
+
return { forge: d.forge, workflow: d.workflow, ok: false, reason: `env ${env} deploys through ${d.forge} per .behold.json, and behold has no ${d.forge} trigger (only GitHub Actions, via your gh) \u2014 Deploy stays disabled here; run the pipeline on the forge` };
|
|
9578
|
+
}
|
|
9579
|
+
const r = resolveWorkflow(cfg.projectDir, pipeline ?? { stages: [], jobs: [] }, d.workflow);
|
|
9580
|
+
return "workflow" in r ? { forge: d.forge, workflow: d.workflow, ok: true } : { forge: d.forge, workflow: d.workflow, ok: false, reason: r.reason };
|
|
9581
|
+
};
|
|
9582
|
+
const forgeRefusal = (env, d, what) => ({
|
|
9583
|
+
error: `${what} is refused: env ${env} deploys through ${"invalid" in d ? "a forge" : `${d.forge} (${d.workflow})`} per .behold.json's executor block \u2014 behold never runs it from this machine`,
|
|
9584
|
+
code: "executor-forge",
|
|
9585
|
+
remedy: "invalid" in d ? `Fix .behold.json: ${d.invalid}` : `Use Deploy (POST /api/ci/dispatch?env=${env}), which dispatches the designated workflow through your gh login.`
|
|
9586
|
+
});
|
|
9091
9587
|
const boundK8sContext = async (env) => {
|
|
9092
9588
|
try {
|
|
9093
9589
|
const { lexicons, k8sProfiles } = await detectProject(cfg.projectDir);
|
|
@@ -9149,6 +9645,8 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
|
|
|
9149
9645
|
if (!info) {
|
|
9150
9646
|
return c.json({ error: `no Op named "${name}" in the estate` }, 404);
|
|
9151
9647
|
}
|
|
9648
|
+
const designatedEnv = info.kind === "apply" ? executorFor(info.env) : void 0;
|
|
9649
|
+
if (designatedEnv && info.env) return c.json(forgeRefusal(info.env, designatedEnv, `running ${name}`), 409);
|
|
9152
9650
|
if (!runner.trigger(name, info.env, info.dir, Boolean(info.gate))) {
|
|
9153
9651
|
return c.json({ error: `an Op is already running (${runner.running})` }, 409);
|
|
9154
9652
|
}
|
|
@@ -9180,9 +9678,9 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
|
|
|
9180
9678
|
return c.json({ started: true, name, ran: label });
|
|
9181
9679
|
});
|
|
9182
9680
|
app.post("/api/local/reset", (c) => {
|
|
9183
|
-
const down =
|
|
9184
|
-
const up =
|
|
9185
|
-
if (!
|
|
9681
|
+
const down = join20(cfg.projectDir, "scripts/local/local-down.sh");
|
|
9682
|
+
const up = join20(cfg.projectDir, "scripts/local/local-up.sh");
|
|
9683
|
+
if (!existsSync14(down) || !existsSync14(up)) {
|
|
9186
9684
|
return c.json({ error: "no local-down.sh / local-up.sh in scripts/local \u2014 reset is only for local emulator projects" }, 400);
|
|
9187
9685
|
}
|
|
9188
9686
|
if (!runner.bringUp("reset local emulator", "bash", ["-c", "bash scripts/local/local-down.sh && bash scripts/local/local-up.sh"], cfg.projectDir)) {
|
|
@@ -9203,7 +9701,7 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
|
|
|
9203
9701
|
const dirs = [];
|
|
9204
9702
|
const declared = [];
|
|
9205
9703
|
for (const f of discoverOpIrs(estateDirs)) {
|
|
9206
|
-
const parsed = readOpIr(f.path, (p) =>
|
|
9704
|
+
const parsed = readOpIr(f.path, (p) => readFileSync15(p, "utf8"));
|
|
9207
9705
|
if (!parsed.ok) continue;
|
|
9208
9706
|
const mine = declaredConvergeOps([parsed.ir]);
|
|
9209
9707
|
if (!mine.length) continue;
|
|
@@ -9318,7 +9816,7 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
|
|
|
9318
9816
|
const k8sTarget = lexicons.includes("k8s") ? resolveK8sTarget(k8sProfiles, cfg.env, await loadKubeconfig()) : void 0;
|
|
9319
9817
|
const axes = deployAxes(tierEnvVar, lexicons, k8sTarget);
|
|
9320
9818
|
const emittedOps = discoverOpIrs(estateDirs).length;
|
|
9321
|
-
const carveState = carveStatePayload(
|
|
9819
|
+
const carveState = carveStatePayload(await carveStatesUnder(estateDirs));
|
|
9322
9820
|
return c.json({
|
|
9323
9821
|
projectDir: cfg.projectDir,
|
|
9324
9822
|
...carveState.manifests ? { carve: { state: carveState } } : {},
|
|
@@ -9345,6 +9843,10 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
|
|
|
9345
9843
|
// no `tiers` key) → no `tiers` field → the SPA's picker doesn't render
|
|
9346
9844
|
// and the graph loads with no tier selected (web/app.js `initPickers`).
|
|
9347
9845
|
...beholdConfig.tiers ? { tiers: beholdConfig.tiers.values } : {},
|
|
9846
|
+
// The executor contract (#165): per designated env, whether Deploy can
|
|
9847
|
+
// dispatch right now and, if not, why — so the SPA disables the gesture
|
|
9848
|
+
// with the reason rather than offering a control that 4xxs.
|
|
9849
|
+
...beholdConfig.executor ? { executor: Object.fromEntries(Object.entries(beholdConfig.executor).map(([env, d]) => [env, executorStatus(env, d)])) } : {},
|
|
9348
9850
|
// The stack picker's options (#76, follow-up to #71): gated exactly like
|
|
9349
9851
|
// `tiers` above — only present when `chant.config.ts` declares `stacks[]`
|
|
9350
9852
|
// at all, so a single-stack/sourceDir-only/legacy project's SPA renders no
|
|
@@ -9369,9 +9871,9 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
|
|
|
9369
9871
|
app.post("/api/project/open", async (c) => {
|
|
9370
9872
|
if (cfg.previewMode) return c.json({ error: "switching projects is locked in preview mode" }, 403);
|
|
9371
9873
|
const body = await c.req.json().catch(() => ({}));
|
|
9372
|
-
const dir = typeof body.dir === "string" && body.dir.trim() ?
|
|
9373
|
-
if (!dir || !
|
|
9374
|
-
if (!
|
|
9874
|
+
const dir = typeof body.dir === "string" && body.dir.trim() ? resolve9(body.dir.trim()) : "";
|
|
9875
|
+
if (!dir || !existsSync14(dir)) return c.json({ error: `no such directory: ${dir || "(no dir given)"}` }, 400);
|
|
9876
|
+
if (!existsSync14(join20(dir, "chant.config.ts"))) {
|
|
9375
9877
|
return c.json({ error: `${dir} doesn't look like a chant project \u2014 no chant.config.ts` }, 400);
|
|
9376
9878
|
}
|
|
9377
9879
|
switchServedProject([dir]);
|
|
@@ -9391,7 +9893,7 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
|
|
|
9391
9893
|
};
|
|
9392
9894
|
app.post("/api/project/reveal", async (c) => {
|
|
9393
9895
|
const body = await c.req.json().catch(() => ({}));
|
|
9394
|
-
const dir = typeof body.dir === "string" && body.dir.trim() ?
|
|
9896
|
+
const dir = typeof body.dir === "string" && body.dir.trim() ? resolve9(body.dir.trim()) : cfg.projectDir;
|
|
9395
9897
|
const known = /* @__PURE__ */ new Set([cfg.projectDir, ...cfg.projectDirs ?? [], ...listRecents().map((r) => r.dir)]);
|
|
9396
9898
|
if (!known.has(dir)) return c.json({ error: "not a served or recent project directory" }, 400);
|
|
9397
9899
|
const opener = process.platform === "darwin" ? "open" : process.platform === "win32" ? "explorer" : "xdg-open";
|
|
@@ -9415,7 +9917,7 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
|
|
|
9415
9917
|
fetches: fetchesFromNetwork(e),
|
|
9416
9918
|
...e.repo ? { repo: e.repo } : {},
|
|
9417
9919
|
target,
|
|
9418
|
-
loaded:
|
|
9920
|
+
loaded: existsSync14(target),
|
|
9419
9921
|
satisfiable: missing.length === 0,
|
|
9420
9922
|
// #254: the carve walkthrough runs fine here — it just isn't a project
|
|
9421
9923
|
// to switch INTO. Carve mode claims `/api/graph` and `/api/project` at
|
|
@@ -9459,7 +9961,7 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
|
|
|
9459
9961
|
const loaded = await loadDemo(entry, { pkgRoot, target, log: (line) => process.stdout.write(line + "\n") });
|
|
9460
9962
|
if (!loaded.ok) return c.json({ error: loaded.error }, 500);
|
|
9461
9963
|
const dirs = loaded.serveDirs;
|
|
9462
|
-
if (!
|
|
9964
|
+
if (!existsSync14(join20(dirs[0], "chant.config.ts"))) {
|
|
9463
9965
|
return c.json({ error: `${entry.name} loaded to ${target} but ${dirs[0]} has no chant.config.ts` }, 500);
|
|
9464
9966
|
}
|
|
9465
9967
|
if (entry.serve.local) {
|
|
@@ -9557,7 +10059,7 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
|
|
|
9557
10059
|
path: "/api/demos/open",
|
|
9558
10060
|
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)"
|
|
9559
10061
|
},
|
|
9560
|
-
{ 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" },
|
|
10062
|
+
{ method: "GET", path: "/api/graph", desc: "the graph {ir, svg, meta} \u2014 params: detail=0..3, components=1, logical=1, ops=1 (+entities=1 to link steps to the estate), env, stack, tier, target, lens, up=1, down=1, radial=1, layout=1" },
|
|
9561
10063
|
...cfg.carveReport ? [
|
|
9562
10064
|
{ method: "GET", path: "/api/carve", desc: "carve mode: the raw `chant carve advise --json` peelability report this server is rendering" },
|
|
9563
10065
|
{
|
|
@@ -9594,7 +10096,9 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
|
|
|
9594
10096
|
{ method: "POST", path: "/api/rollback", desc: "open a rollback PR: ?to=<sha>" },
|
|
9595
10097
|
{ method: "POST", path: "/api/substrates/:name/up", desc: "bring a substrate up" },
|
|
9596
10098
|
{ method: "POST", path: "/api/local/reset", desc: "reset the local emulator" },
|
|
9597
|
-
{ method: "POST", path: "/api/ci/dispatch", desc: "dispatch the GitHub Actions pipeline via the operator's gh" }
|
|
10099
|
+
{ method: "POST", path: "/api/ci/dispatch", desc: "dispatch the GitHub Actions pipeline for ?env= via the operator's gh \u2014 the designated workflow when .behold.json names one" },
|
|
10100
|
+
{ method: "GET", path: "/api/ci/run", desc: "the persisted dispatched-run record (id, verdict) \u2014 what survives a behold restart" },
|
|
10101
|
+
{ method: "POST", path: "/api/ci/readopt", desc: "re-adopt the persisted unconcluded run: follow it by its saved id (also attempted automatically at boot)" }
|
|
9598
10102
|
]
|
|
9599
10103
|
})
|
|
9600
10104
|
);
|
|
@@ -9616,15 +10120,21 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
|
|
|
9616
10120
|
}
|
|
9617
10121
|
const parsedOps = [];
|
|
9618
10122
|
for (const f of files) {
|
|
9619
|
-
const parsed = readOpIr(f.path, (p) =>
|
|
10123
|
+
const parsed = readOpIr(f.path, (p) => readFileSync15(p, "utf8"));
|
|
9620
10124
|
if (!parsed.ok) return c.json(parsed.refusal, 422);
|
|
9621
10125
|
parsedOps.push(parsed.ir);
|
|
9622
10126
|
}
|
|
9623
10127
|
const declaredIr = opsToIr(parsedOps);
|
|
10128
|
+
if (url.searchParams.get("entities") === "1") {
|
|
10129
|
+
const memberDirs = [...new Set(files.map((f) => f.dir))];
|
|
10130
|
+
const memberReads = await Promise.allSettled(memberDirs.map((dir) => memberIr(dir)));
|
|
10131
|
+
const estateNodes = memberReads.flatMap((r) => r.status === "fulfilled" ? r.value.nodes : []);
|
|
10132
|
+
joinStepEntities(declaredIr, estateNodes.length ? { nodes: estateNodes, edges: [], groups: {} } : void 0);
|
|
10133
|
+
}
|
|
9624
10134
|
const runState = runner.runState;
|
|
9625
10135
|
const ranOp = runState.op ? parsedOps.find((o) => o.name === runState.op) : void 0;
|
|
9626
|
-
const
|
|
9627
|
-
const opsIr =
|
|
10136
|
+
const join23 = ranOp ? joinRun(ranOp, runState.records, runState.gate) : null;
|
|
10137
|
+
const opsIr = join23 ? paintPlayhead(declaredIr, join23, runState.gate) : declaredIr;
|
|
9628
10138
|
const { svg: svg2 } = renderGraph(opsIr, { boxes: "byStack" });
|
|
9629
10139
|
const operator = runner.noteDeclaredConvergeOps(declaredConvergeOps(parsedOps));
|
|
9630
10140
|
return c.json({
|
|
@@ -9637,7 +10147,7 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
|
|
|
9637
10147
|
target: null,
|
|
9638
10148
|
mode: "ops",
|
|
9639
10149
|
ops: parsedOps.length,
|
|
9640
|
-
note: `${opsNote(parsedOps, opsIr)} ${playheadNote(runState,
|
|
10150
|
+
note: `${opsNote(parsedOps, opsIr)} ${playheadNote(runState, join23)} ${operatorNote(operator)}`,
|
|
9641
10151
|
// The operator strip's own model: the ConvergeOps this project
|
|
9642
10152
|
// declares, plus whatever the last status read said. `declared`
|
|
9643
10153
|
// without a `strip` is the honest starting state — a loop exists and
|
|
@@ -9709,6 +10219,7 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
|
|
|
9709
10219
|
return c.json({ ir: projected, svg: svg2, byContainer, meta: { projectDir: cfg.projectDir, env: metaEnv, tier: opts.tier ?? null, target: opts.target ?? null, mode: "logical", ...logicalNote ? { note: logicalNote } : {} } });
|
|
9710
10220
|
} else {
|
|
9711
10221
|
ir = mergeClusterRoot(await graphIr(cfg.projectDir, opts), await clusterRootGraphIr(cfg.projectDir, opts));
|
|
10222
|
+
ir = joinCarvedSources(ir, (await carveStatesFor(cfg.projectDir)).values(), cfg.projectDir).ir;
|
|
9712
10223
|
if ((opts.detail ?? 2) < 3) ir = pruneImports(ir);
|
|
9713
10224
|
ir = addK8sDeclaredEdges(ir);
|
|
9714
10225
|
ir = addValueMatchEdges(ir);
|
|
@@ -9726,7 +10237,7 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
|
|
|
9726
10237
|
srcCompositeEdgesAttached = 0;
|
|
9727
10238
|
}
|
|
9728
10239
|
}
|
|
9729
|
-
if (!multi && !components && !logical && !opts.lens && ir.nodes.length === 0 && !
|
|
10240
|
+
if (!multi && !components && !logical && !opts.lens && ir.nodes.length === 0 && !existsSync14(join20(cfg.projectDir, "chant.config.ts"))) {
|
|
9730
10241
|
return c.json(noProjectError(cfg.projectDir), 404);
|
|
9731
10242
|
}
|
|
9732
10243
|
const radial = new URL(c.req.url).searchParams.get("radial") === "1";
|
|
@@ -9773,23 +10284,51 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
|
|
|
9773
10284
|
}
|
|
9774
10285
|
});
|
|
9775
10286
|
app.post("/api/ci/dispatch", async (c) => {
|
|
10287
|
+
const env = new URL(c.req.url).searchParams.get("env") ?? cfg.env;
|
|
10288
|
+
const designated = executorFor(env);
|
|
10289
|
+
if (designated && "invalid" in designated) return c.json({ error: designated.invalid, code: "executor-forge", remedy: "Fix .behold.json's executor block." }, 400);
|
|
10290
|
+
if (designated && designated.forge !== "github") {
|
|
10291
|
+
return c.json({ error: executorStatus(env, designated).reason, code: "executor-forge" }, 400);
|
|
10292
|
+
}
|
|
9776
10293
|
const ready = await ghReady();
|
|
9777
10294
|
if (!ready.ok) return c.json({ error: ready.reason }, 400);
|
|
9778
|
-
const pipeline = await ciPipeline(cfg.projectDir,
|
|
10295
|
+
const pipeline = await ciPipeline(cfg.projectDir, env ? { env } : {}, "github").catch(() => void 0);
|
|
9779
10296
|
if (!pipeline || pipeline.jobs.length === 0) {
|
|
9780
10297
|
return c.json({ error: "no generated GitHub pipeline \u2014 `chant build --components --generate github` produced no jobs" }, 400);
|
|
9781
10298
|
}
|
|
9782
|
-
const
|
|
9783
|
-
if (
|
|
9784
|
-
|
|
9785
|
-
}
|
|
10299
|
+
const resolved = resolveWorkflow(cfg.projectDir, pipeline, designated?.workflow);
|
|
10300
|
+
if ("reason" in resolved) return c.json({ error: resolved.reason, ...designated ? { code: "executor-forge" } : {} }, 400);
|
|
10301
|
+
const workflow = resolved.workflow;
|
|
9786
10302
|
const ref = await execFileP("git", ["-C", cfg.projectDir, "branch", "--show-current"]).then((out) => out.trim() || "main").catch(() => "main");
|
|
10303
|
+
const base = { projectDir: cfg.projectDir, ...env ? { env } : {}, workflow: workflow.file, ref, dispatchedAt: (/* @__PURE__ */ new Date()).toISOString() };
|
|
10304
|
+
let adoptedId;
|
|
9787
10305
|
const started = runner.track(
|
|
9788
10306
|
`GitHub Actions ${workflow.file}`,
|
|
9789
|
-
(io) => dispatchAndFollow(workflow, pipeline, ref, {
|
|
10307
|
+
(io) => dispatchAndFollow(workflow, pipeline, ref, {
|
|
10308
|
+
...followLimits(),
|
|
10309
|
+
onLine: io.line,
|
|
10310
|
+
onProgress: io.progress,
|
|
10311
|
+
onAdopted: ({ runId, url }) => {
|
|
10312
|
+
adoptedId = runId;
|
|
10313
|
+
saveDispatchedRun({ ...base, runId, ...url ? { url } : {} });
|
|
10314
|
+
},
|
|
10315
|
+
onConcluded: ({ verdict }) => {
|
|
10316
|
+
if (adoptedId !== void 0) concludeDispatchedRun(cfg.projectDir, adoptedId, verdict);
|
|
10317
|
+
}
|
|
10318
|
+
})
|
|
9790
10319
|
);
|
|
9791
10320
|
if (!started) return c.json({ error: `busy \u2014 ${runner.running} is running` }, 409);
|
|
9792
|
-
return c.json({ started: true, workflow: workflow.file, ref, jobs: pipeline.jobs.length });
|
|
10321
|
+
return c.json({ started: true, workflow: workflow.file, ref, jobs: pipeline.jobs.length, ...env ? { env } : {}, designated: Boolean(designated) });
|
|
10322
|
+
});
|
|
10323
|
+
app.get("/api/ci/run", (c) => {
|
|
10324
|
+
const run3 = readDispatchedRun(cfg.projectDir);
|
|
10325
|
+
return c.json({ run: run3 ?? null });
|
|
10326
|
+
});
|
|
10327
|
+
app.post("/api/ci/readopt", async (c) => {
|
|
10328
|
+
const result = await readoptDispatchedRun(cfg, runner);
|
|
10329
|
+
if (result.outcome === "none") return c.json({ error: "no unconcluded dispatched run is persisted for this project" }, 404);
|
|
10330
|
+
if (result.outcome === "refused") return c.json({ error: result.reason, run: result.run }, 409);
|
|
10331
|
+
return c.json({ readopted: true, runId: result.run.runId, workflow: result.run.workflow });
|
|
9793
10332
|
});
|
|
9794
10333
|
app.get("/api/resources", async (c) => {
|
|
9795
10334
|
const opts = optsFromQuery(new URL(c.req.url), tierEnvVar, cfg.projectDir);
|
|
@@ -9884,7 +10423,7 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
|
|
|
9884
10423
|
});
|
|
9885
10424
|
}
|
|
9886
10425
|
const opts = { ...query, live: true, overlay: true, env, ...logical || runtime ? { detail: 3 } : {} };
|
|
9887
|
-
let ir = reclassifyOverlay(await graphIr(cfg.projectDir, opts));
|
|
10426
|
+
let ir = joinCarvedSources(reclassifyOverlay(await graphIr(cfg.projectDir, opts)), (await carveStatesFor(cfg.projectDir)).values(), cfg.projectDir).ir;
|
|
9888
10427
|
const boundContext = await boundK8sContext(env);
|
|
9889
10428
|
ir = mergeClusterRoot(ir, await clusterRootGraphIr(cfg.projectDir, query), await runningK3dClusters());
|
|
9890
10429
|
const declaresHelm = await detectProject(cfg.projectDir).then((p) => p.lexicons.includes("helm")).catch(() => false);
|
|
@@ -9892,7 +10431,7 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
|
|
|
9892
10431
|
const observed = await lifecycleDiffLive(cfg.projectDir, env, query).then((d) => d.lexicons?.helm?.observedArtifacts).catch(() => void 0);
|
|
9893
10432
|
applyHelmArtifacts(ir, observed);
|
|
9894
10433
|
synthesizeHelmReleases(ir, observed, discoverReleaseUnits(cfg.projectDir));
|
|
9895
|
-
applyHelmRenderDrift(ir, await readHelmRenderDrift(cfg.projectDir, env, query));
|
|
10434
|
+
applyHelmRenderDrift(ir, await readHelmRenderDrift(cfg.projectDir, env, query, observed));
|
|
9896
10435
|
}
|
|
9897
10436
|
ir = runtime ? attachRuntimeContainment(ir) : pruneRuntimeChildren(ir);
|
|
9898
10437
|
if (logical) {
|
|
@@ -9943,7 +10482,7 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
|
|
|
9943
10482
|
const observed = await lifecycleDiffLive(cfg.projectDir, env).then((d) => d.lexicons?.helm?.observedArtifacts).catch(() => void 0);
|
|
9944
10483
|
applyHelmArtifacts(result.ir, observed);
|
|
9945
10484
|
synthesizeHelmReleases(result.ir, observed, discoverReleaseUnits(cfg.projectDir));
|
|
9946
|
-
applyHelmRenderDrift(result.ir, await readHelmRenderDrift(cfg.projectDir, env));
|
|
10485
|
+
applyHelmRenderDrift(result.ir, await readHelmRenderDrift(cfg.projectDir, env, {}, observed));
|
|
9947
10486
|
}
|
|
9948
10487
|
}
|
|
9949
10488
|
if (env) {
|
|
@@ -10047,6 +10586,8 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
|
|
|
10047
10586
|
}
|
|
10048
10587
|
const component = url.searchParams.get("component") || "all";
|
|
10049
10588
|
const force = url.searchParams.get("force") === "1";
|
|
10589
|
+
const designated = executorFor(env);
|
|
10590
|
+
if (designated) return c.json(forgeRefusal(env, designated, "apply"), 409);
|
|
10050
10591
|
const flociTargeted = !!process.env.AWS_ENDPOINT_URL && await detectProject(cfg.projectDir).then((p) => p.lexicons.includes("aws")).catch(() => false);
|
|
10051
10592
|
if (!force && flociTargeted) {
|
|
10052
10593
|
try {
|
|
@@ -10071,9 +10612,9 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
|
|
|
10071
10612
|
}
|
|
10072
10613
|
return c.json({ started: true, component, env });
|
|
10073
10614
|
});
|
|
10074
|
-
const rel =
|
|
10615
|
+
const rel = relative5(process.cwd(), webRoot) || ".";
|
|
10075
10616
|
app.use("/*", serveStatic({ root: rel }));
|
|
10076
|
-
app.get("/", serveStatic({ path:
|
|
10617
|
+
app.get("/", serveStatic({ path: join20(rel, "index.html") }));
|
|
10077
10618
|
return app;
|
|
10078
10619
|
}
|
|
10079
10620
|
async function bootLocalEmulators(dir, who) {
|
|
@@ -10102,7 +10643,7 @@ async function bootLocalEmulators(dir, who) {
|
|
|
10102
10643
|
async function startServer(cfg) {
|
|
10103
10644
|
if (cfg.local) cfg.emulators = await bootLocalEmulators(cfg.projectDir, "behold serve --local");
|
|
10104
10645
|
for (const dir of cfg.projectDirs ?? [cfg.projectDir]) {
|
|
10105
|
-
if (
|
|
10646
|
+
if (existsSync14(join20(dir, "chant.config.ts"))) addRecent(dir);
|
|
10106
10647
|
}
|
|
10107
10648
|
const broadcaster = new Broadcaster();
|
|
10108
10649
|
const frames = new FrameBuffer();
|
|
@@ -10113,6 +10654,17 @@ async function startServer(cfg) {
|
|
|
10113
10654
|
});
|
|
10114
10655
|
const app = createApp(cfg, broadcaster, frames, runner);
|
|
10115
10656
|
const autoSync = cfg.autoSync ?? "off";
|
|
10657
|
+
if (!cfg.carveReport) {
|
|
10658
|
+
void readoptDispatchedRun(cfg, runner).then((result) => {
|
|
10659
|
+
if (result.outcome === "readopted") {
|
|
10660
|
+
process.stdout.write(` re-adopted dispatched run ${result.run.runId} (${result.run.workflow}) \u2014 following via gh
|
|
10661
|
+
`);
|
|
10662
|
+
} else if (result.outcome === "refused") {
|
|
10663
|
+
process.stdout.write(` a dispatched run (${result.run.runId}, ${result.run.workflow}) is persisted but not re-adopted: ${result.reason}
|
|
10664
|
+
`);
|
|
10665
|
+
}
|
|
10666
|
+
}).catch(() => void 0);
|
|
10667
|
+
}
|
|
10116
10668
|
const capture2 = () => captureFrame(cfg.projectDir, cfg.env, frames, broadcaster);
|
|
10117
10669
|
const onEstateChange = (memberDir) => {
|
|
10118
10670
|
broadcaster.emit("changed", memberDir ?? "");
|
|
@@ -10128,11 +10680,19 @@ async function startServer(cfg) {
|
|
|
10128
10680
|
if (autoSync === "off") return;
|
|
10129
10681
|
void routeAutoSync(dir, movedLexicons);
|
|
10130
10682
|
};
|
|
10683
|
+
const executorDesignations = loadBeholdConfig(cfg.projectDir).executor ?? {};
|
|
10684
|
+
const executorFor = (env) => env ? executorDesignations[env] : void 0;
|
|
10131
10685
|
const routeAutoSync = async (dir, movedLexicons) => {
|
|
10132
10686
|
const suspended = autoSync === "pull-request" ? suspendedByRollback(await openRollbackBranches(dir, cfg.env), movedLexicons) : /* @__PURE__ */ new Set();
|
|
10687
|
+
const allOps = discoverEstateOps(cfg.projectDirs ?? [cfg.projectDir]);
|
|
10688
|
+
const routed = allOps.filter((o) => o.kind === "apply" && o.env && executorFor(o.env));
|
|
10689
|
+
for (const o of routed) {
|
|
10690
|
+
const d = executorFor(o.env);
|
|
10691
|
+
broadcaster.emit("op", `\u27F3 auto-sync (${autoSync})${memberTag(dir)} declined ${o.name}: env ${o.env} deploys through ${"invalid" in d ? "a forge" : d.forge} per .behold.json \u2014 never applied from here`);
|
|
10692
|
+
}
|
|
10133
10693
|
const { picks, declined } = pickAutoSyncOps(
|
|
10134
10694
|
autoSync,
|
|
10135
|
-
|
|
10695
|
+
allOps.filter((o) => !routed.includes(o)),
|
|
10136
10696
|
runner.running,
|
|
10137
10697
|
movedLexicons,
|
|
10138
10698
|
suspended
|
|
@@ -10241,8 +10801,8 @@ async function startServer(cfg) {
|
|
|
10241
10801
|
}
|
|
10242
10802
|
|
|
10243
10803
|
// src/export.ts
|
|
10244
|
-
import { mkdirSync as
|
|
10245
|
-
import { join as
|
|
10804
|
+
import { mkdirSync as mkdirSync5, writeFileSync as writeFileSync5, copyFileSync, readFileSync as readFileSync16, readdirSync as readdirSync8 } from "node:fs";
|
|
10805
|
+
import { join as join21, dirname as dirname6, basename as basename2 } from "node:path";
|
|
10246
10806
|
import { fileURLToPath as fileURLToPath4 } from "node:url";
|
|
10247
10807
|
var LENS_PARAMS = ["components", "detail", "env", "logical", "ops", "radial", "tier"];
|
|
10248
10808
|
function canonicalKey(path, params) {
|
|
@@ -10250,7 +10810,7 @@ function canonicalKey(path, params) {
|
|
|
10250
10810
|
const q = LENS_PARAMS.filter((k) => params.has(k) && !(flat && (k === "detail" || k === "radial"))).map((k) => `${k}=${params.get(k)}`).join("&");
|
|
10251
10811
|
return q ? `${path}?${q}` : path;
|
|
10252
10812
|
}
|
|
10253
|
-
function
|
|
10813
|
+
function slug3(key) {
|
|
10254
10814
|
const base = key.replace(/^\//, "").replace(/[^a-zA-Z0-9=_.-]+/g, "_").slice(0, 120);
|
|
10255
10815
|
return `${base}.json`;
|
|
10256
10816
|
}
|
|
@@ -10260,7 +10820,7 @@ function captureKeys(axes) {
|
|
|
10260
10820
|
add("/api/project", {});
|
|
10261
10821
|
add("/api/substrates", {});
|
|
10262
10822
|
add("/api/ops", {});
|
|
10263
|
-
if (axes.ops) add("/api/graph", { ops: "1" });
|
|
10823
|
+
if (axes.ops) add("/api/graph", { ops: "1", entities: "1" });
|
|
10264
10824
|
const tiers = axes.tiers && axes.tiers.length ? axes.tiers : [""];
|
|
10265
10825
|
const envs = ["", ...axes.environments];
|
|
10266
10826
|
for (const env of envs) {
|
|
@@ -10291,7 +10851,7 @@ function captureKeys(axes) {
|
|
|
10291
10851
|
return [...keys];
|
|
10292
10852
|
}
|
|
10293
10853
|
function webDir() {
|
|
10294
|
-
return
|
|
10854
|
+
return join21(dirname6(fileURLToPath4(import.meta.url)), "..", "web");
|
|
10295
10855
|
}
|
|
10296
10856
|
function workerName(project, override) {
|
|
10297
10857
|
const raw = override ?? `behold-${basename2(project)}`;
|
|
@@ -10302,16 +10862,16 @@ async function runExport(cfg, outDir, opts = {}) {
|
|
|
10302
10862
|
const app = createApp({ ...cfg, layoutWrites: false });
|
|
10303
10863
|
const proj = await (await app.request("/api/project")).json();
|
|
10304
10864
|
const axes = { environments: proj.environments ?? [], tiers: proj.tiers ?? [], ...proj.ops ? { ops: proj.ops } : {} };
|
|
10305
|
-
const snapDir =
|
|
10306
|
-
|
|
10865
|
+
const snapDir = join21(outDir, "snapshots");
|
|
10866
|
+
mkdirSync5(snapDir, { recursive: true });
|
|
10307
10867
|
const keyToFile = {};
|
|
10308
10868
|
let ok = 0;
|
|
10309
10869
|
let failed = 0;
|
|
10310
10870
|
for (const key of captureKeys(axes)) {
|
|
10311
10871
|
const res = await app.request(`${key}${key.includes("?") ? "&" : "?"}layout=1`);
|
|
10312
10872
|
const body = await res.text();
|
|
10313
|
-
const file =
|
|
10314
|
-
|
|
10873
|
+
const file = slug3(key);
|
|
10874
|
+
writeFileSync5(join21(snapDir, file), body);
|
|
10315
10875
|
keyToFile[key] = `snapshots/${file}`;
|
|
10316
10876
|
if (res.ok) ok++;
|
|
10317
10877
|
else failed++;
|
|
@@ -10323,21 +10883,21 @@ async function runExport(cfg, outDir, opts = {}) {
|
|
|
10323
10883
|
axes,
|
|
10324
10884
|
keyToFile
|
|
10325
10885
|
};
|
|
10326
|
-
|
|
10327
|
-
const html =
|
|
10886
|
+
writeFileSync5(join21(outDir, "manifest.json"), JSON.stringify(manifest, null, 2));
|
|
10887
|
+
const html = readFileSync16(join21(webDir(), "index.html"), "utf8").replace(
|
|
10328
10888
|
/<\/head>/i,
|
|
10329
10889
|
` <script>window.__BEHOLD_STATIC__ = true;</script>
|
|
10330
10890
|
</head>`
|
|
10331
10891
|
);
|
|
10332
|
-
|
|
10333
|
-
for (const f of
|
|
10892
|
+
writeFileSync5(join21(outDir, "index.html"), html);
|
|
10893
|
+
for (const f of readdirSync8(webDir())) {
|
|
10334
10894
|
if (f === "index.html") continue;
|
|
10335
|
-
copyFileSync(
|
|
10895
|
+
copyFileSync(join21(webDir(), f), join21(outDir, f));
|
|
10336
10896
|
}
|
|
10337
|
-
|
|
10897
|
+
writeFileSync5(join21(outDir, "README.md"), BUNDLE_README);
|
|
10338
10898
|
const name = workerName(cfg.projectDir, opts.name);
|
|
10339
|
-
|
|
10340
|
-
|
|
10899
|
+
writeFileSync5(
|
|
10900
|
+
join21(outDir, "wrangler.jsonc"),
|
|
10341
10901
|
JSON.stringify(
|
|
10342
10902
|
{ $schema: "node_modules/wrangler/config-schema.json", name, compatibility_date: "2025-06-01", assets: { directory: "." } },
|
|
10343
10903
|
null,
|
|
@@ -10385,7 +10945,7 @@ It's just files \u2014 GitHub Pages, S3, nginx, or Cloudflare Pages
|
|
|
10385
10945
|
`;
|
|
10386
10946
|
|
|
10387
10947
|
// src/doctor.ts
|
|
10388
|
-
import { relative as
|
|
10948
|
+
import { relative as relative6, resolve as resolve10 } from "node:path";
|
|
10389
10949
|
var list2 = (xs) => xs.join(", ");
|
|
10390
10950
|
var EMPTY_KUBECONFIG = { contexts: /* @__PURE__ */ new Map(), servers: /* @__PURE__ */ new Map() };
|
|
10391
10951
|
function summarize(xs, max = 4) {
|
|
@@ -10393,7 +10953,7 @@ function summarize(xs, max = 4) {
|
|
|
10393
10953
|
}
|
|
10394
10954
|
function labelFor(root, target, estate) {
|
|
10395
10955
|
if (!estate) return "";
|
|
10396
|
-
const rel =
|
|
10956
|
+
const rel = relative6(root, target);
|
|
10397
10957
|
return `${rel || "."}: `;
|
|
10398
10958
|
}
|
|
10399
10959
|
function chantCheck(root, targets, estate) {
|
|
@@ -10406,7 +10966,7 @@ function chantCheck(root, targets, estate) {
|
|
|
10406
10966
|
name: "chant",
|
|
10407
10967
|
status: "fail",
|
|
10408
10968
|
detail: `${list2(resolutions.map(describe))} \u2014 behold shells the project's own chant`,
|
|
10409
|
-
fix: `Run \`npm install\` in ${list2(missing.map(({ target }) =>
|
|
10969
|
+
fix: `Run \`npm install\` in ${list2(missing.map(({ target }) => relative6(root, target) || target))}`
|
|
10410
10970
|
};
|
|
10411
10971
|
}
|
|
10412
10972
|
const stale = resolutions.filter(({ res }) => !meetsFloor(res.version, floor));
|
|
@@ -10534,7 +11094,7 @@ function opsCheck(root, ops, chantSource, estate) {
|
|
|
10534
11094
|
return { name: "ops", status: "pass", detail: `${ops.length} committed: ${list2(names)}; ${mcp}` };
|
|
10535
11095
|
}
|
|
10536
11096
|
async function diagnose(dir, probes = {}) {
|
|
10537
|
-
const root =
|
|
11097
|
+
const root = resolve10(dir);
|
|
10538
11098
|
const shape = detectProjectShape(root);
|
|
10539
11099
|
const behold = beholdVersion();
|
|
10540
11100
|
if (shape.kind === "none") {
|
|
@@ -10554,16 +11114,16 @@ async function diagnose(dir, probes = {}) {
|
|
|
10554
11114
|
};
|
|
10555
11115
|
}
|
|
10556
11116
|
const estate = shape.kind === "estate";
|
|
10557
|
-
const members = (shape.members ?? []).map((m) =>
|
|
11117
|
+
const members = (shape.members ?? []).map((m) => resolve10(root, m));
|
|
10558
11118
|
const targets = estate ? members : [root];
|
|
10559
11119
|
const primary = targets[0];
|
|
10560
11120
|
const projectCheck = estate ? {
|
|
10561
11121
|
name: "project",
|
|
10562
11122
|
status: "pass",
|
|
10563
11123
|
detail: `estate of ${members.length} projects (${shape.membersFrom === "behold-config" ? ".behold.json members" : "npm workspaces"}): ${list2(
|
|
10564
|
-
members.map((m) =>
|
|
11124
|
+
members.map((m) => relative6(root, m))
|
|
10565
11125
|
)}`
|
|
10566
|
-
} : { name: "project", status: "pass", detail: `chant project (${
|
|
11126
|
+
} : { name: "project", status: "pass", detail: `chant project (${relative6(root, shape.configFile)})` };
|
|
10567
11127
|
const infos = await Promise.all(targets.map(async (t) => ({ target: t, info: await detectProject(t) })));
|
|
10568
11128
|
const declared = new Map(infos.map(({ target, info }) => [target, info.lexicons]));
|
|
10569
11129
|
const lexicons = [...new Set(infos.flatMap(({ info }) => info.lexicons))];
|
|
@@ -10788,7 +11348,7 @@ ${USAGE}`);
|
|
|
10788
11348
|
process.stderr.write("behold serve: --auto-sync needs --env and --poll (it acts on polled drift)\n");
|
|
10789
11349
|
process.exit(2);
|
|
10790
11350
|
}
|
|
10791
|
-
const dirs = projectDirs.map((d) =>
|
|
11351
|
+
const dirs = projectDirs.map((d) => resolve11(d));
|
|
10792
11352
|
for (const d of dirs) warnIfNotChantProject(d);
|
|
10793
11353
|
await startServer({
|
|
10794
11354
|
projectDir: dirs[0],
|
|
@@ -10807,7 +11367,7 @@ function warnIfNotChantProject(dir) {
|
|
|
10807
11367
|
if (shape.kind === "estate") {
|
|
10808
11368
|
process.stderr.write(
|
|
10809
11369
|
`behold: warning \u2014 ${dir} is an estate root, not a chant project itself.
|
|
10810
|
-
Serve its members composed: behold serve ${shape.members.map((m) =>
|
|
11370
|
+
Serve its members composed: behold serve ${shape.members.map((m) => join22(dir, m)).join(" ")}
|
|
10811
11371
|
`
|
|
10812
11372
|
);
|
|
10813
11373
|
return;
|
|
@@ -10842,8 +11402,8 @@ async function runCarve(rest) {
|
|
|
10842
11402
|
);
|
|
10843
11403
|
process.exit(2);
|
|
10844
11404
|
}
|
|
10845
|
-
const reportPath =
|
|
10846
|
-
const parsed = readCarveReport(reportPath, (p) =>
|
|
11405
|
+
const reportPath = resolve11(fileArg);
|
|
11406
|
+
const parsed = readCarveReport(reportPath, (p) => readFileSync17(p, "utf8"));
|
|
10847
11407
|
if (!parsed.ok) {
|
|
10848
11408
|
process.stderr.write(`behold carve: ${parsed.refusal.error}
|
|
10849
11409
|
${parsed.refusal.remedy}
|
|
@@ -10870,8 +11430,8 @@ async function runDoctor(rest) {
|
|
|
10870
11430
|
}
|
|
10871
11431
|
}
|
|
10872
11432
|
const dir = dirArg ?? ".";
|
|
10873
|
-
if (!
|
|
10874
|
-
process.stderr.write(`behold doctor: no such directory: ${
|
|
11433
|
+
if (!existsSync15(resolve11(dir))) {
|
|
11434
|
+
process.stderr.write(`behold doctor: no such directory: ${resolve11(dir)}
|
|
10875
11435
|
`);
|
|
10876
11436
|
process.exit(2);
|
|
10877
11437
|
}
|
|
@@ -10880,7 +11440,7 @@ async function runDoctor(rest) {
|
|
|
10880
11440
|
if (!report.ok) process.exitCode = 1;
|
|
10881
11441
|
}
|
|
10882
11442
|
async function runDemo(rest) {
|
|
10883
|
-
const pkgRoot2 =
|
|
11443
|
+
const pkgRoot2 = join22(dirname7(fileURLToPath5(import.meta.url)), "..");
|
|
10884
11444
|
const registry = loadDemoRegistry(pkgRoot2);
|
|
10885
11445
|
let port = 4600;
|
|
10886
11446
|
let live = false;
|
|
@@ -10934,7 +11494,7 @@ async function runDemo(rest) {
|
|
|
10934
11494
|
`);
|
|
10935
11495
|
process.exit(2);
|
|
10936
11496
|
}
|
|
10937
|
-
const target = dirArg ?
|
|
11497
|
+
const target = dirArg ? resolve11(dirArg) : demoTargetDir(entry);
|
|
10938
11498
|
const loaded = await loadDemo(entry, { pkgRoot: pkgRoot2, target, log: (line) => process.stdout.write(line + "\n") });
|
|
10939
11499
|
if (!loaded.ok) {
|
|
10940
11500
|
process.stderr.write(`behold demo ${entry.name}: ${loaded.error}
|
|
@@ -10965,7 +11525,7 @@ function spawnStep(cmd, args, cwd) {
|
|
|
10965
11525
|
});
|
|
10966
11526
|
}
|
|
10967
11527
|
async function serveCarveDemo(target, carve, port, live = false) {
|
|
10968
|
-
const at = (rel) =>
|
|
11528
|
+
const at = (rel) => resolve11(target, rel);
|
|
10969
11529
|
const project = at(carve.project);
|
|
10970
11530
|
const from = at(carve.from);
|
|
10971
11531
|
const state = carve.state ? at(carve.state) : void 0;
|
|
@@ -10989,7 +11549,7 @@ async function serveCarveDemo(target, carve, port, live = false) {
|
|
|
10989
11549
|
process.exit(1);
|
|
10990
11550
|
}
|
|
10991
11551
|
process.stdout.write("behold demo carve --live \u2192 terraform init + apply (the starred resources, into the scratch Floci)\u2026\n");
|
|
10992
|
-
if (state)
|
|
11552
|
+
if (state) rmSync3(state, { force: true });
|
|
10993
11553
|
const applyErr = await applyIntoFloci(from, spawnStep);
|
|
10994
11554
|
if (applyErr) {
|
|
10995
11555
|
await teardownScratchFloci();
|
|
@@ -11005,7 +11565,7 @@ async function serveCarveDemo(target, carve, port, live = false) {
|
|
|
11005
11565
|
};
|
|
11006
11566
|
process.stdout.write("behold demo carve --live \u2192 the tfstate is real now; the advisor reads what terraform wrote.\n");
|
|
11007
11567
|
}
|
|
11008
|
-
if (
|
|
11568
|
+
if (existsSync15(join22(project, "package.json")) && !existsSync15(join22(project, "node_modules"))) {
|
|
11009
11569
|
process.stdout.write(`behold demo carve \u2192 npm install in ${carve.project}/ (the chant this walkthrough shells)\u2026
|
|
11010
11570
|
`);
|
|
11011
11571
|
const code = await spawnStep("npm", ["install"], project);
|
|
@@ -11016,7 +11576,7 @@ async function serveCarveDemo(target, carve, port, live = false) {
|
|
|
11016
11576
|
}
|
|
11017
11577
|
}
|
|
11018
11578
|
let degraded;
|
|
11019
|
-
if (!
|
|
11579
|
+
if (!existsSync15(join22(target, "node_modules", "@cdktf", "hcl2json"))) {
|
|
11020
11580
|
process.stdout.write("behold demo carve \u2192 npm install @cdktf/hcl2json (chant's HCL parser, ~2MB, once)\u2026\n");
|
|
11021
11581
|
const code = await spawnStep("npm", ["install", "--no-save", "--no-package-lock", "@cdktf/hcl2json"], target);
|
|
11022
11582
|
if (code !== 0) degraded = "couldn't install @cdktf/hcl2json (chant's HCL parser) \u2014 no network?";
|
|
@@ -11032,7 +11592,7 @@ async function serveCarveDemo(target, carve, port, live = false) {
|
|
|
11032
11592
|
);
|
|
11033
11593
|
if (code !== 0) degraded = `chant carve advise exited ${code}`;
|
|
11034
11594
|
}
|
|
11035
|
-
const serving = !degraded &&
|
|
11595
|
+
const serving = !degraded && existsSync15(report) ? report : committed;
|
|
11036
11596
|
if (degraded) {
|
|
11037
11597
|
process.stderr.write(
|
|
11038
11598
|
`behold demo carve: ${degraded}
|
|
@@ -11040,7 +11600,7 @@ async function serveCarveDemo(target, carve, port, live = false) {
|
|
|
11040
11600
|
`
|
|
11041
11601
|
);
|
|
11042
11602
|
}
|
|
11043
|
-
if (!
|
|
11603
|
+
if (!existsSync15(serving)) {
|
|
11044
11604
|
process.stderr.write(`behold demo carve: no carve report at ${serving}
|
|
11045
11605
|
`);
|
|
11046
11606
|
process.exit(2);
|
|
@@ -11085,8 +11645,8 @@ async function runPreview(rest) {
|
|
|
11085
11645
|
process.stderr.write("behold preview: --port must be a number\n");
|
|
11086
11646
|
process.exit(2);
|
|
11087
11647
|
}
|
|
11088
|
-
const projectDir =
|
|
11089
|
-
if (!
|
|
11648
|
+
const projectDir = resolve11(dirArg ?? process.cwd());
|
|
11649
|
+
if (!existsSync15(projectDir)) {
|
|
11090
11650
|
process.stderr.write(`behold preview: project not found at ${projectDir}
|
|
11091
11651
|
`);
|
|
11092
11652
|
process.exit(2);
|
|
@@ -11106,26 +11666,26 @@ async function runPreview(rest) {
|
|
|
11106
11666
|
await startServer({ projectDir, port, env: "local", previewMode: true });
|
|
11107
11667
|
}
|
|
11108
11668
|
async function runExportCmd(rest) {
|
|
11109
|
-
let outDir =
|
|
11669
|
+
let outDir = resolve11("behold-export");
|
|
11110
11670
|
let env;
|
|
11111
11671
|
let name;
|
|
11112
11672
|
let dirArg;
|
|
11113
11673
|
let emulator = false;
|
|
11114
11674
|
for (let i = 0; i < rest.length; i++) {
|
|
11115
11675
|
const a = rest[i];
|
|
11116
|
-
if (a === "--out") outDir =
|
|
11676
|
+
if (a === "--out") outDir = resolve11(rest[++i]);
|
|
11117
11677
|
else if (a === "--env") env = rest[++i];
|
|
11118
11678
|
else if (a === "--name") name = rest[++i];
|
|
11119
11679
|
else if (a === "--emulator") emulator = true;
|
|
11120
11680
|
else if (a === "-h" || a === "--help") return void process.stdout.write(USAGE);
|
|
11121
11681
|
else if (!a.startsWith("-")) dirArg = a;
|
|
11122
11682
|
}
|
|
11123
|
-
const projectDir =
|
|
11683
|
+
const projectDir = resolve11(dirArg ?? process.cwd());
|
|
11124
11684
|
if (emulator) {
|
|
11125
11685
|
injectEmulatorEnv(env);
|
|
11126
11686
|
env ??= "local";
|
|
11127
11687
|
}
|
|
11128
|
-
if (!
|
|
11688
|
+
if (!existsSync15(projectDir)) {
|
|
11129
11689
|
process.stderr.write(`behold export: project not found at ${projectDir}
|
|
11130
11690
|
`);
|
|
11131
11691
|
process.exit(2);
|