@intentius/behold 0.15.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 +27 -0
- package/dist/cli.js +939 -607
- 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 +134 -10
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
|
}
|
|
@@ -3369,7 +3727,9 @@ function applyHelmArtifacts(ir, observed) {
|
|
|
3369
3727
|
release: key.replace(/^release\//, ""),
|
|
3370
3728
|
...o.status ? { status: o.status } : {},
|
|
3371
3729
|
...typeof o.attributes?.revision === "string" ? { revision: o.attributes.revision } : {},
|
|
3372
|
-
...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 } : {}
|
|
3373
3733
|
};
|
|
3374
3734
|
attrs._artifact = match;
|
|
3375
3735
|
attrs._status = o.status === "deployed" ? "good" : "warn";
|
|
@@ -3378,37 +3738,35 @@ function applyHelmArtifacts(ir, observed) {
|
|
|
3378
3738
|
return { charts, installed };
|
|
3379
3739
|
}
|
|
3380
3740
|
|
|
3381
|
-
// src/
|
|
3382
|
-
import { statSync as
|
|
3383
|
-
import {
|
|
3384
|
-
import { join as join7, resolve as resolve3, sep } from "node:path";
|
|
3385
|
-
import { composeStacks, shortStackNames } from "@intentius/pinhole";
|
|
3741
|
+
// src/carve-discovery.ts
|
|
3742
|
+
import { readdirSync as readdirSync5, statSync as statSync4 } from "node:fs";
|
|
3743
|
+
import { join as join8 } from "node:path";
|
|
3386
3744
|
|
|
3387
3745
|
// src/member-ir.ts
|
|
3388
3746
|
import { createHash } from "node:crypto";
|
|
3389
|
-
import { readdirSync as
|
|
3390
|
-
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";
|
|
3391
3749
|
var SKIP = /* @__PURE__ */ new Set(["node_modules", "dist", ".git"]);
|
|
3392
3750
|
function memberIrCacheSize(env = process.env) {
|
|
3393
3751
|
const override = Number.parseInt(env.BEHOLD_ESTATE_IR_CACHE ?? "", 10);
|
|
3394
3752
|
return Number.isInteger(override) && override >= 0 ? override : 64;
|
|
3395
3753
|
}
|
|
3396
3754
|
function memberSourceStamp(dir) {
|
|
3397
|
-
const root =
|
|
3755
|
+
const root = resolve3(dir);
|
|
3398
3756
|
const h = createHash("sha1");
|
|
3399
3757
|
let any = false;
|
|
3400
3758
|
const walk = (at) => {
|
|
3401
|
-
const entries =
|
|
3759
|
+
const entries = readdirSync4(at, { withFileTypes: true }).sort((a, b) => a.name < b.name ? -1 : 1);
|
|
3402
3760
|
for (const e of entries) {
|
|
3403
3761
|
if (SKIP.has(e.name)) continue;
|
|
3404
|
-
const path =
|
|
3762
|
+
const path = join7(at, e.name);
|
|
3405
3763
|
if (e.isDirectory()) {
|
|
3406
3764
|
walk(path);
|
|
3407
3765
|
continue;
|
|
3408
3766
|
}
|
|
3409
3767
|
if (!e.isFile()) continue;
|
|
3410
|
-
const st =
|
|
3411
|
-
h.update(`${
|
|
3768
|
+
const st = statSync3(path);
|
|
3769
|
+
h.update(`${relative2(root, path)}\0${st.mtimeMs}\0${st.size}
|
|
3412
3770
|
`);
|
|
3413
3771
|
any = true;
|
|
3414
3772
|
}
|
|
@@ -3423,7 +3781,7 @@ function memberSourceStamp(dir) {
|
|
|
3423
3781
|
function cacheKey(dir, opts) {
|
|
3424
3782
|
const chant = resolveChant(dir);
|
|
3425
3783
|
const canonical = JSON.stringify(opts, Object.keys(opts).sort());
|
|
3426
|
-
return `${
|
|
3784
|
+
return `${resolve3(dir)}\0${chant.bin}\0${chant.version ?? ""}\0${canonical}`;
|
|
3427
3785
|
}
|
|
3428
3786
|
var cache = /* @__PURE__ */ new Map();
|
|
3429
3787
|
var hits = 0;
|
|
@@ -3433,7 +3791,7 @@ function invalidateMember(dir) {
|
|
|
3433
3791
|
cache.clear();
|
|
3434
3792
|
return;
|
|
3435
3793
|
}
|
|
3436
|
-
const root =
|
|
3794
|
+
const root = resolve3(dir);
|
|
3437
3795
|
for (const [key, entry] of cache) if (entry.dir === root) cache.delete(key);
|
|
3438
3796
|
}
|
|
3439
3797
|
async function memberIr(dir, opts = {}) {
|
|
@@ -3452,7 +3810,7 @@ async function memberIr(dir, opts = {}) {
|
|
|
3452
3810
|
const ir = await graphIr(dir, opts);
|
|
3453
3811
|
if (stamp === void 0 || memberSourceStamp(dir) !== stamp) return ir;
|
|
3454
3812
|
cache.delete(key);
|
|
3455
|
-
cache.set(key, { dir:
|
|
3813
|
+
cache.set(key, { dir: resolve3(dir), stamp, ir });
|
|
3456
3814
|
for (const oldest of cache.keys()) {
|
|
3457
3815
|
if (cache.size <= size) break;
|
|
3458
3816
|
cache.delete(oldest);
|
|
@@ -3460,6 +3818,70 @@ async function memberIr(dir, opts = {}) {
|
|
|
3460
3818
|
return structuredClone(ir);
|
|
3461
3819
|
}
|
|
3462
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
|
+
}
|
|
3875
|
+
}
|
|
3876
|
+
return out;
|
|
3877
|
+
}
|
|
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
|
+
|
|
3463
3885
|
// src/zoom-notes.ts
|
|
3464
3886
|
function zoomNote(zoom, ir, compositeEdgesAttached) {
|
|
3465
3887
|
const empty = ir.nodes.length === 0;
|
|
@@ -3562,7 +3984,10 @@ async function composeEstate(projectDirs, opts = {}) {
|
|
|
3562
3984
|
const names = shortStackNames(projectDirs);
|
|
3563
3985
|
const stacks = await mapPool(projectDirs, estateReadPool(projectDirs.length), async (dir, i) => ({
|
|
3564
3986
|
name: names[i],
|
|
3565
|
-
|
|
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
|
|
3566
3991
|
}));
|
|
3567
3992
|
return composeStacks(stacks);
|
|
3568
3993
|
}
|
|
@@ -3583,7 +4008,7 @@ var rec3 = (v) => v && typeof v === "object" && !Array.isArray(v) ? v : void 0;
|
|
|
3583
4008
|
var str3 = (v) => typeof v === "string" && v.length > 0 ? v : void 0;
|
|
3584
4009
|
var realIsDir = (p) => {
|
|
3585
4010
|
try {
|
|
3586
|
-
return
|
|
4011
|
+
return statSync5(p).isDirectory();
|
|
3587
4012
|
} catch {
|
|
3588
4013
|
return false;
|
|
3589
4014
|
}
|
|
@@ -3613,11 +4038,11 @@ function declaredSegments(path) {
|
|
|
3613
4038
|
function pathAlignment(declared, dir, isDir = realIsDir) {
|
|
3614
4039
|
const want = declaredSegments(declared);
|
|
3615
4040
|
if (!want) return 0;
|
|
3616
|
-
const root =
|
|
4041
|
+
const root = resolve4(dir);
|
|
3617
4042
|
const have = root.split(sep).filter(Boolean);
|
|
3618
4043
|
for (let k = Math.min(want.length, have.length); k >= 1; k--) {
|
|
3619
4044
|
if (!have.slice(-k).every((s, i) => s === want[i])) continue;
|
|
3620
|
-
if (!isDir(
|
|
4045
|
+
if (!isDir(join9(root, ...want.slice(k)))) continue;
|
|
3621
4046
|
return k;
|
|
3622
4047
|
}
|
|
3623
4048
|
return 0;
|
|
@@ -3647,10 +4072,10 @@ function joinNamespaceBindings(members, isDir = realIsDir) {
|
|
|
3647
4072
|
if (!winner || tied) continue;
|
|
3648
4073
|
const prev = best.get(winner.dir);
|
|
3649
4074
|
if (prev === "ambiguous") continue;
|
|
3650
|
-
const
|
|
3651
|
-
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 });
|
|
3652
4077
|
else if (prev.join.namespace !== b.namespace) best.set(winner.dir, "ambiguous");
|
|
3653
|
-
else if (winner.score > prev.score) best.set(winner.dir, { score: winner.score, join:
|
|
4078
|
+
else if (winner.score > prev.score) best.set(winner.dir, { score: winner.score, join: join22 });
|
|
3654
4079
|
}
|
|
3655
4080
|
return [...best.values()].filter((v) => v !== "ambiguous").map((v) => v.join);
|
|
3656
4081
|
}
|
|
@@ -3691,7 +4116,7 @@ async function composeEstateOverlay(projectDirs, opts, classify) {
|
|
|
3691
4116
|
const reason = firstLine2(err);
|
|
3692
4117
|
try {
|
|
3693
4118
|
const { env: _env, live: _live, overlay: _overlay, ...srcOpts } = opts;
|
|
3694
|
-
const src = await memberIr(dir, srcOpts);
|
|
4119
|
+
const src = joinCarvedSources(await memberIr(dir, srcOpts), (await carveStatesFor(dir)).values(), dir).ir;
|
|
3695
4120
|
for (const n of src.nodes) n.attrs = { ...n.attrs, _status: "neutral", _unobserved: reason };
|
|
3696
4121
|
stacks[i] = { name, ir: src };
|
|
3697
4122
|
unobserved.push({ name, reason });
|
|
@@ -3714,6 +4139,36 @@ async function composeEstateOverlay(projectDirs, opts, classify) {
|
|
|
3714
4139
|
}
|
|
3715
4140
|
|
|
3716
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
|
+
}
|
|
3717
4172
|
function countChanges(docs) {
|
|
3718
4173
|
return docs.reduce((n, d) => n + d.changes.length, 0);
|
|
3719
4174
|
}
|
|
@@ -3736,8 +4191,9 @@ function provenanceOf(live) {
|
|
|
3736
4191
|
sourceRef: m.sourceRef
|
|
3737
4192
|
};
|
|
3738
4193
|
}
|
|
3739
|
-
function helmRenderReport(record, live) {
|
|
3740
|
-
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 };
|
|
3741
4197
|
if (!live) {
|
|
3742
4198
|
return { ...base, verdict: "unobserved", reason: record.contentDigest ? "no-stored-render" : "unpinned" };
|
|
3743
4199
|
}
|
|
@@ -3761,21 +4217,24 @@ function helmRenderReport(record, live) {
|
|
|
3761
4217
|
}
|
|
3762
4218
|
return { ...base, verdict: "in-sync", counts, provenance };
|
|
3763
4219
|
}
|
|
3764
|
-
async function readHelmRenderDrift(projectDir, env, opts = {}) {
|
|
4220
|
+
async function readHelmRenderDrift(projectDir, env, opts = {}, observed) {
|
|
3765
4221
|
const report = await helmRenders(projectDir, opts).catch(() => void 0);
|
|
3766
4222
|
const records = report?.records ?? [];
|
|
3767
4223
|
if (records.length === 0) return [];
|
|
3768
4224
|
return mapPool(records, estateReadPool(records.length), async (record) => {
|
|
3769
|
-
if (!record.contentDigest) return helmRenderReport(record, void 0);
|
|
4225
|
+
if (!record.contentDigest) return helmRenderReport(record, void 0, observed);
|
|
3770
4226
|
const live = await helmRenderDiffLive(projectDir, record.contentDigest, env, opts).catch(() => void 0);
|
|
3771
|
-
return helmRenderReport(record, live);
|
|
4227
|
+
return helmRenderReport(record, live, observed);
|
|
3772
4228
|
});
|
|
3773
4229
|
}
|
|
3774
4230
|
function chartNameOf2(node) {
|
|
3775
4231
|
const name = node.attrs?.name;
|
|
3776
4232
|
return typeof name === "string" && name.length > 0 ? name : void 0;
|
|
3777
4233
|
}
|
|
3778
|
-
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
|
+
}
|
|
3779
4238
|
function applyHelmRenderDrift(ir, reports) {
|
|
3780
4239
|
if (reports.length === 0) return { charts: 0, drifted: 0 };
|
|
3781
4240
|
const byChart = /* @__PURE__ */ new Map();
|
|
@@ -3791,11 +4250,11 @@ function applyHelmRenderDrift(ir, reports) {
|
|
|
3791
4250
|
const chartName = chartNameOf2(node);
|
|
3792
4251
|
const matched = chartName ? byChart.get(chartName) : void 0;
|
|
3793
4252
|
if (!matched || matched.length === 0) continue;
|
|
3794
|
-
const worst = matched.reduce((a, b) =>
|
|
4253
|
+
const worst = matched.reduce((a, b) => severityOf(b) > severityOf(a) ? b : a);
|
|
3795
4254
|
const attrs = node.attrs ??= {};
|
|
3796
4255
|
attrs._renderDrift = matched.length === 1 ? worst : { ...worst, renders: matched };
|
|
3797
4256
|
charts++;
|
|
3798
|
-
if (worst.verdict === "drifted") {
|
|
4257
|
+
if (worst.verdict === "drifted" || matched.some((r) => r.identity?.match === "mismatch")) {
|
|
3799
4258
|
attrs._status = "warn";
|
|
3800
4259
|
drifted++;
|
|
3801
4260
|
}
|
|
@@ -4335,7 +4794,7 @@ function projectHelmLogical(ir) {
|
|
|
4335
4794
|
|
|
4336
4795
|
// src/logical-kustomize.ts
|
|
4337
4796
|
import { existsSync as existsSync5 } from "node:fs";
|
|
4338
|
-
import { join as
|
|
4797
|
+
import { join as join10 } from "node:path";
|
|
4339
4798
|
function overlayBoxTitle(name) {
|
|
4340
4799
|
return `overlay ${name}`;
|
|
4341
4800
|
}
|
|
@@ -4355,7 +4814,7 @@ function kustomizationRoot(bases, dir, exists) {
|
|
|
4355
4814
|
let current = dir;
|
|
4356
4815
|
for (let i = 0; i < 4 && current; i++) {
|
|
4357
4816
|
for (const base of bases) {
|
|
4358
|
-
if (exists(
|
|
4817
|
+
if (exists(join10(base, current, "kustomization.yaml")) || exists(join10(base, current, "kustomization.yml"))) {
|
|
4359
4818
|
return current;
|
|
4360
4819
|
}
|
|
4361
4820
|
}
|
|
@@ -5086,11 +5545,11 @@ import {
|
|
|
5086
5545
|
} from "@intentius/pinhole";
|
|
5087
5546
|
|
|
5088
5547
|
// src/icon-packs.ts
|
|
5089
|
-
import { readFileSync as
|
|
5090
|
-
import { dirname as
|
|
5548
|
+
import { readFileSync as readFileSync7 } from "node:fs";
|
|
5549
|
+
import { dirname as dirname4, join as join11 } from "node:path";
|
|
5091
5550
|
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
5092
|
-
var ICON_ROOT =
|
|
5093
|
-
var
|
|
5551
|
+
var ICON_ROOT = join11(dirname4(fileURLToPath2(import.meta.url)), "..", "web", "icons");
|
|
5552
|
+
var cache3 = /* @__PURE__ */ new Map();
|
|
5094
5553
|
function unprefix(body) {
|
|
5095
5554
|
return body.replace(/\s(?:inkscape|sodipodi):[\w-]+\s*=\s*(["'])[\s\S]*?\1/g, "").replace(/\sxlink:href\s*=/g, " href=");
|
|
5096
5555
|
}
|
|
@@ -5109,9 +5568,9 @@ function plate(glyph) {
|
|
|
5109
5568
|
return { ...glyph, body: `${ground}<g transform="${inset}">${glyph.body}</g>` };
|
|
5110
5569
|
}
|
|
5111
5570
|
function loadIcon(rel) {
|
|
5112
|
-
const hit =
|
|
5571
|
+
const hit = cache3.get(rel);
|
|
5113
5572
|
if (hit) return hit;
|
|
5114
|
-
const raw =
|
|
5573
|
+
const raw = readFileSync7(join11(ICON_ROOT, `${rel}.svg`), "utf8");
|
|
5115
5574
|
const open = raw.indexOf("<svg");
|
|
5116
5575
|
const openEnd = raw.indexOf(">", open);
|
|
5117
5576
|
const close = raw.lastIndexOf("</svg");
|
|
@@ -5120,7 +5579,7 @@ function loadIcon(rel) {
|
|
|
5120
5579
|
if (!viewBox) throw new Error(`vendored icon has no viewBox: ${rel}.svg`);
|
|
5121
5580
|
const bare = { body: unprefix(raw.slice(openEnd + 1, close).trim()), colored: true, viewBox };
|
|
5122
5581
|
const glyph = PLATED.has(rel) ? plate(bare) : bare;
|
|
5123
|
-
|
|
5582
|
+
cache3.set(rel, glyph);
|
|
5124
5583
|
return glyph;
|
|
5125
5584
|
}
|
|
5126
5585
|
var K8S_ICON_BY_KIND = {
|
|
@@ -5176,9 +5635,9 @@ function helmIconFor(kind) {
|
|
|
5176
5635
|
var SUPPORTED_MAJOR = 1;
|
|
5177
5636
|
var refuse = (error, remedy) => ({ ok: false, refusal: { error, code: "carve-report", remedy } });
|
|
5178
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`.";
|
|
5179
|
-
var
|
|
5638
|
+
var isRecord3 = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
5180
5639
|
function parseCarveReport(value) {
|
|
5181
|
-
if (!
|
|
5640
|
+
if (!isRecord3(value)) {
|
|
5182
5641
|
return refuse("That file isn't a carve report \u2014 its top level isn't a JSON object.", HOW_TO_GET_ONE);
|
|
5183
5642
|
}
|
|
5184
5643
|
if (!Array.isArray(value.resources)) {
|
|
@@ -5188,7 +5647,7 @@ function parseCarveReport(value) {
|
|
|
5188
5647
|
);
|
|
5189
5648
|
}
|
|
5190
5649
|
const bad = value.resources.findIndex(
|
|
5191
|
-
(r) => !
|
|
5650
|
+
(r) => !isRecord3(r) || typeof r.address !== "string" || typeof r.score !== "number" || typeof r.band !== "string"
|
|
5192
5651
|
);
|
|
5193
5652
|
if (bad >= 0) {
|
|
5194
5653
|
return refuse(
|
|
@@ -5233,384 +5692,136 @@ function statusForBand(band, score) {
|
|
|
5233
5692
|
switch (band) {
|
|
5234
5693
|
case "clean leaf":
|
|
5235
5694
|
return "good";
|
|
5236
|
-
case "carvable w/ edits":
|
|
5237
|
-
return "warn";
|
|
5238
|
-
case "leave in Terraform":
|
|
5239
|
-
return "neutral";
|
|
5240
|
-
default:
|
|
5241
|
-
return score >= 80 ? "good" : score >= 50 ? "warn" : "neutral";
|
|
5242
|
-
}
|
|
5243
|
-
}
|
|
5244
|
-
function verdictForStatus(status) {
|
|
5245
|
-
return status === "good" ? "carve now" : status === "warn" ? "boundary work" : "leave in Terraform";
|
|
5246
|
-
}
|
|
5247
|
-
function tfTypeOf(address, kind) {
|
|
5248
|
-
if (kind === "module" || address.startsWith("module.")) return "module";
|
|
5249
|
-
const dot = address.indexOf(".");
|
|
5250
|
-
return dot > 0 ? address.slice(0, dot) : address;
|
|
5251
|
-
}
|
|
5252
|
-
var directionOf = (e) => e.direction ?? "inbound";
|
|
5253
|
-
function boundaryEdgesOf(r) {
|
|
5254
|
-
const b = r.boundary;
|
|
5255
|
-
if (!b) return [];
|
|
5256
|
-
const list3 = Array.isArray(b) ? b : [...b.inbound ?? [], ...b.outbound ?? []];
|
|
5257
|
-
return list3.filter((e) => isRecord2(e) && typeof e.survivor === "string" && typeof e.carved === "string");
|
|
5258
|
-
}
|
|
5259
|
-
function scoreArithmetic(r) {
|
|
5260
|
-
const b = r.breakdown;
|
|
5261
|
-
if (!b || b.tier === null) return `${r.score} (no known native mapping \u2014 nothing to carve into)`;
|
|
5262
|
-
const p = b.penalties ?? {};
|
|
5263
|
-
const terms = [];
|
|
5264
|
-
if (p.inbound) terms.push(`- 12x${b.inbound ?? 0} inbound`);
|
|
5265
|
-
if (p.outbound) terms.push(`- 4x${b.outbound ?? 0} outbound`);
|
|
5266
|
-
if (p.outputs) terms.push(`- 4x${b.outputs ?? 0} output`);
|
|
5267
|
-
if (p.tier) terms.push(`- 15x${(b.tier ?? 1) - 1} tier${b.tier}`);
|
|
5268
|
-
if (p.dynamic) terms.push(`- 10 dynamic`);
|
|
5269
|
-
if (p.instances) terms.push(`- 3x${(b.instances ?? 1) - 1} instances`);
|
|
5270
|
-
if (!terms.length) return `100 (no penalties) = ${r.score}`;
|
|
5271
|
-
const raw = 100 + Object.values(p).reduce((s, n) => s + (n ?? 0), 0);
|
|
5272
|
-
const clamped = raw !== r.score ? ` (clamped from ${raw})` : "";
|
|
5273
|
-
return `100 ${terms.join(" ")} = ${r.score}${clamped}`;
|
|
5274
|
-
}
|
|
5275
|
-
function boundaryWorkOf(r) {
|
|
5276
|
-
const b = r.breakdown;
|
|
5277
|
-
if (!b || b.tier === null) return "no known native mapping (unsupported provider/type)";
|
|
5278
|
-
const parts = [];
|
|
5279
|
-
if (b.inbound) parts.push(`${b.inbound} inbound (a data-source patch each)`);
|
|
5280
|
-
if (b.outbound) parts.push(`${b.outbound} outbound (a deferred input each)`);
|
|
5281
|
-
if (b.outputs) parts.push(`${b.outputs} output block(s) reading it (a one-line rewrite each)`);
|
|
5282
|
-
if ((b.tier ?? 1) > 1) parts.push(`tier ${b.tier} map`);
|
|
5283
|
-
if (b.hasDynamic) parts.push("count/for_each/data present");
|
|
5284
|
-
if ((b.instances ?? 1) > 1) parts.push(`${b.instances} instances`);
|
|
5285
|
-
return parts.length ? parts.join(", ") : "clean 1:1 native map, no boundary edges";
|
|
5286
|
-
}
|
|
5287
|
-
function carveCardFields(node) {
|
|
5288
|
-
const score = node.attrs.score;
|
|
5289
|
-
const carve = node.attrs.carve;
|
|
5290
|
-
if (typeof score !== "number") return void 0;
|
|
5291
|
-
return [
|
|
5292
|
-
{ label: "score", value: String(score) },
|
|
5293
|
-
...typeof carve === "string" ? [{ label: "carve", value: carve }] : []
|
|
5294
|
-
];
|
|
5295
|
-
}
|
|
5296
|
-
function carveReportToIr(report) {
|
|
5297
|
-
const resources = report.resources ?? [];
|
|
5298
|
-
const known = new Set(resources.map((r) => r.address));
|
|
5299
|
-
const nodes = resources.map((r) => {
|
|
5300
|
-
const status = statusForBand(r.band, r.score);
|
|
5301
|
-
const b = r.breakdown ?? {};
|
|
5302
|
-
const edges2 = boundaryEdgesOf(r);
|
|
5303
|
-
const patches = edges2.filter((e) => e.carved === r.address && directionOf(e) === "inbound").map((e) => e.survivor);
|
|
5304
|
-
const inputs = edges2.filter((e) => e.carved === r.address && directionOf(e) === "outbound").map((e) => e.survivor);
|
|
5305
|
-
return {
|
|
5306
|
-
id: r.address,
|
|
5307
|
-
kind: tfTypeOf(r.address, r.kind),
|
|
5308
|
-
lexicon: "terraform",
|
|
5309
|
-
attrs: {
|
|
5310
|
-
// The drift palette's channel — `_`-prefixed, so it paints the card and
|
|
5311
|
-
// stays out of both the card's fields and the inspect pane's list.
|
|
5312
|
-
_status: status,
|
|
5313
|
-
score: r.score,
|
|
5314
|
-
carve: verdictForStatus(status),
|
|
5315
|
-
band: r.band,
|
|
5316
|
-
arithmetic: scoreArithmetic(r),
|
|
5317
|
-
boundaryWork: boundaryWorkOf(r),
|
|
5318
|
-
...r.mapsTo ? { mapsTo: r.mapsTo } : {},
|
|
5319
|
-
tier: b.tier === null || b.tier === void 0 ? "none" : b.tier,
|
|
5320
|
-
inbound: b.inbound ?? 0,
|
|
5321
|
-
outbound: b.outbound ?? 0,
|
|
5322
|
-
outputs: b.outputs ?? 0,
|
|
5323
|
-
instances: b.instances ?? 1,
|
|
5324
|
-
dynamic: b.hasDynamic ?? false,
|
|
5325
|
-
// The predicted diff, when the report carries the edge lists: who needs
|
|
5326
|
-
// a `data` source the moment this is carved, and what becomes a
|
|
5327
|
-
// deploy-time input. Absent (not empty) when the report has no lists —
|
|
5328
|
-
// "none" and "not reported" are different claims.
|
|
5329
|
-
...patches.length ? { patchOnCarve: patches.join(", ") } : {},
|
|
5330
|
-
...inputs.length ? { deferredInputs: inputs.join(", ") } : {}
|
|
5331
|
-
}
|
|
5332
|
-
};
|
|
5333
|
-
});
|
|
5334
|
-
const seen = /* @__PURE__ */ new Set();
|
|
5335
|
-
const edges = [];
|
|
5336
|
-
for (const r of resources) {
|
|
5337
|
-
for (const e of boundaryEdgesOf(r)) {
|
|
5338
|
-
const direction = directionOf(e);
|
|
5339
|
-
const from = direction === "inbound" ? e.survivor : e.carved;
|
|
5340
|
-
const to = direction === "inbound" ? e.carved : e.survivor;
|
|
5341
|
-
if (!known.has(from) || !known.has(to)) continue;
|
|
5342
|
-
const key = `${from}|${to}`;
|
|
5343
|
-
if (seen.has(key)) continue;
|
|
5344
|
-
seen.add(key);
|
|
5345
|
-
edges.push({
|
|
5346
|
-
from,
|
|
5347
|
-
to,
|
|
5348
|
-
kind: "ref",
|
|
5349
|
-
viaAttr: direction,
|
|
5350
|
-
...e.attrs?.length ? { toAttr: e.attrs.join(", ") } : {}
|
|
5351
|
-
});
|
|
5352
|
-
}
|
|
5353
|
-
}
|
|
5354
|
-
const byStack = {};
|
|
5355
|
-
for (const order of ["good", "warn", "neutral"]) {
|
|
5356
|
-
const members = resources.filter((r) => statusForBand(r.band, r.score) === order).map((r) => r.address);
|
|
5357
|
-
if (members.length) byStack[verdictForStatus(order)] = members;
|
|
5358
|
-
}
|
|
5359
|
-
return { nodes, edges, groups: { byStack } };
|
|
5360
|
-
}
|
|
5361
|
-
function carveNote(report, ir) {
|
|
5362
|
-
const counts = Object.entries(report.bands ?? {}).filter(([, n]) => n > 0).map(([band, n]) => `${n} ${band}`).join(" \xB7 ");
|
|
5363
|
-
const head = `chant carve advisory${report.from ? ` for ${report.from}` : ""}${counts ? ` \u2014 ${counts}` : ""}`;
|
|
5364
|
-
if (ir.edges.length) return `${head}. Read-only: nothing is emitted, patched, or applied.`;
|
|
5365
|
-
return `${head}. This report carries per-resource boundary COUNTS but no edge lists, so no boundary edges are drawn (chant#1636). Read-only: nothing is emitted, patched, or applied.`;
|
|
5366
|
-
}
|
|
5367
|
-
|
|
5368
|
-
// src/carve-manifest.ts
|
|
5369
|
-
import { readdirSync as readdirSync4, readFileSync as readFileSync7, statSync as statSync4 } from "node:fs";
|
|
5370
|
-
import { dirname as dirname4, join as join10 } from "node:path";
|
|
5371
|
-
var CARVE_MANIFEST_SUFFIX = ".carve.json";
|
|
5372
|
-
var APPLY_IS_HUMAN = "`chant carve apply` graduates ownership \u2014 it resolves the marker that makes chant the owner of a live resource. behold shows what the manifest records and echoes the command; it has no apply endpoint and no apply button, by design. Read the graduation plan, then run it yourself.";
|
|
5373
|
-
var isRecord3 = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
5374
|
-
var strings = (v) => Array.isArray(v) ? v.filter((x) => typeof x === "string") : [];
|
|
5375
|
-
function parseCarveManifest(value) {
|
|
5376
|
-
if (!isRecord3(value)) return null;
|
|
5377
|
-
if (value.version !== 1) return null;
|
|
5378
|
-
if (typeof value.target !== "string" || !value.target) return null;
|
|
5379
|
-
return value;
|
|
5380
|
-
}
|
|
5381
|
-
function readCarveManifest(path, readFile = (p) => readFileSync7(p, "utf8")) {
|
|
5382
|
-
let text;
|
|
5383
|
-
try {
|
|
5384
|
-
text = readFile(path);
|
|
5385
|
-
} catch {
|
|
5386
|
-
return null;
|
|
5387
|
-
}
|
|
5388
|
-
try {
|
|
5389
|
-
return parseCarveManifest(JSON.parse(text));
|
|
5390
|
-
} catch {
|
|
5391
|
-
return null;
|
|
5392
|
-
}
|
|
5393
|
-
}
|
|
5394
|
-
function carveStageOf(m) {
|
|
5395
|
-
if (m.apply) return "applied";
|
|
5396
|
-
if (m.bridge) return "bridged";
|
|
5397
|
-
return "emitted";
|
|
5398
|
-
}
|
|
5399
|
-
function carveWordFor(stage) {
|
|
5400
|
-
switch (stage) {
|
|
5401
|
-
case "applied":
|
|
5402
|
-
return "carved \u2192 chant";
|
|
5403
|
-
case "bridged":
|
|
5404
|
-
return "bridged \u2014 apply is yours";
|
|
5405
|
-
default:
|
|
5406
|
-
return "emitted \u2014 not bridged";
|
|
5407
|
-
}
|
|
5408
|
-
}
|
|
5409
|
-
function carveToneFor(stage) {
|
|
5410
|
-
return stage === "applied" ? "good" : "accent";
|
|
5411
|
-
}
|
|
5412
|
-
function carveStageNote(state) {
|
|
5413
|
-
switch (state.stage) {
|
|
5414
|
-
case "applied":
|
|
5415
|
-
return `graduated \u2014 \`carve apply\` recorded the ownership marker` + (state.ownership?.stack ? ` (stack ${state.ownership.stack}${state.ownership.env ? `, env ${state.ownership.env}` : ""})` : "") + ". chant owns it.";
|
|
5416
|
-
case "bridged":
|
|
5417
|
-
return "emitted and bridged \u2014 the survivor patch is proposed. Terraform still owns the resource until `carve apply`.";
|
|
5418
|
-
default:
|
|
5419
|
-
return "emitted \u2014 typed chant source exists, and the surviving Terraform still reads the resource directly. `carve bridge` is next.";
|
|
5420
|
-
}
|
|
5421
|
-
}
|
|
5422
|
-
function carveStateOf(m, path) {
|
|
5423
|
-
const stage = carveStageOf(m);
|
|
5424
|
-
const deferred = Object.entries(m.emit?.params ?? {}).map(([name, p]) => `${name} \u2190 ${p?.survivor ?? "a survivor"}`);
|
|
5425
|
-
return {
|
|
5426
|
-
target: m.target,
|
|
5427
|
-
...m.tfType ? { tfType: m.tfType } : {},
|
|
5428
|
-
stage,
|
|
5429
|
-
graduated: stage === "applied",
|
|
5430
|
-
// The last stamped step wins — that is the age of the state, not the age
|
|
5431
|
-
// of the carve.
|
|
5432
|
-
...m.apply?.at || m.bridge?.at || m.emit?.at ? { at: m.apply?.at ?? m.bridge?.at ?? m.emit?.at } : {},
|
|
5433
|
-
files: strings(m.emit?.files),
|
|
5434
|
-
excised: strings(m.bridge?.excised),
|
|
5435
|
-
deferredInputs: deferred,
|
|
5436
|
-
...m.apply ? {
|
|
5437
|
-
ownership: {
|
|
5438
|
-
...m.apply.marker?.stack ? { stack: m.apply.marker.stack } : {},
|
|
5439
|
-
...m.apply.marker?.env ? { env: m.apply.marker.env } : {},
|
|
5440
|
-
...m.apply.ownershipTags ? { tags: m.apply.ownershipTags } : {}
|
|
5441
|
-
}
|
|
5442
|
-
} : {},
|
|
5443
|
-
...m.from ? { from: m.from } : {},
|
|
5444
|
-
outDir: dirname4(path),
|
|
5445
|
-
path
|
|
5446
|
-
};
|
|
5447
|
-
}
|
|
5448
|
-
var nodeCarveIo = {
|
|
5449
|
-
readdir: (dir) => readdirSync4(dir),
|
|
5450
|
-
isDirectory: (p) => {
|
|
5451
|
-
try {
|
|
5452
|
-
return statSync4(p).isDirectory();
|
|
5453
|
-
} catch {
|
|
5454
|
-
return false;
|
|
5455
|
-
}
|
|
5456
|
-
},
|
|
5457
|
-
readFile: (p) => readFileSync7(p, "utf8")
|
|
5458
|
-
};
|
|
5459
|
-
var MANIFEST_SCAN_DEPTH = 2;
|
|
5460
|
-
var SKIP_DIRS = /* @__PURE__ */ new Set(["node_modules", "dist", "cdk.out", ".terraform", ".git"]);
|
|
5461
|
-
function listCarveManifests(dir, io = nodeCarveIo, depth = MANIFEST_SCAN_DEPTH) {
|
|
5462
|
-
const out = [];
|
|
5463
|
-
const walk = (d, level) => {
|
|
5464
|
-
let entries;
|
|
5465
|
-
try {
|
|
5466
|
-
entries = io.readdir(d).sort();
|
|
5467
|
-
} catch {
|
|
5468
|
-
return;
|
|
5469
|
-
}
|
|
5470
|
-
for (const name of entries) {
|
|
5471
|
-
const full = join10(d, name);
|
|
5472
|
-
if (name.endsWith(CARVE_MANIFEST_SUFFIX)) {
|
|
5473
|
-
out.push(full);
|
|
5474
|
-
continue;
|
|
5475
|
-
}
|
|
5476
|
-
if (level >= depth) continue;
|
|
5477
|
-
if (name.startsWith(".") || SKIP_DIRS.has(name)) continue;
|
|
5478
|
-
if (io.isDirectory(full)) walk(full, level + 1);
|
|
5479
|
-
}
|
|
5480
|
-
};
|
|
5481
|
-
walk(dir, 0);
|
|
5482
|
-
return out;
|
|
5483
|
-
}
|
|
5484
|
-
function readCarveStates(dirs, io = nodeCarveIo, depth = MANIFEST_SCAN_DEPTH) {
|
|
5485
|
-
const rank = { emitted: 0, bridged: 1, applied: 2 };
|
|
5486
|
-
const byTarget = /* @__PURE__ */ new Map();
|
|
5487
|
-
for (const dir of dirs) {
|
|
5488
|
-
for (const path of listCarveManifests(dir, io, depth)) {
|
|
5489
|
-
const manifest = readCarveManifest(path, io.readFile);
|
|
5490
|
-
if (!manifest) continue;
|
|
5491
|
-
const state = carveStateOf(manifest, path);
|
|
5492
|
-
const held = byTarget.get(state.target);
|
|
5493
|
-
if (!held || rank[state.stage] > rank[held.stage]) byTarget.set(state.target, state);
|
|
5494
|
-
}
|
|
5695
|
+
case "carvable w/ edits":
|
|
5696
|
+
return "warn";
|
|
5697
|
+
case "leave in Terraform":
|
|
5698
|
+
return "neutral";
|
|
5699
|
+
default:
|
|
5700
|
+
return score >= 80 ? "good" : score >= 50 ? "warn" : "neutral";
|
|
5495
5701
|
}
|
|
5496
|
-
return byTarget;
|
|
5497
5702
|
}
|
|
5498
|
-
function
|
|
5499
|
-
|
|
5500
|
-
let bridged = 0;
|
|
5501
|
-
let emitted = 0;
|
|
5502
|
-
for (const s of states) {
|
|
5503
|
-
if (s.stage === "applied") applied++;
|
|
5504
|
-
else if (s.stage === "bridged") bridged++;
|
|
5505
|
-
else emitted++;
|
|
5506
|
-
}
|
|
5507
|
-
const inFlight = bridged + emitted;
|
|
5508
|
-
const parts = [];
|
|
5509
|
-
if (emitted) parts.push(`${emitted} emitted`);
|
|
5510
|
-
if (bridged) parts.push(`${bridged} bridged`);
|
|
5511
|
-
return {
|
|
5512
|
-
applied,
|
|
5513
|
-
bridged,
|
|
5514
|
-
emitted,
|
|
5515
|
-
inFlight,
|
|
5516
|
-
total: typeof total === "number" ? total : null,
|
|
5517
|
-
label: typeof total === "number" ? `${applied} of ${total} carved` : `${applied} carved`,
|
|
5518
|
-
detail: parts.length ? `${parts.join(", ")} \u2014 apply is yours` : ""
|
|
5519
|
-
};
|
|
5703
|
+
function verdictForStatus(status) {
|
|
5704
|
+
return status === "good" ? "carve now" : status === "warn" ? "boundary work" : "leave in Terraform";
|
|
5520
5705
|
}
|
|
5521
|
-
function
|
|
5522
|
-
|
|
5523
|
-
const
|
|
5524
|
-
|
|
5525
|
-
const out = labels.out ?? state.outDir;
|
|
5526
|
-
return `chant carve apply --from ${from} --output ${out} --env ${env} --stack ${stack} --write-source`;
|
|
5706
|
+
function tfTypeOf(address, kind) {
|
|
5707
|
+
if (kind === "module" || address.startsWith("module.")) return "module";
|
|
5708
|
+
const dot = address.indexOf(".");
|
|
5709
|
+
return dot > 0 ? address.slice(0, dot) : address;
|
|
5527
5710
|
}
|
|
5528
|
-
|
|
5529
|
-
|
|
5530
|
-
|
|
5531
|
-
|
|
5532
|
-
|
|
5533
|
-
|
|
5534
|
-
apply: { human: true, note: APPLY_IS_HUMAN }
|
|
5535
|
-
};
|
|
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");
|
|
5536
5717
|
}
|
|
5537
|
-
function
|
|
5538
|
-
|
|
5539
|
-
|
|
5540
|
-
const
|
|
5541
|
-
const
|
|
5542
|
-
|
|
5543
|
-
|
|
5544
|
-
|
|
5545
|
-
|
|
5546
|
-
|
|
5547
|
-
|
|
5548
|
-
|
|
5549
|
-
|
|
5550
|
-
|
|
5551
|
-
|
|
5552
|
-
|
|
5553
|
-
|
|
5554
|
-
|
|
5555
|
-
|
|
5556
|
-
|
|
5557
|
-
|
|
5558
|
-
|
|
5559
|
-
|
|
5560
|
-
|
|
5561
|
-
|
|
5562
|
-
|
|
5563
|
-
|
|
5564
|
-
|
|
5565
|
-
|
|
5566
|
-
|
|
5567
|
-
|
|
5568
|
-
|
|
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}`;
|
|
5733
|
+
}
|
|
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";
|
|
5745
|
+
}
|
|
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
|
+
];
|
|
5754
|
+
}
|
|
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",
|
|
5569
5768
|
attrs: {
|
|
5570
|
-
|
|
5571
|
-
|
|
5572
|
-
|
|
5573
|
-
|
|
5574
|
-
|
|
5575
|
-
|
|
5576
|
-
|
|
5577
|
-
|
|
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(", ") } : {}
|
|
5578
5790
|
}
|
|
5579
|
-
}
|
|
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
|
+
}
|
|
5580
5812
|
}
|
|
5581
5813
|
const byStack = {};
|
|
5582
|
-
for (const
|
|
5583
|
-
const
|
|
5584
|
-
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;
|
|
5585
5817
|
}
|
|
5586
|
-
return {
|
|
5587
|
-
tf: {
|
|
5588
|
-
nodes,
|
|
5589
|
-
edges: tfIr.edges.filter((e) => !gone.has(e.from) && !gone.has(e.to)),
|
|
5590
|
-
groups: { ...tfIr.groups, byStack }
|
|
5591
|
-
},
|
|
5592
|
-
graduated
|
|
5593
|
-
};
|
|
5594
|
-
}
|
|
5595
|
-
var GRADUATED_BAND = "carved \u2192 chant";
|
|
5596
|
-
function bandGraduated(tf, graduated) {
|
|
5597
|
-
if (graduated.length === 0) return tf;
|
|
5598
|
-
return {
|
|
5599
|
-
nodes: [...graduated, ...tf.nodes],
|
|
5600
|
-
edges: tf.edges,
|
|
5601
|
-
groups: {
|
|
5602
|
-
...tf.groups,
|
|
5603
|
-
byStack: {
|
|
5604
|
-
[GRADUATED_BAND]: graduated.map((n) => n.id),
|
|
5605
|
-
...tf.groups?.byStack ?? {}
|
|
5606
|
-
}
|
|
5607
|
-
}
|
|
5608
|
-
};
|
|
5818
|
+
return { nodes, edges, groups: { byStack } };
|
|
5609
5819
|
}
|
|
5610
|
-
function
|
|
5611
|
-
|
|
5612
|
-
const head = `
|
|
5613
|
-
|
|
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.`;
|
|
5614
5825
|
}
|
|
5615
5826
|
|
|
5616
5827
|
// src/ops-lens.ts
|
|
@@ -5884,9 +6095,12 @@ function nodeFor(op, placed, guards) {
|
|
|
5884
6095
|
...profile ? { profile } : {},
|
|
5885
6096
|
...policy ? { policy } : {},
|
|
5886
6097
|
// The resolved args, verbatim from the IR. They carry the Op's SCOPE
|
|
5887
|
-
// (env, stack, template path)
|
|
5888
|
-
//
|
|
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.
|
|
5889
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 } : {},
|
|
5890
6104
|
...step.outcomeAttribute ? { outcome: step.outcomeAttribute.from ? `${step.outcomeAttribute.name} \u2190 ${step.outcomeAttribute.from}` : step.outcomeAttribute.name } : {},
|
|
5891
6105
|
...op.activityContracts?.[step.fn] ? { contract: "declared (args/returns JSON Schema in op.json)" } : {}
|
|
5892
6106
|
}
|
|
@@ -5988,17 +6202,83 @@ function opCardFields(node) {
|
|
|
5988
6202
|
function opsNote(ops, ir) {
|
|
5989
6203
|
const gates = ir.nodes.filter((n) => n.attrs._step === "gate").length;
|
|
5990
6204
|
const rules = ir.nodes.filter((n) => n.attrs._step === "rule");
|
|
5991
|
-
const
|
|
6205
|
+
const estate = ir.nodes.filter((n) => n.lexicon !== "op").length;
|
|
6206
|
+
const steps = ir.nodes.length - rules.length - estate;
|
|
5992
6207
|
const dangling = rules.filter((n) => n.attrs.dangling !== void 0).length;
|
|
5993
6208
|
const counts = [
|
|
5994
6209
|
`${ops.length} declared Op${ops.length === 1 ? "" : "s"}`,
|
|
5995
6210
|
`${steps} step${steps === 1 ? "" : "s"}`,
|
|
5996
6211
|
...gates ? [`${gates} gate${gates === 1 ? "" : "s"}`] : [],
|
|
5997
|
-
...rules.length ? [`${rules.length} converge rule${rules.length === 1 ? "" : "s"}`] : []
|
|
6212
|
+
...rules.length ? [`${rules.length} converge rule${rules.length === 1 ? "" : "s"}`] : [],
|
|
6213
|
+
...entityCountClause(ir)
|
|
5998
6214
|
].join(" \xB7 ");
|
|
5999
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.` : "") : "";
|
|
6000
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.`;
|
|
6001
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
|
+
}
|
|
6002
6282
|
|
|
6003
6283
|
// src/render.ts
|
|
6004
6284
|
registerPack({ lexicon: "k8s", iconFor: k8sIconFor });
|
|
@@ -6112,7 +6392,10 @@ function renderCarveEstate(tfIr, appIr, opts) {
|
|
|
6112
6392
|
}
|
|
6113
6393
|
function withCarveState(tfIr, appIr, opts) {
|
|
6114
6394
|
const states = opts.carved ?? /* @__PURE__ */ new Map();
|
|
6115
|
-
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
|
+
});
|
|
6116
6399
|
const ns = namespaceAppIr(appIr, opts.appTitle);
|
|
6117
6400
|
const progress = carveProgress(states.values(), tfIr.nodes.length);
|
|
6118
6401
|
return {
|
|
@@ -6452,12 +6735,12 @@ function radializeLayout(layout, groupOf, size = /* @__PURE__ */ new Map()) {
|
|
|
6452
6735
|
}
|
|
6453
6736
|
|
|
6454
6737
|
// src/carve-actions.ts
|
|
6455
|
-
import { existsSync as existsSync8, mkdirSync as mkdirSync3, readFileSync as readFileSync10, readdirSync as
|
|
6456
|
-
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";
|
|
6457
6740
|
|
|
6458
6741
|
// src/layout.ts
|
|
6459
6742
|
import { existsSync as existsSync6, mkdirSync as mkdirSync2, readFileSync as readFileSync8, renameSync, rmSync, writeFileSync as writeFileSync2, accessSync, constants } from "node:fs";
|
|
6460
|
-
import { join as
|
|
6743
|
+
import { join as join12 } from "node:path";
|
|
6461
6744
|
var LAYOUT_DIR = ".behold";
|
|
6462
6745
|
var LAYOUT_FILE = "layout.json";
|
|
6463
6746
|
var MAX_BODY_BYTES = 64 * 1024;
|
|
@@ -6498,7 +6781,7 @@ function lensFromQuery(params) {
|
|
|
6498
6781
|
return [slug(zoom), params.get("radial") === "1" ? "radial" : null, stack ? slug(`stack-${stack}`) : null].filter(Boolean).join("+");
|
|
6499
6782
|
}
|
|
6500
6783
|
function layoutPath(projectDir) {
|
|
6501
|
-
return
|
|
6784
|
+
return join12(projectDir, LAYOUT_DIR, LAYOUT_FILE);
|
|
6502
6785
|
}
|
|
6503
6786
|
function unwritableReason(projectDir) {
|
|
6504
6787
|
if (!projectDir || !existsSync6(projectDir)) return `no such project directory: ${projectDir || "(none)"}`;
|
|
@@ -6554,7 +6837,7 @@ function writeLens(projectDir, lens, deltas) {
|
|
|
6554
6837
|
}
|
|
6555
6838
|
const body = JSON.stringify({ version: 1, lenses }, null, 2) + "\n";
|
|
6556
6839
|
if (body.length > MAX_FILE_BYTES) throw new LayoutTooLarge(`a layout sidecar is capped at ${MAX_FILE_BYTES} bytes`);
|
|
6557
|
-
mkdirSync2(
|
|
6840
|
+
mkdirSync2(join12(projectDir, LAYOUT_DIR), { recursive: true });
|
|
6558
6841
|
const tmp = `${file}.tmp`;
|
|
6559
6842
|
writeFileSync2(tmp, body, "utf8");
|
|
6560
6843
|
renameSync(tmp, file);
|
|
@@ -6640,7 +6923,7 @@ function reanchorEdges(svg, moved) {
|
|
|
6640
6923
|
// src/carve-live.ts
|
|
6641
6924
|
import { spawn as spawn3 } from "node:child_process";
|
|
6642
6925
|
import { readFileSync as readFileSync9, writeFileSync as writeFileSync3, existsSync as existsSync7 } from "node:fs";
|
|
6643
|
-
import { join as
|
|
6926
|
+
import { join as join13 } from "node:path";
|
|
6644
6927
|
|
|
6645
6928
|
// src/scratch.ts
|
|
6646
6929
|
var PROTECTED_INFRA = [
|
|
@@ -6728,9 +7011,9 @@ async function teardownScratchFloci() {
|
|
|
6728
7011
|
await capture("docker", ["rm", "-f", LIVE_CONTAINER], process.cwd()).catch(() => void 0);
|
|
6729
7012
|
}
|
|
6730
7013
|
async function applyIntoFloci(fromDir, step) {
|
|
6731
|
-
const template =
|
|
7014
|
+
const template = join13(fromDir, "floci-override.tf.disabled");
|
|
6732
7015
|
if (!existsSync7(template)) return `no floci-override.tf.disabled in ${fromDir} \u2014 this estate wasn't authored for the live tier`;
|
|
6733
|
-
writeFileSync3(
|
|
7016
|
+
writeFileSync3(join13(fromDir, "floci_override.tf"), armOverride(readFileSync9(template, "utf8"), LIVE_PORT));
|
|
6734
7017
|
const init = await step("terraform", ["init", "-input=false", "-no-color"], fromDir);
|
|
6735
7018
|
if (init !== 0) return `terraform init exited ${init} (provider downloads need network)`;
|
|
6736
7019
|
const apply = await step(
|
|
@@ -6780,7 +7063,7 @@ async function runCarveObserve(demo, select) {
|
|
|
6780
7063
|
"run `behold demo carve --live` (needs docker + terraform)"
|
|
6781
7064
|
);
|
|
6782
7065
|
}
|
|
6783
|
-
if (!existsSync8(
|
|
7066
|
+
if (!existsSync8(join14(demo.out, "src"))) {
|
|
6784
7067
|
return refuse3("carve-action", "nothing has been emitted yet", "run Emit first \u2014 observe reads the carveout emit writes");
|
|
6785
7068
|
}
|
|
6786
7069
|
const args = ["lifecycle", "diff", "prod", "--live", "--json"];
|
|
@@ -6850,8 +7133,8 @@ var refuse3 = (code, error, remedy) => ({
|
|
|
6850
7133
|
});
|
|
6851
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.";
|
|
6852
7135
|
function insideDemo(root, p) {
|
|
6853
|
-
const rel =
|
|
6854
|
-
return rel !== "" && !rel.startsWith("..") && !
|
|
7136
|
+
const rel = relative3(resolve5(root), resolve5(p));
|
|
7137
|
+
return rel !== "" && !rel.startsWith("..") && !isAbsolute2(rel);
|
|
6855
7138
|
}
|
|
6856
7139
|
function selectFromReport(report, raw) {
|
|
6857
7140
|
if (typeof raw !== "string" || !raw || raw.length > 512) return null;
|
|
@@ -6871,17 +7154,17 @@ function readArtifacts(demo, dir, filter) {
|
|
|
6871
7154
|
if (out.length >= MAX_ARTIFACTS || depth > 2) return;
|
|
6872
7155
|
let entries;
|
|
6873
7156
|
try {
|
|
6874
|
-
entries =
|
|
7157
|
+
entries = readdirSync6(d).sort();
|
|
6875
7158
|
} catch {
|
|
6876
7159
|
return;
|
|
6877
7160
|
}
|
|
6878
7161
|
for (const name of entries) {
|
|
6879
7162
|
if (out.length >= MAX_ARTIFACTS) return;
|
|
6880
7163
|
if (name === "node_modules" || name.startsWith(".")) continue;
|
|
6881
|
-
const full =
|
|
7164
|
+
const full = join14(d, name);
|
|
6882
7165
|
let st;
|
|
6883
7166
|
try {
|
|
6884
|
-
st =
|
|
7167
|
+
st = statSync6(full);
|
|
6885
7168
|
} catch {
|
|
6886
7169
|
continue;
|
|
6887
7170
|
}
|
|
@@ -6889,7 +7172,7 @@ function readArtifacts(demo, dir, filter) {
|
|
|
6889
7172
|
walk(full, depth + 1);
|
|
6890
7173
|
continue;
|
|
6891
7174
|
}
|
|
6892
|
-
const rel =
|
|
7175
|
+
const rel = relative3(demo.root, full).split(sep2).join("/");
|
|
6893
7176
|
if (!filter(rel)) continue;
|
|
6894
7177
|
let text = "";
|
|
6895
7178
|
try {
|
|
@@ -6925,7 +7208,7 @@ async function runCarveEmit(demo, select) {
|
|
|
6925
7208
|
const block = carveWriteBlock(demo);
|
|
6926
7209
|
if (block) return refuse3("read-only", block, "Start the walkthrough with `behold demo carve`.");
|
|
6927
7210
|
mkdirSync3(demo.out, { recursive: true });
|
|
6928
|
-
const reportFile =
|
|
7211
|
+
const reportFile = join14(demo.out, `${carveSlug(select)}-boundary.json`);
|
|
6929
7212
|
const args = [
|
|
6930
7213
|
"carve",
|
|
6931
7214
|
"emit",
|
|
@@ -6957,7 +7240,7 @@ async function runCarveEmit(demo, select) {
|
|
|
6957
7240
|
boundary = JSON.parse(readFileSync10(reportFile, "utf8"));
|
|
6958
7241
|
} catch {
|
|
6959
7242
|
}
|
|
6960
|
-
const lintPath =
|
|
7243
|
+
const lintPath = relative3(demo.project, join14(demo.out, "src")).split(sep2).join("/");
|
|
6961
7244
|
const lintArgs = ["lint", lintPath];
|
|
6962
7245
|
const lint = await runChantRaw(lintArgs, demo.project).catch((err) => ({
|
|
6963
7246
|
code: 127,
|
|
@@ -7020,8 +7303,8 @@ async function runCarveBridge(demo, select) {
|
|
|
7020
7303
|
}
|
|
7021
7304
|
|
|
7022
7305
|
// src/ops.ts
|
|
7023
|
-
import { readdirSync as
|
|
7024
|
-
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";
|
|
7025
7308
|
var APPLY_TARGET_LEXICON = {
|
|
7026
7309
|
cloudformation: "aws",
|
|
7027
7310
|
kubectl: "k8s",
|
|
@@ -7043,11 +7326,11 @@ function discoverOps(projectDir) {
|
|
|
7043
7326
|
const seen = /* @__PURE__ */ new Set();
|
|
7044
7327
|
const out = [];
|
|
7045
7328
|
for (const sub of ["ops", "src", "."]) {
|
|
7046
|
-
const dir =
|
|
7329
|
+
const dir = join15(projectDir, sub);
|
|
7047
7330
|
if (!existsSync9(dir)) continue;
|
|
7048
|
-
for (const f of
|
|
7331
|
+
for (const f of readdirSync7(dir)) {
|
|
7049
7332
|
if (!f.endsWith(".op.ts")) continue;
|
|
7050
|
-
const content = readFileSync11(
|
|
7333
|
+
const content = readFileSync11(join15(dir, f), "utf8");
|
|
7051
7334
|
const name = content.match(/name:\s*["'`]([^"'`]+)["'`]/)?.[1];
|
|
7052
7335
|
if (!name || seen.has(name)) continue;
|
|
7053
7336
|
seen.add(name);
|
|
@@ -7070,15 +7353,15 @@ function discoverOps(projectDir) {
|
|
|
7070
7353
|
return out.sort((a, b) => a.name.localeCompare(b.name));
|
|
7071
7354
|
}
|
|
7072
7355
|
function opIrPath(projectDir, name) {
|
|
7073
|
-
return
|
|
7356
|
+
return join15(projectDir, "dist", "ops", name, "op.json");
|
|
7074
7357
|
}
|
|
7075
7358
|
function discoverOpIrs(projectDirs) {
|
|
7076
7359
|
const seen = /* @__PURE__ */ new Set();
|
|
7077
7360
|
const out = [];
|
|
7078
7361
|
for (const dir of projectDirs) {
|
|
7079
|
-
const root =
|
|
7362
|
+
const root = join15(dir, "dist", "ops");
|
|
7080
7363
|
if (!existsSync9(root)) continue;
|
|
7081
|
-
for (const name of
|
|
7364
|
+
for (const name of readdirSync7(root)) {
|
|
7082
7365
|
if (seen.has(name)) continue;
|
|
7083
7366
|
const path = opIrPath(dir, name);
|
|
7084
7367
|
if (!existsSync9(path)) continue;
|
|
@@ -7316,11 +7599,11 @@ var RAN = { ok: "ok", fail: "failed", skipped: "skipped" };
|
|
|
7316
7599
|
function tookLabel(ms) {
|
|
7317
7600
|
return ms < 1e3 ? `${ms}ms` : `${(ms / 1e3).toFixed(1)}s`;
|
|
7318
7601
|
}
|
|
7319
|
-
function paintPlayhead(ir,
|
|
7320
|
-
if (!
|
|
7602
|
+
function paintPlayhead(ir, join22, gate) {
|
|
7603
|
+
if (!join22.byNode.size && !join22.at) return ir;
|
|
7321
7604
|
const nodes = ir.nodes.map((node) => {
|
|
7322
|
-
const run3 =
|
|
7323
|
-
const here =
|
|
7605
|
+
const run3 = join22.byNode.get(node.id);
|
|
7606
|
+
const here = join22.at === node.id;
|
|
7324
7607
|
if (!run3 && !here) return node;
|
|
7325
7608
|
const isGate = node.attrs?._step === "gate";
|
|
7326
7609
|
const attrs = { ...node.attrs };
|
|
@@ -7335,7 +7618,7 @@ function paintPlayhead(ir, join21, gate) {
|
|
|
7335
7618
|
if (here) {
|
|
7336
7619
|
attrs._playhead = true;
|
|
7337
7620
|
attrs._status = isGate && gate ? "warn" : "accent";
|
|
7338
|
-
attrs.run = isGate && gate ? `waiting for approval since ${gate.since}` :
|
|
7621
|
+
attrs.run = isGate && gate ? `waiting for approval since ${gate.since}` : join22.atReason ?? "here";
|
|
7339
7622
|
if (isGate && gate?.description) attrs.gateDescription = gate.description;
|
|
7340
7623
|
}
|
|
7341
7624
|
return { ...node, attrs };
|
|
@@ -7357,11 +7640,11 @@ function pendingGateCard(op, gate, ir) {
|
|
|
7357
7640
|
}
|
|
7358
7641
|
};
|
|
7359
7642
|
}
|
|
7360
|
-
function playheadNote(state,
|
|
7643
|
+
function playheadNote(state, join22) {
|
|
7361
7644
|
if (!state.op) {
|
|
7362
7645
|
return "No run state: nothing has been run from this behold session, so every step paints as declared.";
|
|
7363
7646
|
}
|
|
7364
|
-
const settled =
|
|
7647
|
+
const settled = join22 ? join22.byNode.size : state.records.length;
|
|
7365
7648
|
const bits = [];
|
|
7366
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`;
|
|
7367
7650
|
if (state.status === "running") {
|
|
@@ -7377,9 +7660,9 @@ function playheadNote(state, join21) {
|
|
|
7377
7660
|
} else if (state.gateNote) {
|
|
7378
7661
|
bits.push(`gate state unavailable \u2014 ${state.gateNote}`);
|
|
7379
7662
|
}
|
|
7380
|
-
if (
|
|
7663
|
+
if (join22?.unplaced.length) {
|
|
7381
7664
|
bits.push(
|
|
7382
|
-
`${
|
|
7665
|
+
`${join22.unplaced.length} record${join22.unplaced.length === 1 ? "" : "s"} matched no declared step and ${join22.unplaced.length === 1 ? "is" : "are"} not painted`
|
|
7383
7666
|
);
|
|
7384
7667
|
}
|
|
7385
7668
|
if (!bits.length) return `${head}.`;
|
|
@@ -8046,23 +8329,23 @@ var OpRunner = class {
|
|
|
8046
8329
|
// src/substrates.ts
|
|
8047
8330
|
import { spawn as spawn4 } from "node:child_process";
|
|
8048
8331
|
import { existsSync as existsSync10, readFileSync as readFileSync12 } from "node:fs";
|
|
8049
|
-
import { join as
|
|
8332
|
+
import { join as join16 } from "node:path";
|
|
8050
8333
|
import { platform } from "node:os";
|
|
8051
8334
|
var DOCKER_DOWN_DETAIL = "docker is down";
|
|
8052
8335
|
function probe(cmd, args) {
|
|
8053
|
-
return new Promise((
|
|
8336
|
+
return new Promise((resolve10) => {
|
|
8054
8337
|
let out = "";
|
|
8055
8338
|
let proc;
|
|
8056
8339
|
try {
|
|
8057
8340
|
proc = spawn4(cmd, args, { stdio: ["ignore", "pipe", "pipe"] });
|
|
8058
8341
|
} catch {
|
|
8059
|
-
|
|
8342
|
+
resolve10({ code: 127, out: "" });
|
|
8060
8343
|
return;
|
|
8061
8344
|
}
|
|
8062
8345
|
proc.stdout.on("data", (d) => out += d);
|
|
8063
8346
|
proc.stderr.on("data", (d) => out += d);
|
|
8064
|
-
proc.on("error", () =>
|
|
8065
|
-
proc.on("close", (code) =>
|
|
8347
|
+
proc.on("error", () => resolve10({ code: 127, out }));
|
|
8348
|
+
proc.on("close", (code) => resolve10({ code: code ?? 1, out }));
|
|
8066
8349
|
});
|
|
8067
8350
|
}
|
|
8068
8351
|
async function dockerAvailable(run3) {
|
|
@@ -8075,11 +8358,11 @@ async function dockerRunning(run3, nameFilter) {
|
|
|
8075
8358
|
return out.split(/\r?\n/).map((s) => s.trim()).filter(Boolean);
|
|
8076
8359
|
}
|
|
8077
8360
|
function scriptBringUp(projectDir, relPath, label) {
|
|
8078
|
-
return existsSync10(
|
|
8361
|
+
return existsSync10(join16(projectDir, relPath)) ? { label, cmd: "bash", args: [relPath] } : void 0;
|
|
8079
8362
|
}
|
|
8080
8363
|
function projectLexicons(projectDir) {
|
|
8081
8364
|
try {
|
|
8082
|
-
const src = readFileSync12(
|
|
8365
|
+
const src = readFileSync12(join16(projectDir, "chant.config.ts"), "utf-8");
|
|
8083
8366
|
const m = src.match(/lexicons\s*:\s*\[([^\]]*)\]/);
|
|
8084
8367
|
if (!m) return [];
|
|
8085
8368
|
return [...m[1].matchAll(/["']([^"']+)["']/g)].map((x) => x[1]);
|
|
@@ -8131,7 +8414,7 @@ async function detectSubstrates(projectDir, preview = false, boundContext, probe
|
|
|
8131
8414
|
["forgejo", "Forgejo", ".forgejo", "test/forgejo-runtime-e2e.sh"]
|
|
8132
8415
|
];
|
|
8133
8416
|
for (const [name, label, marker, script] of forges) {
|
|
8134
|
-
if (!existsSync10(
|
|
8417
|
+
if (!existsSync10(join16(projectDir, marker))) continue;
|
|
8135
8418
|
const c = docker ? await dockerRunning(run3, name) : [];
|
|
8136
8419
|
const d = dep(c.length > 0, "container up", "on-demand (pipeline run)");
|
|
8137
8420
|
subs.push({
|
|
@@ -8165,7 +8448,7 @@ async function detectSubstrates(projectDir, preview = false, boundContext, probe
|
|
|
8165
8448
|
detail: endpoint ? `targeting ${endpoint}` : "real Fly (FLY_FLAPS_BASE_URL unset)"
|
|
8166
8449
|
});
|
|
8167
8450
|
}
|
|
8168
|
-
if (existsSync10(
|
|
8451
|
+
if (existsSync10(join16(projectDir, ".github", "workflows"))) {
|
|
8169
8452
|
const gh = await run3("gh", ["auth", "status"]);
|
|
8170
8453
|
const ready = gh.code === 0;
|
|
8171
8454
|
subs.push({
|
|
@@ -8178,7 +8461,7 @@ async function detectSubstrates(projectDir, preview = false, boundContext, probe
|
|
|
8178
8461
|
if (lexicons.includes("temporal")) {
|
|
8179
8462
|
let hasProfiles = false;
|
|
8180
8463
|
try {
|
|
8181
|
-
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"));
|
|
8182
8465
|
} catch {
|
|
8183
8466
|
}
|
|
8184
8467
|
subs.push({
|
|
@@ -8282,12 +8565,12 @@ async function openRollbackBranches(projectDir, env) {
|
|
|
8282
8565
|
}
|
|
8283
8566
|
|
|
8284
8567
|
// src/estate-edges.ts
|
|
8285
|
-
import { statSync as
|
|
8568
|
+
import { statSync as statSync7 } from "node:fs";
|
|
8286
8569
|
var rec5 = (v) => v && typeof v === "object" && !Array.isArray(v) ? v : void 0;
|
|
8287
8570
|
var str5 = (v) => typeof v === "string" && v.length > 0 ? v : void 0;
|
|
8288
8571
|
var realIsDir2 = (p) => {
|
|
8289
8572
|
try {
|
|
8290
|
-
return
|
|
8573
|
+
return statSync7(p).isDirectory();
|
|
8291
8574
|
} catch {
|
|
8292
8575
|
return false;
|
|
8293
8576
|
}
|
|
@@ -8387,7 +8670,7 @@ function addEstateMemberEdges(ir, members, isDir = realIsDir2) {
|
|
|
8387
8670
|
|
|
8388
8671
|
// src/events.ts
|
|
8389
8672
|
import { watch, existsSync as existsSync11 } from "node:fs";
|
|
8390
|
-
import { join as
|
|
8673
|
+
import { join as join17 } from "node:path";
|
|
8391
8674
|
var Broadcaster = class {
|
|
8392
8675
|
listeners = /* @__PURE__ */ new Set();
|
|
8393
8676
|
subscribe(fn) {
|
|
@@ -8405,7 +8688,7 @@ var Broadcaster = class {
|
|
|
8405
8688
|
};
|
|
8406
8689
|
var IGNORE = /(^|[\\/])(node_modules|dist|\.git)([\\/]|$)/;
|
|
8407
8690
|
function watchSource(projectDir, onChange, debounceMs = 200) {
|
|
8408
|
-
const dir = existsSync11(
|
|
8691
|
+
const dir = existsSync11(join17(projectDir, "src")) ? join17(projectDir, "src") : projectDir;
|
|
8409
8692
|
let timer;
|
|
8410
8693
|
const watcher = watch(dir, { recursive: true }, (_event, file) => {
|
|
8411
8694
|
const name = typeof file === "string" ? file : "";
|
|
@@ -8694,12 +8977,12 @@ async function emulatorDown(projectDir) {
|
|
|
8694
8977
|
|
|
8695
8978
|
// src/demos.ts
|
|
8696
8979
|
import { readFileSync as readFileSync13, existsSync as existsSync12, cpSync } from "node:fs";
|
|
8697
|
-
import { join as
|
|
8980
|
+
import { join as join18, relative as relative4, resolve as resolve6, sep as sep3 } from "node:path";
|
|
8698
8981
|
import { spawn as spawn5, spawnSync } from "node:child_process";
|
|
8699
8982
|
function loadDemoRegistry(pkgRoot2) {
|
|
8700
8983
|
let raw;
|
|
8701
8984
|
try {
|
|
8702
|
-
raw = JSON.parse(readFileSync13(
|
|
8985
|
+
raw = JSON.parse(readFileSync13(join18(pkgRoot2, "demos.json"), "utf8"));
|
|
8703
8986
|
} catch {
|
|
8704
8987
|
return [];
|
|
8705
8988
|
}
|
|
@@ -8739,22 +9022,22 @@ function fetchesFromNetwork(entry) {
|
|
|
8739
9022
|
return entry.source === "git";
|
|
8740
9023
|
}
|
|
8741
9024
|
function demoTargetDir(entry, cwd = process.cwd()) {
|
|
8742
|
-
const legacy =
|
|
9025
|
+
const legacy = resolve6(cwd, "behold-demo");
|
|
8743
9026
|
if (entry.name === "writes" && existsSync12(legacy)) return legacy;
|
|
8744
|
-
return
|
|
9027
|
+
return resolve6(cwd, "behold-demos", entry.name);
|
|
8745
9028
|
}
|
|
8746
9029
|
async function loadDemo(entry, opts) {
|
|
8747
9030
|
const { pkgRoot: pkgRoot2, target } = opts;
|
|
8748
9031
|
const say = (line) => opts.log?.(`behold demo ${entry.name} \u2192 ${line}`);
|
|
8749
9032
|
if (!existsSync12(target)) {
|
|
8750
9033
|
if (entry.source === "bundled") {
|
|
8751
|
-
const bundled =
|
|
9034
|
+
const bundled = join18(pkgRoot2, entry.dir);
|
|
8752
9035
|
if (!existsSync12(bundled)) return { ok: false, error: `this install has no bundled ${entry.dir}` };
|
|
8753
9036
|
say(`copying to ${target} (it's yours \u2014 edit it)`);
|
|
8754
9037
|
try {
|
|
8755
9038
|
cpSync(bundled, target, {
|
|
8756
9039
|
recursive: true,
|
|
8757
|
-
filter: (src) => !
|
|
9040
|
+
filter: (src) => !relative4(bundled, src).split(sep3).includes("node_modules")
|
|
8758
9041
|
});
|
|
8759
9042
|
} catch (err) {
|
|
8760
9043
|
return { ok: false, error: `copy failed: ${err instanceof Error ? err.message : String(err)}` };
|
|
@@ -8768,7 +9051,7 @@ async function loadDemo(entry, opts) {
|
|
|
8768
9051
|
} else {
|
|
8769
9052
|
say(`reusing ${target}`);
|
|
8770
9053
|
}
|
|
8771
|
-
if (existsSync12(
|
|
9054
|
+
if (existsSync12(join18(target, "package.json")) && !existsSync12(join18(target, "node_modules"))) {
|
|
8772
9055
|
say("npm install\u2026");
|
|
8773
9056
|
if (await runStep("npm", ["install"], { cwd: target, shell: process.platform === "win32" }) !== 0) {
|
|
8774
9057
|
return { ok: false, error: `npm install failed in ${target}` };
|
|
@@ -8780,7 +9063,7 @@ async function loadDemo(entry, opts) {
|
|
|
8780
9063
|
return { ok: false, error: `setup failed (${entry.setup})` };
|
|
8781
9064
|
}
|
|
8782
9065
|
}
|
|
8783
|
-
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] };
|
|
8784
9067
|
}
|
|
8785
9068
|
function runStep(cmd, args, opts = {}) {
|
|
8786
9069
|
return new Promise((res) => {
|
|
@@ -8791,8 +9074,8 @@ function runStep(cmd, args, opts = {}) {
|
|
|
8791
9074
|
}
|
|
8792
9075
|
|
|
8793
9076
|
// src/server.ts
|
|
8794
|
-
var pkgRoot =
|
|
8795
|
-
var webRoot =
|
|
9077
|
+
var pkgRoot = join19(dirname5(fileURLToPath3(import.meta.url)), "..");
|
|
9078
|
+
var webRoot = join19(pkgRoot, "web");
|
|
8796
9079
|
var execFileP = async (cmd, args) => (await promisify3(execFile3)(cmd, args, { encoding: "utf8", timeout: 1e4 })).stdout;
|
|
8797
9080
|
function optsFromQuery(url, tierEnvVar, projectDir) {
|
|
8798
9081
|
const q = url.searchParams;
|
|
@@ -8842,7 +9125,7 @@ function tierFailure(tier, message) {
|
|
|
8842
9125
|
function beholdVersion() {
|
|
8843
9126
|
try {
|
|
8844
9127
|
const here = dirname5(fileURLToPath3(import.meta.url));
|
|
8845
|
-
return JSON.parse(readFileSync14(
|
|
9128
|
+
return JSON.parse(readFileSync14(join19(here, "..", "package.json"), "utf8")).version ?? "unknown";
|
|
8846
9129
|
} catch {
|
|
8847
9130
|
return "unknown";
|
|
8848
9131
|
}
|
|
@@ -8890,6 +9173,11 @@ function carveRoutes(app, reportPath, demo) {
|
|
|
8890
9173
|
const load = () => readCarveReport(reportPath, (p) => readFileSync14(p, "utf8"));
|
|
8891
9174
|
const carveDirs = [dirname5(reportPath), ...demo ? [demo.out] : []];
|
|
8892
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
|
+
};
|
|
8893
9181
|
const demoBlock = () => carveWriteBlock(demo);
|
|
8894
9182
|
const demoInfo = () => {
|
|
8895
9183
|
if (!demo) return null;
|
|
@@ -8901,8 +9189,8 @@ function carveRoutes(app, reportPath, demo) {
|
|
|
8901
9189
|
project: demo.project,
|
|
8902
9190
|
out: demo.out,
|
|
8903
9191
|
// Paths the UI can print without leaking the operator's whole home dir.
|
|
8904
|
-
outLabel:
|
|
8905
|
-
fromLabel:
|
|
9192
|
+
outLabel: relative5(demo.root, demo.out).split(sep4).join("/"),
|
|
9193
|
+
fromLabel: relative5(demo.root, demo.from).split(sep4).join("/"),
|
|
8906
9194
|
runnable: !block,
|
|
8907
9195
|
...block ? { reason: block } : {},
|
|
8908
9196
|
// The live tier, when this boot armed it: the scratch endpoint the
|
|
@@ -8919,7 +9207,7 @@ function carveRoutes(app, reportPath, demo) {
|
|
|
8919
9207
|
let appGraph;
|
|
8920
9208
|
const appGraphOnce = () => {
|
|
8921
9209
|
if (!demo) return Promise.resolve(null);
|
|
8922
|
-
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);
|
|
8923
9211
|
return appGraph;
|
|
8924
9212
|
};
|
|
8925
9213
|
app.get("/api/graph", async (c) => {
|
|
@@ -8928,16 +9216,18 @@ function carveRoutes(app, reportPath, demo) {
|
|
|
8928
9216
|
const tfIr = carveReportToIr(parsed.report);
|
|
8929
9217
|
const appSide = await appGraphOnce();
|
|
8930
9218
|
const states = carveStates();
|
|
9219
|
+
const emitted = await emittedNodes(states);
|
|
8931
9220
|
const shorten = demo ? (p) => shortenIn(p, demo.root) : (p) => p;
|
|
8932
9221
|
const single = () => {
|
|
8933
|
-
const split = splitCarveState(tfIr, states, { shorten });
|
|
9222
|
+
const split = splitCarveState(tfIr, states, { shorten, emitted });
|
|
8934
9223
|
return bandGraduated(split.tf, split.graduated);
|
|
8935
9224
|
};
|
|
8936
9225
|
const { svg, ir } = appSide ? renderCarveEstate(tfIr, appSide.ir, {
|
|
8937
|
-
tfTitle: `${
|
|
9226
|
+
tfTitle: `${relative5(demo.root, demo.from).split(sep4).join("/")} \u2014 terraform`,
|
|
8938
9227
|
appTitle: `${appSide.label} \u2014 chant`,
|
|
8939
9228
|
carved: states,
|
|
8940
|
-
shorten
|
|
9229
|
+
shorten,
|
|
9230
|
+
emitted
|
|
8941
9231
|
}) : (() => {
|
|
8942
9232
|
const banded = single();
|
|
8943
9233
|
return { ...renderBanded(banded), ir: banded };
|
|
@@ -8985,7 +9275,7 @@ function carveRoutes(app, reportPath, demo) {
|
|
|
8985
9275
|
}
|
|
8986
9276
|
const tfIr = carveReportToIr(parsed.report);
|
|
8987
9277
|
const html = renderCarveMorph(tfIr, appSide.ir, [select], {
|
|
8988
|
-
tfTitle: `${
|
|
9278
|
+
tfTitle: `${relative5(demo.root, demo.from).split(sep4).join("/")} \u2014 terraform`,
|
|
8989
9279
|
appTitle: `${appSide.label} \u2014 chant`,
|
|
8990
9280
|
title: `carve \u2014 ${select}`,
|
|
8991
9281
|
carved: carveStates(),
|
|
@@ -8993,7 +9283,7 @@ function carveRoutes(app, reportPath, demo) {
|
|
|
8993
9283
|
});
|
|
8994
9284
|
return c.html(html);
|
|
8995
9285
|
});
|
|
8996
|
-
app.get("/api/project", (c) => {
|
|
9286
|
+
app.get("/api/project", async (c) => {
|
|
8997
9287
|
const parsed = load();
|
|
8998
9288
|
const total = parsed.ok ? parsed.report.count ?? parsed.report.resources.length : null;
|
|
8999
9289
|
const state = carveStatePayload(
|
|
@@ -9088,6 +9378,20 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
|
|
|
9088
9378
|
if (cfg.carveReport) carveRoutes(app, cfg.carveReport, cfg.carveDemo);
|
|
9089
9379
|
let beholdConfig = loadBeholdConfig(cfg.projectDir);
|
|
9090
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
|
+
});
|
|
9091
9395
|
const boundK8sContext = async (env) => {
|
|
9092
9396
|
try {
|
|
9093
9397
|
const { lexicons, k8sProfiles } = await detectProject(cfg.projectDir);
|
|
@@ -9149,6 +9453,8 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
|
|
|
9149
9453
|
if (!info) {
|
|
9150
9454
|
return c.json({ error: `no Op named "${name}" in the estate` }, 404);
|
|
9151
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);
|
|
9152
9458
|
if (!runner.trigger(name, info.env, info.dir, Boolean(info.gate))) {
|
|
9153
9459
|
return c.json({ error: `an Op is already running (${runner.running})` }, 409);
|
|
9154
9460
|
}
|
|
@@ -9180,8 +9486,8 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
|
|
|
9180
9486
|
return c.json({ started: true, name, ran: label });
|
|
9181
9487
|
});
|
|
9182
9488
|
app.post("/api/local/reset", (c) => {
|
|
9183
|
-
const down =
|
|
9184
|
-
const up =
|
|
9489
|
+
const down = join19(cfg.projectDir, "scripts/local/local-down.sh");
|
|
9490
|
+
const up = join19(cfg.projectDir, "scripts/local/local-up.sh");
|
|
9185
9491
|
if (!existsSync13(down) || !existsSync13(up)) {
|
|
9186
9492
|
return c.json({ error: "no local-down.sh / local-up.sh in scripts/local \u2014 reset is only for local emulator projects" }, 400);
|
|
9187
9493
|
}
|
|
@@ -9318,7 +9624,7 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
|
|
|
9318
9624
|
const k8sTarget = lexicons.includes("k8s") ? resolveK8sTarget(k8sProfiles, cfg.env, await loadKubeconfig()) : void 0;
|
|
9319
9625
|
const axes = deployAxes(tierEnvVar, lexicons, k8sTarget);
|
|
9320
9626
|
const emittedOps = discoverOpIrs(estateDirs).length;
|
|
9321
|
-
const carveState = carveStatePayload(
|
|
9627
|
+
const carveState = carveStatePayload(await carveStatesUnder(estateDirs));
|
|
9322
9628
|
return c.json({
|
|
9323
9629
|
projectDir: cfg.projectDir,
|
|
9324
9630
|
...carveState.manifests ? { carve: { state: carveState } } : {},
|
|
@@ -9345,6 +9651,10 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
|
|
|
9345
9651
|
// no `tiers` key) → no `tiers` field → the SPA's picker doesn't render
|
|
9346
9652
|
// and the graph loads with no tier selected (web/app.js `initPickers`).
|
|
9347
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)])) } : {},
|
|
9348
9658
|
// The stack picker's options (#76, follow-up to #71): gated exactly like
|
|
9349
9659
|
// `tiers` above — only present when `chant.config.ts` declares `stacks[]`
|
|
9350
9660
|
// at all, so a single-stack/sourceDir-only/legacy project's SPA renders no
|
|
@@ -9369,9 +9679,9 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
|
|
|
9369
9679
|
app.post("/api/project/open", async (c) => {
|
|
9370
9680
|
if (cfg.previewMode) return c.json({ error: "switching projects is locked in preview mode" }, 403);
|
|
9371
9681
|
const body = await c.req.json().catch(() => ({}));
|
|
9372
|
-
const dir = typeof body.dir === "string" && body.dir.trim() ?
|
|
9682
|
+
const dir = typeof body.dir === "string" && body.dir.trim() ? resolve7(body.dir.trim()) : "";
|
|
9373
9683
|
if (!dir || !existsSync13(dir)) return c.json({ error: `no such directory: ${dir || "(no dir given)"}` }, 400);
|
|
9374
|
-
if (!existsSync13(
|
|
9684
|
+
if (!existsSync13(join19(dir, "chant.config.ts"))) {
|
|
9375
9685
|
return c.json({ error: `${dir} doesn't look like a chant project \u2014 no chant.config.ts` }, 400);
|
|
9376
9686
|
}
|
|
9377
9687
|
switchServedProject([dir]);
|
|
@@ -9391,7 +9701,7 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
|
|
|
9391
9701
|
};
|
|
9392
9702
|
app.post("/api/project/reveal", async (c) => {
|
|
9393
9703
|
const body = await c.req.json().catch(() => ({}));
|
|
9394
|
-
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;
|
|
9395
9705
|
const known = /* @__PURE__ */ new Set([cfg.projectDir, ...cfg.projectDirs ?? [], ...listRecents().map((r) => r.dir)]);
|
|
9396
9706
|
if (!known.has(dir)) return c.json({ error: "not a served or recent project directory" }, 400);
|
|
9397
9707
|
const opener = process.platform === "darwin" ? "open" : process.platform === "win32" ? "explorer" : "xdg-open";
|
|
@@ -9459,7 +9769,7 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
|
|
|
9459
9769
|
const loaded = await loadDemo(entry, { pkgRoot, target, log: (line) => process.stdout.write(line + "\n") });
|
|
9460
9770
|
if (!loaded.ok) return c.json({ error: loaded.error }, 500);
|
|
9461
9771
|
const dirs = loaded.serveDirs;
|
|
9462
|
-
if (!existsSync13(
|
|
9772
|
+
if (!existsSync13(join19(dirs[0], "chant.config.ts"))) {
|
|
9463
9773
|
return c.json({ error: `${entry.name} loaded to ${target} but ${dirs[0]} has no chant.config.ts` }, 500);
|
|
9464
9774
|
}
|
|
9465
9775
|
if (entry.serve.local) {
|
|
@@ -9557,7 +9867,7 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
|
|
|
9557
9867
|
path: "/api/demos/open",
|
|
9558
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)"
|
|
9559
9869
|
},
|
|
9560
|
-
{ method: "GET", path: "/api/graph", desc: "the graph {ir, svg, meta} \u2014 params: detail=0..3, components=1, logical=1, ops=1, env, stack, tier, target, lens, up=1, down=1, radial=1, layout=1" },
|
|
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" },
|
|
9561
9871
|
...cfg.carveReport ? [
|
|
9562
9872
|
{ method: "GET", path: "/api/carve", desc: "carve mode: the raw `chant carve advise --json` peelability report this server is rendering" },
|
|
9563
9873
|
{
|
|
@@ -9594,7 +9904,7 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
|
|
|
9594
9904
|
{ method: "POST", path: "/api/rollback", desc: "open a rollback PR: ?to=<sha>" },
|
|
9595
9905
|
{ method: "POST", path: "/api/substrates/:name/up", desc: "bring a substrate up" },
|
|
9596
9906
|
{ method: "POST", path: "/api/local/reset", desc: "reset the local emulator" },
|
|
9597
|
-
{ method: "POST", path: "/api/ci/dispatch", desc: "dispatch the GitHub Actions pipeline via the operator's gh" }
|
|
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" }
|
|
9598
9908
|
]
|
|
9599
9909
|
})
|
|
9600
9910
|
);
|
|
@@ -9621,10 +9931,16 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
|
|
|
9621
9931
|
parsedOps.push(parsed.ir);
|
|
9622
9932
|
}
|
|
9623
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
|
+
}
|
|
9624
9940
|
const runState = runner.runState;
|
|
9625
9941
|
const ranOp = runState.op ? parsedOps.find((o) => o.name === runState.op) : void 0;
|
|
9626
|
-
const
|
|
9627
|
-
const opsIr =
|
|
9942
|
+
const join22 = ranOp ? joinRun(ranOp, runState.records, runState.gate) : null;
|
|
9943
|
+
const opsIr = join22 ? paintPlayhead(declaredIr, join22, runState.gate) : declaredIr;
|
|
9628
9944
|
const { svg: svg2 } = renderGraph(opsIr, { boxes: "byStack" });
|
|
9629
9945
|
const operator = runner.noteDeclaredConvergeOps(declaredConvergeOps(parsedOps));
|
|
9630
9946
|
return c.json({
|
|
@@ -9637,7 +9953,7 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
|
|
|
9637
9953
|
target: null,
|
|
9638
9954
|
mode: "ops",
|
|
9639
9955
|
ops: parsedOps.length,
|
|
9640
|
-
note: `${opsNote(parsedOps, opsIr)} ${playheadNote(runState,
|
|
9956
|
+
note: `${opsNote(parsedOps, opsIr)} ${playheadNote(runState, join22)} ${operatorNote(operator)}`,
|
|
9641
9957
|
// The operator strip's own model: the ConvergeOps this project
|
|
9642
9958
|
// declares, plus whatever the last status read said. `declared`
|
|
9643
9959
|
// without a `strip` is the honest starting state — a loop exists and
|
|
@@ -9709,6 +10025,7 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
|
|
|
9709
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 } : {} } });
|
|
9710
10026
|
} else {
|
|
9711
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;
|
|
9712
10029
|
if ((opts.detail ?? 2) < 3) ir = pruneImports(ir);
|
|
9713
10030
|
ir = addK8sDeclaredEdges(ir);
|
|
9714
10031
|
ir = addValueMatchEdges(ir);
|
|
@@ -9726,7 +10043,7 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
|
|
|
9726
10043
|
srcCompositeEdgesAttached = 0;
|
|
9727
10044
|
}
|
|
9728
10045
|
}
|
|
9729
|
-
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"))) {
|
|
9730
10047
|
return c.json(noProjectError(cfg.projectDir), 404);
|
|
9731
10048
|
}
|
|
9732
10049
|
const radial = new URL(c.req.url).searchParams.get("radial") === "1";
|
|
@@ -9773,23 +10090,28 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
|
|
|
9773
10090
|
}
|
|
9774
10091
|
});
|
|
9775
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
|
+
}
|
|
9776
10099
|
const ready = await ghReady();
|
|
9777
10100
|
if (!ready.ok) return c.json({ error: ready.reason }, 400);
|
|
9778
|
-
const pipeline = await ciPipeline(cfg.projectDir,
|
|
10101
|
+
const pipeline = await ciPipeline(cfg.projectDir, env ? { env } : {}, "github").catch(() => void 0);
|
|
9779
10102
|
if (!pipeline || pipeline.jobs.length === 0) {
|
|
9780
10103
|
return c.json({ error: "no generated GitHub pipeline \u2014 `chant build --components --generate github` produced no jobs" }, 400);
|
|
9781
10104
|
}
|
|
9782
|
-
const
|
|
9783
|
-
if (
|
|
9784
|
-
|
|
9785
|
-
}
|
|
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;
|
|
9786
10108
|
const ref = await execFileP("git", ["-C", cfg.projectDir, "branch", "--show-current"]).then((out) => out.trim() || "main").catch(() => "main");
|
|
9787
10109
|
const started = runner.track(
|
|
9788
10110
|
`GitHub Actions ${workflow.file}`,
|
|
9789
10111
|
(io) => dispatchAndFollow(workflow, pipeline, ref, { onLine: io.line, onProgress: io.progress })
|
|
9790
10112
|
);
|
|
9791
10113
|
if (!started) return c.json({ error: `busy \u2014 ${runner.running} is running` }, 409);
|
|
9792
|
-
return c.json({ started: true, workflow: workflow.file, ref, jobs: pipeline.jobs.length });
|
|
10114
|
+
return c.json({ started: true, workflow: workflow.file, ref, jobs: pipeline.jobs.length, ...env ? { env } : {}, designated: Boolean(designated) });
|
|
9793
10115
|
});
|
|
9794
10116
|
app.get("/api/resources", async (c) => {
|
|
9795
10117
|
const opts = optsFromQuery(new URL(c.req.url), tierEnvVar, cfg.projectDir);
|
|
@@ -9884,7 +10206,7 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
|
|
|
9884
10206
|
});
|
|
9885
10207
|
}
|
|
9886
10208
|
const opts = { ...query, live: true, overlay: true, env, ...logical || runtime ? { detail: 3 } : {} };
|
|
9887
|
-
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;
|
|
9888
10210
|
const boundContext = await boundK8sContext(env);
|
|
9889
10211
|
ir = mergeClusterRoot(ir, await clusterRootGraphIr(cfg.projectDir, query), await runningK3dClusters());
|
|
9890
10212
|
const declaresHelm = await detectProject(cfg.projectDir).then((p) => p.lexicons.includes("helm")).catch(() => false);
|
|
@@ -9892,7 +10214,7 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
|
|
|
9892
10214
|
const observed = await lifecycleDiffLive(cfg.projectDir, env, query).then((d) => d.lexicons?.helm?.observedArtifacts).catch(() => void 0);
|
|
9893
10215
|
applyHelmArtifacts(ir, observed);
|
|
9894
10216
|
synthesizeHelmReleases(ir, observed, discoverReleaseUnits(cfg.projectDir));
|
|
9895
|
-
applyHelmRenderDrift(ir, await readHelmRenderDrift(cfg.projectDir, env, query));
|
|
10217
|
+
applyHelmRenderDrift(ir, await readHelmRenderDrift(cfg.projectDir, env, query, observed));
|
|
9896
10218
|
}
|
|
9897
10219
|
ir = runtime ? attachRuntimeContainment(ir) : pruneRuntimeChildren(ir);
|
|
9898
10220
|
if (logical) {
|
|
@@ -9943,7 +10265,7 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
|
|
|
9943
10265
|
const observed = await lifecycleDiffLive(cfg.projectDir, env).then((d) => d.lexicons?.helm?.observedArtifacts).catch(() => void 0);
|
|
9944
10266
|
applyHelmArtifacts(result.ir, observed);
|
|
9945
10267
|
synthesizeHelmReleases(result.ir, observed, discoverReleaseUnits(cfg.projectDir));
|
|
9946
|
-
applyHelmRenderDrift(result.ir, await readHelmRenderDrift(cfg.projectDir, env));
|
|
10268
|
+
applyHelmRenderDrift(result.ir, await readHelmRenderDrift(cfg.projectDir, env, {}, observed));
|
|
9947
10269
|
}
|
|
9948
10270
|
}
|
|
9949
10271
|
if (env) {
|
|
@@ -10047,6 +10369,8 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
|
|
|
10047
10369
|
}
|
|
10048
10370
|
const component = url.searchParams.get("component") || "all";
|
|
10049
10371
|
const force = url.searchParams.get("force") === "1";
|
|
10372
|
+
const designated = executorFor(env);
|
|
10373
|
+
if (designated) return c.json(forgeRefusal(env, designated, "apply"), 409);
|
|
10050
10374
|
const flociTargeted = !!process.env.AWS_ENDPOINT_URL && await detectProject(cfg.projectDir).then((p) => p.lexicons.includes("aws")).catch(() => false);
|
|
10051
10375
|
if (!force && flociTargeted) {
|
|
10052
10376
|
try {
|
|
@@ -10071,9 +10395,9 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
|
|
|
10071
10395
|
}
|
|
10072
10396
|
return c.json({ started: true, component, env });
|
|
10073
10397
|
});
|
|
10074
|
-
const rel =
|
|
10398
|
+
const rel = relative5(process.cwd(), webRoot) || ".";
|
|
10075
10399
|
app.use("/*", serveStatic({ root: rel }));
|
|
10076
|
-
app.get("/", serveStatic({ path:
|
|
10400
|
+
app.get("/", serveStatic({ path: join19(rel, "index.html") }));
|
|
10077
10401
|
return app;
|
|
10078
10402
|
}
|
|
10079
10403
|
async function bootLocalEmulators(dir, who) {
|
|
@@ -10102,7 +10426,7 @@ async function bootLocalEmulators(dir, who) {
|
|
|
10102
10426
|
async function startServer(cfg) {
|
|
10103
10427
|
if (cfg.local) cfg.emulators = await bootLocalEmulators(cfg.projectDir, "behold serve --local");
|
|
10104
10428
|
for (const dir of cfg.projectDirs ?? [cfg.projectDir]) {
|
|
10105
|
-
if (existsSync13(
|
|
10429
|
+
if (existsSync13(join19(dir, "chant.config.ts"))) addRecent(dir);
|
|
10106
10430
|
}
|
|
10107
10431
|
const broadcaster = new Broadcaster();
|
|
10108
10432
|
const frames = new FrameBuffer();
|
|
@@ -10128,11 +10452,19 @@ async function startServer(cfg) {
|
|
|
10128
10452
|
if (autoSync === "off") return;
|
|
10129
10453
|
void routeAutoSync(dir, movedLexicons);
|
|
10130
10454
|
};
|
|
10455
|
+
const executorDesignations = loadBeholdConfig(cfg.projectDir).executor ?? {};
|
|
10456
|
+
const executorFor = (env) => env ? executorDesignations[env] : void 0;
|
|
10131
10457
|
const routeAutoSync = async (dir, movedLexicons) => {
|
|
10132
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
|
+
}
|
|
10133
10465
|
const { picks, declined } = pickAutoSyncOps(
|
|
10134
10466
|
autoSync,
|
|
10135
|
-
|
|
10467
|
+
allOps.filter((o) => !routed.includes(o)),
|
|
10136
10468
|
runner.running,
|
|
10137
10469
|
movedLexicons,
|
|
10138
10470
|
suspended
|
|
@@ -10241,8 +10573,8 @@ async function startServer(cfg) {
|
|
|
10241
10573
|
}
|
|
10242
10574
|
|
|
10243
10575
|
// src/export.ts
|
|
10244
|
-
import { mkdirSync as mkdirSync4, writeFileSync as writeFileSync4, copyFileSync, readFileSync as readFileSync15, readdirSync as
|
|
10245
|
-
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";
|
|
10246
10578
|
import { fileURLToPath as fileURLToPath4 } from "node:url";
|
|
10247
10579
|
var LENS_PARAMS = ["components", "detail", "env", "logical", "ops", "radial", "tier"];
|
|
10248
10580
|
function canonicalKey(path, params) {
|
|
@@ -10260,7 +10592,7 @@ function captureKeys(axes) {
|
|
|
10260
10592
|
add("/api/project", {});
|
|
10261
10593
|
add("/api/substrates", {});
|
|
10262
10594
|
add("/api/ops", {});
|
|
10263
|
-
if (axes.ops) add("/api/graph", { ops: "1" });
|
|
10595
|
+
if (axes.ops) add("/api/graph", { ops: "1", entities: "1" });
|
|
10264
10596
|
const tiers = axes.tiers && axes.tiers.length ? axes.tiers : [""];
|
|
10265
10597
|
const envs = ["", ...axes.environments];
|
|
10266
10598
|
for (const env of envs) {
|
|
@@ -10291,7 +10623,7 @@ function captureKeys(axes) {
|
|
|
10291
10623
|
return [...keys];
|
|
10292
10624
|
}
|
|
10293
10625
|
function webDir() {
|
|
10294
|
-
return
|
|
10626
|
+
return join20(dirname6(fileURLToPath4(import.meta.url)), "..", "web");
|
|
10295
10627
|
}
|
|
10296
10628
|
function workerName(project, override) {
|
|
10297
10629
|
const raw = override ?? `behold-${basename2(project)}`;
|
|
@@ -10302,7 +10634,7 @@ async function runExport(cfg, outDir, opts = {}) {
|
|
|
10302
10634
|
const app = createApp({ ...cfg, layoutWrites: false });
|
|
10303
10635
|
const proj = await (await app.request("/api/project")).json();
|
|
10304
10636
|
const axes = { environments: proj.environments ?? [], tiers: proj.tiers ?? [], ...proj.ops ? { ops: proj.ops } : {} };
|
|
10305
|
-
const snapDir =
|
|
10637
|
+
const snapDir = join20(outDir, "snapshots");
|
|
10306
10638
|
mkdirSync4(snapDir, { recursive: true });
|
|
10307
10639
|
const keyToFile = {};
|
|
10308
10640
|
let ok = 0;
|
|
@@ -10311,7 +10643,7 @@ async function runExport(cfg, outDir, opts = {}) {
|
|
|
10311
10643
|
const res = await app.request(`${key}${key.includes("?") ? "&" : "?"}layout=1`);
|
|
10312
10644
|
const body = await res.text();
|
|
10313
10645
|
const file = slug2(key);
|
|
10314
|
-
writeFileSync4(
|
|
10646
|
+
writeFileSync4(join20(snapDir, file), body);
|
|
10315
10647
|
keyToFile[key] = `snapshots/${file}`;
|
|
10316
10648
|
if (res.ok) ok++;
|
|
10317
10649
|
else failed++;
|
|
@@ -10323,21 +10655,21 @@ async function runExport(cfg, outDir, opts = {}) {
|
|
|
10323
10655
|
axes,
|
|
10324
10656
|
keyToFile
|
|
10325
10657
|
};
|
|
10326
|
-
writeFileSync4(
|
|
10327
|
-
const html = readFileSync15(
|
|
10658
|
+
writeFileSync4(join20(outDir, "manifest.json"), JSON.stringify(manifest, null, 2));
|
|
10659
|
+
const html = readFileSync15(join20(webDir(), "index.html"), "utf8").replace(
|
|
10328
10660
|
/<\/head>/i,
|
|
10329
10661
|
` <script>window.__BEHOLD_STATIC__ = true;</script>
|
|
10330
10662
|
</head>`
|
|
10331
10663
|
);
|
|
10332
|
-
writeFileSync4(
|
|
10333
|
-
for (const f of
|
|
10664
|
+
writeFileSync4(join20(outDir, "index.html"), html);
|
|
10665
|
+
for (const f of readdirSync8(webDir())) {
|
|
10334
10666
|
if (f === "index.html") continue;
|
|
10335
|
-
copyFileSync(
|
|
10667
|
+
copyFileSync(join20(webDir(), f), join20(outDir, f));
|
|
10336
10668
|
}
|
|
10337
|
-
writeFileSync4(
|
|
10669
|
+
writeFileSync4(join20(outDir, "README.md"), BUNDLE_README);
|
|
10338
10670
|
const name = workerName(cfg.projectDir, opts.name);
|
|
10339
10671
|
writeFileSync4(
|
|
10340
|
-
|
|
10672
|
+
join20(outDir, "wrangler.jsonc"),
|
|
10341
10673
|
JSON.stringify(
|
|
10342
10674
|
{ $schema: "node_modules/wrangler/config-schema.json", name, compatibility_date: "2025-06-01", assets: { directory: "." } },
|
|
10343
10675
|
null,
|
|
@@ -10385,7 +10717,7 @@ It's just files \u2014 GitHub Pages, S3, nginx, or Cloudflare Pages
|
|
|
10385
10717
|
`;
|
|
10386
10718
|
|
|
10387
10719
|
// src/doctor.ts
|
|
10388
|
-
import { relative as
|
|
10720
|
+
import { relative as relative6, resolve as resolve8 } from "node:path";
|
|
10389
10721
|
var list2 = (xs) => xs.join(", ");
|
|
10390
10722
|
var EMPTY_KUBECONFIG = { contexts: /* @__PURE__ */ new Map(), servers: /* @__PURE__ */ new Map() };
|
|
10391
10723
|
function summarize(xs, max = 4) {
|
|
@@ -10393,7 +10725,7 @@ function summarize(xs, max = 4) {
|
|
|
10393
10725
|
}
|
|
10394
10726
|
function labelFor(root, target, estate) {
|
|
10395
10727
|
if (!estate) return "";
|
|
10396
|
-
const rel =
|
|
10728
|
+
const rel = relative6(root, target);
|
|
10397
10729
|
return `${rel || "."}: `;
|
|
10398
10730
|
}
|
|
10399
10731
|
function chantCheck(root, targets, estate) {
|
|
@@ -10406,7 +10738,7 @@ function chantCheck(root, targets, estate) {
|
|
|
10406
10738
|
name: "chant",
|
|
10407
10739
|
status: "fail",
|
|
10408
10740
|
detail: `${list2(resolutions.map(describe))} \u2014 behold shells the project's own chant`,
|
|
10409
|
-
fix: `Run \`npm install\` in ${list2(missing.map(({ target }) =>
|
|
10741
|
+
fix: `Run \`npm install\` in ${list2(missing.map(({ target }) => relative6(root, target) || target))}`
|
|
10410
10742
|
};
|
|
10411
10743
|
}
|
|
10412
10744
|
const stale = resolutions.filter(({ res }) => !meetsFloor(res.version, floor));
|
|
@@ -10534,7 +10866,7 @@ function opsCheck(root, ops, chantSource, estate) {
|
|
|
10534
10866
|
return { name: "ops", status: "pass", detail: `${ops.length} committed: ${list2(names)}; ${mcp}` };
|
|
10535
10867
|
}
|
|
10536
10868
|
async function diagnose(dir, probes = {}) {
|
|
10537
|
-
const root =
|
|
10869
|
+
const root = resolve8(dir);
|
|
10538
10870
|
const shape = detectProjectShape(root);
|
|
10539
10871
|
const behold = beholdVersion();
|
|
10540
10872
|
if (shape.kind === "none") {
|
|
@@ -10554,16 +10886,16 @@ async function diagnose(dir, probes = {}) {
|
|
|
10554
10886
|
};
|
|
10555
10887
|
}
|
|
10556
10888
|
const estate = shape.kind === "estate";
|
|
10557
|
-
const members = (shape.members ?? []).map((m) =>
|
|
10889
|
+
const members = (shape.members ?? []).map((m) => resolve8(root, m));
|
|
10558
10890
|
const targets = estate ? members : [root];
|
|
10559
10891
|
const primary = targets[0];
|
|
10560
10892
|
const projectCheck = estate ? {
|
|
10561
10893
|
name: "project",
|
|
10562
10894
|
status: "pass",
|
|
10563
10895
|
detail: `estate of ${members.length} projects (${shape.membersFrom === "behold-config" ? ".behold.json members" : "npm workspaces"}): ${list2(
|
|
10564
|
-
members.map((m) =>
|
|
10896
|
+
members.map((m) => relative6(root, m))
|
|
10565
10897
|
)}`
|
|
10566
|
-
} : { name: "project", status: "pass", detail: `chant project (${
|
|
10898
|
+
} : { name: "project", status: "pass", detail: `chant project (${relative6(root, shape.configFile)})` };
|
|
10567
10899
|
const infos = await Promise.all(targets.map(async (t) => ({ target: t, info: await detectProject(t) })));
|
|
10568
10900
|
const declared = new Map(infos.map(({ target, info }) => [target, info.lexicons]));
|
|
10569
10901
|
const lexicons = [...new Set(infos.flatMap(({ info }) => info.lexicons))];
|
|
@@ -10788,7 +11120,7 @@ ${USAGE}`);
|
|
|
10788
11120
|
process.stderr.write("behold serve: --auto-sync needs --env and --poll (it acts on polled drift)\n");
|
|
10789
11121
|
process.exit(2);
|
|
10790
11122
|
}
|
|
10791
|
-
const dirs = projectDirs.map((d) =>
|
|
11123
|
+
const dirs = projectDirs.map((d) => resolve9(d));
|
|
10792
11124
|
for (const d of dirs) warnIfNotChantProject(d);
|
|
10793
11125
|
await startServer({
|
|
10794
11126
|
projectDir: dirs[0],
|
|
@@ -10807,7 +11139,7 @@ function warnIfNotChantProject(dir) {
|
|
|
10807
11139
|
if (shape.kind === "estate") {
|
|
10808
11140
|
process.stderr.write(
|
|
10809
11141
|
`behold: warning \u2014 ${dir} is an estate root, not a chant project itself.
|
|
10810
|
-
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(" ")}
|
|
10811
11143
|
`
|
|
10812
11144
|
);
|
|
10813
11145
|
return;
|
|
@@ -10842,7 +11174,7 @@ async function runCarve(rest) {
|
|
|
10842
11174
|
);
|
|
10843
11175
|
process.exit(2);
|
|
10844
11176
|
}
|
|
10845
|
-
const reportPath =
|
|
11177
|
+
const reportPath = resolve9(fileArg);
|
|
10846
11178
|
const parsed = readCarveReport(reportPath, (p) => readFileSync16(p, "utf8"));
|
|
10847
11179
|
if (!parsed.ok) {
|
|
10848
11180
|
process.stderr.write(`behold carve: ${parsed.refusal.error}
|
|
@@ -10870,8 +11202,8 @@ async function runDoctor(rest) {
|
|
|
10870
11202
|
}
|
|
10871
11203
|
}
|
|
10872
11204
|
const dir = dirArg ?? ".";
|
|
10873
|
-
if (!existsSync14(
|
|
10874
|
-
process.stderr.write(`behold doctor: no such directory: ${
|
|
11205
|
+
if (!existsSync14(resolve9(dir))) {
|
|
11206
|
+
process.stderr.write(`behold doctor: no such directory: ${resolve9(dir)}
|
|
10875
11207
|
`);
|
|
10876
11208
|
process.exit(2);
|
|
10877
11209
|
}
|
|
@@ -10880,7 +11212,7 @@ async function runDoctor(rest) {
|
|
|
10880
11212
|
if (!report.ok) process.exitCode = 1;
|
|
10881
11213
|
}
|
|
10882
11214
|
async function runDemo(rest) {
|
|
10883
|
-
const pkgRoot2 =
|
|
11215
|
+
const pkgRoot2 = join21(dirname7(fileURLToPath5(import.meta.url)), "..");
|
|
10884
11216
|
const registry = loadDemoRegistry(pkgRoot2);
|
|
10885
11217
|
let port = 4600;
|
|
10886
11218
|
let live = false;
|
|
@@ -10934,7 +11266,7 @@ async function runDemo(rest) {
|
|
|
10934
11266
|
`);
|
|
10935
11267
|
process.exit(2);
|
|
10936
11268
|
}
|
|
10937
|
-
const target = dirArg ?
|
|
11269
|
+
const target = dirArg ? resolve9(dirArg) : demoTargetDir(entry);
|
|
10938
11270
|
const loaded = await loadDemo(entry, { pkgRoot: pkgRoot2, target, log: (line) => process.stdout.write(line + "\n") });
|
|
10939
11271
|
if (!loaded.ok) {
|
|
10940
11272
|
process.stderr.write(`behold demo ${entry.name}: ${loaded.error}
|
|
@@ -10965,7 +11297,7 @@ function spawnStep(cmd, args, cwd) {
|
|
|
10965
11297
|
});
|
|
10966
11298
|
}
|
|
10967
11299
|
async function serveCarveDemo(target, carve, port, live = false) {
|
|
10968
|
-
const at = (rel) =>
|
|
11300
|
+
const at = (rel) => resolve9(target, rel);
|
|
10969
11301
|
const project = at(carve.project);
|
|
10970
11302
|
const from = at(carve.from);
|
|
10971
11303
|
const state = carve.state ? at(carve.state) : void 0;
|
|
@@ -11005,7 +11337,7 @@ async function serveCarveDemo(target, carve, port, live = false) {
|
|
|
11005
11337
|
};
|
|
11006
11338
|
process.stdout.write("behold demo carve --live \u2192 the tfstate is real now; the advisor reads what terraform wrote.\n");
|
|
11007
11339
|
}
|
|
11008
|
-
if (existsSync14(
|
|
11340
|
+
if (existsSync14(join21(project, "package.json")) && !existsSync14(join21(project, "node_modules"))) {
|
|
11009
11341
|
process.stdout.write(`behold demo carve \u2192 npm install in ${carve.project}/ (the chant this walkthrough shells)\u2026
|
|
11010
11342
|
`);
|
|
11011
11343
|
const code = await spawnStep("npm", ["install"], project);
|
|
@@ -11016,7 +11348,7 @@ async function serveCarveDemo(target, carve, port, live = false) {
|
|
|
11016
11348
|
}
|
|
11017
11349
|
}
|
|
11018
11350
|
let degraded;
|
|
11019
|
-
if (!existsSync14(
|
|
11351
|
+
if (!existsSync14(join21(target, "node_modules", "@cdktf", "hcl2json"))) {
|
|
11020
11352
|
process.stdout.write("behold demo carve \u2192 npm install @cdktf/hcl2json (chant's HCL parser, ~2MB, once)\u2026\n");
|
|
11021
11353
|
const code = await spawnStep("npm", ["install", "--no-save", "--no-package-lock", "@cdktf/hcl2json"], target);
|
|
11022
11354
|
if (code !== 0) degraded = "couldn't install @cdktf/hcl2json (chant's HCL parser) \u2014 no network?";
|
|
@@ -11085,7 +11417,7 @@ async function runPreview(rest) {
|
|
|
11085
11417
|
process.stderr.write("behold preview: --port must be a number\n");
|
|
11086
11418
|
process.exit(2);
|
|
11087
11419
|
}
|
|
11088
|
-
const projectDir =
|
|
11420
|
+
const projectDir = resolve9(dirArg ?? process.cwd());
|
|
11089
11421
|
if (!existsSync14(projectDir)) {
|
|
11090
11422
|
process.stderr.write(`behold preview: project not found at ${projectDir}
|
|
11091
11423
|
`);
|
|
@@ -11106,21 +11438,21 @@ async function runPreview(rest) {
|
|
|
11106
11438
|
await startServer({ projectDir, port, env: "local", previewMode: true });
|
|
11107
11439
|
}
|
|
11108
11440
|
async function runExportCmd(rest) {
|
|
11109
|
-
let outDir =
|
|
11441
|
+
let outDir = resolve9("behold-export");
|
|
11110
11442
|
let env;
|
|
11111
11443
|
let name;
|
|
11112
11444
|
let dirArg;
|
|
11113
11445
|
let emulator = false;
|
|
11114
11446
|
for (let i = 0; i < rest.length; i++) {
|
|
11115
11447
|
const a = rest[i];
|
|
11116
|
-
if (a === "--out") outDir =
|
|
11448
|
+
if (a === "--out") outDir = resolve9(rest[++i]);
|
|
11117
11449
|
else if (a === "--env") env = rest[++i];
|
|
11118
11450
|
else if (a === "--name") name = rest[++i];
|
|
11119
11451
|
else if (a === "--emulator") emulator = true;
|
|
11120
11452
|
else if (a === "-h" || a === "--help") return void process.stdout.write(USAGE);
|
|
11121
11453
|
else if (!a.startsWith("-")) dirArg = a;
|
|
11122
11454
|
}
|
|
11123
|
-
const projectDir =
|
|
11455
|
+
const projectDir = resolve9(dirArg ?? process.cwd());
|
|
11124
11456
|
if (emulator) {
|
|
11125
11457
|
injectEmulatorEnv(env);
|
|
11126
11458
|
env ??= "local";
|