@intentius/behold 0.14.0 → 0.16.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 +70 -0
- package/dist/cli.js +1007 -600
- 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 +166 -11
- package/web/release.js +66 -0
- package/web/release.test.js +52 -0
package/dist/cli.js
CHANGED
|
@@ -716,11 +716,11 @@ async function createK8sClient(options = {}) {
|
|
|
716
716
|
}
|
|
717
717
|
return void 0;
|
|
718
718
|
}
|
|
719
|
-
async function
|
|
719
|
+
async function resolve10(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 resolve10(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 resolve10({ 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: resolve10,
|
|
876
876
|
read,
|
|
877
877
|
pathFor,
|
|
878
878
|
readIfPresent,
|
|
@@ -1065,7 +1065,7 @@ var init_src = __esm({
|
|
|
1065
1065
|
});
|
|
1066
1066
|
|
|
1067
1067
|
// src/cli.ts
|
|
1068
|
-
import { resolve as
|
|
1068
|
+
import { resolve as resolve9, dirname as dirname7, join as join21 } from "node:path";
|
|
1069
1069
|
import { realpathSync, existsSync as existsSync14, readFileSync as readFileSync16, rmSync as rmSync2 } 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";
|
|
@@ -1148,7 +1148,7 @@ 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
1150
|
import { existsSync as existsSync13, readFileSync as readFileSync14 } from "node:fs";
|
|
1151
|
-
import { basename, dirname as dirname5, join as
|
|
1151
|
+
import { basename, dirname as dirname5, join as join19, relative as relative5, resolve as resolve7, sep as sep4 } 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 resolve2 } 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,309 @@ 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 } 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) 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 byFile = /* @__PURE__ */ new Map();
|
|
1839
|
+
for (const s of states) for (const f of s.sourceFiles) byFile.set(resolve(f), s);
|
|
1840
|
+
if (byFile.size === 0) return { ir, joined: 0 };
|
|
1841
|
+
let joined = 0;
|
|
1842
|
+
const nodes = ir.nodes.map((n) => {
|
|
1843
|
+
const file = n.sourceLoc?.file;
|
|
1844
|
+
if (!file) return n;
|
|
1845
|
+
const abs = resolve(memberDir, file);
|
|
1846
|
+
const state = byFile.get(abs);
|
|
1847
|
+
if (!state) return n;
|
|
1848
|
+
joined++;
|
|
1849
|
+
const carve = {
|
|
1850
|
+
target: state.target,
|
|
1851
|
+
...state.tfType ? { tfType: state.tfType } : {},
|
|
1852
|
+
stage: state.stage,
|
|
1853
|
+
graduated: state.graduated,
|
|
1854
|
+
...state.at ? { at: state.at } : {},
|
|
1855
|
+
...state.from ? { from: state.from } : {},
|
|
1856
|
+
file: relative(memberDir, abs).split("\\").join("/")
|
|
1857
|
+
};
|
|
1858
|
+
return { ...n, attrs: { ...n.attrs, carved: state.target, _carve: carve } };
|
|
1859
|
+
});
|
|
1860
|
+
return { ir: joined ? { ...ir, nodes } : ir, joined };
|
|
1861
|
+
}
|
|
1862
|
+
function emittedNodesFor(ir, states, memberDir) {
|
|
1863
|
+
const out = /* @__PURE__ */ new Map();
|
|
1864
|
+
if (!ir) return out;
|
|
1865
|
+
for (const n of joinCarvedSources(ir, states, memberDir).ir.nodes) {
|
|
1866
|
+
const carve = n.attrs._carve;
|
|
1867
|
+
if (carve && !out.has(carve.target)) out.set(carve.target, n);
|
|
1868
|
+
}
|
|
1869
|
+
return out;
|
|
1870
|
+
}
|
|
1871
|
+
function carveProgress(states, total) {
|
|
1872
|
+
let applied = 0;
|
|
1873
|
+
let bridged = 0;
|
|
1874
|
+
let emitted = 0;
|
|
1875
|
+
for (const s of states) {
|
|
1876
|
+
if (s.stage === "applied") applied++;
|
|
1877
|
+
else if (s.stage === "bridged") bridged++;
|
|
1878
|
+
else emitted++;
|
|
1879
|
+
}
|
|
1880
|
+
const inFlight = bridged + emitted;
|
|
1881
|
+
const parts = [];
|
|
1882
|
+
if (emitted) parts.push(`${emitted} emitted`);
|
|
1883
|
+
if (bridged) parts.push(`${bridged} bridged`);
|
|
1884
|
+
return {
|
|
1885
|
+
applied,
|
|
1886
|
+
bridged,
|
|
1887
|
+
emitted,
|
|
1888
|
+
inFlight,
|
|
1889
|
+
total: typeof total === "number" ? total : null,
|
|
1890
|
+
label: typeof total === "number" ? `${applied} of ${total} carved` : `${applied} carved`,
|
|
1891
|
+
detail: parts.length ? `${parts.join(", ")} \u2014 apply is yours` : ""
|
|
1892
|
+
};
|
|
1893
|
+
}
|
|
1894
|
+
function applyCommandFor(state, labels = {}) {
|
|
1895
|
+
const env = state.ownership?.env ?? "<env>";
|
|
1896
|
+
const stack = state.ownership?.stack ?? "<stack>";
|
|
1897
|
+
const from = labels.from ?? state.from ?? "<terraform-dir>";
|
|
1898
|
+
const out = labels.out ?? state.outDir;
|
|
1899
|
+
return `chant carve apply --from ${from} --output ${out} --env ${env} --stack ${stack} --write-source`;
|
|
1900
|
+
}
|
|
1901
|
+
function carveStatePayload(states, total, labels = () => ({})) {
|
|
1902
|
+
const list3 = [...states.values()].sort((a, b) => a.target.localeCompare(b.target));
|
|
1903
|
+
return {
|
|
1904
|
+
manifests: list3.length,
|
|
1905
|
+
progress: carveProgress(list3, total),
|
|
1906
|
+
states: list3.map((s) => ({ ...s, note: carveStageNote(s), applyCommand: applyCommandFor(s, labels(s)) })),
|
|
1907
|
+
apply: { human: true, note: APPLY_IS_HUMAN }
|
|
1908
|
+
};
|
|
1909
|
+
}
|
|
1910
|
+
function splitCarveState(tfIr, states, opts = {}) {
|
|
1911
|
+
if (states.size === 0) return { tf: tfIr, graduated: [] };
|
|
1912
|
+
const shorten = opts.shorten ?? ((p) => p);
|
|
1913
|
+
const emitted = opts.emitted ?? /* @__PURE__ */ new Map();
|
|
1914
|
+
const declaredAttrs = (n) => Object.fromEntries(Object.entries(n.attrs ?? {}).filter(([k]) => !k.startsWith("_") && k !== "carved"));
|
|
1915
|
+
const source = (files) => files.map(shorten).join(", ");
|
|
1916
|
+
const gone = /* @__PURE__ */ new Set();
|
|
1917
|
+
const graduated = [];
|
|
1918
|
+
const nodes = [];
|
|
1919
|
+
for (const node of tfIr.nodes) {
|
|
1920
|
+
const state = states.get(node.id);
|
|
1921
|
+
if (!state) {
|
|
1922
|
+
nodes.push(node);
|
|
1923
|
+
continue;
|
|
1924
|
+
}
|
|
1925
|
+
const became = emitted.get(node.id);
|
|
1926
|
+
if (state.graduated) {
|
|
1927
|
+
gone.add(node.id);
|
|
1928
|
+
graduated.push({
|
|
1929
|
+
...node,
|
|
1930
|
+
...became ? { kind: became.kind, lexicon: became.lexicon, ...became.sourceLoc ? { sourceLoc: became.sourceLoc } : {} } : {},
|
|
1931
|
+
attrs: {
|
|
1932
|
+
...node.attrs,
|
|
1933
|
+
...became ? { ...declaredAttrs(became), chantEntity: `${became.kind} ${became.id}` } : {},
|
|
1934
|
+
_status: "good",
|
|
1935
|
+
carve: carveWordFor(state.stage),
|
|
1936
|
+
carveState: state.stage,
|
|
1937
|
+
...state.ownership?.stack ? { ownedStack: state.ownership.stack } : {},
|
|
1938
|
+
...state.ownership?.env ? { ownedEnv: state.ownership.env } : {},
|
|
1939
|
+
...state.at ? { carvedAt: state.at } : {},
|
|
1940
|
+
...state.files.length ? { chantSource: source(state.files) } : {}
|
|
1941
|
+
}
|
|
1942
|
+
});
|
|
1943
|
+
continue;
|
|
1944
|
+
}
|
|
1945
|
+
nodes.push({
|
|
1946
|
+
...node,
|
|
1947
|
+
attrs: {
|
|
1948
|
+
...node.attrs,
|
|
1949
|
+
_status: carveToneFor(state.stage),
|
|
1950
|
+
carve: carveWordFor(state.stage),
|
|
1951
|
+
carveState: state.stage,
|
|
1952
|
+
...became ? { chantEntity: `${became.kind} ${became.id}` } : {},
|
|
1953
|
+
...state.at ? { carvedAt: state.at } : {},
|
|
1954
|
+
...state.files.length ? { chantSource: source(state.files) } : {},
|
|
1955
|
+
...state.excised.length ? { excisesOnApply: state.excised.join(", ") } : {},
|
|
1956
|
+
...state.deferredInputs.length ? { deferredInputs: state.deferredInputs.join(", ") } : {}
|
|
1957
|
+
}
|
|
1958
|
+
});
|
|
1959
|
+
}
|
|
1960
|
+
const byStack = {};
|
|
1961
|
+
for (const [band, members] of Object.entries(tfIr.groups?.byStack ?? {})) {
|
|
1962
|
+
const left = members.filter((id) => !gone.has(id));
|
|
1963
|
+
if (left.length) byStack[band] = left;
|
|
1964
|
+
}
|
|
1965
|
+
return {
|
|
1966
|
+
tf: {
|
|
1967
|
+
nodes,
|
|
1968
|
+
edges: tfIr.edges.filter((e) => !gone.has(e.from) && !gone.has(e.to)),
|
|
1969
|
+
groups: { ...tfIr.groups, byStack }
|
|
1970
|
+
},
|
|
1971
|
+
graduated
|
|
1972
|
+
};
|
|
1973
|
+
}
|
|
1974
|
+
var GRADUATED_BAND = "carved \u2192 chant";
|
|
1975
|
+
function bandGraduated(tf, graduated) {
|
|
1976
|
+
if (graduated.length === 0) return tf;
|
|
1977
|
+
return {
|
|
1978
|
+
nodes: [...graduated, ...tf.nodes],
|
|
1979
|
+
edges: tf.edges,
|
|
1980
|
+
groups: {
|
|
1981
|
+
...tf.groups,
|
|
1982
|
+
byStack: {
|
|
1983
|
+
[GRADUATED_BAND]: graduated.map((n) => n.id),
|
|
1984
|
+
...tf.groups?.byStack ?? {}
|
|
1985
|
+
}
|
|
1986
|
+
}
|
|
1987
|
+
};
|
|
1988
|
+
}
|
|
1989
|
+
function carveStateNote(progress, states) {
|
|
1990
|
+
if (states.size === 0) return "";
|
|
1991
|
+
const head = ` Carve state (from ${states.size} manifest${states.size === 1 ? "" : "s"}): ${progress.label}`;
|
|
1992
|
+
return progress.detail ? `${head}, ${progress.detail}.` : `${head}.`;
|
|
1993
|
+
}
|
|
1994
|
+
|
|
1667
1995
|
// src/chant.ts
|
|
1668
1996
|
function envOverridesFor(opts) {
|
|
1669
1997
|
const overrides = {};
|
|
@@ -1694,14 +2022,14 @@ function packageManifestFrom(req, name) {
|
|
|
1694
2022
|
} catch {
|
|
1695
2023
|
return void 0;
|
|
1696
2024
|
}
|
|
1697
|
-
let dir =
|
|
2025
|
+
let dir = dirname3(entry);
|
|
1698
2026
|
for (; ; ) {
|
|
1699
2027
|
try {
|
|
1700
|
-
const pkg = createRequire(import.meta.url)(
|
|
2028
|
+
const pkg = createRequire(import.meta.url)(join4(dir, "package.json"));
|
|
1701
2029
|
if (pkg.name === name) return { root: dir, manifest: pkg };
|
|
1702
2030
|
} catch {
|
|
1703
2031
|
}
|
|
1704
|
-
const parent =
|
|
2032
|
+
const parent = dirname3(dir);
|
|
1705
2033
|
if (parent === dir) break;
|
|
1706
2034
|
dir = parent;
|
|
1707
2035
|
}
|
|
@@ -1711,10 +2039,10 @@ function chantPackageFrom(req) {
|
|
|
1711
2039
|
const found = packageManifestFrom(req, "@intentius/chant");
|
|
1712
2040
|
if (!found) return void 0;
|
|
1713
2041
|
const { root, manifest } = found;
|
|
1714
|
-
return { root, bin:
|
|
2042
|
+
return { root, bin: join4(root, manifest.bin?.chant ?? "bin/chant"), ...manifest.version ? { version: manifest.version } : {} };
|
|
1715
2043
|
}
|
|
1716
2044
|
function resolveLexicons(projectDir, lexicons) {
|
|
1717
|
-
const req = createRequire(
|
|
2045
|
+
const req = createRequire(join4(resolve2(projectDir), "noop.js"));
|
|
1718
2046
|
return lexicons.map((lexicon) => {
|
|
1719
2047
|
const pkg = `@intentius/chant-lexicon-${lexicon}`;
|
|
1720
2048
|
const found = packageManifestFrom(req, pkg);
|
|
@@ -1724,7 +2052,7 @@ function resolveLexicons(projectDir, lexicons) {
|
|
|
1724
2052
|
function resolveChant(projectDir) {
|
|
1725
2053
|
const own = chantPackageFrom(createRequire(import.meta.url));
|
|
1726
2054
|
if (projectDir) {
|
|
1727
|
-
const fromProject = chantPackageFrom(createRequire(
|
|
2055
|
+
const fromProject = chantPackageFrom(createRequire(join4(resolve2(projectDir), "noop.js")));
|
|
1728
2056
|
if (fromProject) return { source: fromProject.root === own?.root ? "behold" : "project", ...fromProject };
|
|
1729
2057
|
}
|
|
1730
2058
|
if (own) return { source: "behold", ...own };
|
|
@@ -1732,8 +2060,8 @@ function resolveChant(projectDir) {
|
|
|
1732
2060
|
}
|
|
1733
2061
|
function chantFloor() {
|
|
1734
2062
|
try {
|
|
1735
|
-
const here =
|
|
1736
|
-
const pkg = JSON.parse(
|
|
2063
|
+
const here = dirname3(fileURLToPath(import.meta.url));
|
|
2064
|
+
const pkg = JSON.parse(readFileSync4(join4(here, "..", "package.json"), "utf8"));
|
|
1737
2065
|
return pkg.dependencies?.["@intentius/chant"]?.replace(/^[^0-9]*/, "") || void 0;
|
|
1738
2066
|
} catch {
|
|
1739
2067
|
return void 0;
|
|
@@ -1839,7 +2167,7 @@ function runCommandStream(cmd, args, cwd, onLine) {
|
|
|
1839
2167
|
return { pid: proc.pid ?? -1, kill: () => proc.kill(), done };
|
|
1840
2168
|
}
|
|
1841
2169
|
function legacyGraphPath(projectDir) {
|
|
1842
|
-
const src =
|
|
2170
|
+
const src = join4(projectDir, "src");
|
|
1843
2171
|
return existsSync3(src) ? src : projectDir;
|
|
1844
2172
|
}
|
|
1845
2173
|
async function graphPath(projectDir, opts = {}) {
|
|
@@ -1851,9 +2179,9 @@ async function graphPath(projectDir, opts = {}) {
|
|
|
1851
2179
|
}
|
|
1852
2180
|
if (info.stacks?.length) {
|
|
1853
2181
|
const picked = opts.stack ? info.stacks.find((s) => s.name === opts.stack) : void 0;
|
|
1854
|
-
return
|
|
2182
|
+
return resolve2(projectDir, (picked ?? info.stacks[0]).src);
|
|
1855
2183
|
}
|
|
1856
|
-
if (info.sourceDir) return
|
|
2184
|
+
if (info.sourceDir) return resolve2(projectDir, info.sourceDir);
|
|
1857
2185
|
return legacyGraphPath(projectDir);
|
|
1858
2186
|
}
|
|
1859
2187
|
function graphArgs(src, format, opts, components) {
|
|
@@ -1864,7 +2192,7 @@ async function graphIr(projectDir, opts = {}) {
|
|
|
1864
2192
|
return runChantJson(graphArgs(src, "ir", opts, false), projectDir, envOverridesFor(opts));
|
|
1865
2193
|
}
|
|
1866
2194
|
async function clusterRootGraphIr(projectDir, opts = {}) {
|
|
1867
|
-
if (!existsSync3(
|
|
2195
|
+
if (!existsSync3(join4(projectDir, "cluster"))) return void 0;
|
|
1868
2196
|
const { live: _live, overlay: _overlay, env: _env, ...sourceOnly } = opts;
|
|
1869
2197
|
try {
|
|
1870
2198
|
return await runChantJson(graphArgs("cluster", "ir", sourceOnly, false), projectDir, envOverridesFor(sourceOnly));
|
|
@@ -1882,6 +2210,9 @@ function componentStatusArgs(env) {
|
|
|
1882
2210
|
function componentStatus(projectDir, env, opts = {}) {
|
|
1883
2211
|
return runChantJson(componentStatusArgs(env), projectDir, envOverridesFor(opts));
|
|
1884
2212
|
}
|
|
2213
|
+
function ciWorkflowName(env) {
|
|
2214
|
+
return `chant-components-${env}`;
|
|
2215
|
+
}
|
|
1885
2216
|
var CI_FORGES = ["gitlab", "github", "forgejo"];
|
|
1886
2217
|
function ciForgeFor(lexicons) {
|
|
1887
2218
|
return CI_FORGES.find((forge) => lexicons.includes(forge));
|
|
@@ -1899,7 +2230,7 @@ function parseCiPipeline(stdout) {
|
|
|
1899
2230
|
const script = Array.isArray(props?.script) ? props.script.map(String) : [];
|
|
1900
2231
|
return { ...j, script };
|
|
1901
2232
|
});
|
|
1902
|
-
return { stages: parsed.stages, jobs };
|
|
2233
|
+
return { stages: parsed.stages, jobs, ...typeof parsed.env === "string" && parsed.env ? { env: parsed.env } : {} };
|
|
1903
2234
|
}
|
|
1904
2235
|
function ciPipeline(projectDir, opts = {}, forge = "gitlab") {
|
|
1905
2236
|
const args = ciPipelineArgs(opts, forge);
|
|
@@ -1920,6 +2251,9 @@ function lifecycleDiffArgs(env) {
|
|
|
1920
2251
|
function lifecycleDiffLive(projectDir, env, opts = {}) {
|
|
1921
2252
|
return runChantJson(lifecycleDiffArgs(env), projectDir, envOverridesFor(opts));
|
|
1922
2253
|
}
|
|
2254
|
+
function carveStatus(from, projectDir) {
|
|
2255
|
+
return runChantJson(carveStatusArgs(from), projectDir);
|
|
2256
|
+
}
|
|
1923
2257
|
function helmRendersArgs() {
|
|
1924
2258
|
return ["helm", "renders", "--json"];
|
|
1925
2259
|
}
|
|
@@ -1996,8 +2330,8 @@ function mergeClusterRoot(ir, clusterIr, running) {
|
|
|
1996
2330
|
}
|
|
1997
2331
|
|
|
1998
2332
|
// src/helm-releases.ts
|
|
1999
|
-
import { existsSync as existsSync4, readdirSync, readFileSync as
|
|
2000
|
-
import { join as
|
|
2333
|
+
import { existsSync as existsSync4, readdirSync as readdirSync2, readFileSync as readFileSync5, statSync as statSync2 } from "node:fs";
|
|
2334
|
+
import { join as join5 } from "node:path";
|
|
2001
2335
|
var RELEASE_KEY = /^release\/([^/]+)\/(.+)$/;
|
|
2002
2336
|
function declaredChartNames(ir) {
|
|
2003
2337
|
const names = /* @__PURE__ */ new Set();
|
|
@@ -2017,15 +2351,15 @@ function matchesDeclaredChart(chart, declared) {
|
|
|
2017
2351
|
}
|
|
2018
2352
|
function discoverReleaseUnits(projectDir) {
|
|
2019
2353
|
const owners = /* @__PURE__ */ new Map();
|
|
2020
|
-
const roots = [
|
|
2354
|
+
const roots = [join5(projectDir, "src"), join5(projectDir, "ops")];
|
|
2021
2355
|
const files = [];
|
|
2022
2356
|
const walk = (dir, depth) => {
|
|
2023
2357
|
if (depth > 4 || !existsSync4(dir)) return;
|
|
2024
|
-
for (const f of
|
|
2025
|
-
const p =
|
|
2358
|
+
for (const f of readdirSync2(dir)) {
|
|
2359
|
+
const p = join5(dir, f);
|
|
2026
2360
|
let s;
|
|
2027
2361
|
try {
|
|
2028
|
-
s =
|
|
2362
|
+
s = statSync2(p);
|
|
2029
2363
|
} catch {
|
|
2030
2364
|
continue;
|
|
2031
2365
|
}
|
|
@@ -2037,7 +2371,7 @@ function discoverReleaseUnits(projectDir) {
|
|
|
2037
2371
|
for (const file of files) {
|
|
2038
2372
|
let content;
|
|
2039
2373
|
try {
|
|
2040
|
-
content =
|
|
2374
|
+
content = readFileSync5(file, "utf8");
|
|
2041
2375
|
} catch {
|
|
2042
2376
|
continue;
|
|
2043
2377
|
}
|
|
@@ -2090,8 +2424,8 @@ function synthesizeHelmReleases(ir, observed, owners) {
|
|
|
2090
2424
|
|
|
2091
2425
|
// src/gh-run.ts
|
|
2092
2426
|
import { spawn as spawn2 } from "node:child_process";
|
|
2093
|
-
import { readdirSync as
|
|
2094
|
-
import { join as
|
|
2427
|
+
import { readdirSync as readdirSync3, readFileSync as readFileSync6 } from "node:fs";
|
|
2428
|
+
import { join as join6 } from "node:path";
|
|
2095
2429
|
|
|
2096
2430
|
// src/ci-run.ts
|
|
2097
2431
|
function pipelineProgress(pipeline) {
|
|
@@ -2145,19 +2479,19 @@ function finishPipelineProgress(state, exitCode) {
|
|
|
2145
2479
|
}
|
|
2146
2480
|
|
|
2147
2481
|
// src/gh-run.ts
|
|
2148
|
-
var defaultGhExec = (args) => new Promise((
|
|
2482
|
+
var defaultGhExec = (args) => new Promise((resolve10) => {
|
|
2149
2483
|
let out = "";
|
|
2150
2484
|
let proc;
|
|
2151
2485
|
try {
|
|
2152
2486
|
proc = spawn2("gh", args, { stdio: ["ignore", "pipe", "pipe"] });
|
|
2153
2487
|
} catch {
|
|
2154
|
-
|
|
2488
|
+
resolve10({ code: 127, out: "" });
|
|
2155
2489
|
return;
|
|
2156
2490
|
}
|
|
2157
2491
|
proc.stdout.on("data", (d) => out += d);
|
|
2158
2492
|
proc.stderr.on("data", (d) => out += d);
|
|
2159
|
-
proc.on("error", () =>
|
|
2160
|
-
proc.on("close", (code) =>
|
|
2493
|
+
proc.on("error", () => resolve10({ code: 127, out }));
|
|
2494
|
+
proc.on("close", (code) => resolve10({ code: code ?? 1, out }));
|
|
2161
2495
|
});
|
|
2162
2496
|
async function ghReady(exec = defaultGhExec) {
|
|
2163
2497
|
const { code, out } = await exec(["auth", "status"]);
|
|
@@ -2176,31 +2510,49 @@ function parseWorkflow(file, text) {
|
|
|
2176
2510
|
const jobs = d.jobs && typeof d.jobs === "object" && !Array.isArray(d.jobs) ? Object.keys(d.jobs) : [];
|
|
2177
2511
|
const on = d.on ?? d[true];
|
|
2178
2512
|
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 };
|
|
2513
|
+
return { file, jobIds: jobs, dispatchable, ...typeof d.name === "string" && d.name ? { name: d.name } : {} };
|
|
2180
2514
|
}
|
|
2181
|
-
function
|
|
2182
|
-
const dir =
|
|
2515
|
+
function committedWorkflows(projectDir) {
|
|
2516
|
+
const dir = join6(projectDir, ".github", "workflows");
|
|
2183
2517
|
let files;
|
|
2184
2518
|
try {
|
|
2185
|
-
files =
|
|
2519
|
+
files = readdirSync3(dir).filter((f) => f.endsWith(".yml") || f.endsWith(".yaml"));
|
|
2186
2520
|
} catch {
|
|
2187
|
-
return
|
|
2521
|
+
return [];
|
|
2188
2522
|
}
|
|
2189
|
-
const
|
|
2190
|
-
let best;
|
|
2523
|
+
const out = [];
|
|
2191
2524
|
for (const f of files) {
|
|
2192
|
-
let text;
|
|
2193
2525
|
try {
|
|
2194
|
-
|
|
2526
|
+
out.push(parseWorkflow(f, readFileSync6(join6(dir, f), "utf8")));
|
|
2195
2527
|
} catch {
|
|
2196
|
-
continue;
|
|
2197
2528
|
}
|
|
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
2529
|
}
|
|
2203
|
-
return
|
|
2530
|
+
return out;
|
|
2531
|
+
}
|
|
2532
|
+
function resolveWorkflow(projectDir, pipeline, designated) {
|
|
2533
|
+
const all = committedWorkflows(projectDir);
|
|
2534
|
+
if (designated) {
|
|
2535
|
+
const hit = all.find((w) => w.file === designated);
|
|
2536
|
+
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)` };
|
|
2537
|
+
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` };
|
|
2538
|
+
return { workflow: hit };
|
|
2539
|
+
}
|
|
2540
|
+
const dispatchable = all.filter((w) => w.dispatchable);
|
|
2541
|
+
if (pipeline.env) {
|
|
2542
|
+
const named = dispatchable.filter((w) => w.name === ciWorkflowName(pipeline.env));
|
|
2543
|
+
if (named.length === 1) return { workflow: named[0] };
|
|
2544
|
+
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` };
|
|
2545
|
+
}
|
|
2546
|
+
const wanted = new Set(pipeline.jobs.map((j) => j.jobName));
|
|
2547
|
+
const scored = dispatchable.map((w) => ({ w, overlap: w.jobIds.filter((id) => wanted.has(id)).length })).filter((x) => x.overlap > 0).sort((a, b) => b.overlap - a.overlap);
|
|
2548
|
+
if (scored.length === 0) {
|
|
2549
|
+
return { reason: "no committed workflow_dispatch workflow whose jobs match the generated pipeline \u2014 commit one (or add `workflow_dispatch:` to its `on:` block)" };
|
|
2550
|
+
}
|
|
2551
|
+
if (scored.length > 1 && scored[1].overlap === scored[0].overlap) {
|
|
2552
|
+
const tied = scored.filter((x) => x.overlap === scored[0].overlap).map((x) => x.w.file);
|
|
2553
|
+
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` };
|
|
2554
|
+
}
|
|
2555
|
+
return { workflow: scored[0].w };
|
|
2204
2556
|
}
|
|
2205
2557
|
async function latestRunId(workflowFile, exec = defaultGhExec) {
|
|
2206
2558
|
const { code, out } = await exec(["run", "list", "--workflow", workflowFile, "--limit", "1", "--json", "databaseId"]);
|
|
@@ -2234,6 +2586,7 @@ function runViewToProgress(pipeline, view) {
|
|
|
2234
2586
|
return { ...w, status };
|
|
2235
2587
|
})
|
|
2236
2588
|
};
|
|
2589
|
+
if (view.url) state = { ...state, url: view.url };
|
|
2237
2590
|
if (view.status === "completed") {
|
|
2238
2591
|
return { ...state, status: view.conclusion === "success" ? "ok" : "failed" };
|
|
2239
2592
|
}
|
|
@@ -2267,8 +2620,9 @@ async function dispatchAndFollow(workflow, pipeline, ref, deps) {
|
|
|
2267
2620
|
return 1;
|
|
2268
2621
|
}
|
|
2269
2622
|
deps.onLine(`\u25CF run ${runId} started \u2014 following`);
|
|
2623
|
+
let urlSaid = false;
|
|
2270
2624
|
for (; ; ) {
|
|
2271
|
-
const { code, out } = await exec(["run", "view", String(runId), "--json", "status,conclusion,jobs"]);
|
|
2625
|
+
const { code, out } = await exec(["run", "view", String(runId), "--json", "status,conclusion,jobs,url"]);
|
|
2272
2626
|
if (code === 0) {
|
|
2273
2627
|
let view;
|
|
2274
2628
|
try {
|
|
@@ -2277,6 +2631,10 @@ async function dispatchAndFollow(workflow, pipeline, ref, deps) {
|
|
|
2277
2631
|
view = void 0;
|
|
2278
2632
|
}
|
|
2279
2633
|
if (view) {
|
|
2634
|
+
if (view.url && !urlSaid) {
|
|
2635
|
+
urlSaid = true;
|
|
2636
|
+
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`);
|
|
2637
|
+
}
|
|
2280
2638
|
deps.onProgress(runViewToProgress(pipeline, view));
|
|
2281
2639
|
if (view.status === "completed") {
|
|
2282
2640
|
deps.onLine(`\u25A0 run ${runId} ${view.conclusion ?? "completed"}`);
|
|
@@ -2313,15 +2671,15 @@ function joinCiProgress(ir, state) {
|
|
|
2313
2671
|
// src/operator.ts
|
|
2314
2672
|
var ANSI = /\x1b\[[0-9;]*m/g;
|
|
2315
2673
|
var strip = (s) => s.replace(ANSI, "");
|
|
2316
|
-
var
|
|
2674
|
+
var isRecord2 = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
2317
2675
|
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
2676
|
function readRow(value) {
|
|
2319
|
-
if (!
|
|
2677
|
+
if (!isRecord2(value)) return null;
|
|
2320
2678
|
if (typeof value.op !== "string" || typeof value.env !== "string") return null;
|
|
2321
2679
|
if (!Array.isArray(value.pendingGates)) return null;
|
|
2322
2680
|
const gates = [];
|
|
2323
2681
|
for (const g of value.pendingGates) {
|
|
2324
|
-
if (!
|
|
2682
|
+
if (!isRecord2(g) || typeof g.rule !== "string" || typeof g.gate !== "string") return null;
|
|
2325
2683
|
gates.push({
|
|
2326
2684
|
rule: g.rule,
|
|
2327
2685
|
gate: g.gate,
|
|
@@ -2333,9 +2691,9 @@ function readRow(value) {
|
|
|
2333
2691
|
...typeof g.url === "string" && g.url ? { url: g.url } : {}
|
|
2334
2692
|
});
|
|
2335
2693
|
}
|
|
2336
|
-
const tick =
|
|
2694
|
+
const tick = isRecord2(value.lastTick) ? value.lastTick : void 0;
|
|
2337
2695
|
const usable = tick && typeof tick.timestamp === "string" && typeof tick.log === "string";
|
|
2338
|
-
const lease =
|
|
2696
|
+
const lease = isRecord2(value.lease) ? value.lease : void 0;
|
|
2339
2697
|
return {
|
|
2340
2698
|
op: value.op,
|
|
2341
2699
|
env: value.env,
|
|
@@ -2433,7 +2791,7 @@ var CHANT = "chant";
|
|
|
2433
2791
|
var CONVERGE_TICK = "converge-tick";
|
|
2434
2792
|
var CRONJOB_KIND = "K8s::Batch::CronJob";
|
|
2435
2793
|
var NAMESPACE_KIND = "K8s::Core::Namespace";
|
|
2436
|
-
var rec = (v) =>
|
|
2794
|
+
var rec = (v) => isRecord2(v) ? v : void 0;
|
|
2437
2795
|
var str = (v) => typeof v === "string" && v ? v : void 0;
|
|
2438
2796
|
var labelsOf = (n) => rec(rec(n.attrs?.metadata)?.labels) ?? {};
|
|
2439
2797
|
function operatorHomes(ir) {
|
|
@@ -2541,10 +2899,10 @@ function tickFreshness(at, now = Date.now()) {
|
|
|
2541
2899
|
}
|
|
2542
2900
|
var RECONCILIATIONS = /* @__PURE__ */ new Set(["reconciled", "unrecorded", "stale", "drifted", "unknown"]);
|
|
2543
2901
|
function readVerdict(value) {
|
|
2544
|
-
if (!
|
|
2902
|
+
if (!isRecord2(value)) return null;
|
|
2545
2903
|
if (typeof value.component !== "string" || !value.component) return null;
|
|
2546
2904
|
if (typeof value.reconciliation !== "string" || !RECONCILIATIONS.has(value.reconciliation)) return null;
|
|
2547
|
-
const unobserved =
|
|
2905
|
+
const unobserved = isRecord2(value.unobserved) && typeof value.unobserved.reason === "string" ? value.unobserved : void 0;
|
|
2548
2906
|
return {
|
|
2549
2907
|
component: value.component,
|
|
2550
2908
|
reconciliation: value.reconciliation,
|
|
@@ -2655,12 +3013,12 @@ function operatorLogArgs(window) {
|
|
|
2655
3013
|
];
|
|
2656
3014
|
}
|
|
2657
3015
|
function readTickRecord(value) {
|
|
2658
|
-
if (!
|
|
3016
|
+
if (!isRecord2(value)) return null;
|
|
2659
3017
|
if (typeof value.op !== "string" || typeof value.timestamp !== "string" || typeof value.log !== "string") return null;
|
|
2660
3018
|
return value;
|
|
2661
3019
|
}
|
|
2662
3020
|
function readResolutionRecord(value) {
|
|
2663
|
-
if (!
|
|
3021
|
+
if (!isRecord2(value)) return null;
|
|
2664
3022
|
const needed = ["op", "gate", "resolvedBy", "timestamp"];
|
|
2665
3023
|
if (needed.some((k) => typeof value[k] !== "string")) return null;
|
|
2666
3024
|
return value;
|
|
@@ -2721,10 +3079,10 @@ function readOperatorLog(run3) {
|
|
|
2721
3079
|
remedy: "behold reads `{entries: [{kind, timestamp, record}], malformed: {converge, gates}}`."
|
|
2722
3080
|
}
|
|
2723
3081
|
});
|
|
2724
|
-
if (!
|
|
3082
|
+
if (!isRecord2(parsed) || !Array.isArray(parsed.entries)) return unreadable("no `entries` array");
|
|
2725
3083
|
const entries = [];
|
|
2726
3084
|
for (const value of parsed.entries) {
|
|
2727
|
-
if (!
|
|
3085
|
+
if (!isRecord2(value) || typeof value.timestamp !== "string") return unreadable("an entry with no timestamp");
|
|
2728
3086
|
if (value.kind === "tick") {
|
|
2729
3087
|
const record = readTickRecord(value.record);
|
|
2730
3088
|
if (!record) return unreadable("a tick entry with no `op`/`timestamp`/`log`");
|
|
@@ -2737,7 +3095,7 @@ function readOperatorLog(run3) {
|
|
|
2737
3095
|
return unreadable(`an entry of an unknown kind "${String(value.kind)}"`);
|
|
2738
3096
|
}
|
|
2739
3097
|
}
|
|
2740
|
-
const counts =
|
|
3098
|
+
const counts = isRecord2(parsed.malformed) ? parsed.malformed : {};
|
|
2741
3099
|
const count = (v) => typeof v === "number" && Number.isFinite(v) && v > 0 ? v : 0;
|
|
2742
3100
|
return { ok: true, entries, malformed: { converge: count(counts.converge), gates: count(counts.gates) } };
|
|
2743
3101
|
}
|
|
@@ -2871,6 +3229,50 @@ function componentStatusColor(row) {
|
|
|
2871
3229
|
return "neutral";
|
|
2872
3230
|
}
|
|
2873
3231
|
}
|
|
3232
|
+
function httpUrl(url) {
|
|
3233
|
+
if (typeof url !== "string" || !url) return void 0;
|
|
3234
|
+
try {
|
|
3235
|
+
const p = new URL(url);
|
|
3236
|
+
return p.protocol === "http:" || p.protocol === "https:" ? url : void 0;
|
|
3237
|
+
} catch {
|
|
3238
|
+
return void 0;
|
|
3239
|
+
}
|
|
3240
|
+
}
|
|
3241
|
+
function releaseOrigin(rec6) {
|
|
3242
|
+
const origin = rec6.runOrigin;
|
|
3243
|
+
if (origin && typeof origin.forge === "string") {
|
|
3244
|
+
const url = httpUrl(origin.url);
|
|
3245
|
+
return {
|
|
3246
|
+
forge: origin.forge,
|
|
3247
|
+
originSource: "record",
|
|
3248
|
+
...origin.repo ? { repo: origin.repo } : {},
|
|
3249
|
+
...url ? { url } : {}
|
|
3250
|
+
};
|
|
3251
|
+
}
|
|
3252
|
+
return { forge: rec6.runId.startsWith("local-") ? "local" : "unknown", originSource: "inferred" };
|
|
3253
|
+
}
|
|
3254
|
+
var FORGE_WORD = {
|
|
3255
|
+
github: "github",
|
|
3256
|
+
gitlab: "gitlab",
|
|
3257
|
+
op: "op",
|
|
3258
|
+
local: "laptop",
|
|
3259
|
+
unknown: "?"
|
|
3260
|
+
};
|
|
3261
|
+
function releaseChip(status) {
|
|
3262
|
+
return `${status.gitSha.slice(0, 7)} via ${FORGE_WORD[status.forge]}`;
|
|
3263
|
+
}
|
|
3264
|
+
function releaseStatus(rec6) {
|
|
3265
|
+
if (!rec6) return void 0;
|
|
3266
|
+
return {
|
|
3267
|
+
runId: rec6.runId,
|
|
3268
|
+
...releaseOrigin(rec6),
|
|
3269
|
+
gitSha: rec6.gitSha,
|
|
3270
|
+
digest: rec6.digest,
|
|
3271
|
+
timestamp: rec6.timestamp,
|
|
3272
|
+
actor: rec6.actor,
|
|
3273
|
+
...rec6.approver ? { approver: rec6.approver } : {}
|
|
3274
|
+
};
|
|
3275
|
+
}
|
|
2874
3276
|
function joinComponentStatus(ir, rows) {
|
|
2875
3277
|
const byComponent = new Map(rows.map((r) => [r.component, r]));
|
|
2876
3278
|
return {
|
|
@@ -2878,10 +3280,12 @@ function joinComponentStatus(ir, rows) {
|
|
|
2878
3280
|
nodes: ir.nodes.map((n) => {
|
|
2879
3281
|
const row = byComponent.get(n.id);
|
|
2880
3282
|
if (!row) return n;
|
|
3283
|
+
const release = releaseStatus(row.recorded);
|
|
2881
3284
|
return {
|
|
2882
3285
|
...n,
|
|
2883
3286
|
attrs: {
|
|
2884
3287
|
...n.attrs,
|
|
3288
|
+
...release ? { release: releaseChip(release), _release: release } : {},
|
|
2885
3289
|
_status: componentStatusColor(row),
|
|
2886
3290
|
_liveStatus: {
|
|
2887
3291
|
reconciliation: row.reconciliation,
|
|
@@ -3323,7 +3727,9 @@ function applyHelmArtifacts(ir, observed) {
|
|
|
3323
3727
|
release: key.replace(/^release\//, ""),
|
|
3324
3728
|
...o.status ? { status: o.status } : {},
|
|
3325
3729
|
...typeof o.attributes?.revision === "string" ? { revision: o.attributes.revision } : {},
|
|
3326
|
-
...typeof o.attributes?.chart === "string" ? { chart: o.attributes.chart } : {}
|
|
3730
|
+
...typeof o.attributes?.chart === "string" ? { chart: o.attributes.chart } : {},
|
|
3731
|
+
...typeof o.attributes?.inputDigest === "string" ? { inputDigest: o.attributes.inputDigest } : {},
|
|
3732
|
+
...typeof o.attributes?.contentDigest === "string" ? { contentDigest: o.attributes.contentDigest } : {}
|
|
3327
3733
|
};
|
|
3328
3734
|
attrs._artifact = match;
|
|
3329
3735
|
attrs._status = o.status === "deployed" ? "good" : "warn";
|
|
@@ -3332,37 +3738,35 @@ function applyHelmArtifacts(ir, observed) {
|
|
|
3332
3738
|
return { charts, installed };
|
|
3333
3739
|
}
|
|
3334
3740
|
|
|
3335
|
-
// src/
|
|
3336
|
-
import { statSync as
|
|
3337
|
-
import {
|
|
3338
|
-
import { join as join7, resolve as resolve3, sep } from "node:path";
|
|
3339
|
-
import { composeStacks, shortStackNames } from "@intentius/pinhole";
|
|
3741
|
+
// src/carve-discovery.ts
|
|
3742
|
+
import { readdirSync as readdirSync5, statSync as statSync4 } from "node:fs";
|
|
3743
|
+
import { join as join8 } from "node:path";
|
|
3340
3744
|
|
|
3341
3745
|
// src/member-ir.ts
|
|
3342
3746
|
import { createHash } from "node:crypto";
|
|
3343
|
-
import { readdirSync as
|
|
3344
|
-
import { join as
|
|
3747
|
+
import { readdirSync as readdirSync4, statSync as statSync3 } from "node:fs";
|
|
3748
|
+
import { join as join7, relative as relative2, resolve as resolve3 } from "node:path";
|
|
3345
3749
|
var SKIP = /* @__PURE__ */ new Set(["node_modules", "dist", ".git"]);
|
|
3346
3750
|
function memberIrCacheSize(env = process.env) {
|
|
3347
3751
|
const override = Number.parseInt(env.BEHOLD_ESTATE_IR_CACHE ?? "", 10);
|
|
3348
3752
|
return Number.isInteger(override) && override >= 0 ? override : 64;
|
|
3349
3753
|
}
|
|
3350
3754
|
function memberSourceStamp(dir) {
|
|
3351
|
-
const root =
|
|
3755
|
+
const root = resolve3(dir);
|
|
3352
3756
|
const h = createHash("sha1");
|
|
3353
3757
|
let any = false;
|
|
3354
3758
|
const walk = (at) => {
|
|
3355
|
-
const entries =
|
|
3759
|
+
const entries = readdirSync4(at, { withFileTypes: true }).sort((a, b) => a.name < b.name ? -1 : 1);
|
|
3356
3760
|
for (const e of entries) {
|
|
3357
3761
|
if (SKIP.has(e.name)) continue;
|
|
3358
|
-
const path =
|
|
3762
|
+
const path = join7(at, e.name);
|
|
3359
3763
|
if (e.isDirectory()) {
|
|
3360
3764
|
walk(path);
|
|
3361
3765
|
continue;
|
|
3362
3766
|
}
|
|
3363
3767
|
if (!e.isFile()) continue;
|
|
3364
|
-
const st =
|
|
3365
|
-
h.update(`${
|
|
3768
|
+
const st = statSync3(path);
|
|
3769
|
+
h.update(`${relative2(root, path)}\0${st.mtimeMs}\0${st.size}
|
|
3366
3770
|
`);
|
|
3367
3771
|
any = true;
|
|
3368
3772
|
}
|
|
@@ -3377,7 +3781,7 @@ function memberSourceStamp(dir) {
|
|
|
3377
3781
|
function cacheKey(dir, opts) {
|
|
3378
3782
|
const chant = resolveChant(dir);
|
|
3379
3783
|
const canonical = JSON.stringify(opts, Object.keys(opts).sort());
|
|
3380
|
-
return `${
|
|
3784
|
+
return `${resolve3(dir)}\0${chant.bin}\0${chant.version ?? ""}\0${canonical}`;
|
|
3381
3785
|
}
|
|
3382
3786
|
var cache = /* @__PURE__ */ new Map();
|
|
3383
3787
|
var hits = 0;
|
|
@@ -3387,7 +3791,7 @@ function invalidateMember(dir) {
|
|
|
3387
3791
|
cache.clear();
|
|
3388
3792
|
return;
|
|
3389
3793
|
}
|
|
3390
|
-
const root =
|
|
3794
|
+
const root = resolve3(dir);
|
|
3391
3795
|
for (const [key, entry] of cache) if (entry.dir === root) cache.delete(key);
|
|
3392
3796
|
}
|
|
3393
3797
|
async function memberIr(dir, opts = {}) {
|
|
@@ -3402,18 +3806,82 @@ async function memberIr(dir, opts = {}) {
|
|
|
3402
3806
|
cache.set(key, hit);
|
|
3403
3807
|
return structuredClone(hit.ir);
|
|
3404
3808
|
}
|
|
3405
|
-
misses++;
|
|
3406
|
-
const ir = await graphIr(dir, opts);
|
|
3407
|
-
if (stamp === void 0 || memberSourceStamp(dir) !== stamp) return ir;
|
|
3408
|
-
cache.delete(key);
|
|
3409
|
-
cache.set(key, { dir:
|
|
3410
|
-
for (const oldest of cache.keys()) {
|
|
3411
|
-
if (cache.size <= size) break;
|
|
3412
|
-
cache.delete(oldest);
|
|
3809
|
+
misses++;
|
|
3810
|
+
const ir = await graphIr(dir, opts);
|
|
3811
|
+
if (stamp === void 0 || memberSourceStamp(dir) !== stamp) return ir;
|
|
3812
|
+
cache.delete(key);
|
|
3813
|
+
cache.set(key, { dir: resolve3(dir), stamp, ir });
|
|
3814
|
+
for (const oldest of cache.keys()) {
|
|
3815
|
+
if (cache.size <= size) break;
|
|
3816
|
+
cache.delete(oldest);
|
|
3817
|
+
}
|
|
3818
|
+
return structuredClone(ir);
|
|
3819
|
+
}
|
|
3820
|
+
|
|
3821
|
+
// src/carve-discovery.ts
|
|
3822
|
+
function carveStatusReader(projectDir) {
|
|
3823
|
+
return async (dir) => {
|
|
3824
|
+
const via = projectDir ?? dir;
|
|
3825
|
+
if (!meetsFloor(resolveChant(via).version, CARVE_STATUS_FLOOR)) return void 0;
|
|
3826
|
+
return carveStatus(dir, via).catch(() => void 0);
|
|
3827
|
+
};
|
|
3828
|
+
}
|
|
3829
|
+
var SKIP2 = /* @__PURE__ */ new Set(["node_modules", ".git", "dist", ".chant", ".terraform", "cdk.out"]);
|
|
3830
|
+
function hasAnyCarveManifest(dir, io = fsIo) {
|
|
3831
|
+
const walk = (d) => {
|
|
3832
|
+
let entries;
|
|
3833
|
+
try {
|
|
3834
|
+
entries = io.readdir(d);
|
|
3835
|
+
} catch {
|
|
3836
|
+
return false;
|
|
3837
|
+
}
|
|
3838
|
+
for (const name of entries) {
|
|
3839
|
+
if (name.endsWith(CARVE_MANIFEST_SUFFIX)) return true;
|
|
3840
|
+
if (name.startsWith(".") || SKIP2.has(name)) continue;
|
|
3841
|
+
const full = join8(d, name);
|
|
3842
|
+
if (io.isDirectory(full) && walk(full)) return true;
|
|
3843
|
+
}
|
|
3844
|
+
return false;
|
|
3845
|
+
};
|
|
3846
|
+
return walk(dir);
|
|
3847
|
+
}
|
|
3848
|
+
var fsIo = {
|
|
3849
|
+
readdir: (d) => readdirSync5(d),
|
|
3850
|
+
isDirectory: (p) => {
|
|
3851
|
+
try {
|
|
3852
|
+
return statSync4(p).isDirectory();
|
|
3853
|
+
} catch {
|
|
3854
|
+
return false;
|
|
3855
|
+
}
|
|
3856
|
+
}
|
|
3857
|
+
};
|
|
3858
|
+
var cache2 = /* @__PURE__ */ new Map();
|
|
3859
|
+
async function carveStatesFor(dir) {
|
|
3860
|
+
const stamp = memberSourceStamp(dir);
|
|
3861
|
+
const held = cache2.get(dir);
|
|
3862
|
+
if (held && stamp !== void 0 && held.stamp === stamp) return held.states;
|
|
3863
|
+
const states = hasAnyCarveManifest(dir) ? await discoverCarveStates([dir], carveStatusReader(), nodeCarveIo, MANIFEST_SCAN_DEPTH) : /* @__PURE__ */ new Map();
|
|
3864
|
+
if (stamp !== void 0) cache2.set(dir, { stamp, states });
|
|
3865
|
+
return states;
|
|
3866
|
+
}
|
|
3867
|
+
async function carveStatesUnder(dirs) {
|
|
3868
|
+
const rank = { emitted: 0, bridged: 1, applied: 2 };
|
|
3869
|
+
const out = /* @__PURE__ */ new Map();
|
|
3870
|
+
for (const dir of dirs) {
|
|
3871
|
+
for (const s of (await carveStatesFor(dir)).values()) {
|
|
3872
|
+
const held = out.get(s.target);
|
|
3873
|
+
if (!held || rank[s.stage] > rank[held.stage]) out.set(s.target, s);
|
|
3874
|
+
}
|
|
3413
3875
|
}
|
|
3414
|
-
return
|
|
3876
|
+
return out;
|
|
3415
3877
|
}
|
|
3416
3878
|
|
|
3879
|
+
// src/estate.ts
|
|
3880
|
+
import { statSync as statSync5 } from "node:fs";
|
|
3881
|
+
import { availableParallelism } from "node:os";
|
|
3882
|
+
import { join as join9, resolve as resolve4, sep } from "node:path";
|
|
3883
|
+
import { composeStacks, shortStackNames } from "@intentius/pinhole";
|
|
3884
|
+
|
|
3417
3885
|
// src/zoom-notes.ts
|
|
3418
3886
|
function zoomNote(zoom, ir, compositeEdgesAttached) {
|
|
3419
3887
|
const empty = ir.nodes.length === 0;
|
|
@@ -3516,7 +3984,10 @@ async function composeEstate(projectDirs, opts = {}) {
|
|
|
3516
3984
|
const names = shortStackNames(projectDirs);
|
|
3517
3985
|
const stacks = await mapPool(projectDirs, estateReadPool(projectDirs.length), async (dir, i) => ({
|
|
3518
3986
|
name: names[i],
|
|
3519
|
-
|
|
3987
|
+
// chant#2040: the manifests a member holds name the files its carved
|
|
3988
|
+
// entities were emitted into — joined here, where the member's root is
|
|
3989
|
+
// known, onto a COPY (the member IR is the #312 cache's own object).
|
|
3990
|
+
ir: joinCarvedSources(await memberIr(dir, opts), (await carveStatesFor(dir)).values(), dir).ir
|
|
3520
3991
|
}));
|
|
3521
3992
|
return composeStacks(stacks);
|
|
3522
3993
|
}
|
|
@@ -3537,7 +4008,7 @@ var rec3 = (v) => v && typeof v === "object" && !Array.isArray(v) ? v : void 0;
|
|
|
3537
4008
|
var str3 = (v) => typeof v === "string" && v.length > 0 ? v : void 0;
|
|
3538
4009
|
var realIsDir = (p) => {
|
|
3539
4010
|
try {
|
|
3540
|
-
return
|
|
4011
|
+
return statSync5(p).isDirectory();
|
|
3541
4012
|
} catch {
|
|
3542
4013
|
return false;
|
|
3543
4014
|
}
|
|
@@ -3567,11 +4038,11 @@ function declaredSegments(path) {
|
|
|
3567
4038
|
function pathAlignment(declared, dir, isDir = realIsDir) {
|
|
3568
4039
|
const want = declaredSegments(declared);
|
|
3569
4040
|
if (!want) return 0;
|
|
3570
|
-
const root =
|
|
4041
|
+
const root = resolve4(dir);
|
|
3571
4042
|
const have = root.split(sep).filter(Boolean);
|
|
3572
4043
|
for (let k = Math.min(want.length, have.length); k >= 1; k--) {
|
|
3573
4044
|
if (!have.slice(-k).every((s, i) => s === want[i])) continue;
|
|
3574
|
-
if (!isDir(
|
|
4045
|
+
if (!isDir(join9(root, ...want.slice(k)))) continue;
|
|
3575
4046
|
return k;
|
|
3576
4047
|
}
|
|
3577
4048
|
return 0;
|
|
@@ -3601,10 +4072,10 @@ function joinNamespaceBindings(members, isDir = realIsDir) {
|
|
|
3601
4072
|
if (!winner || tied) continue;
|
|
3602
4073
|
const prev = best.get(winner.dir);
|
|
3603
4074
|
if (prev === "ambiguous") continue;
|
|
3604
|
-
const
|
|
3605
|
-
if (!prev) best.set(winner.dir, { score: winner.score, join:
|
|
4075
|
+
const join22 = { dir: winner.dir, namespace: b.namespace, path: b.path, declaredBy: b.declaredBy };
|
|
4076
|
+
if (!prev) best.set(winner.dir, { score: winner.score, join: join22 });
|
|
3606
4077
|
else if (prev.join.namespace !== b.namespace) best.set(winner.dir, "ambiguous");
|
|
3607
|
-
else if (winner.score > prev.score) best.set(winner.dir, { score: winner.score, join:
|
|
4078
|
+
else if (winner.score > prev.score) best.set(winner.dir, { score: winner.score, join: join22 });
|
|
3608
4079
|
}
|
|
3609
4080
|
return [...best.values()].filter((v) => v !== "ambiguous").map((v) => v.join);
|
|
3610
4081
|
}
|
|
@@ -3645,7 +4116,7 @@ async function composeEstateOverlay(projectDirs, opts, classify) {
|
|
|
3645
4116
|
const reason = firstLine2(err);
|
|
3646
4117
|
try {
|
|
3647
4118
|
const { env: _env, live: _live, overlay: _overlay, ...srcOpts } = opts;
|
|
3648
|
-
const src = await memberIr(dir, srcOpts);
|
|
4119
|
+
const src = joinCarvedSources(await memberIr(dir, srcOpts), (await carveStatesFor(dir)).values(), dir).ir;
|
|
3649
4120
|
for (const n of src.nodes) n.attrs = { ...n.attrs, _status: "neutral", _unobserved: reason };
|
|
3650
4121
|
stacks[i] = { name, ir: src };
|
|
3651
4122
|
unobserved.push({ name, reason });
|
|
@@ -3668,6 +4139,36 @@ async function composeEstateOverlay(projectDirs, opts, classify) {
|
|
|
3668
4139
|
}
|
|
3669
4140
|
|
|
3670
4141
|
// src/helm-drift.ts
|
|
4142
|
+
function renderIdentity(record, observed) {
|
|
4143
|
+
const declared = {
|
|
4144
|
+
...record.inputDigest ? { inputDigest: record.inputDigest } : {},
|
|
4145
|
+
...record.contentDigest ? { contentDigest: record.contentDigest } : {}
|
|
4146
|
+
};
|
|
4147
|
+
const a = observed?.observation.attributes ?? {};
|
|
4148
|
+
const obs = {
|
|
4149
|
+
...observed ? { release: observed.key.replace(/^release\//, "") } : {},
|
|
4150
|
+
...typeof a.inputDigest === "string" ? { inputDigest: a.inputDigest } : {},
|
|
4151
|
+
...typeof a.contentDigest === "string" ? { contentDigest: a.contentDigest } : {}
|
|
4152
|
+
};
|
|
4153
|
+
const base = { declared, observed: obs };
|
|
4154
|
+
if (!declared.inputDigest && !declared.contentDigest) return { match: "unpinned", ...base };
|
|
4155
|
+
if (!obs.inputDigest && !obs.contentDigest) return { match: "unrecorded", ...base };
|
|
4156
|
+
if (declared.contentDigest && obs.contentDigest) {
|
|
4157
|
+
return { match: declared.contentDigest === obs.contentDigest ? "content" : "mismatch", ...base };
|
|
4158
|
+
}
|
|
4159
|
+
if (declared.inputDigest && obs.inputDigest) {
|
|
4160
|
+
return { match: declared.inputDigest === obs.inputDigest ? "input" : "mismatch", ...base };
|
|
4161
|
+
}
|
|
4162
|
+
return { match: "unrecorded", ...base };
|
|
4163
|
+
}
|
|
4164
|
+
function observedReleaseFor(record, observed, namespace) {
|
|
4165
|
+
if (!observed) return void 0;
|
|
4166
|
+
const candidates = Object.entries(observed).filter(([key2]) => key2.startsWith("release/") && key2.split("/").pop() === record.name);
|
|
4167
|
+
if (candidates.length === 0) return void 0;
|
|
4168
|
+
const scoped = namespace ? candidates.find(([key2]) => key2 === `release/${namespace}/${record.name}`) : void 0;
|
|
4169
|
+
const [key, observation] = scoped ?? candidates[0];
|
|
4170
|
+
return { key, observation };
|
|
4171
|
+
}
|
|
3671
4172
|
function countChanges(docs) {
|
|
3672
4173
|
return docs.reduce((n, d) => n + d.changes.length, 0);
|
|
3673
4174
|
}
|
|
@@ -3690,8 +4191,9 @@ function provenanceOf(live) {
|
|
|
3690
4191
|
sourceRef: m.sourceRef
|
|
3691
4192
|
};
|
|
3692
4193
|
}
|
|
3693
|
-
function helmRenderReport(record, live) {
|
|
3694
|
-
const
|
|
4194
|
+
function helmRenderReport(record, live, observed) {
|
|
4195
|
+
const identity = observed ? { identity: renderIdentity(record, observedReleaseFor(record, observed, live?.manifest.namespace)) } : {};
|
|
4196
|
+
const base = { render: record.name, chart: record.chart, ...identity };
|
|
3695
4197
|
if (!live) {
|
|
3696
4198
|
return { ...base, verdict: "unobserved", reason: record.contentDigest ? "no-stored-render" : "unpinned" };
|
|
3697
4199
|
}
|
|
@@ -3715,21 +4217,24 @@ function helmRenderReport(record, live) {
|
|
|
3715
4217
|
}
|
|
3716
4218
|
return { ...base, verdict: "in-sync", counts, provenance };
|
|
3717
4219
|
}
|
|
3718
|
-
async function readHelmRenderDrift(projectDir, env, opts = {}) {
|
|
4220
|
+
async function readHelmRenderDrift(projectDir, env, opts = {}, observed) {
|
|
3719
4221
|
const report = await helmRenders(projectDir, opts).catch(() => void 0);
|
|
3720
4222
|
const records = report?.records ?? [];
|
|
3721
4223
|
if (records.length === 0) return [];
|
|
3722
4224
|
return mapPool(records, estateReadPool(records.length), async (record) => {
|
|
3723
|
-
if (!record.contentDigest) return helmRenderReport(record, void 0);
|
|
4225
|
+
if (!record.contentDigest) return helmRenderReport(record, void 0, observed);
|
|
3724
4226
|
const live = await helmRenderDiffLive(projectDir, record.contentDigest, env, opts).catch(() => void 0);
|
|
3725
|
-
return helmRenderReport(record, live);
|
|
4227
|
+
return helmRenderReport(record, live, observed);
|
|
3726
4228
|
});
|
|
3727
4229
|
}
|
|
3728
4230
|
function chartNameOf2(node) {
|
|
3729
4231
|
const name = node.attrs?.name;
|
|
3730
4232
|
return typeof name === "string" && name.length > 0 ? name : void 0;
|
|
3731
4233
|
}
|
|
3732
|
-
var SEVERITY = { drifted:
|
|
4234
|
+
var SEVERITY = { drifted: 3, unobserved: 1, "in-sync": 0 };
|
|
4235
|
+
function severityOf(r) {
|
|
4236
|
+
return Math.max(SEVERITY[r.verdict], r.identity?.match === "mismatch" ? 2 : 0);
|
|
4237
|
+
}
|
|
3733
4238
|
function applyHelmRenderDrift(ir, reports) {
|
|
3734
4239
|
if (reports.length === 0) return { charts: 0, drifted: 0 };
|
|
3735
4240
|
const byChart = /* @__PURE__ */ new Map();
|
|
@@ -3745,11 +4250,11 @@ function applyHelmRenderDrift(ir, reports) {
|
|
|
3745
4250
|
const chartName = chartNameOf2(node);
|
|
3746
4251
|
const matched = chartName ? byChart.get(chartName) : void 0;
|
|
3747
4252
|
if (!matched || matched.length === 0) continue;
|
|
3748
|
-
const worst = matched.reduce((a, b) =>
|
|
4253
|
+
const worst = matched.reduce((a, b) => severityOf(b) > severityOf(a) ? b : a);
|
|
3749
4254
|
const attrs = node.attrs ??= {};
|
|
3750
4255
|
attrs._renderDrift = matched.length === 1 ? worst : { ...worst, renders: matched };
|
|
3751
4256
|
charts++;
|
|
3752
|
-
if (worst.verdict === "drifted") {
|
|
4257
|
+
if (worst.verdict === "drifted" || matched.some((r) => r.identity?.match === "mismatch")) {
|
|
3753
4258
|
attrs._status = "warn";
|
|
3754
4259
|
drifted++;
|
|
3755
4260
|
}
|
|
@@ -4289,7 +4794,7 @@ function projectHelmLogical(ir) {
|
|
|
4289
4794
|
|
|
4290
4795
|
// src/logical-kustomize.ts
|
|
4291
4796
|
import { existsSync as existsSync5 } from "node:fs";
|
|
4292
|
-
import { join as
|
|
4797
|
+
import { join as join10 } from "node:path";
|
|
4293
4798
|
function overlayBoxTitle(name) {
|
|
4294
4799
|
return `overlay ${name}`;
|
|
4295
4800
|
}
|
|
@@ -4309,7 +4814,7 @@ function kustomizationRoot(bases, dir, exists) {
|
|
|
4309
4814
|
let current = dir;
|
|
4310
4815
|
for (let i = 0; i < 4 && current; i++) {
|
|
4311
4816
|
for (const base of bases) {
|
|
4312
|
-
if (exists(
|
|
4817
|
+
if (exists(join10(base, current, "kustomization.yaml")) || exists(join10(base, current, "kustomization.yml"))) {
|
|
4313
4818
|
return current;
|
|
4314
4819
|
}
|
|
4315
4820
|
}
|
|
@@ -5040,11 +5545,11 @@ import {
|
|
|
5040
5545
|
} from "@intentius/pinhole";
|
|
5041
5546
|
|
|
5042
5547
|
// src/icon-packs.ts
|
|
5043
|
-
import { readFileSync as
|
|
5044
|
-
import { dirname as
|
|
5548
|
+
import { readFileSync as readFileSync7 } from "node:fs";
|
|
5549
|
+
import { dirname as dirname4, join as join11 } from "node:path";
|
|
5045
5550
|
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
5046
|
-
var ICON_ROOT =
|
|
5047
|
-
var
|
|
5551
|
+
var ICON_ROOT = join11(dirname4(fileURLToPath2(import.meta.url)), "..", "web", "icons");
|
|
5552
|
+
var cache3 = /* @__PURE__ */ new Map();
|
|
5048
5553
|
function unprefix(body) {
|
|
5049
5554
|
return body.replace(/\s(?:inkscape|sodipodi):[\w-]+\s*=\s*(["'])[\s\S]*?\1/g, "").replace(/\sxlink:href\s*=/g, " href=");
|
|
5050
5555
|
}
|
|
@@ -5063,9 +5568,9 @@ function plate(glyph) {
|
|
|
5063
5568
|
return { ...glyph, body: `${ground}<g transform="${inset}">${glyph.body}</g>` };
|
|
5064
5569
|
}
|
|
5065
5570
|
function loadIcon(rel) {
|
|
5066
|
-
const hit =
|
|
5571
|
+
const hit = cache3.get(rel);
|
|
5067
5572
|
if (hit) return hit;
|
|
5068
|
-
const raw =
|
|
5573
|
+
const raw = readFileSync7(join11(ICON_ROOT, `${rel}.svg`), "utf8");
|
|
5069
5574
|
const open = raw.indexOf("<svg");
|
|
5070
5575
|
const openEnd = raw.indexOf(">", open);
|
|
5071
5576
|
const close = raw.lastIndexOf("</svg");
|
|
@@ -5074,7 +5579,7 @@ function loadIcon(rel) {
|
|
|
5074
5579
|
if (!viewBox) throw new Error(`vendored icon has no viewBox: ${rel}.svg`);
|
|
5075
5580
|
const bare = { body: unprefix(raw.slice(openEnd + 1, close).trim()), colored: true, viewBox };
|
|
5076
5581
|
const glyph = PLATED.has(rel) ? plate(bare) : bare;
|
|
5077
|
-
|
|
5582
|
+
cache3.set(rel, glyph);
|
|
5078
5583
|
return glyph;
|
|
5079
5584
|
}
|
|
5080
5585
|
var K8S_ICON_BY_KIND = {
|
|
@@ -5130,9 +5635,9 @@ function helmIconFor(kind) {
|
|
|
5130
5635
|
var SUPPORTED_MAJOR = 1;
|
|
5131
5636
|
var refuse = (error, remedy) => ({ ok: false, refusal: { error, code: "carve-report", remedy } });
|
|
5132
5637
|
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`.";
|
|
5133
|
-
var
|
|
5638
|
+
var isRecord3 = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
5134
5639
|
function parseCarveReport(value) {
|
|
5135
|
-
if (!
|
|
5640
|
+
if (!isRecord3(value)) {
|
|
5136
5641
|
return refuse("That file isn't a carve report \u2014 its top level isn't a JSON object.", HOW_TO_GET_ONE);
|
|
5137
5642
|
}
|
|
5138
5643
|
if (!Array.isArray(value.resources)) {
|
|
@@ -5142,7 +5647,7 @@ function parseCarveReport(value) {
|
|
|
5142
5647
|
);
|
|
5143
5648
|
}
|
|
5144
5649
|
const bad = value.resources.findIndex(
|
|
5145
|
-
(r) => !
|
|
5650
|
+
(r) => !isRecord3(r) || typeof r.address !== "string" || typeof r.score !== "number" || typeof r.band !== "string"
|
|
5146
5651
|
);
|
|
5147
5652
|
if (bad >= 0) {
|
|
5148
5653
|
return refuse(
|
|
@@ -5203,368 +5708,120 @@ function tfTypeOf(address, kind) {
|
|
|
5203
5708
|
const dot = address.indexOf(".");
|
|
5204
5709
|
return dot > 0 ? address.slice(0, dot) : address;
|
|
5205
5710
|
}
|
|
5206
|
-
var directionOf = (e) => e.direction ?? "inbound";
|
|
5207
|
-
function boundaryEdgesOf(r) {
|
|
5208
|
-
const b = r.boundary;
|
|
5209
|
-
if (!b) return [];
|
|
5210
|
-
const list3 = Array.isArray(b) ? b : [...b.inbound ?? [], ...b.outbound ?? []];
|
|
5211
|
-
return list3.filter((e) =>
|
|
5212
|
-
}
|
|
5213
|
-
function scoreArithmetic(r) {
|
|
5214
|
-
const b = r.breakdown;
|
|
5215
|
-
if (!b || b.tier === null) return `${r.score} (no known native mapping \u2014 nothing to carve into)`;
|
|
5216
|
-
const p = b.penalties ?? {};
|
|
5217
|
-
const terms = [];
|
|
5218
|
-
if (p.inbound) terms.push(`- 12x${b.inbound ?? 0} inbound`);
|
|
5219
|
-
if (p.outbound) terms.push(`- 4x${b.outbound ?? 0} outbound`);
|
|
5220
|
-
if (p.outputs) terms.push(`- 4x${b.outputs ?? 0} output`);
|
|
5221
|
-
if (p.tier) terms.push(`- 15x${(b.tier ?? 1) - 1} tier${b.tier}`);
|
|
5222
|
-
if (p.dynamic) terms.push(`- 10 dynamic`);
|
|
5223
|
-
if (p.instances) terms.push(`- 3x${(b.instances ?? 1) - 1} instances`);
|
|
5224
|
-
if (!terms.length) return `100 (no penalties) = ${r.score}`;
|
|
5225
|
-
const raw = 100 + Object.values(p).reduce((s, n) => s + (n ?? 0), 0);
|
|
5226
|
-
const clamped = raw !== r.score ? ` (clamped from ${raw})` : "";
|
|
5227
|
-
return `100 ${terms.join(" ")} = ${r.score}${clamped}`;
|
|
5228
|
-
}
|
|
5229
|
-
function boundaryWorkOf(r) {
|
|
5230
|
-
const b = r.breakdown;
|
|
5231
|
-
if (!b || b.tier === null) return "no known native mapping (unsupported provider/type)";
|
|
5232
|
-
const parts = [];
|
|
5233
|
-
if (b.inbound) parts.push(`${b.inbound} inbound (a data-source patch each)`);
|
|
5234
|
-
if (b.outbound) parts.push(`${b.outbound} outbound (a deferred input each)`);
|
|
5235
|
-
if (b.outputs) parts.push(`${b.outputs} output block(s) reading it (a one-line rewrite each)`);
|
|
5236
|
-
if ((b.tier ?? 1) > 1) parts.push(`tier ${b.tier} map`);
|
|
5237
|
-
if (b.hasDynamic) parts.push("count/for_each/data present");
|
|
5238
|
-
if ((b.instances ?? 1) > 1) parts.push(`${b.instances} instances`);
|
|
5239
|
-
return parts.length ? parts.join(", ") : "clean 1:1 native map, no boundary edges";
|
|
5240
|
-
}
|
|
5241
|
-
function carveCardFields(node) {
|
|
5242
|
-
const score = node.attrs.score;
|
|
5243
|
-
const carve = node.attrs.carve;
|
|
5244
|
-
if (typeof score !== "number") return void 0;
|
|
5245
|
-
return [
|
|
5246
|
-
{ label: "score", value: String(score) },
|
|
5247
|
-
...typeof carve === "string" ? [{ label: "carve", value: carve }] : []
|
|
5248
|
-
];
|
|
5249
|
-
}
|
|
5250
|
-
function carveReportToIr(report) {
|
|
5251
|
-
const resources = report.resources ?? [];
|
|
5252
|
-
const known = new Set(resources.map((r) => r.address));
|
|
5253
|
-
const nodes = resources.map((r) => {
|
|
5254
|
-
const status = statusForBand(r.band, r.score);
|
|
5255
|
-
const b = r.breakdown ?? {};
|
|
5256
|
-
const edges2 = boundaryEdgesOf(r);
|
|
5257
|
-
const patches = edges2.filter((e) => e.carved === r.address && directionOf(e) === "inbound").map((e) => e.survivor);
|
|
5258
|
-
const inputs = edges2.filter((e) => e.carved === r.address && directionOf(e) === "outbound").map((e) => e.survivor);
|
|
5259
|
-
return {
|
|
5260
|
-
id: r.address,
|
|
5261
|
-
kind: tfTypeOf(r.address, r.kind),
|
|
5262
|
-
lexicon: "terraform",
|
|
5263
|
-
attrs: {
|
|
5264
|
-
// The drift palette's channel — `_`-prefixed, so it paints the card and
|
|
5265
|
-
// stays out of both the card's fields and the inspect pane's list.
|
|
5266
|
-
_status: status,
|
|
5267
|
-
score: r.score,
|
|
5268
|
-
carve: verdictForStatus(status),
|
|
5269
|
-
band: r.band,
|
|
5270
|
-
arithmetic: scoreArithmetic(r),
|
|
5271
|
-
boundaryWork: boundaryWorkOf(r),
|
|
5272
|
-
...r.mapsTo ? { mapsTo: r.mapsTo } : {},
|
|
5273
|
-
tier: b.tier === null || b.tier === void 0 ? "none" : b.tier,
|
|
5274
|
-
inbound: b.inbound ?? 0,
|
|
5275
|
-
outbound: b.outbound ?? 0,
|
|
5276
|
-
outputs: b.outputs ?? 0,
|
|
5277
|
-
instances: b.instances ?? 1,
|
|
5278
|
-
dynamic: b.hasDynamic ?? false,
|
|
5279
|
-
// The predicted diff, when the report carries the edge lists: who needs
|
|
5280
|
-
// a `data` source the moment this is carved, and what becomes a
|
|
5281
|
-
// deploy-time input. Absent (not empty) when the report has no lists —
|
|
5282
|
-
// "none" and "not reported" are different claims.
|
|
5283
|
-
...patches.length ? { patchOnCarve: patches.join(", ") } : {},
|
|
5284
|
-
...inputs.length ? { deferredInputs: inputs.join(", ") } : {}
|
|
5285
|
-
}
|
|
5286
|
-
};
|
|
5287
|
-
});
|
|
5288
|
-
const seen = /* @__PURE__ */ new Set();
|
|
5289
|
-
const edges = [];
|
|
5290
|
-
for (const r of resources) {
|
|
5291
|
-
for (const e of boundaryEdgesOf(r)) {
|
|
5292
|
-
const direction = directionOf(e);
|
|
5293
|
-
const from = direction === "inbound" ? e.survivor : e.carved;
|
|
5294
|
-
const to = direction === "inbound" ? e.carved : e.survivor;
|
|
5295
|
-
if (!known.has(from) || !known.has(to)) continue;
|
|
5296
|
-
const key = `${from}|${to}`;
|
|
5297
|
-
if (seen.has(key)) continue;
|
|
5298
|
-
seen.add(key);
|
|
5299
|
-
edges.push({
|
|
5300
|
-
from,
|
|
5301
|
-
to,
|
|
5302
|
-
kind: "ref",
|
|
5303
|
-
viaAttr: direction,
|
|
5304
|
-
...e.attrs?.length ? { toAttr: e.attrs.join(", ") } : {}
|
|
5305
|
-
});
|
|
5306
|
-
}
|
|
5307
|
-
}
|
|
5308
|
-
const byStack = {};
|
|
5309
|
-
for (const order of ["good", "warn", "neutral"]) {
|
|
5310
|
-
const members = resources.filter((r) => statusForBand(r.band, r.score) === order).map((r) => r.address);
|
|
5311
|
-
if (members.length) byStack[verdictForStatus(order)] = members;
|
|
5312
|
-
}
|
|
5313
|
-
return { nodes, edges, groups: { byStack } };
|
|
5314
|
-
}
|
|
5315
|
-
function carveNote(report, ir) {
|
|
5316
|
-
const counts = Object.entries(report.bands ?? {}).filter(([, n]) => n > 0).map(([band, n]) => `${n} ${band}`).join(" \xB7 ");
|
|
5317
|
-
const head = `chant carve advisory${report.from ? ` for ${report.from}` : ""}${counts ? ` \u2014 ${counts}` : ""}`;
|
|
5318
|
-
if (ir.edges.length) return `${head}. Read-only: nothing is emitted, patched, or applied.`;
|
|
5319
|
-
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.`;
|
|
5320
|
-
}
|
|
5321
|
-
|
|
5322
|
-
// src/carve-manifest.ts
|
|
5323
|
-
import { readdirSync as readdirSync4, readFileSync as readFileSync7, statSync as statSync4 } from "node:fs";
|
|
5324
|
-
import { dirname as dirname4, join as join10 } from "node:path";
|
|
5325
|
-
var CARVE_MANIFEST_SUFFIX = ".carve.json";
|
|
5326
|
-
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.";
|
|
5327
|
-
var isRecord3 = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
5328
|
-
var strings = (v) => Array.isArray(v) ? v.filter((x) => typeof x === "string") : [];
|
|
5329
|
-
function parseCarveManifest(value) {
|
|
5330
|
-
if (!isRecord3(value)) return null;
|
|
5331
|
-
if (value.version !== 1) return null;
|
|
5332
|
-
if (typeof value.target !== "string" || !value.target) return null;
|
|
5333
|
-
return value;
|
|
5334
|
-
}
|
|
5335
|
-
function readCarveManifest(path, readFile = (p) => readFileSync7(p, "utf8")) {
|
|
5336
|
-
let text;
|
|
5337
|
-
try {
|
|
5338
|
-
text = readFile(path);
|
|
5339
|
-
} catch {
|
|
5340
|
-
return null;
|
|
5341
|
-
}
|
|
5342
|
-
try {
|
|
5343
|
-
return parseCarveManifest(JSON.parse(text));
|
|
5344
|
-
} catch {
|
|
5345
|
-
return null;
|
|
5346
|
-
}
|
|
5347
|
-
}
|
|
5348
|
-
function carveStageOf(m) {
|
|
5349
|
-
if (m.apply) return "applied";
|
|
5350
|
-
if (m.bridge) return "bridged";
|
|
5351
|
-
return "emitted";
|
|
5352
|
-
}
|
|
5353
|
-
function carveWordFor(stage) {
|
|
5354
|
-
switch (stage) {
|
|
5355
|
-
case "applied":
|
|
5356
|
-
return "carved \u2192 chant";
|
|
5357
|
-
case "bridged":
|
|
5358
|
-
return "bridged \u2014 apply is yours";
|
|
5359
|
-
default:
|
|
5360
|
-
return "emitted \u2014 not bridged";
|
|
5361
|
-
}
|
|
5362
|
-
}
|
|
5363
|
-
function carveToneFor(stage) {
|
|
5364
|
-
return stage === "applied" ? "good" : "accent";
|
|
5365
|
-
}
|
|
5366
|
-
function carveStageNote(state) {
|
|
5367
|
-
switch (state.stage) {
|
|
5368
|
-
case "applied":
|
|
5369
|
-
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.";
|
|
5370
|
-
case "bridged":
|
|
5371
|
-
return "emitted and bridged \u2014 the survivor patch is proposed. Terraform still owns the resource until `carve apply`.";
|
|
5372
|
-
default:
|
|
5373
|
-
return "emitted \u2014 typed chant source exists, and the surviving Terraform still reads the resource directly. `carve bridge` is next.";
|
|
5374
|
-
}
|
|
5375
|
-
}
|
|
5376
|
-
function carveStateOf(m, path) {
|
|
5377
|
-
const stage = carveStageOf(m);
|
|
5378
|
-
const deferred = Object.entries(m.emit?.params ?? {}).map(([name, p]) => `${name} \u2190 ${p?.survivor ?? "a survivor"}`);
|
|
5379
|
-
return {
|
|
5380
|
-
target: m.target,
|
|
5381
|
-
...m.tfType ? { tfType: m.tfType } : {},
|
|
5382
|
-
stage,
|
|
5383
|
-
graduated: stage === "applied",
|
|
5384
|
-
// The last stamped step wins — that is the age of the state, not the age
|
|
5385
|
-
// of the carve.
|
|
5386
|
-
...m.apply?.at || m.bridge?.at || m.emit?.at ? { at: m.apply?.at ?? m.bridge?.at ?? m.emit?.at } : {},
|
|
5387
|
-
files: strings(m.emit?.files),
|
|
5388
|
-
excised: strings(m.bridge?.excised),
|
|
5389
|
-
deferredInputs: deferred,
|
|
5390
|
-
...m.apply ? {
|
|
5391
|
-
ownership: {
|
|
5392
|
-
...m.apply.marker?.stack ? { stack: m.apply.marker.stack } : {},
|
|
5393
|
-
...m.apply.marker?.env ? { env: m.apply.marker.env } : {},
|
|
5394
|
-
...m.apply.ownershipTags ? { tags: m.apply.ownershipTags } : {}
|
|
5395
|
-
}
|
|
5396
|
-
} : {},
|
|
5397
|
-
...m.from ? { from: m.from } : {},
|
|
5398
|
-
outDir: dirname4(path),
|
|
5399
|
-
path
|
|
5400
|
-
};
|
|
5401
|
-
}
|
|
5402
|
-
var nodeCarveIo = {
|
|
5403
|
-
readdir: (dir) => readdirSync4(dir),
|
|
5404
|
-
isDirectory: (p) => {
|
|
5405
|
-
try {
|
|
5406
|
-
return statSync4(p).isDirectory();
|
|
5407
|
-
} catch {
|
|
5408
|
-
return false;
|
|
5409
|
-
}
|
|
5410
|
-
},
|
|
5411
|
-
readFile: (p) => readFileSync7(p, "utf8")
|
|
5412
|
-
};
|
|
5413
|
-
var MANIFEST_SCAN_DEPTH = 2;
|
|
5414
|
-
var SKIP_DIRS = /* @__PURE__ */ new Set(["node_modules", "dist", "cdk.out", ".terraform", ".git"]);
|
|
5415
|
-
function listCarveManifests(dir, io = nodeCarveIo, depth = MANIFEST_SCAN_DEPTH) {
|
|
5416
|
-
const out = [];
|
|
5417
|
-
const walk = (d, level) => {
|
|
5418
|
-
let entries;
|
|
5419
|
-
try {
|
|
5420
|
-
entries = io.readdir(d).sort();
|
|
5421
|
-
} catch {
|
|
5422
|
-
return;
|
|
5423
|
-
}
|
|
5424
|
-
for (const name of entries) {
|
|
5425
|
-
const full = join10(d, name);
|
|
5426
|
-
if (name.endsWith(CARVE_MANIFEST_SUFFIX)) {
|
|
5427
|
-
out.push(full);
|
|
5428
|
-
continue;
|
|
5429
|
-
}
|
|
5430
|
-
if (level >= depth) continue;
|
|
5431
|
-
if (name.startsWith(".") || SKIP_DIRS.has(name)) continue;
|
|
5432
|
-
if (io.isDirectory(full)) walk(full, level + 1);
|
|
5433
|
-
}
|
|
5434
|
-
};
|
|
5435
|
-
walk(dir, 0);
|
|
5436
|
-
return out;
|
|
5437
|
-
}
|
|
5438
|
-
function readCarveStates(dirs, io = nodeCarveIo, depth = MANIFEST_SCAN_DEPTH) {
|
|
5439
|
-
const rank = { emitted: 0, bridged: 1, applied: 2 };
|
|
5440
|
-
const byTarget = /* @__PURE__ */ new Map();
|
|
5441
|
-
for (const dir of dirs) {
|
|
5442
|
-
for (const path of listCarveManifests(dir, io, depth)) {
|
|
5443
|
-
const manifest = readCarveManifest(path, io.readFile);
|
|
5444
|
-
if (!manifest) continue;
|
|
5445
|
-
const state = carveStateOf(manifest, path);
|
|
5446
|
-
const held = byTarget.get(state.target);
|
|
5447
|
-
if (!held || rank[state.stage] > rank[held.stage]) byTarget.set(state.target, state);
|
|
5448
|
-
}
|
|
5449
|
-
}
|
|
5450
|
-
return byTarget;
|
|
5711
|
+
var directionOf = (e) => e.direction ?? "inbound";
|
|
5712
|
+
function boundaryEdgesOf(r) {
|
|
5713
|
+
const b = r.boundary;
|
|
5714
|
+
if (!b) return [];
|
|
5715
|
+
const list3 = Array.isArray(b) ? b : [...b.inbound ?? [], ...b.outbound ?? []];
|
|
5716
|
+
return list3.filter((e) => isRecord3(e) && typeof e.survivor === "string" && typeof e.carved === "string");
|
|
5451
5717
|
}
|
|
5452
|
-
function
|
|
5453
|
-
|
|
5454
|
-
|
|
5455
|
-
|
|
5456
|
-
|
|
5457
|
-
|
|
5458
|
-
|
|
5459
|
-
|
|
5460
|
-
}
|
|
5461
|
-
|
|
5462
|
-
|
|
5463
|
-
if (
|
|
5464
|
-
|
|
5465
|
-
|
|
5466
|
-
|
|
5467
|
-
bridged,
|
|
5468
|
-
emitted,
|
|
5469
|
-
inFlight,
|
|
5470
|
-
total: typeof total === "number" ? total : null,
|
|
5471
|
-
label: typeof total === "number" ? `${applied} of ${total} carved` : `${applied} carved`,
|
|
5472
|
-
detail: parts.length ? `${parts.join(", ")} \u2014 apply is yours` : ""
|
|
5473
|
-
};
|
|
5718
|
+
function scoreArithmetic(r) {
|
|
5719
|
+
const b = r.breakdown;
|
|
5720
|
+
if (!b || b.tier === null) return `${r.score} (no known native mapping \u2014 nothing to carve into)`;
|
|
5721
|
+
const p = b.penalties ?? {};
|
|
5722
|
+
const terms = [];
|
|
5723
|
+
if (p.inbound) terms.push(`- 12x${b.inbound ?? 0} inbound`);
|
|
5724
|
+
if (p.outbound) terms.push(`- 4x${b.outbound ?? 0} outbound`);
|
|
5725
|
+
if (p.outputs) terms.push(`- 4x${b.outputs ?? 0} output`);
|
|
5726
|
+
if (p.tier) terms.push(`- 15x${(b.tier ?? 1) - 1} tier${b.tier}`);
|
|
5727
|
+
if (p.dynamic) terms.push(`- 10 dynamic`);
|
|
5728
|
+
if (p.instances) terms.push(`- 3x${(b.instances ?? 1) - 1} instances`);
|
|
5729
|
+
if (!terms.length) return `100 (no penalties) = ${r.score}`;
|
|
5730
|
+
const raw = 100 + Object.values(p).reduce((s, n) => s + (n ?? 0), 0);
|
|
5731
|
+
const clamped = raw !== r.score ? ` (clamped from ${raw})` : "";
|
|
5732
|
+
return `100 ${terms.join(" ")} = ${r.score}${clamped}`;
|
|
5474
5733
|
}
|
|
5475
|
-
function
|
|
5476
|
-
const
|
|
5477
|
-
|
|
5478
|
-
const
|
|
5479
|
-
|
|
5480
|
-
|
|
5734
|
+
function boundaryWorkOf(r) {
|
|
5735
|
+
const b = r.breakdown;
|
|
5736
|
+
if (!b || b.tier === null) return "no known native mapping (unsupported provider/type)";
|
|
5737
|
+
const parts = [];
|
|
5738
|
+
if (b.inbound) parts.push(`${b.inbound} inbound (a data-source patch each)`);
|
|
5739
|
+
if (b.outbound) parts.push(`${b.outbound} outbound (a deferred input each)`);
|
|
5740
|
+
if (b.outputs) parts.push(`${b.outputs} output block(s) reading it (a one-line rewrite each)`);
|
|
5741
|
+
if ((b.tier ?? 1) > 1) parts.push(`tier ${b.tier} map`);
|
|
5742
|
+
if (b.hasDynamic) parts.push("count/for_each/data present");
|
|
5743
|
+
if ((b.instances ?? 1) > 1) parts.push(`${b.instances} instances`);
|
|
5744
|
+
return parts.length ? parts.join(", ") : "clean 1:1 native map, no boundary edges";
|
|
5481
5745
|
}
|
|
5482
|
-
function
|
|
5483
|
-
const
|
|
5484
|
-
|
|
5485
|
-
|
|
5486
|
-
|
|
5487
|
-
|
|
5488
|
-
|
|
5489
|
-
|
|
5746
|
+
function carveCardFields(node) {
|
|
5747
|
+
const score = node.attrs.score;
|
|
5748
|
+
const carve = node.attrs.carve;
|
|
5749
|
+
if (typeof score !== "number") return void 0;
|
|
5750
|
+
return [
|
|
5751
|
+
{ label: "score", value: String(score) },
|
|
5752
|
+
...typeof carve === "string" ? [{ label: "carve", value: carve }] : []
|
|
5753
|
+
];
|
|
5490
5754
|
}
|
|
5491
|
-
function
|
|
5492
|
-
|
|
5493
|
-
const
|
|
5494
|
-
const
|
|
5495
|
-
|
|
5496
|
-
|
|
5497
|
-
|
|
5498
|
-
|
|
5499
|
-
const
|
|
5500
|
-
|
|
5501
|
-
|
|
5502
|
-
|
|
5503
|
-
|
|
5504
|
-
if (state.graduated) {
|
|
5505
|
-
gone.add(node.id);
|
|
5506
|
-
graduated.push({
|
|
5507
|
-
...node,
|
|
5508
|
-
attrs: {
|
|
5509
|
-
...node.attrs,
|
|
5510
|
-
_status: "good",
|
|
5511
|
-
carve: carveWordFor(state.stage),
|
|
5512
|
-
carveState: state.stage,
|
|
5513
|
-
...state.ownership?.stack ? { ownedStack: state.ownership.stack } : {},
|
|
5514
|
-
...state.ownership?.env ? { ownedEnv: state.ownership.env } : {},
|
|
5515
|
-
...state.at ? { carvedAt: state.at } : {},
|
|
5516
|
-
...state.files.length ? { chantSource: source(state.files) } : {}
|
|
5517
|
-
}
|
|
5518
|
-
});
|
|
5519
|
-
continue;
|
|
5520
|
-
}
|
|
5521
|
-
nodes.push({
|
|
5522
|
-
...node,
|
|
5755
|
+
function carveReportToIr(report) {
|
|
5756
|
+
const resources = report.resources ?? [];
|
|
5757
|
+
const known = new Set(resources.map((r) => r.address));
|
|
5758
|
+
const nodes = resources.map((r) => {
|
|
5759
|
+
const status = statusForBand(r.band, r.score);
|
|
5760
|
+
const b = r.breakdown ?? {};
|
|
5761
|
+
const edges2 = boundaryEdgesOf(r);
|
|
5762
|
+
const patches = edges2.filter((e) => e.carved === r.address && directionOf(e) === "inbound").map((e) => e.survivor);
|
|
5763
|
+
const inputs = edges2.filter((e) => e.carved === r.address && directionOf(e) === "outbound").map((e) => e.survivor);
|
|
5764
|
+
return {
|
|
5765
|
+
id: r.address,
|
|
5766
|
+
kind: tfTypeOf(r.address, r.kind),
|
|
5767
|
+
lexicon: "terraform",
|
|
5523
5768
|
attrs: {
|
|
5524
|
-
|
|
5525
|
-
|
|
5526
|
-
|
|
5527
|
-
|
|
5528
|
-
|
|
5529
|
-
|
|
5530
|
-
|
|
5531
|
-
|
|
5769
|
+
// The drift palette's channel — `_`-prefixed, so it paints the card and
|
|
5770
|
+
// stays out of both the card's fields and the inspect pane's list.
|
|
5771
|
+
_status: status,
|
|
5772
|
+
score: r.score,
|
|
5773
|
+
carve: verdictForStatus(status),
|
|
5774
|
+
band: r.band,
|
|
5775
|
+
arithmetic: scoreArithmetic(r),
|
|
5776
|
+
boundaryWork: boundaryWorkOf(r),
|
|
5777
|
+
...r.mapsTo ? { mapsTo: r.mapsTo } : {},
|
|
5778
|
+
tier: b.tier === null || b.tier === void 0 ? "none" : b.tier,
|
|
5779
|
+
inbound: b.inbound ?? 0,
|
|
5780
|
+
outbound: b.outbound ?? 0,
|
|
5781
|
+
outputs: b.outputs ?? 0,
|
|
5782
|
+
instances: b.instances ?? 1,
|
|
5783
|
+
dynamic: b.hasDynamic ?? false,
|
|
5784
|
+
// The predicted diff, when the report carries the edge lists: who needs
|
|
5785
|
+
// a `data` source the moment this is carved, and what becomes a
|
|
5786
|
+
// deploy-time input. Absent (not empty) when the report has no lists —
|
|
5787
|
+
// "none" and "not reported" are different claims.
|
|
5788
|
+
...patches.length ? { patchOnCarve: patches.join(", ") } : {},
|
|
5789
|
+
...inputs.length ? { deferredInputs: inputs.join(", ") } : {}
|
|
5532
5790
|
}
|
|
5533
|
-
}
|
|
5791
|
+
};
|
|
5792
|
+
});
|
|
5793
|
+
const seen = /* @__PURE__ */ new Set();
|
|
5794
|
+
const edges = [];
|
|
5795
|
+
for (const r of resources) {
|
|
5796
|
+
for (const e of boundaryEdgesOf(r)) {
|
|
5797
|
+
const direction = directionOf(e);
|
|
5798
|
+
const from = direction === "inbound" ? e.survivor : e.carved;
|
|
5799
|
+
const to = direction === "inbound" ? e.carved : e.survivor;
|
|
5800
|
+
if (!known.has(from) || !known.has(to)) continue;
|
|
5801
|
+
const key = `${from}|${to}`;
|
|
5802
|
+
if (seen.has(key)) continue;
|
|
5803
|
+
seen.add(key);
|
|
5804
|
+
edges.push({
|
|
5805
|
+
from,
|
|
5806
|
+
to,
|
|
5807
|
+
kind: "ref",
|
|
5808
|
+
viaAttr: direction,
|
|
5809
|
+
...e.attrs?.length ? { toAttr: e.attrs.join(", ") } : {}
|
|
5810
|
+
});
|
|
5811
|
+
}
|
|
5534
5812
|
}
|
|
5535
5813
|
const byStack = {};
|
|
5536
|
-
for (const
|
|
5537
|
-
const
|
|
5538
|
-
if (
|
|
5814
|
+
for (const order of ["good", "warn", "neutral"]) {
|
|
5815
|
+
const members = resources.filter((r) => statusForBand(r.band, r.score) === order).map((r) => r.address);
|
|
5816
|
+
if (members.length) byStack[verdictForStatus(order)] = members;
|
|
5539
5817
|
}
|
|
5540
|
-
return {
|
|
5541
|
-
tf: {
|
|
5542
|
-
nodes,
|
|
5543
|
-
edges: tfIr.edges.filter((e) => !gone.has(e.from) && !gone.has(e.to)),
|
|
5544
|
-
groups: { ...tfIr.groups, byStack }
|
|
5545
|
-
},
|
|
5546
|
-
graduated
|
|
5547
|
-
};
|
|
5548
|
-
}
|
|
5549
|
-
var GRADUATED_BAND = "carved \u2192 chant";
|
|
5550
|
-
function bandGraduated(tf, graduated) {
|
|
5551
|
-
if (graduated.length === 0) return tf;
|
|
5552
|
-
return {
|
|
5553
|
-
nodes: [...graduated, ...tf.nodes],
|
|
5554
|
-
edges: tf.edges,
|
|
5555
|
-
groups: {
|
|
5556
|
-
...tf.groups,
|
|
5557
|
-
byStack: {
|
|
5558
|
-
[GRADUATED_BAND]: graduated.map((n) => n.id),
|
|
5559
|
-
...tf.groups?.byStack ?? {}
|
|
5560
|
-
}
|
|
5561
|
-
}
|
|
5562
|
-
};
|
|
5818
|
+
return { nodes, edges, groups: { byStack } };
|
|
5563
5819
|
}
|
|
5564
|
-
function
|
|
5565
|
-
|
|
5566
|
-
const head = `
|
|
5567
|
-
|
|
5820
|
+
function carveNote(report, ir) {
|
|
5821
|
+
const counts = Object.entries(report.bands ?? {}).filter(([, n]) => n > 0).map(([band, n]) => `${n} ${band}`).join(" \xB7 ");
|
|
5822
|
+
const head = `chant carve advisory${report.from ? ` for ${report.from}` : ""}${counts ? ` \u2014 ${counts}` : ""}`;
|
|
5823
|
+
if (ir.edges.length) return `${head}. Read-only: nothing is emitted, patched, or applied.`;
|
|
5824
|
+
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.`;
|
|
5568
5825
|
}
|
|
5569
5826
|
|
|
5570
5827
|
// src/ops-lens.ts
|
|
@@ -5838,9 +6095,12 @@ function nodeFor(op, placed, guards) {
|
|
|
5838
6095
|
...profile ? { profile } : {},
|
|
5839
6096
|
...policy ? { policy } : {},
|
|
5840
6097
|
// The resolved args, verbatim from the IR. They carry the Op's SCOPE
|
|
5841
|
-
// (env, stack, template path)
|
|
5842
|
-
//
|
|
6098
|
+
// (env, stack, template path); which of them identify an ENTITY is the
|
|
6099
|
+
// contract's to say, and 0.54's op.json says it in `entities` below.
|
|
5843
6100
|
...args ? { args } : {},
|
|
6101
|
+
// chant#2022: what the step touches, verbatim. `entities` prints on the
|
|
6102
|
+
// card; `_entities` is what `joinStepEntities` resolves.
|
|
6103
|
+
...step.entities?.length ? { entities: step.entities.join(", "), _entities: step.entities } : {},
|
|
5844
6104
|
...step.outcomeAttribute ? { outcome: step.outcomeAttribute.from ? `${step.outcomeAttribute.name} \u2190 ${step.outcomeAttribute.from}` : step.outcomeAttribute.name } : {},
|
|
5845
6105
|
...op.activityContracts?.[step.fn] ? { contract: "declared (args/returns JSON Schema in op.json)" } : {}
|
|
5846
6106
|
}
|
|
@@ -5942,17 +6202,83 @@ function opCardFields(node) {
|
|
|
5942
6202
|
function opsNote(ops, ir) {
|
|
5943
6203
|
const gates = ir.nodes.filter((n) => n.attrs._step === "gate").length;
|
|
5944
6204
|
const rules = ir.nodes.filter((n) => n.attrs._step === "rule");
|
|
5945
|
-
const
|
|
6205
|
+
const estate = ir.nodes.filter((n) => n.lexicon !== "op").length;
|
|
6206
|
+
const steps = ir.nodes.length - rules.length - estate;
|
|
5946
6207
|
const dangling = rules.filter((n) => n.attrs.dangling !== void 0).length;
|
|
5947
6208
|
const counts = [
|
|
5948
6209
|
`${ops.length} declared Op${ops.length === 1 ? "" : "s"}`,
|
|
5949
6210
|
`${steps} step${steps === 1 ? "" : "s"}`,
|
|
5950
6211
|
...gates ? [`${gates} gate${gates === 1 ? "" : "s"}`] : [],
|
|
5951
|
-
...rules.length ? [`${rules.length} converge rule${rules.length === 1 ? "" : "s"}`] : []
|
|
6212
|
+
...rules.length ? [`${rules.length} converge rule${rules.length === 1 ? "" : "s"}`] : [],
|
|
6213
|
+
...entityCountClause(ir)
|
|
5952
6214
|
].join(" \xB7 ");
|
|
5953
6215
|
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.` : "") : "";
|
|
5954
6216
|
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.`;
|
|
5955
6217
|
}
|
|
6218
|
+
var ESTATE_BOX = "estate";
|
|
6219
|
+
function joinStepEntities(ir, estate) {
|
|
6220
|
+
const steps = ir.nodes.filter((n) => Array.isArray(n.attrs._entities));
|
|
6221
|
+
if (steps.length === 0) return { refs: 0, resolved: 0 };
|
|
6222
|
+
const byId = /* @__PURE__ */ new Map();
|
|
6223
|
+
const byLeaf = /* @__PURE__ */ new Map();
|
|
6224
|
+
for (const n of estate?.nodes ?? []) {
|
|
6225
|
+
byId.set(n.id, n);
|
|
6226
|
+
for (const leaf of stringLeaves(n.attrs)) {
|
|
6227
|
+
const list3 = byLeaf.get(leaf.value);
|
|
6228
|
+
if (list3) {
|
|
6229
|
+
if (!list3.includes(n)) list3.push(n);
|
|
6230
|
+
} else byLeaf.set(leaf.value, [n]);
|
|
6231
|
+
}
|
|
6232
|
+
}
|
|
6233
|
+
const present = new Set(ir.nodes.map((n) => n.id));
|
|
6234
|
+
let refs = 0;
|
|
6235
|
+
let resolvedCount = 0;
|
|
6236
|
+
for (const step of steps) {
|
|
6237
|
+
const links = { resolved: [], unresolved: [] };
|
|
6238
|
+
for (const value of step.attrs._entities) {
|
|
6239
|
+
refs++;
|
|
6240
|
+
const targets = [];
|
|
6241
|
+
const idHit = byId.get(value);
|
|
6242
|
+
if (idHit) targets.push({ node: idHit, via: "id" });
|
|
6243
|
+
for (const n of byLeaf.get(value) ?? []) if (n !== idHit) targets.push({ node: n, via: "attr" });
|
|
6244
|
+
if (targets.length === 0) {
|
|
6245
|
+
links.unresolved.push(value);
|
|
6246
|
+
continue;
|
|
6247
|
+
}
|
|
6248
|
+
resolvedCount++;
|
|
6249
|
+
for (const { node, via } of targets) {
|
|
6250
|
+
links.resolved.push({ value, node: node.id, via });
|
|
6251
|
+
if (!present.has(node.id)) {
|
|
6252
|
+
present.add(node.id);
|
|
6253
|
+
ir.nodes.push({ ...node, attrs: { ...node.attrs, _touchedBy: [step.id] } });
|
|
6254
|
+
(ir.groups.byStack ??= {})[ESTATE_BOX] = [...ir.groups.byStack[ESTATE_BOX] ?? [], node.id];
|
|
6255
|
+
} else {
|
|
6256
|
+
const held = ir.nodes.find((n) => n.id === node.id);
|
|
6257
|
+
const touched = held.attrs._touchedBy ?? [];
|
|
6258
|
+
if (!touched.includes(step.id)) held.attrs._touchedBy = [...touched, step.id];
|
|
6259
|
+
}
|
|
6260
|
+
ir.edges.push({ from: step.id, to: node.id, kind: "ref", viaAttr: "entities" });
|
|
6261
|
+
}
|
|
6262
|
+
}
|
|
6263
|
+
step.attrs._entityLinks = links;
|
|
6264
|
+
}
|
|
6265
|
+
return { refs, resolved: resolvedCount };
|
|
6266
|
+
}
|
|
6267
|
+
function entityCountClause(ir) {
|
|
6268
|
+
let refs = 0;
|
|
6269
|
+
let resolved = 0;
|
|
6270
|
+
for (const n of ir.nodes) {
|
|
6271
|
+
const links = n.attrs._entityLinks;
|
|
6272
|
+
if (links) {
|
|
6273
|
+
refs += links.resolved.length + links.unresolved.length;
|
|
6274
|
+
resolved += links.resolved.length;
|
|
6275
|
+
} else if (Array.isArray(n.attrs._entities)) {
|
|
6276
|
+
refs += n.attrs._entities.length;
|
|
6277
|
+
}
|
|
6278
|
+
}
|
|
6279
|
+
if (refs === 0) return [];
|
|
6280
|
+
return [`${refs} entity ref${refs === 1 ? "" : "s"}, ${resolved} linked`];
|
|
6281
|
+
}
|
|
5956
6282
|
|
|
5957
6283
|
// src/render.ts
|
|
5958
6284
|
registerPack({ lexicon: "k8s", iconFor: k8sIconFor });
|
|
@@ -6066,7 +6392,10 @@ function renderCarveEstate(tfIr, appIr, opts) {
|
|
|
6066
6392
|
}
|
|
6067
6393
|
function withCarveState(tfIr, appIr, opts) {
|
|
6068
6394
|
const states = opts.carved ?? /* @__PURE__ */ new Map();
|
|
6069
|
-
const { tf, graduated } = splitCarveState(tfIr, states, {
|
|
6395
|
+
const { tf, graduated } = splitCarveState(tfIr, states, {
|
|
6396
|
+
...opts.shorten ? { shorten: opts.shorten } : {},
|
|
6397
|
+
...opts.emitted ? { emitted: opts.emitted } : {}
|
|
6398
|
+
});
|
|
6070
6399
|
const ns = namespaceAppIr(appIr, opts.appTitle);
|
|
6071
6400
|
const progress = carveProgress(states.values(), tfIr.nodes.length);
|
|
6072
6401
|
return {
|
|
@@ -6406,12 +6735,12 @@ function radializeLayout(layout, groupOf, size = /* @__PURE__ */ new Map()) {
|
|
|
6406
6735
|
}
|
|
6407
6736
|
|
|
6408
6737
|
// src/carve-actions.ts
|
|
6409
|
-
import { existsSync as existsSync8, mkdirSync as mkdirSync3, readFileSync as readFileSync10, readdirSync as
|
|
6410
|
-
import { isAbsolute, join as
|
|
6738
|
+
import { existsSync as existsSync8, mkdirSync as mkdirSync3, readFileSync as readFileSync10, readdirSync as readdirSync6, statSync as statSync6 } from "node:fs";
|
|
6739
|
+
import { isAbsolute as isAbsolute2, join as join14, relative as relative3, resolve as resolve5, sep as sep2 } from "node:path";
|
|
6411
6740
|
|
|
6412
6741
|
// src/layout.ts
|
|
6413
6742
|
import { existsSync as existsSync6, mkdirSync as mkdirSync2, readFileSync as readFileSync8, renameSync, rmSync, writeFileSync as writeFileSync2, accessSync, constants } from "node:fs";
|
|
6414
|
-
import { join as
|
|
6743
|
+
import { join as join12 } from "node:path";
|
|
6415
6744
|
var LAYOUT_DIR = ".behold";
|
|
6416
6745
|
var LAYOUT_FILE = "layout.json";
|
|
6417
6746
|
var MAX_BODY_BYTES = 64 * 1024;
|
|
@@ -6452,7 +6781,7 @@ function lensFromQuery(params) {
|
|
|
6452
6781
|
return [slug(zoom), params.get("radial") === "1" ? "radial" : null, stack ? slug(`stack-${stack}`) : null].filter(Boolean).join("+");
|
|
6453
6782
|
}
|
|
6454
6783
|
function layoutPath(projectDir) {
|
|
6455
|
-
return
|
|
6784
|
+
return join12(projectDir, LAYOUT_DIR, LAYOUT_FILE);
|
|
6456
6785
|
}
|
|
6457
6786
|
function unwritableReason(projectDir) {
|
|
6458
6787
|
if (!projectDir || !existsSync6(projectDir)) return `no such project directory: ${projectDir || "(none)"}`;
|
|
@@ -6508,7 +6837,7 @@ function writeLens(projectDir, lens, deltas) {
|
|
|
6508
6837
|
}
|
|
6509
6838
|
const body = JSON.stringify({ version: 1, lenses }, null, 2) + "\n";
|
|
6510
6839
|
if (body.length > MAX_FILE_BYTES) throw new LayoutTooLarge(`a layout sidecar is capped at ${MAX_FILE_BYTES} bytes`);
|
|
6511
|
-
mkdirSync2(
|
|
6840
|
+
mkdirSync2(join12(projectDir, LAYOUT_DIR), { recursive: true });
|
|
6512
6841
|
const tmp = `${file}.tmp`;
|
|
6513
6842
|
writeFileSync2(tmp, body, "utf8");
|
|
6514
6843
|
renameSync(tmp, file);
|
|
@@ -6594,7 +6923,35 @@ function reanchorEdges(svg, moved) {
|
|
|
6594
6923
|
// src/carve-live.ts
|
|
6595
6924
|
import { spawn as spawn3 } from "node:child_process";
|
|
6596
6925
|
import { readFileSync as readFileSync9, writeFileSync as writeFileSync3, existsSync as existsSync7 } from "node:fs";
|
|
6597
|
-
import { join as
|
|
6926
|
+
import { join as join13 } from "node:path";
|
|
6927
|
+
|
|
6928
|
+
// src/scratch.ts
|
|
6929
|
+
var PROTECTED_INFRA = [
|
|
6930
|
+
/^floci(-|$)/,
|
|
6931
|
+
/^chant-floci(-|$)/,
|
|
6932
|
+
/^k3d-kubemicrovm-local$/,
|
|
6933
|
+
/^k3d-fountain-local$/,
|
|
6934
|
+
/^kubemicrovm-local$/,
|
|
6935
|
+
/^fountain-local$/
|
|
6936
|
+
];
|
|
6937
|
+
var PROTECTED_PORTS = [4566, 4577, 4588];
|
|
6938
|
+
var SCRATCH_PREFIX = "behold-";
|
|
6939
|
+
function scratchNameProblem(name) {
|
|
6940
|
+
if (!name.startsWith(SCRATCH_PREFIX)) return `scratch infra must be named ${SCRATCH_PREFIX}*, got ${JSON.stringify(name)}`;
|
|
6941
|
+
const hit = PROTECTED_INFRA.find((re) => re.test(name));
|
|
6942
|
+
if (hit) return `${JSON.stringify(name)} is protected infrastructure (${hit}) \u2014 behold never touches it`;
|
|
6943
|
+
return void 0;
|
|
6944
|
+
}
|
|
6945
|
+
function scratchPortProblem(port) {
|
|
6946
|
+
if (PROTECTED_PORTS.includes(port)) return `port ${port} belongs to a shared emulator \u2014 a scratch substrate gets its own`;
|
|
6947
|
+
return void 0;
|
|
6948
|
+
}
|
|
6949
|
+
function assertScratch(name, port) {
|
|
6950
|
+
const p = scratchNameProblem(name) ?? (port === void 0 ? void 0 : scratchPortProblem(port));
|
|
6951
|
+
if (p) throw new Error(`scratch discipline: ${p}`);
|
|
6952
|
+
}
|
|
6953
|
+
|
|
6954
|
+
// src/carve-live.ts
|
|
6598
6955
|
var LIVE_CONTAINER = "behold-carve-floci";
|
|
6599
6956
|
var LIVE_PORT = 4602;
|
|
6600
6957
|
var LIVE_IMAGE = "floci/floci:1.5.34";
|
|
@@ -6627,6 +6984,7 @@ function capture(cmd, args, cwd) {
|
|
|
6627
6984
|
});
|
|
6628
6985
|
}
|
|
6629
6986
|
async function bootScratchFloci() {
|
|
6987
|
+
assertScratch(LIVE_CONTAINER, LIVE_PORT);
|
|
6630
6988
|
const ps = await capture("docker", ["ps", "-a", "--format", "{{.Names}}"], process.cwd());
|
|
6631
6989
|
if (ps.code !== 0) return "docker isn't answering \u2014 is the daemon up?";
|
|
6632
6990
|
if (ps.stdout.split("\n").includes(LIVE_CONTAINER)) {
|
|
@@ -6653,9 +7011,9 @@ async function teardownScratchFloci() {
|
|
|
6653
7011
|
await capture("docker", ["rm", "-f", LIVE_CONTAINER], process.cwd()).catch(() => void 0);
|
|
6654
7012
|
}
|
|
6655
7013
|
async function applyIntoFloci(fromDir, step) {
|
|
6656
|
-
const template =
|
|
7014
|
+
const template = join13(fromDir, "floci-override.tf.disabled");
|
|
6657
7015
|
if (!existsSync7(template)) return `no floci-override.tf.disabled in ${fromDir} \u2014 this estate wasn't authored for the live tier`;
|
|
6658
|
-
writeFileSync3(
|
|
7016
|
+
writeFileSync3(join13(fromDir, "floci_override.tf"), armOverride(readFileSync9(template, "utf8"), LIVE_PORT));
|
|
6659
7017
|
const init = await step("terraform", ["init", "-input=false", "-no-color"], fromDir);
|
|
6660
7018
|
if (init !== 0) return `terraform init exited ${init} (provider downloads need network)`;
|
|
6661
7019
|
const apply = await step(
|
|
@@ -6705,7 +7063,7 @@ async function runCarveObserve(demo, select) {
|
|
|
6705
7063
|
"run `behold demo carve --live` (needs docker + terraform)"
|
|
6706
7064
|
);
|
|
6707
7065
|
}
|
|
6708
|
-
if (!existsSync8(
|
|
7066
|
+
if (!existsSync8(join14(demo.out, "src"))) {
|
|
6709
7067
|
return refuse3("carve-action", "nothing has been emitted yet", "run Emit first \u2014 observe reads the carveout emit writes");
|
|
6710
7068
|
}
|
|
6711
7069
|
const args = ["lifecycle", "diff", "prod", "--live", "--json"];
|
|
@@ -6775,8 +7133,8 @@ var refuse3 = (code, error, remedy) => ({
|
|
|
6775
7133
|
});
|
|
6776
7134
|
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.";
|
|
6777
7135
|
function insideDemo(root, p) {
|
|
6778
|
-
const rel =
|
|
6779
|
-
return rel !== "" && !rel.startsWith("..") && !
|
|
7136
|
+
const rel = relative3(resolve5(root), resolve5(p));
|
|
7137
|
+
return rel !== "" && !rel.startsWith("..") && !isAbsolute2(rel);
|
|
6780
7138
|
}
|
|
6781
7139
|
function selectFromReport(report, raw) {
|
|
6782
7140
|
if (typeof raw !== "string" || !raw || raw.length > 512) return null;
|
|
@@ -6796,17 +7154,17 @@ function readArtifacts(demo, dir, filter) {
|
|
|
6796
7154
|
if (out.length >= MAX_ARTIFACTS || depth > 2) return;
|
|
6797
7155
|
let entries;
|
|
6798
7156
|
try {
|
|
6799
|
-
entries =
|
|
7157
|
+
entries = readdirSync6(d).sort();
|
|
6800
7158
|
} catch {
|
|
6801
7159
|
return;
|
|
6802
7160
|
}
|
|
6803
7161
|
for (const name of entries) {
|
|
6804
7162
|
if (out.length >= MAX_ARTIFACTS) return;
|
|
6805
7163
|
if (name === "node_modules" || name.startsWith(".")) continue;
|
|
6806
|
-
const full =
|
|
7164
|
+
const full = join14(d, name);
|
|
6807
7165
|
let st;
|
|
6808
7166
|
try {
|
|
6809
|
-
st =
|
|
7167
|
+
st = statSync6(full);
|
|
6810
7168
|
} catch {
|
|
6811
7169
|
continue;
|
|
6812
7170
|
}
|
|
@@ -6814,7 +7172,7 @@ function readArtifacts(demo, dir, filter) {
|
|
|
6814
7172
|
walk(full, depth + 1);
|
|
6815
7173
|
continue;
|
|
6816
7174
|
}
|
|
6817
|
-
const rel =
|
|
7175
|
+
const rel = relative3(demo.root, full).split(sep2).join("/");
|
|
6818
7176
|
if (!filter(rel)) continue;
|
|
6819
7177
|
let text = "";
|
|
6820
7178
|
try {
|
|
@@ -6850,7 +7208,7 @@ async function runCarveEmit(demo, select) {
|
|
|
6850
7208
|
const block = carveWriteBlock(demo);
|
|
6851
7209
|
if (block) return refuse3("read-only", block, "Start the walkthrough with `behold demo carve`.");
|
|
6852
7210
|
mkdirSync3(demo.out, { recursive: true });
|
|
6853
|
-
const reportFile =
|
|
7211
|
+
const reportFile = join14(demo.out, `${carveSlug(select)}-boundary.json`);
|
|
6854
7212
|
const args = [
|
|
6855
7213
|
"carve",
|
|
6856
7214
|
"emit",
|
|
@@ -6882,7 +7240,7 @@ async function runCarveEmit(demo, select) {
|
|
|
6882
7240
|
boundary = JSON.parse(readFileSync10(reportFile, "utf8"));
|
|
6883
7241
|
} catch {
|
|
6884
7242
|
}
|
|
6885
|
-
const lintPath =
|
|
7243
|
+
const lintPath = relative3(demo.project, join14(demo.out, "src")).split(sep2).join("/");
|
|
6886
7244
|
const lintArgs = ["lint", lintPath];
|
|
6887
7245
|
const lint = await runChantRaw(lintArgs, demo.project).catch((err) => ({
|
|
6888
7246
|
code: 127,
|
|
@@ -6945,8 +7303,8 @@ async function runCarveBridge(demo, select) {
|
|
|
6945
7303
|
}
|
|
6946
7304
|
|
|
6947
7305
|
// src/ops.ts
|
|
6948
|
-
import { readdirSync as
|
|
6949
|
-
import { join as
|
|
7306
|
+
import { readdirSync as readdirSync7, readFileSync as readFileSync11, existsSync as existsSync9 } from "node:fs";
|
|
7307
|
+
import { join as join15 } from "node:path";
|
|
6950
7308
|
var APPLY_TARGET_LEXICON = {
|
|
6951
7309
|
cloudformation: "aws",
|
|
6952
7310
|
kubectl: "k8s",
|
|
@@ -6968,11 +7326,11 @@ function discoverOps(projectDir) {
|
|
|
6968
7326
|
const seen = /* @__PURE__ */ new Set();
|
|
6969
7327
|
const out = [];
|
|
6970
7328
|
for (const sub of ["ops", "src", "."]) {
|
|
6971
|
-
const dir =
|
|
7329
|
+
const dir = join15(projectDir, sub);
|
|
6972
7330
|
if (!existsSync9(dir)) continue;
|
|
6973
|
-
for (const f of
|
|
7331
|
+
for (const f of readdirSync7(dir)) {
|
|
6974
7332
|
if (!f.endsWith(".op.ts")) continue;
|
|
6975
|
-
const content = readFileSync11(
|
|
7333
|
+
const content = readFileSync11(join15(dir, f), "utf8");
|
|
6976
7334
|
const name = content.match(/name:\s*["'`]([^"'`]+)["'`]/)?.[1];
|
|
6977
7335
|
if (!name || seen.has(name)) continue;
|
|
6978
7336
|
seen.add(name);
|
|
@@ -6995,15 +7353,15 @@ function discoverOps(projectDir) {
|
|
|
6995
7353
|
return out.sort((a, b) => a.name.localeCompare(b.name));
|
|
6996
7354
|
}
|
|
6997
7355
|
function opIrPath(projectDir, name) {
|
|
6998
|
-
return
|
|
7356
|
+
return join15(projectDir, "dist", "ops", name, "op.json");
|
|
6999
7357
|
}
|
|
7000
7358
|
function discoverOpIrs(projectDirs) {
|
|
7001
7359
|
const seen = /* @__PURE__ */ new Set();
|
|
7002
7360
|
const out = [];
|
|
7003
7361
|
for (const dir of projectDirs) {
|
|
7004
|
-
const root =
|
|
7362
|
+
const root = join15(dir, "dist", "ops");
|
|
7005
7363
|
if (!existsSync9(root)) continue;
|
|
7006
|
-
for (const name of
|
|
7364
|
+
for (const name of readdirSync7(root)) {
|
|
7007
7365
|
if (seen.has(name)) continue;
|
|
7008
7366
|
const path = opIrPath(dir, name);
|
|
7009
7367
|
if (!existsSync9(path)) continue;
|
|
@@ -7241,11 +7599,11 @@ var RAN = { ok: "ok", fail: "failed", skipped: "skipped" };
|
|
|
7241
7599
|
function tookLabel(ms) {
|
|
7242
7600
|
return ms < 1e3 ? `${ms}ms` : `${(ms / 1e3).toFixed(1)}s`;
|
|
7243
7601
|
}
|
|
7244
|
-
function paintPlayhead(ir,
|
|
7245
|
-
if (!
|
|
7602
|
+
function paintPlayhead(ir, join22, gate) {
|
|
7603
|
+
if (!join22.byNode.size && !join22.at) return ir;
|
|
7246
7604
|
const nodes = ir.nodes.map((node) => {
|
|
7247
|
-
const run3 =
|
|
7248
|
-
const here =
|
|
7605
|
+
const run3 = join22.byNode.get(node.id);
|
|
7606
|
+
const here = join22.at === node.id;
|
|
7249
7607
|
if (!run3 && !here) return node;
|
|
7250
7608
|
const isGate = node.attrs?._step === "gate";
|
|
7251
7609
|
const attrs = { ...node.attrs };
|
|
@@ -7260,7 +7618,7 @@ function paintPlayhead(ir, join21, gate) {
|
|
|
7260
7618
|
if (here) {
|
|
7261
7619
|
attrs._playhead = true;
|
|
7262
7620
|
attrs._status = isGate && gate ? "warn" : "accent";
|
|
7263
|
-
attrs.run = isGate && gate ? `waiting for approval since ${gate.since}` :
|
|
7621
|
+
attrs.run = isGate && gate ? `waiting for approval since ${gate.since}` : join22.atReason ?? "here";
|
|
7264
7622
|
if (isGate && gate?.description) attrs.gateDescription = gate.description;
|
|
7265
7623
|
}
|
|
7266
7624
|
return { ...node, attrs };
|
|
@@ -7282,11 +7640,11 @@ function pendingGateCard(op, gate, ir) {
|
|
|
7282
7640
|
}
|
|
7283
7641
|
};
|
|
7284
7642
|
}
|
|
7285
|
-
function playheadNote(state,
|
|
7643
|
+
function playheadNote(state, join22) {
|
|
7286
7644
|
if (!state.op) {
|
|
7287
7645
|
return "No run state: nothing has been run from this behold session, so every step paints as declared.";
|
|
7288
7646
|
}
|
|
7289
|
-
const settled =
|
|
7647
|
+
const settled = join22 ? join22.byNode.size : state.records.length;
|
|
7290
7648
|
const bits = [];
|
|
7291
7649
|
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`;
|
|
7292
7650
|
if (state.status === "running") {
|
|
@@ -7302,9 +7660,9 @@ function playheadNote(state, join21) {
|
|
|
7302
7660
|
} else if (state.gateNote) {
|
|
7303
7661
|
bits.push(`gate state unavailable \u2014 ${state.gateNote}`);
|
|
7304
7662
|
}
|
|
7305
|
-
if (
|
|
7663
|
+
if (join22?.unplaced.length) {
|
|
7306
7664
|
bits.push(
|
|
7307
|
-
`${
|
|
7665
|
+
`${join22.unplaced.length} record${join22.unplaced.length === 1 ? "" : "s"} matched no declared step and ${join22.unplaced.length === 1 ? "is" : "are"} not painted`
|
|
7308
7666
|
);
|
|
7309
7667
|
}
|
|
7310
7668
|
if (!bits.length) return `${head}.`;
|
|
@@ -7971,23 +8329,23 @@ var OpRunner = class {
|
|
|
7971
8329
|
// src/substrates.ts
|
|
7972
8330
|
import { spawn as spawn4 } from "node:child_process";
|
|
7973
8331
|
import { existsSync as existsSync10, readFileSync as readFileSync12 } from "node:fs";
|
|
7974
|
-
import { join as
|
|
8332
|
+
import { join as join16 } from "node:path";
|
|
7975
8333
|
import { platform } from "node:os";
|
|
7976
8334
|
var DOCKER_DOWN_DETAIL = "docker is down";
|
|
7977
8335
|
function probe(cmd, args) {
|
|
7978
|
-
return new Promise((
|
|
8336
|
+
return new Promise((resolve10) => {
|
|
7979
8337
|
let out = "";
|
|
7980
8338
|
let proc;
|
|
7981
8339
|
try {
|
|
7982
8340
|
proc = spawn4(cmd, args, { stdio: ["ignore", "pipe", "pipe"] });
|
|
7983
8341
|
} catch {
|
|
7984
|
-
|
|
8342
|
+
resolve10({ code: 127, out: "" });
|
|
7985
8343
|
return;
|
|
7986
8344
|
}
|
|
7987
8345
|
proc.stdout.on("data", (d) => out += d);
|
|
7988
8346
|
proc.stderr.on("data", (d) => out += d);
|
|
7989
|
-
proc.on("error", () =>
|
|
7990
|
-
proc.on("close", (code) =>
|
|
8347
|
+
proc.on("error", () => resolve10({ code: 127, out }));
|
|
8348
|
+
proc.on("close", (code) => resolve10({ code: code ?? 1, out }));
|
|
7991
8349
|
});
|
|
7992
8350
|
}
|
|
7993
8351
|
async function dockerAvailable(run3) {
|
|
@@ -8000,11 +8358,11 @@ async function dockerRunning(run3, nameFilter) {
|
|
|
8000
8358
|
return out.split(/\r?\n/).map((s) => s.trim()).filter(Boolean);
|
|
8001
8359
|
}
|
|
8002
8360
|
function scriptBringUp(projectDir, relPath, label) {
|
|
8003
|
-
return existsSync10(
|
|
8361
|
+
return existsSync10(join16(projectDir, relPath)) ? { label, cmd: "bash", args: [relPath] } : void 0;
|
|
8004
8362
|
}
|
|
8005
8363
|
function projectLexicons(projectDir) {
|
|
8006
8364
|
try {
|
|
8007
|
-
const src = readFileSync12(
|
|
8365
|
+
const src = readFileSync12(join16(projectDir, "chant.config.ts"), "utf-8");
|
|
8008
8366
|
const m = src.match(/lexicons\s*:\s*\[([^\]]*)\]/);
|
|
8009
8367
|
if (!m) return [];
|
|
8010
8368
|
return [...m[1].matchAll(/["']([^"']+)["']/g)].map((x) => x[1]);
|
|
@@ -8056,7 +8414,7 @@ async function detectSubstrates(projectDir, preview = false, boundContext, probe
|
|
|
8056
8414
|
["forgejo", "Forgejo", ".forgejo", "test/forgejo-runtime-e2e.sh"]
|
|
8057
8415
|
];
|
|
8058
8416
|
for (const [name, label, marker, script] of forges) {
|
|
8059
|
-
if (!existsSync10(
|
|
8417
|
+
if (!existsSync10(join16(projectDir, marker))) continue;
|
|
8060
8418
|
const c = docker ? await dockerRunning(run3, name) : [];
|
|
8061
8419
|
const d = dep(c.length > 0, "container up", "on-demand (pipeline run)");
|
|
8062
8420
|
subs.push({
|
|
@@ -8090,7 +8448,7 @@ async function detectSubstrates(projectDir, preview = false, boundContext, probe
|
|
|
8090
8448
|
detail: endpoint ? `targeting ${endpoint}` : "real Fly (FLY_FLAPS_BASE_URL unset)"
|
|
8091
8449
|
});
|
|
8092
8450
|
}
|
|
8093
|
-
if (existsSync10(
|
|
8451
|
+
if (existsSync10(join16(projectDir, ".github", "workflows"))) {
|
|
8094
8452
|
const gh = await run3("gh", ["auth", "status"]);
|
|
8095
8453
|
const ready = gh.code === 0;
|
|
8096
8454
|
subs.push({
|
|
@@ -8103,7 +8461,7 @@ async function detectSubstrates(projectDir, preview = false, boundContext, probe
|
|
|
8103
8461
|
if (lexicons.includes("temporal")) {
|
|
8104
8462
|
let hasProfiles = false;
|
|
8105
8463
|
try {
|
|
8106
|
-
hasProfiles = /temporal\s*:\s*\{[\s\S]{0,400}?profiles\s*:/.test(readFileSync12(
|
|
8464
|
+
hasProfiles = /temporal\s*:\s*\{[\s\S]{0,400}?profiles\s*:/.test(readFileSync12(join16(projectDir, "chant.config.ts"), "utf-8"));
|
|
8107
8465
|
} catch {
|
|
8108
8466
|
}
|
|
8109
8467
|
subs.push({
|
|
@@ -8207,12 +8565,12 @@ async function openRollbackBranches(projectDir, env) {
|
|
|
8207
8565
|
}
|
|
8208
8566
|
|
|
8209
8567
|
// src/estate-edges.ts
|
|
8210
|
-
import { statSync as
|
|
8568
|
+
import { statSync as statSync7 } from "node:fs";
|
|
8211
8569
|
var rec5 = (v) => v && typeof v === "object" && !Array.isArray(v) ? v : void 0;
|
|
8212
8570
|
var str5 = (v) => typeof v === "string" && v.length > 0 ? v : void 0;
|
|
8213
8571
|
var realIsDir2 = (p) => {
|
|
8214
8572
|
try {
|
|
8215
|
-
return
|
|
8573
|
+
return statSync7(p).isDirectory();
|
|
8216
8574
|
} catch {
|
|
8217
8575
|
return false;
|
|
8218
8576
|
}
|
|
@@ -8312,7 +8670,7 @@ function addEstateMemberEdges(ir, members, isDir = realIsDir2) {
|
|
|
8312
8670
|
|
|
8313
8671
|
// src/events.ts
|
|
8314
8672
|
import { watch, existsSync as existsSync11 } from "node:fs";
|
|
8315
|
-
import { join as
|
|
8673
|
+
import { join as join17 } from "node:path";
|
|
8316
8674
|
var Broadcaster = class {
|
|
8317
8675
|
listeners = /* @__PURE__ */ new Set();
|
|
8318
8676
|
subscribe(fn) {
|
|
@@ -8330,7 +8688,7 @@ var Broadcaster = class {
|
|
|
8330
8688
|
};
|
|
8331
8689
|
var IGNORE = /(^|[\\/])(node_modules|dist|\.git)([\\/]|$)/;
|
|
8332
8690
|
function watchSource(projectDir, onChange, debounceMs = 200) {
|
|
8333
|
-
const dir = existsSync11(
|
|
8691
|
+
const dir = existsSync11(join17(projectDir, "src")) ? join17(projectDir, "src") : projectDir;
|
|
8334
8692
|
let timer;
|
|
8335
8693
|
const watcher = watch(dir, { recursive: true }, (_event, file) => {
|
|
8336
8694
|
const name = typeof file === "string" ? file : "";
|
|
@@ -8619,12 +8977,12 @@ async function emulatorDown(projectDir) {
|
|
|
8619
8977
|
|
|
8620
8978
|
// src/demos.ts
|
|
8621
8979
|
import { readFileSync as readFileSync13, existsSync as existsSync12, cpSync } from "node:fs";
|
|
8622
|
-
import { join as
|
|
8980
|
+
import { join as join18, relative as relative4, resolve as resolve6, sep as sep3 } from "node:path";
|
|
8623
8981
|
import { spawn as spawn5, spawnSync } from "node:child_process";
|
|
8624
8982
|
function loadDemoRegistry(pkgRoot2) {
|
|
8625
8983
|
let raw;
|
|
8626
8984
|
try {
|
|
8627
|
-
raw = JSON.parse(readFileSync13(
|
|
8985
|
+
raw = JSON.parse(readFileSync13(join18(pkgRoot2, "demos.json"), "utf8"));
|
|
8628
8986
|
} catch {
|
|
8629
8987
|
return [];
|
|
8630
8988
|
}
|
|
@@ -8664,22 +9022,22 @@ function fetchesFromNetwork(entry) {
|
|
|
8664
9022
|
return entry.source === "git";
|
|
8665
9023
|
}
|
|
8666
9024
|
function demoTargetDir(entry, cwd = process.cwd()) {
|
|
8667
|
-
const legacy =
|
|
9025
|
+
const legacy = resolve6(cwd, "behold-demo");
|
|
8668
9026
|
if (entry.name === "writes" && existsSync12(legacy)) return legacy;
|
|
8669
|
-
return
|
|
9027
|
+
return resolve6(cwd, "behold-demos", entry.name);
|
|
8670
9028
|
}
|
|
8671
9029
|
async function loadDemo(entry, opts) {
|
|
8672
9030
|
const { pkgRoot: pkgRoot2, target } = opts;
|
|
8673
9031
|
const say = (line) => opts.log?.(`behold demo ${entry.name} \u2192 ${line}`);
|
|
8674
9032
|
if (!existsSync12(target)) {
|
|
8675
9033
|
if (entry.source === "bundled") {
|
|
8676
|
-
const bundled =
|
|
9034
|
+
const bundled = join18(pkgRoot2, entry.dir);
|
|
8677
9035
|
if (!existsSync12(bundled)) return { ok: false, error: `this install has no bundled ${entry.dir}` };
|
|
8678
9036
|
say(`copying to ${target} (it's yours \u2014 edit it)`);
|
|
8679
9037
|
try {
|
|
8680
9038
|
cpSync(bundled, target, {
|
|
8681
9039
|
recursive: true,
|
|
8682
|
-
filter: (src) => !
|
|
9040
|
+
filter: (src) => !relative4(bundled, src).split(sep3).includes("node_modules")
|
|
8683
9041
|
});
|
|
8684
9042
|
} catch (err) {
|
|
8685
9043
|
return { ok: false, error: `copy failed: ${err instanceof Error ? err.message : String(err)}` };
|
|
@@ -8693,7 +9051,7 @@ async function loadDemo(entry, opts) {
|
|
|
8693
9051
|
} else {
|
|
8694
9052
|
say(`reusing ${target}`);
|
|
8695
9053
|
}
|
|
8696
|
-
if (existsSync12(
|
|
9054
|
+
if (existsSync12(join18(target, "package.json")) && !existsSync12(join18(target, "node_modules"))) {
|
|
8697
9055
|
say("npm install\u2026");
|
|
8698
9056
|
if (await runStep("npm", ["install"], { cwd: target, shell: process.platform === "win32" }) !== 0) {
|
|
8699
9057
|
return { ok: false, error: `npm install failed in ${target}` };
|
|
@@ -8705,7 +9063,7 @@ async function loadDemo(entry, opts) {
|
|
|
8705
9063
|
return { ok: false, error: `setup failed (${entry.setup})` };
|
|
8706
9064
|
}
|
|
8707
9065
|
}
|
|
8708
|
-
return { ok: true, serveDirs: entry.serve.dirs?.length ? entry.serve.dirs.map((d) =>
|
|
9066
|
+
return { ok: true, serveDirs: entry.serve.dirs?.length ? entry.serve.dirs.map((d) => join18(target, d)) : [target] };
|
|
8709
9067
|
}
|
|
8710
9068
|
function runStep(cmd, args, opts = {}) {
|
|
8711
9069
|
return new Promise((res) => {
|
|
@@ -8716,8 +9074,8 @@ function runStep(cmd, args, opts = {}) {
|
|
|
8716
9074
|
}
|
|
8717
9075
|
|
|
8718
9076
|
// src/server.ts
|
|
8719
|
-
var pkgRoot =
|
|
8720
|
-
var webRoot =
|
|
9077
|
+
var pkgRoot = join19(dirname5(fileURLToPath3(import.meta.url)), "..");
|
|
9078
|
+
var webRoot = join19(pkgRoot, "web");
|
|
8721
9079
|
var execFileP = async (cmd, args) => (await promisify3(execFile3)(cmd, args, { encoding: "utf8", timeout: 1e4 })).stdout;
|
|
8722
9080
|
function optsFromQuery(url, tierEnvVar, projectDir) {
|
|
8723
9081
|
const q = url.searchParams;
|
|
@@ -8767,7 +9125,7 @@ function tierFailure(tier, message) {
|
|
|
8767
9125
|
function beholdVersion() {
|
|
8768
9126
|
try {
|
|
8769
9127
|
const here = dirname5(fileURLToPath3(import.meta.url));
|
|
8770
|
-
return JSON.parse(readFileSync14(
|
|
9128
|
+
return JSON.parse(readFileSync14(join19(here, "..", "package.json"), "utf8")).version ?? "unknown";
|
|
8771
9129
|
} catch {
|
|
8772
9130
|
return "unknown";
|
|
8773
9131
|
}
|
|
@@ -8815,6 +9173,11 @@ function carveRoutes(app, reportPath, demo) {
|
|
|
8815
9173
|
const load = () => readCarveReport(reportPath, (p) => readFileSync14(p, "utf8"));
|
|
8816
9174
|
const carveDirs = [dirname5(reportPath), ...demo ? [demo.out] : []];
|
|
8817
9175
|
const carveStates = () => readCarveStates(carveDirs, nodeCarveIo, 0);
|
|
9176
|
+
const emittedNodes = async (states) => {
|
|
9177
|
+
if (!demo || states.size === 0) return /* @__PURE__ */ new Map();
|
|
9178
|
+
const outIr = await memberIr(demo.out).catch(() => void 0);
|
|
9179
|
+
return emittedNodesFor(outIr, states.values(), demo.out);
|
|
9180
|
+
};
|
|
8818
9181
|
const demoBlock = () => carveWriteBlock(demo);
|
|
8819
9182
|
const demoInfo = () => {
|
|
8820
9183
|
if (!demo) return null;
|
|
@@ -8826,8 +9189,8 @@ function carveRoutes(app, reportPath, demo) {
|
|
|
8826
9189
|
project: demo.project,
|
|
8827
9190
|
out: demo.out,
|
|
8828
9191
|
// Paths the UI can print without leaking the operator's whole home dir.
|
|
8829
|
-
outLabel:
|
|
8830
|
-
fromLabel:
|
|
9192
|
+
outLabel: relative5(demo.root, demo.out).split(sep4).join("/"),
|
|
9193
|
+
fromLabel: relative5(demo.root, demo.from).split(sep4).join("/"),
|
|
8831
9194
|
runnable: !block,
|
|
8832
9195
|
...block ? { reason: block } : {},
|
|
8833
9196
|
// The live tier, when this boot armed it: the scratch endpoint the
|
|
@@ -8844,7 +9207,7 @@ function carveRoutes(app, reportPath, demo) {
|
|
|
8844
9207
|
let appGraph;
|
|
8845
9208
|
const appGraphOnce = () => {
|
|
8846
9209
|
if (!demo) return Promise.resolve(null);
|
|
8847
|
-
appGraph ??= graphIr(demo.project).then((ir) => ir.nodes.length ? { ir, label:
|
|
9210
|
+
appGraph ??= graphIr(demo.project).then((ir) => ir.nodes.length ? { ir, label: relative5(demo.root, demo.project).split(sep4).join("/") } : null).catch(() => null);
|
|
8848
9211
|
return appGraph;
|
|
8849
9212
|
};
|
|
8850
9213
|
app.get("/api/graph", async (c) => {
|
|
@@ -8853,16 +9216,18 @@ function carveRoutes(app, reportPath, demo) {
|
|
|
8853
9216
|
const tfIr = carveReportToIr(parsed.report);
|
|
8854
9217
|
const appSide = await appGraphOnce();
|
|
8855
9218
|
const states = carveStates();
|
|
9219
|
+
const emitted = await emittedNodes(states);
|
|
8856
9220
|
const shorten = demo ? (p) => shortenIn(p, demo.root) : (p) => p;
|
|
8857
9221
|
const single = () => {
|
|
8858
|
-
const split = splitCarveState(tfIr, states, { shorten });
|
|
9222
|
+
const split = splitCarveState(tfIr, states, { shorten, emitted });
|
|
8859
9223
|
return bandGraduated(split.tf, split.graduated);
|
|
8860
9224
|
};
|
|
8861
9225
|
const { svg, ir } = appSide ? renderCarveEstate(tfIr, appSide.ir, {
|
|
8862
|
-
tfTitle: `${
|
|
9226
|
+
tfTitle: `${relative5(demo.root, demo.from).split(sep4).join("/")} \u2014 terraform`,
|
|
8863
9227
|
appTitle: `${appSide.label} \u2014 chant`,
|
|
8864
9228
|
carved: states,
|
|
8865
|
-
shorten
|
|
9229
|
+
shorten,
|
|
9230
|
+
emitted
|
|
8866
9231
|
}) : (() => {
|
|
8867
9232
|
const banded = single();
|
|
8868
9233
|
return { ...renderBanded(banded), ir: banded };
|
|
@@ -8910,7 +9275,7 @@ function carveRoutes(app, reportPath, demo) {
|
|
|
8910
9275
|
}
|
|
8911
9276
|
const tfIr = carveReportToIr(parsed.report);
|
|
8912
9277
|
const html = renderCarveMorph(tfIr, appSide.ir, [select], {
|
|
8913
|
-
tfTitle: `${
|
|
9278
|
+
tfTitle: `${relative5(demo.root, demo.from).split(sep4).join("/")} \u2014 terraform`,
|
|
8914
9279
|
appTitle: `${appSide.label} \u2014 chant`,
|
|
8915
9280
|
title: `carve \u2014 ${select}`,
|
|
8916
9281
|
carved: carveStates(),
|
|
@@ -8918,7 +9283,7 @@ function carveRoutes(app, reportPath, demo) {
|
|
|
8918
9283
|
});
|
|
8919
9284
|
return c.html(html);
|
|
8920
9285
|
});
|
|
8921
|
-
app.get("/api/project", (c) => {
|
|
9286
|
+
app.get("/api/project", async (c) => {
|
|
8922
9287
|
const parsed = load();
|
|
8923
9288
|
const total = parsed.ok ? parsed.report.count ?? parsed.report.resources.length : null;
|
|
8924
9289
|
const state = carveStatePayload(
|
|
@@ -9013,6 +9378,20 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
|
|
|
9013
9378
|
if (cfg.carveReport) carveRoutes(app, cfg.carveReport, cfg.carveDemo);
|
|
9014
9379
|
let beholdConfig = loadBeholdConfig(cfg.projectDir);
|
|
9015
9380
|
let tierEnvVar = beholdConfig.tiers?.envVar;
|
|
9381
|
+
const executorFor = (env) => env ? beholdConfig.executor?.[env] : void 0;
|
|
9382
|
+
const executorStatus = (env, d, pipeline) => {
|
|
9383
|
+
if ("invalid" in d) return { ok: false, reason: d.invalid };
|
|
9384
|
+
if (d.forge !== "github") {
|
|
9385
|
+
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` };
|
|
9386
|
+
}
|
|
9387
|
+
const r = resolveWorkflow(cfg.projectDir, pipeline ?? { stages: [], jobs: [] }, d.workflow);
|
|
9388
|
+
return "workflow" in r ? { forge: d.forge, workflow: d.workflow, ok: true } : { forge: d.forge, workflow: d.workflow, ok: false, reason: r.reason };
|
|
9389
|
+
};
|
|
9390
|
+
const forgeRefusal = (env, d, what) => ({
|
|
9391
|
+
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`,
|
|
9392
|
+
code: "executor-forge",
|
|
9393
|
+
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.`
|
|
9394
|
+
});
|
|
9016
9395
|
const boundK8sContext = async (env) => {
|
|
9017
9396
|
try {
|
|
9018
9397
|
const { lexicons, k8sProfiles } = await detectProject(cfg.projectDir);
|
|
@@ -9074,6 +9453,8 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
|
|
|
9074
9453
|
if (!info) {
|
|
9075
9454
|
return c.json({ error: `no Op named "${name}" in the estate` }, 404);
|
|
9076
9455
|
}
|
|
9456
|
+
const designatedEnv = info.kind === "apply" ? executorFor(info.env) : void 0;
|
|
9457
|
+
if (designatedEnv && info.env) return c.json(forgeRefusal(info.env, designatedEnv, `running ${name}`), 409);
|
|
9077
9458
|
if (!runner.trigger(name, info.env, info.dir, Boolean(info.gate))) {
|
|
9078
9459
|
return c.json({ error: `an Op is already running (${runner.running})` }, 409);
|
|
9079
9460
|
}
|
|
@@ -9105,8 +9486,8 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
|
|
|
9105
9486
|
return c.json({ started: true, name, ran: label });
|
|
9106
9487
|
});
|
|
9107
9488
|
app.post("/api/local/reset", (c) => {
|
|
9108
|
-
const down =
|
|
9109
|
-
const up =
|
|
9489
|
+
const down = join19(cfg.projectDir, "scripts/local/local-down.sh");
|
|
9490
|
+
const up = join19(cfg.projectDir, "scripts/local/local-up.sh");
|
|
9110
9491
|
if (!existsSync13(down) || !existsSync13(up)) {
|
|
9111
9492
|
return c.json({ error: "no local-down.sh / local-up.sh in scripts/local \u2014 reset is only for local emulator projects" }, 400);
|
|
9112
9493
|
}
|
|
@@ -9243,7 +9624,7 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
|
|
|
9243
9624
|
const k8sTarget = lexicons.includes("k8s") ? resolveK8sTarget(k8sProfiles, cfg.env, await loadKubeconfig()) : void 0;
|
|
9244
9625
|
const axes = deployAxes(tierEnvVar, lexicons, k8sTarget);
|
|
9245
9626
|
const emittedOps = discoverOpIrs(estateDirs).length;
|
|
9246
|
-
const carveState = carveStatePayload(
|
|
9627
|
+
const carveState = carveStatePayload(await carveStatesUnder(estateDirs));
|
|
9247
9628
|
return c.json({
|
|
9248
9629
|
projectDir: cfg.projectDir,
|
|
9249
9630
|
...carveState.manifests ? { carve: { state: carveState } } : {},
|
|
@@ -9270,6 +9651,10 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
|
|
|
9270
9651
|
// no `tiers` key) → no `tiers` field → the SPA's picker doesn't render
|
|
9271
9652
|
// and the graph loads with no tier selected (web/app.js `initPickers`).
|
|
9272
9653
|
...beholdConfig.tiers ? { tiers: beholdConfig.tiers.values } : {},
|
|
9654
|
+
// The executor contract (#165): per designated env, whether Deploy can
|
|
9655
|
+
// dispatch right now and, if not, why — so the SPA disables the gesture
|
|
9656
|
+
// with the reason rather than offering a control that 4xxs.
|
|
9657
|
+
...beholdConfig.executor ? { executor: Object.fromEntries(Object.entries(beholdConfig.executor).map(([env, d]) => [env, executorStatus(env, d)])) } : {},
|
|
9273
9658
|
// The stack picker's options (#76, follow-up to #71): gated exactly like
|
|
9274
9659
|
// `tiers` above — only present when `chant.config.ts` declares `stacks[]`
|
|
9275
9660
|
// at all, so a single-stack/sourceDir-only/legacy project's SPA renders no
|
|
@@ -9294,9 +9679,9 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
|
|
|
9294
9679
|
app.post("/api/project/open", async (c) => {
|
|
9295
9680
|
if (cfg.previewMode) return c.json({ error: "switching projects is locked in preview mode" }, 403);
|
|
9296
9681
|
const body = await c.req.json().catch(() => ({}));
|
|
9297
|
-
const dir = typeof body.dir === "string" && body.dir.trim() ?
|
|
9682
|
+
const dir = typeof body.dir === "string" && body.dir.trim() ? resolve7(body.dir.trim()) : "";
|
|
9298
9683
|
if (!dir || !existsSync13(dir)) return c.json({ error: `no such directory: ${dir || "(no dir given)"}` }, 400);
|
|
9299
|
-
if (!existsSync13(
|
|
9684
|
+
if (!existsSync13(join19(dir, "chant.config.ts"))) {
|
|
9300
9685
|
return c.json({ error: `${dir} doesn't look like a chant project \u2014 no chant.config.ts` }, 400);
|
|
9301
9686
|
}
|
|
9302
9687
|
switchServedProject([dir]);
|
|
@@ -9316,7 +9701,7 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
|
|
|
9316
9701
|
};
|
|
9317
9702
|
app.post("/api/project/reveal", async (c) => {
|
|
9318
9703
|
const body = await c.req.json().catch(() => ({}));
|
|
9319
|
-
const dir = typeof body.dir === "string" && body.dir.trim() ?
|
|
9704
|
+
const dir = typeof body.dir === "string" && body.dir.trim() ? resolve7(body.dir.trim()) : cfg.projectDir;
|
|
9320
9705
|
const known = /* @__PURE__ */ new Set([cfg.projectDir, ...cfg.projectDirs ?? [], ...listRecents().map((r) => r.dir)]);
|
|
9321
9706
|
if (!known.has(dir)) return c.json({ error: "not a served or recent project directory" }, 400);
|
|
9322
9707
|
const opener = process.platform === "darwin" ? "open" : process.platform === "win32" ? "explorer" : "xdg-open";
|
|
@@ -9384,7 +9769,7 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
|
|
|
9384
9769
|
const loaded = await loadDemo(entry, { pkgRoot, target, log: (line) => process.stdout.write(line + "\n") });
|
|
9385
9770
|
if (!loaded.ok) return c.json({ error: loaded.error }, 500);
|
|
9386
9771
|
const dirs = loaded.serveDirs;
|
|
9387
|
-
if (!existsSync13(
|
|
9772
|
+
if (!existsSync13(join19(dirs[0], "chant.config.ts"))) {
|
|
9388
9773
|
return c.json({ error: `${entry.name} loaded to ${target} but ${dirs[0]} has no chant.config.ts` }, 500);
|
|
9389
9774
|
}
|
|
9390
9775
|
if (entry.serve.local) {
|
|
@@ -9482,7 +9867,7 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
|
|
|
9482
9867
|
path: "/api/demos/open",
|
|
9483
9868
|
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)"
|
|
9484
9869
|
},
|
|
9485
|
-
{ 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" },
|
|
9870
|
+
{ 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" },
|
|
9486
9871
|
...cfg.carveReport ? [
|
|
9487
9872
|
{ method: "GET", path: "/api/carve", desc: "carve mode: the raw `chant carve advise --json` peelability report this server is rendering" },
|
|
9488
9873
|
{
|
|
@@ -9519,7 +9904,7 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
|
|
|
9519
9904
|
{ method: "POST", path: "/api/rollback", desc: "open a rollback PR: ?to=<sha>" },
|
|
9520
9905
|
{ method: "POST", path: "/api/substrates/:name/up", desc: "bring a substrate up" },
|
|
9521
9906
|
{ method: "POST", path: "/api/local/reset", desc: "reset the local emulator" },
|
|
9522
|
-
{ method: "POST", path: "/api/ci/dispatch", desc: "dispatch the GitHub Actions pipeline via the operator's gh" }
|
|
9907
|
+
{ 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" }
|
|
9523
9908
|
]
|
|
9524
9909
|
})
|
|
9525
9910
|
);
|
|
@@ -9546,10 +9931,16 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
|
|
|
9546
9931
|
parsedOps.push(parsed.ir);
|
|
9547
9932
|
}
|
|
9548
9933
|
const declaredIr = opsToIr(parsedOps);
|
|
9934
|
+
if (url.searchParams.get("entities") === "1") {
|
|
9935
|
+
const memberDirs = [...new Set(files.map((f) => f.dir))];
|
|
9936
|
+
const memberReads = await Promise.allSettled(memberDirs.map((dir) => memberIr(dir)));
|
|
9937
|
+
const estateNodes = memberReads.flatMap((r) => r.status === "fulfilled" ? r.value.nodes : []);
|
|
9938
|
+
joinStepEntities(declaredIr, estateNodes.length ? { nodes: estateNodes, edges: [], groups: {} } : void 0);
|
|
9939
|
+
}
|
|
9549
9940
|
const runState = runner.runState;
|
|
9550
9941
|
const ranOp = runState.op ? parsedOps.find((o) => o.name === runState.op) : void 0;
|
|
9551
|
-
const
|
|
9552
|
-
const opsIr =
|
|
9942
|
+
const join22 = ranOp ? joinRun(ranOp, runState.records, runState.gate) : null;
|
|
9943
|
+
const opsIr = join22 ? paintPlayhead(declaredIr, join22, runState.gate) : declaredIr;
|
|
9553
9944
|
const { svg: svg2 } = renderGraph(opsIr, { boxes: "byStack" });
|
|
9554
9945
|
const operator = runner.noteDeclaredConvergeOps(declaredConvergeOps(parsedOps));
|
|
9555
9946
|
return c.json({
|
|
@@ -9562,7 +9953,7 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
|
|
|
9562
9953
|
target: null,
|
|
9563
9954
|
mode: "ops",
|
|
9564
9955
|
ops: parsedOps.length,
|
|
9565
|
-
note: `${opsNote(parsedOps, opsIr)} ${playheadNote(runState,
|
|
9956
|
+
note: `${opsNote(parsedOps, opsIr)} ${playheadNote(runState, join22)} ${operatorNote(operator)}`,
|
|
9566
9957
|
// The operator strip's own model: the ConvergeOps this project
|
|
9567
9958
|
// declares, plus whatever the last status read said. `declared`
|
|
9568
9959
|
// without a `strip` is the honest starting state — a loop exists and
|
|
@@ -9634,6 +10025,7 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
|
|
|
9634
10025
|
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 } : {} } });
|
|
9635
10026
|
} else {
|
|
9636
10027
|
ir = mergeClusterRoot(await graphIr(cfg.projectDir, opts), await clusterRootGraphIr(cfg.projectDir, opts));
|
|
10028
|
+
ir = joinCarvedSources(ir, (await carveStatesFor(cfg.projectDir)).values(), cfg.projectDir).ir;
|
|
9637
10029
|
if ((opts.detail ?? 2) < 3) ir = pruneImports(ir);
|
|
9638
10030
|
ir = addK8sDeclaredEdges(ir);
|
|
9639
10031
|
ir = addValueMatchEdges(ir);
|
|
@@ -9651,7 +10043,7 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
|
|
|
9651
10043
|
srcCompositeEdgesAttached = 0;
|
|
9652
10044
|
}
|
|
9653
10045
|
}
|
|
9654
|
-
if (!multi && !components && !logical && !opts.lens && ir.nodes.length === 0 && !existsSync13(
|
|
10046
|
+
if (!multi && !components && !logical && !opts.lens && ir.nodes.length === 0 && !existsSync13(join19(cfg.projectDir, "chant.config.ts"))) {
|
|
9655
10047
|
return c.json(noProjectError(cfg.projectDir), 404);
|
|
9656
10048
|
}
|
|
9657
10049
|
const radial = new URL(c.req.url).searchParams.get("radial") === "1";
|
|
@@ -9698,23 +10090,28 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
|
|
|
9698
10090
|
}
|
|
9699
10091
|
});
|
|
9700
10092
|
app.post("/api/ci/dispatch", async (c) => {
|
|
10093
|
+
const env = new URL(c.req.url).searchParams.get("env") ?? cfg.env;
|
|
10094
|
+
const designated = executorFor(env);
|
|
10095
|
+
if (designated && "invalid" in designated) return c.json({ error: designated.invalid, code: "executor-forge", remedy: "Fix .behold.json's executor block." }, 400);
|
|
10096
|
+
if (designated && designated.forge !== "github") {
|
|
10097
|
+
return c.json({ error: executorStatus(env, designated).reason, code: "executor-forge" }, 400);
|
|
10098
|
+
}
|
|
9701
10099
|
const ready = await ghReady();
|
|
9702
10100
|
if (!ready.ok) return c.json({ error: ready.reason }, 400);
|
|
9703
|
-
const pipeline = await ciPipeline(cfg.projectDir,
|
|
10101
|
+
const pipeline = await ciPipeline(cfg.projectDir, env ? { env } : {}, "github").catch(() => void 0);
|
|
9704
10102
|
if (!pipeline || pipeline.jobs.length === 0) {
|
|
9705
10103
|
return c.json({ error: "no generated GitHub pipeline \u2014 `chant build --components --generate github` produced no jobs" }, 400);
|
|
9706
10104
|
}
|
|
9707
|
-
const
|
|
9708
|
-
if (
|
|
9709
|
-
|
|
9710
|
-
}
|
|
10105
|
+
const resolved = resolveWorkflow(cfg.projectDir, pipeline, designated?.workflow);
|
|
10106
|
+
if ("reason" in resolved) return c.json({ error: resolved.reason, ...designated ? { code: "executor-forge" } : {} }, 400);
|
|
10107
|
+
const workflow = resolved.workflow;
|
|
9711
10108
|
const ref = await execFileP("git", ["-C", cfg.projectDir, "branch", "--show-current"]).then((out) => out.trim() || "main").catch(() => "main");
|
|
9712
10109
|
const started = runner.track(
|
|
9713
10110
|
`GitHub Actions ${workflow.file}`,
|
|
9714
10111
|
(io) => dispatchAndFollow(workflow, pipeline, ref, { onLine: io.line, onProgress: io.progress })
|
|
9715
10112
|
);
|
|
9716
10113
|
if (!started) return c.json({ error: `busy \u2014 ${runner.running} is running` }, 409);
|
|
9717
|
-
return c.json({ started: true, workflow: workflow.file, ref, jobs: pipeline.jobs.length });
|
|
10114
|
+
return c.json({ started: true, workflow: workflow.file, ref, jobs: pipeline.jobs.length, ...env ? { env } : {}, designated: Boolean(designated) });
|
|
9718
10115
|
});
|
|
9719
10116
|
app.get("/api/resources", async (c) => {
|
|
9720
10117
|
const opts = optsFromQuery(new URL(c.req.url), tierEnvVar, cfg.projectDir);
|
|
@@ -9809,7 +10206,7 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
|
|
|
9809
10206
|
});
|
|
9810
10207
|
}
|
|
9811
10208
|
const opts = { ...query, live: true, overlay: true, env, ...logical || runtime ? { detail: 3 } : {} };
|
|
9812
|
-
let ir = reclassifyOverlay(await graphIr(cfg.projectDir, opts));
|
|
10209
|
+
let ir = joinCarvedSources(reclassifyOverlay(await graphIr(cfg.projectDir, opts)), (await carveStatesFor(cfg.projectDir)).values(), cfg.projectDir).ir;
|
|
9813
10210
|
const boundContext = await boundK8sContext(env);
|
|
9814
10211
|
ir = mergeClusterRoot(ir, await clusterRootGraphIr(cfg.projectDir, query), await runningK3dClusters());
|
|
9815
10212
|
const declaresHelm = await detectProject(cfg.projectDir).then((p) => p.lexicons.includes("helm")).catch(() => false);
|
|
@@ -9817,7 +10214,7 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
|
|
|
9817
10214
|
const observed = await lifecycleDiffLive(cfg.projectDir, env, query).then((d) => d.lexicons?.helm?.observedArtifacts).catch(() => void 0);
|
|
9818
10215
|
applyHelmArtifacts(ir, observed);
|
|
9819
10216
|
synthesizeHelmReleases(ir, observed, discoverReleaseUnits(cfg.projectDir));
|
|
9820
|
-
applyHelmRenderDrift(ir, await readHelmRenderDrift(cfg.projectDir, env, query));
|
|
10217
|
+
applyHelmRenderDrift(ir, await readHelmRenderDrift(cfg.projectDir, env, query, observed));
|
|
9821
10218
|
}
|
|
9822
10219
|
ir = runtime ? attachRuntimeContainment(ir) : pruneRuntimeChildren(ir);
|
|
9823
10220
|
if (logical) {
|
|
@@ -9868,7 +10265,7 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
|
|
|
9868
10265
|
const observed = await lifecycleDiffLive(cfg.projectDir, env).then((d) => d.lexicons?.helm?.observedArtifacts).catch(() => void 0);
|
|
9869
10266
|
applyHelmArtifacts(result.ir, observed);
|
|
9870
10267
|
synthesizeHelmReleases(result.ir, observed, discoverReleaseUnits(cfg.projectDir));
|
|
9871
|
-
applyHelmRenderDrift(result.ir, await readHelmRenderDrift(cfg.projectDir, env));
|
|
10268
|
+
applyHelmRenderDrift(result.ir, await readHelmRenderDrift(cfg.projectDir, env, {}, observed));
|
|
9872
10269
|
}
|
|
9873
10270
|
}
|
|
9874
10271
|
if (env) {
|
|
@@ -9972,6 +10369,8 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
|
|
|
9972
10369
|
}
|
|
9973
10370
|
const component = url.searchParams.get("component") || "all";
|
|
9974
10371
|
const force = url.searchParams.get("force") === "1";
|
|
10372
|
+
const designated = executorFor(env);
|
|
10373
|
+
if (designated) return c.json(forgeRefusal(env, designated, "apply"), 409);
|
|
9975
10374
|
const flociTargeted = !!process.env.AWS_ENDPOINT_URL && await detectProject(cfg.projectDir).then((p) => p.lexicons.includes("aws")).catch(() => false);
|
|
9976
10375
|
if (!force && flociTargeted) {
|
|
9977
10376
|
try {
|
|
@@ -9996,9 +10395,9 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
|
|
|
9996
10395
|
}
|
|
9997
10396
|
return c.json({ started: true, component, env });
|
|
9998
10397
|
});
|
|
9999
|
-
const rel =
|
|
10398
|
+
const rel = relative5(process.cwd(), webRoot) || ".";
|
|
10000
10399
|
app.use("/*", serveStatic({ root: rel }));
|
|
10001
|
-
app.get("/", serveStatic({ path:
|
|
10400
|
+
app.get("/", serveStatic({ path: join19(rel, "index.html") }));
|
|
10002
10401
|
return app;
|
|
10003
10402
|
}
|
|
10004
10403
|
async function bootLocalEmulators(dir, who) {
|
|
@@ -10027,7 +10426,7 @@ async function bootLocalEmulators(dir, who) {
|
|
|
10027
10426
|
async function startServer(cfg) {
|
|
10028
10427
|
if (cfg.local) cfg.emulators = await bootLocalEmulators(cfg.projectDir, "behold serve --local");
|
|
10029
10428
|
for (const dir of cfg.projectDirs ?? [cfg.projectDir]) {
|
|
10030
|
-
if (existsSync13(
|
|
10429
|
+
if (existsSync13(join19(dir, "chant.config.ts"))) addRecent(dir);
|
|
10031
10430
|
}
|
|
10032
10431
|
const broadcaster = new Broadcaster();
|
|
10033
10432
|
const frames = new FrameBuffer();
|
|
@@ -10053,11 +10452,19 @@ async function startServer(cfg) {
|
|
|
10053
10452
|
if (autoSync === "off") return;
|
|
10054
10453
|
void routeAutoSync(dir, movedLexicons);
|
|
10055
10454
|
};
|
|
10455
|
+
const executorDesignations = loadBeholdConfig(cfg.projectDir).executor ?? {};
|
|
10456
|
+
const executorFor = (env) => env ? executorDesignations[env] : void 0;
|
|
10056
10457
|
const routeAutoSync = async (dir, movedLexicons) => {
|
|
10057
10458
|
const suspended = autoSync === "pull-request" ? suspendedByRollback(await openRollbackBranches(dir, cfg.env), movedLexicons) : /* @__PURE__ */ new Set();
|
|
10459
|
+
const allOps = discoverEstateOps(cfg.projectDirs ?? [cfg.projectDir]);
|
|
10460
|
+
const routed = allOps.filter((o) => o.kind === "apply" && o.env && executorFor(o.env));
|
|
10461
|
+
for (const o of routed) {
|
|
10462
|
+
const d = executorFor(o.env);
|
|
10463
|
+
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`);
|
|
10464
|
+
}
|
|
10058
10465
|
const { picks, declined } = pickAutoSyncOps(
|
|
10059
10466
|
autoSync,
|
|
10060
|
-
|
|
10467
|
+
allOps.filter((o) => !routed.includes(o)),
|
|
10061
10468
|
runner.running,
|
|
10062
10469
|
movedLexicons,
|
|
10063
10470
|
suspended
|
|
@@ -10166,8 +10573,8 @@ async function startServer(cfg) {
|
|
|
10166
10573
|
}
|
|
10167
10574
|
|
|
10168
10575
|
// src/export.ts
|
|
10169
|
-
import { mkdirSync as mkdirSync4, writeFileSync as writeFileSync4, copyFileSync, readFileSync as readFileSync15, readdirSync as
|
|
10170
|
-
import { join as
|
|
10576
|
+
import { mkdirSync as mkdirSync4, writeFileSync as writeFileSync4, copyFileSync, readFileSync as readFileSync15, readdirSync as readdirSync8 } from "node:fs";
|
|
10577
|
+
import { join as join20, dirname as dirname6, basename as basename2 } from "node:path";
|
|
10171
10578
|
import { fileURLToPath as fileURLToPath4 } from "node:url";
|
|
10172
10579
|
var LENS_PARAMS = ["components", "detail", "env", "logical", "ops", "radial", "tier"];
|
|
10173
10580
|
function canonicalKey(path, params) {
|
|
@@ -10185,7 +10592,7 @@ function captureKeys(axes) {
|
|
|
10185
10592
|
add("/api/project", {});
|
|
10186
10593
|
add("/api/substrates", {});
|
|
10187
10594
|
add("/api/ops", {});
|
|
10188
|
-
if (axes.ops) add("/api/graph", { ops: "1" });
|
|
10595
|
+
if (axes.ops) add("/api/graph", { ops: "1", entities: "1" });
|
|
10189
10596
|
const tiers = axes.tiers && axes.tiers.length ? axes.tiers : [""];
|
|
10190
10597
|
const envs = ["", ...axes.environments];
|
|
10191
10598
|
for (const env of envs) {
|
|
@@ -10216,7 +10623,7 @@ function captureKeys(axes) {
|
|
|
10216
10623
|
return [...keys];
|
|
10217
10624
|
}
|
|
10218
10625
|
function webDir() {
|
|
10219
|
-
return
|
|
10626
|
+
return join20(dirname6(fileURLToPath4(import.meta.url)), "..", "web");
|
|
10220
10627
|
}
|
|
10221
10628
|
function workerName(project, override) {
|
|
10222
10629
|
const raw = override ?? `behold-${basename2(project)}`;
|
|
@@ -10227,7 +10634,7 @@ async function runExport(cfg, outDir, opts = {}) {
|
|
|
10227
10634
|
const app = createApp({ ...cfg, layoutWrites: false });
|
|
10228
10635
|
const proj = await (await app.request("/api/project")).json();
|
|
10229
10636
|
const axes = { environments: proj.environments ?? [], tiers: proj.tiers ?? [], ...proj.ops ? { ops: proj.ops } : {} };
|
|
10230
|
-
const snapDir =
|
|
10637
|
+
const snapDir = join20(outDir, "snapshots");
|
|
10231
10638
|
mkdirSync4(snapDir, { recursive: true });
|
|
10232
10639
|
const keyToFile = {};
|
|
10233
10640
|
let ok = 0;
|
|
@@ -10236,7 +10643,7 @@ async function runExport(cfg, outDir, opts = {}) {
|
|
|
10236
10643
|
const res = await app.request(`${key}${key.includes("?") ? "&" : "?"}layout=1`);
|
|
10237
10644
|
const body = await res.text();
|
|
10238
10645
|
const file = slug2(key);
|
|
10239
|
-
writeFileSync4(
|
|
10646
|
+
writeFileSync4(join20(snapDir, file), body);
|
|
10240
10647
|
keyToFile[key] = `snapshots/${file}`;
|
|
10241
10648
|
if (res.ok) ok++;
|
|
10242
10649
|
else failed++;
|
|
@@ -10248,21 +10655,21 @@ async function runExport(cfg, outDir, opts = {}) {
|
|
|
10248
10655
|
axes,
|
|
10249
10656
|
keyToFile
|
|
10250
10657
|
};
|
|
10251
|
-
writeFileSync4(
|
|
10252
|
-
const html = readFileSync15(
|
|
10658
|
+
writeFileSync4(join20(outDir, "manifest.json"), JSON.stringify(manifest, null, 2));
|
|
10659
|
+
const html = readFileSync15(join20(webDir(), "index.html"), "utf8").replace(
|
|
10253
10660
|
/<\/head>/i,
|
|
10254
10661
|
` <script>window.__BEHOLD_STATIC__ = true;</script>
|
|
10255
10662
|
</head>`
|
|
10256
10663
|
);
|
|
10257
|
-
writeFileSync4(
|
|
10258
|
-
for (const f of
|
|
10664
|
+
writeFileSync4(join20(outDir, "index.html"), html);
|
|
10665
|
+
for (const f of readdirSync8(webDir())) {
|
|
10259
10666
|
if (f === "index.html") continue;
|
|
10260
|
-
copyFileSync(
|
|
10667
|
+
copyFileSync(join20(webDir(), f), join20(outDir, f));
|
|
10261
10668
|
}
|
|
10262
|
-
writeFileSync4(
|
|
10669
|
+
writeFileSync4(join20(outDir, "README.md"), BUNDLE_README);
|
|
10263
10670
|
const name = workerName(cfg.projectDir, opts.name);
|
|
10264
10671
|
writeFileSync4(
|
|
10265
|
-
|
|
10672
|
+
join20(outDir, "wrangler.jsonc"),
|
|
10266
10673
|
JSON.stringify(
|
|
10267
10674
|
{ $schema: "node_modules/wrangler/config-schema.json", name, compatibility_date: "2025-06-01", assets: { directory: "." } },
|
|
10268
10675
|
null,
|
|
@@ -10310,7 +10717,7 @@ It's just files \u2014 GitHub Pages, S3, nginx, or Cloudflare Pages
|
|
|
10310
10717
|
`;
|
|
10311
10718
|
|
|
10312
10719
|
// src/doctor.ts
|
|
10313
|
-
import { relative as
|
|
10720
|
+
import { relative as relative6, resolve as resolve8 } from "node:path";
|
|
10314
10721
|
var list2 = (xs) => xs.join(", ");
|
|
10315
10722
|
var EMPTY_KUBECONFIG = { contexts: /* @__PURE__ */ new Map(), servers: /* @__PURE__ */ new Map() };
|
|
10316
10723
|
function summarize(xs, max = 4) {
|
|
@@ -10318,7 +10725,7 @@ function summarize(xs, max = 4) {
|
|
|
10318
10725
|
}
|
|
10319
10726
|
function labelFor(root, target, estate) {
|
|
10320
10727
|
if (!estate) return "";
|
|
10321
|
-
const rel =
|
|
10728
|
+
const rel = relative6(root, target);
|
|
10322
10729
|
return `${rel || "."}: `;
|
|
10323
10730
|
}
|
|
10324
10731
|
function chantCheck(root, targets, estate) {
|
|
@@ -10331,7 +10738,7 @@ function chantCheck(root, targets, estate) {
|
|
|
10331
10738
|
name: "chant",
|
|
10332
10739
|
status: "fail",
|
|
10333
10740
|
detail: `${list2(resolutions.map(describe))} \u2014 behold shells the project's own chant`,
|
|
10334
|
-
fix: `Run \`npm install\` in ${list2(missing.map(({ target }) =>
|
|
10741
|
+
fix: `Run \`npm install\` in ${list2(missing.map(({ target }) => relative6(root, target) || target))}`
|
|
10335
10742
|
};
|
|
10336
10743
|
}
|
|
10337
10744
|
const stale = resolutions.filter(({ res }) => !meetsFloor(res.version, floor));
|
|
@@ -10459,7 +10866,7 @@ function opsCheck(root, ops, chantSource, estate) {
|
|
|
10459
10866
|
return { name: "ops", status: "pass", detail: `${ops.length} committed: ${list2(names)}; ${mcp}` };
|
|
10460
10867
|
}
|
|
10461
10868
|
async function diagnose(dir, probes = {}) {
|
|
10462
|
-
const root =
|
|
10869
|
+
const root = resolve8(dir);
|
|
10463
10870
|
const shape = detectProjectShape(root);
|
|
10464
10871
|
const behold = beholdVersion();
|
|
10465
10872
|
if (shape.kind === "none") {
|
|
@@ -10479,16 +10886,16 @@ async function diagnose(dir, probes = {}) {
|
|
|
10479
10886
|
};
|
|
10480
10887
|
}
|
|
10481
10888
|
const estate = shape.kind === "estate";
|
|
10482
|
-
const members = (shape.members ?? []).map((m) =>
|
|
10889
|
+
const members = (shape.members ?? []).map((m) => resolve8(root, m));
|
|
10483
10890
|
const targets = estate ? members : [root];
|
|
10484
10891
|
const primary = targets[0];
|
|
10485
10892
|
const projectCheck = estate ? {
|
|
10486
10893
|
name: "project",
|
|
10487
10894
|
status: "pass",
|
|
10488
10895
|
detail: `estate of ${members.length} projects (${shape.membersFrom === "behold-config" ? ".behold.json members" : "npm workspaces"}): ${list2(
|
|
10489
|
-
members.map((m) =>
|
|
10896
|
+
members.map((m) => relative6(root, m))
|
|
10490
10897
|
)}`
|
|
10491
|
-
} : { name: "project", status: "pass", detail: `chant project (${
|
|
10898
|
+
} : { name: "project", status: "pass", detail: `chant project (${relative6(root, shape.configFile)})` };
|
|
10492
10899
|
const infos = await Promise.all(targets.map(async (t) => ({ target: t, info: await detectProject(t) })));
|
|
10493
10900
|
const declared = new Map(infos.map(({ target, info }) => [target, info.lexicons]));
|
|
10494
10901
|
const lexicons = [...new Set(infos.flatMap(({ info }) => info.lexicons))];
|
|
@@ -10713,7 +11120,7 @@ ${USAGE}`);
|
|
|
10713
11120
|
process.stderr.write("behold serve: --auto-sync needs --env and --poll (it acts on polled drift)\n");
|
|
10714
11121
|
process.exit(2);
|
|
10715
11122
|
}
|
|
10716
|
-
const dirs = projectDirs.map((d) =>
|
|
11123
|
+
const dirs = projectDirs.map((d) => resolve9(d));
|
|
10717
11124
|
for (const d of dirs) warnIfNotChantProject(d);
|
|
10718
11125
|
await startServer({
|
|
10719
11126
|
projectDir: dirs[0],
|
|
@@ -10732,7 +11139,7 @@ function warnIfNotChantProject(dir) {
|
|
|
10732
11139
|
if (shape.kind === "estate") {
|
|
10733
11140
|
process.stderr.write(
|
|
10734
11141
|
`behold: warning \u2014 ${dir} is an estate root, not a chant project itself.
|
|
10735
|
-
Serve its members composed: behold serve ${shape.members.map((m) =>
|
|
11142
|
+
Serve its members composed: behold serve ${shape.members.map((m) => join21(dir, m)).join(" ")}
|
|
10736
11143
|
`
|
|
10737
11144
|
);
|
|
10738
11145
|
return;
|
|
@@ -10767,7 +11174,7 @@ async function runCarve(rest) {
|
|
|
10767
11174
|
);
|
|
10768
11175
|
process.exit(2);
|
|
10769
11176
|
}
|
|
10770
|
-
const reportPath =
|
|
11177
|
+
const reportPath = resolve9(fileArg);
|
|
10771
11178
|
const parsed = readCarveReport(reportPath, (p) => readFileSync16(p, "utf8"));
|
|
10772
11179
|
if (!parsed.ok) {
|
|
10773
11180
|
process.stderr.write(`behold carve: ${parsed.refusal.error}
|
|
@@ -10795,8 +11202,8 @@ async function runDoctor(rest) {
|
|
|
10795
11202
|
}
|
|
10796
11203
|
}
|
|
10797
11204
|
const dir = dirArg ?? ".";
|
|
10798
|
-
if (!existsSync14(
|
|
10799
|
-
process.stderr.write(`behold doctor: no such directory: ${
|
|
11205
|
+
if (!existsSync14(resolve9(dir))) {
|
|
11206
|
+
process.stderr.write(`behold doctor: no such directory: ${resolve9(dir)}
|
|
10800
11207
|
`);
|
|
10801
11208
|
process.exit(2);
|
|
10802
11209
|
}
|
|
@@ -10805,7 +11212,7 @@ async function runDoctor(rest) {
|
|
|
10805
11212
|
if (!report.ok) process.exitCode = 1;
|
|
10806
11213
|
}
|
|
10807
11214
|
async function runDemo(rest) {
|
|
10808
|
-
const pkgRoot2 =
|
|
11215
|
+
const pkgRoot2 = join21(dirname7(fileURLToPath5(import.meta.url)), "..");
|
|
10809
11216
|
const registry = loadDemoRegistry(pkgRoot2);
|
|
10810
11217
|
let port = 4600;
|
|
10811
11218
|
let live = false;
|
|
@@ -10859,7 +11266,7 @@ async function runDemo(rest) {
|
|
|
10859
11266
|
`);
|
|
10860
11267
|
process.exit(2);
|
|
10861
11268
|
}
|
|
10862
|
-
const target = dirArg ?
|
|
11269
|
+
const target = dirArg ? resolve9(dirArg) : demoTargetDir(entry);
|
|
10863
11270
|
const loaded = await loadDemo(entry, { pkgRoot: pkgRoot2, target, log: (line) => process.stdout.write(line + "\n") });
|
|
10864
11271
|
if (!loaded.ok) {
|
|
10865
11272
|
process.stderr.write(`behold demo ${entry.name}: ${loaded.error}
|
|
@@ -10890,7 +11297,7 @@ function spawnStep(cmd, args, cwd) {
|
|
|
10890
11297
|
});
|
|
10891
11298
|
}
|
|
10892
11299
|
async function serveCarveDemo(target, carve, port, live = false) {
|
|
10893
|
-
const at = (rel) =>
|
|
11300
|
+
const at = (rel) => resolve9(target, rel);
|
|
10894
11301
|
const project = at(carve.project);
|
|
10895
11302
|
const from = at(carve.from);
|
|
10896
11303
|
const state = carve.state ? at(carve.state) : void 0;
|
|
@@ -10930,7 +11337,7 @@ async function serveCarveDemo(target, carve, port, live = false) {
|
|
|
10930
11337
|
};
|
|
10931
11338
|
process.stdout.write("behold demo carve --live \u2192 the tfstate is real now; the advisor reads what terraform wrote.\n");
|
|
10932
11339
|
}
|
|
10933
|
-
if (existsSync14(
|
|
11340
|
+
if (existsSync14(join21(project, "package.json")) && !existsSync14(join21(project, "node_modules"))) {
|
|
10934
11341
|
process.stdout.write(`behold demo carve \u2192 npm install in ${carve.project}/ (the chant this walkthrough shells)\u2026
|
|
10935
11342
|
`);
|
|
10936
11343
|
const code = await spawnStep("npm", ["install"], project);
|
|
@@ -10941,7 +11348,7 @@ async function serveCarveDemo(target, carve, port, live = false) {
|
|
|
10941
11348
|
}
|
|
10942
11349
|
}
|
|
10943
11350
|
let degraded;
|
|
10944
|
-
if (!existsSync14(
|
|
11351
|
+
if (!existsSync14(join21(target, "node_modules", "@cdktf", "hcl2json"))) {
|
|
10945
11352
|
process.stdout.write("behold demo carve \u2192 npm install @cdktf/hcl2json (chant's HCL parser, ~2MB, once)\u2026\n");
|
|
10946
11353
|
const code = await spawnStep("npm", ["install", "--no-save", "--no-package-lock", "@cdktf/hcl2json"], target);
|
|
10947
11354
|
if (code !== 0) degraded = "couldn't install @cdktf/hcl2json (chant's HCL parser) \u2014 no network?";
|
|
@@ -11010,7 +11417,7 @@ async function runPreview(rest) {
|
|
|
11010
11417
|
process.stderr.write("behold preview: --port must be a number\n");
|
|
11011
11418
|
process.exit(2);
|
|
11012
11419
|
}
|
|
11013
|
-
const projectDir =
|
|
11420
|
+
const projectDir = resolve9(dirArg ?? process.cwd());
|
|
11014
11421
|
if (!existsSync14(projectDir)) {
|
|
11015
11422
|
process.stderr.write(`behold preview: project not found at ${projectDir}
|
|
11016
11423
|
`);
|
|
@@ -11031,21 +11438,21 @@ async function runPreview(rest) {
|
|
|
11031
11438
|
await startServer({ projectDir, port, env: "local", previewMode: true });
|
|
11032
11439
|
}
|
|
11033
11440
|
async function runExportCmd(rest) {
|
|
11034
|
-
let outDir =
|
|
11441
|
+
let outDir = resolve9("behold-export");
|
|
11035
11442
|
let env;
|
|
11036
11443
|
let name;
|
|
11037
11444
|
let dirArg;
|
|
11038
11445
|
let emulator = false;
|
|
11039
11446
|
for (let i = 0; i < rest.length; i++) {
|
|
11040
11447
|
const a = rest[i];
|
|
11041
|
-
if (a === "--out") outDir =
|
|
11448
|
+
if (a === "--out") outDir = resolve9(rest[++i]);
|
|
11042
11449
|
else if (a === "--env") env = rest[++i];
|
|
11043
11450
|
else if (a === "--name") name = rest[++i];
|
|
11044
11451
|
else if (a === "--emulator") emulator = true;
|
|
11045
11452
|
else if (a === "-h" || a === "--help") return void process.stdout.write(USAGE);
|
|
11046
11453
|
else if (!a.startsWith("-")) dirArg = a;
|
|
11047
11454
|
}
|
|
11048
|
-
const projectDir =
|
|
11455
|
+
const projectDir = resolve9(dirArg ?? process.cwd());
|
|
11049
11456
|
if (emulator) {
|
|
11050
11457
|
injectEmulatorEnv(env);
|
|
11051
11458
|
env ??= "local";
|