@intentius/behold 0.16.0 → 0.17.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +25 -7
- package/README.md +34 -2
- package/dist/cli.js +434 -206
- package/package.json +1 -1
- package/web/app.js +3 -1
package/dist/cli.js
CHANGED
|
@@ -716,11 +716,11 @@ async function createK8sClient(options = {}) {
|
|
|
716
716
|
}
|
|
717
717
|
return void 0;
|
|
718
718
|
}
|
|
719
|
-
async function
|
|
719
|
+
async function resolve12(selector, signal) {
|
|
720
720
|
return "apiVersion" in selector ? resolveByGvk(selector.apiVersion, selector.kind, signal) : resolveByResourceString(selector.resource, selector.group, signal);
|
|
721
721
|
}
|
|
722
722
|
async function resolveOrThrow(selector, signal) {
|
|
723
|
-
const info = await
|
|
723
|
+
const info = await resolve12(selector, signal);
|
|
724
724
|
if (!info) throw new UnknownResourceError(selectorText(selector));
|
|
725
725
|
return info;
|
|
726
726
|
}
|
|
@@ -732,7 +732,7 @@ async function createK8sClient(options = {}) {
|
|
|
732
732
|
return parts.join("/");
|
|
733
733
|
}
|
|
734
734
|
async function pathFor(ref, signal) {
|
|
735
|
-
const info = await
|
|
735
|
+
const info = await resolve12({ apiVersion: ref.apiVersion, kind: ref.kind }, signal);
|
|
736
736
|
if (!info) return void 0;
|
|
737
737
|
return objectPath(info, ref.name, ref.namespace);
|
|
738
738
|
}
|
|
@@ -872,7 +872,7 @@ async function createK8sClient(options = {}) {
|
|
|
872
872
|
return {
|
|
873
873
|
provenance,
|
|
874
874
|
defaultNamespace,
|
|
875
|
-
resolve:
|
|
875
|
+
resolve: resolve12,
|
|
876
876
|
read,
|
|
877
877
|
pathFor,
|
|
878
878
|
readIfPresent,
|
|
@@ -1065,8 +1065,8 @@ var init_src = __esm({
|
|
|
1065
1065
|
});
|
|
1066
1066
|
|
|
1067
1067
|
// src/cli.ts
|
|
1068
|
-
import { resolve as
|
|
1069
|
-
import { realpathSync, existsSync as
|
|
1068
|
+
import { resolve as resolve11, dirname as dirname7, join as join22 } from "node:path";
|
|
1069
|
+
import { realpathSync, existsSync as existsSync15, readFileSync as readFileSync17, rmSync as rmSync3 } from "node:fs";
|
|
1070
1070
|
import { fileURLToPath as fileURLToPath5 } from "node:url";
|
|
1071
1071
|
import { spawn as spawn6, spawnSync as spawnSync2 } from "node:child_process";
|
|
1072
1072
|
|
|
@@ -1147,8 +1147,8 @@ import { serve } from "@hono/node-server";
|
|
|
1147
1147
|
import { fileURLToPath as fileURLToPath3 } from "node:url";
|
|
1148
1148
|
import { execFile as execFile3 } from "node:child_process";
|
|
1149
1149
|
import { promisify as promisify3 } from "node:util";
|
|
1150
|
-
import { existsSync as
|
|
1151
|
-
import { basename, dirname as dirname5, join as
|
|
1150
|
+
import { existsSync as existsSync14, readFileSync as readFileSync15 } from "node:fs";
|
|
1151
|
+
import { basename, dirname as dirname5, join as join20, relative as relative5, resolve as resolve9, sep as sep6 } from "node:path";
|
|
1152
1152
|
|
|
1153
1153
|
// src/recents.ts
|
|
1154
1154
|
import { readFileSync, writeFileSync, mkdirSync, existsSync } from "node:fs";
|
|
@@ -1185,7 +1185,7 @@ import { spawn } from "node:child_process";
|
|
|
1185
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 dirname3, join as join4, resolve as
|
|
1188
|
+
import { dirname as dirname3, join as join4, resolve as resolve3 } from "node:path";
|
|
1189
1189
|
|
|
1190
1190
|
// src/project.ts
|
|
1191
1191
|
import { readFileSync as readFileSync2, existsSync as existsSync2 } from "node:fs";
|
|
@@ -1691,14 +1691,14 @@ function unescapeDoubleQuoted(body) {
|
|
|
1691
1691
|
|
|
1692
1692
|
// src/carve-manifest.ts
|
|
1693
1693
|
import { readdirSync, readFileSync as readFileSync3, statSync } from "node:fs";
|
|
1694
|
-
import { dirname as dirname2, isAbsolute, join as join3, relative, resolve } from "node:path";
|
|
1694
|
+
import { dirname as dirname2, isAbsolute, join as join3, relative, resolve, sep } from "node:path";
|
|
1695
1695
|
var CARVE_MANIFEST_SUFFIX = ".carve.json";
|
|
1696
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
1697
|
var isRecord = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
1698
1698
|
var strings = (v) => Array.isArray(v) ? v.filter((x) => typeof x === "string") : [];
|
|
1699
1699
|
function parseCarveManifest(value) {
|
|
1700
1700
|
if (!isRecord(value)) return null;
|
|
1701
|
-
if (value.version !== 1) return null;
|
|
1701
|
+
if (value.version !== 1 && value.version !== 2) return null;
|
|
1702
1702
|
if (typeof value.target !== "string" || !value.target) return null;
|
|
1703
1703
|
return value;
|
|
1704
1704
|
}
|
|
@@ -1835,17 +1835,23 @@ async function discoverCarveStates(dirs, read, io = nodeCarveIo, depth = MANIFES
|
|
|
1835
1835
|
return byTarget;
|
|
1836
1836
|
}
|
|
1837
1837
|
function joinCarvedSources(ir, states, memberDir) {
|
|
1838
|
-
const
|
|
1839
|
-
|
|
1840
|
-
|
|
1838
|
+
const root = resolve(memberDir);
|
|
1839
|
+
const candidates = [];
|
|
1840
|
+
for (const s of states) {
|
|
1841
|
+
for (const f of s.sourceFiles) {
|
|
1842
|
+
const abs = resolve(f);
|
|
1843
|
+
if (abs === root || abs.startsWith(root + sep)) candidates.push({ abs, state: s });
|
|
1844
|
+
}
|
|
1845
|
+
}
|
|
1846
|
+
if (candidates.length === 0) return { ir, joined: 0 };
|
|
1841
1847
|
let joined = 0;
|
|
1842
1848
|
const nodes = ir.nodes.map((n) => {
|
|
1843
1849
|
const file = n.sourceLoc?.file;
|
|
1844
1850
|
if (!file) return n;
|
|
1845
|
-
const
|
|
1846
|
-
|
|
1847
|
-
if (!state) return n;
|
|
1851
|
+
const hit = emittedFileFor(file, root, candidates);
|
|
1852
|
+
if (!hit) return n;
|
|
1848
1853
|
joined++;
|
|
1854
|
+
const { state, abs } = hit;
|
|
1849
1855
|
const carve = {
|
|
1850
1856
|
target: state.target,
|
|
1851
1857
|
...state.tfType ? { tfType: state.tfType } : {},
|
|
@@ -1853,12 +1859,24 @@ function joinCarvedSources(ir, states, memberDir) {
|
|
|
1853
1859
|
graduated: state.graduated,
|
|
1854
1860
|
...state.at ? { at: state.at } : {},
|
|
1855
1861
|
...state.from ? { from: state.from } : {},
|
|
1856
|
-
file: relative(
|
|
1862
|
+
file: relative(root, abs).split(sep).join("/")
|
|
1857
1863
|
};
|
|
1858
1864
|
return { ...n, attrs: { ...n.attrs, carved: state.target, _carve: carve } };
|
|
1859
1865
|
});
|
|
1860
1866
|
return { ir: joined ? { ...ir, nodes } : ir, joined };
|
|
1861
1867
|
}
|
|
1868
|
+
function emittedFileFor(file, root, candidates) {
|
|
1869
|
+
const rel = file.split("\\").join("/").replace(/^\.\//, "");
|
|
1870
|
+
if (isAbsolute(rel)) {
|
|
1871
|
+
const abs = resolve(rel);
|
|
1872
|
+
return candidates.find((c) => c.abs === abs);
|
|
1873
|
+
}
|
|
1874
|
+
const exact = resolve(root, rel);
|
|
1875
|
+
const direct = candidates.find((c) => c.abs === exact);
|
|
1876
|
+
if (direct) return direct;
|
|
1877
|
+
const suffix = candidates.filter((c) => c.abs.split(sep).join("/").endsWith("/" + rel));
|
|
1878
|
+
return suffix.length === 1 ? suffix[0] : void 0;
|
|
1879
|
+
}
|
|
1862
1880
|
function emittedNodesFor(ir, states, memberDir) {
|
|
1863
1881
|
const out = /* @__PURE__ */ new Map();
|
|
1864
1882
|
if (!ir) return out;
|
|
@@ -1992,6 +2010,46 @@ function carveStateNote(progress, states) {
|
|
|
1992
2010
|
return progress.detail ? `${head}, ${progress.detail}.` : `${head}.`;
|
|
1993
2011
|
}
|
|
1994
2012
|
|
|
2013
|
+
// src/foreign.ts
|
|
2014
|
+
import { isAbsolute as isAbsolute2, resolve as resolve2, sep as sep2 } from "node:path";
|
|
2015
|
+
function dropForeignDeclarations(ir, projectDir) {
|
|
2016
|
+
const root = resolve2(projectDir);
|
|
2017
|
+
const foreign = [];
|
|
2018
|
+
for (const n of ir.nodes) {
|
|
2019
|
+
const file = n.sourceLoc?.file;
|
|
2020
|
+
if (!file || !isAbsolute2(file)) continue;
|
|
2021
|
+
const abs = resolve2(file);
|
|
2022
|
+
if (abs === root || abs.startsWith(root + sep2)) continue;
|
|
2023
|
+
foreign.push({ id: n.id, kind: n.kind, file });
|
|
2024
|
+
}
|
|
2025
|
+
if (foreign.length === 0) return ir;
|
|
2026
|
+
const gone = new Set(foreign.map((f) => f.id));
|
|
2027
|
+
const groups = {};
|
|
2028
|
+
for (const [axis, members] of Object.entries(ir.groups ?? {})) {
|
|
2029
|
+
if (!members || typeof members !== "object") continue;
|
|
2030
|
+
const kept = {};
|
|
2031
|
+
for (const [box, ids] of Object.entries(members)) {
|
|
2032
|
+
const left = ids.filter((id) => !gone.has(id));
|
|
2033
|
+
if (left.length) kept[box] = left;
|
|
2034
|
+
}
|
|
2035
|
+
groups[axis] = kept;
|
|
2036
|
+
}
|
|
2037
|
+
return {
|
|
2038
|
+
...ir,
|
|
2039
|
+
nodes: ir.nodes.filter((n) => !gone.has(n.id)),
|
|
2040
|
+
edges: ir.edges.filter((e) => !gone.has(e.from) && !gone.has(e.to)),
|
|
2041
|
+
groups,
|
|
2042
|
+
foreign
|
|
2043
|
+
};
|
|
2044
|
+
}
|
|
2045
|
+
function foreignNote(ir) {
|
|
2046
|
+
const f = ir.foreign;
|
|
2047
|
+
if (!f || f.length === 0) return void 0;
|
|
2048
|
+
const named = f.slice(0, 3).map((d) => d.id).join(", ");
|
|
2049
|
+
const more = f.length > 3 ? ` +${f.length - 3}` : "";
|
|
2050
|
+
return `${f.length} declaration${f.length === 1 ? "" : "s"} outside this project dropped (${named}${more}) \u2014 chant graphs Ops from the git root (chant#2058)`;
|
|
2051
|
+
}
|
|
2052
|
+
|
|
1995
2053
|
// src/chant.ts
|
|
1996
2054
|
function envOverridesFor(opts) {
|
|
1997
2055
|
const overrides = {};
|
|
@@ -2042,7 +2100,7 @@ function chantPackageFrom(req) {
|
|
|
2042
2100
|
return { root, bin: join4(root, manifest.bin?.chant ?? "bin/chant"), ...manifest.version ? { version: manifest.version } : {} };
|
|
2043
2101
|
}
|
|
2044
2102
|
function resolveLexicons(projectDir, lexicons) {
|
|
2045
|
-
const req = createRequire(join4(
|
|
2103
|
+
const req = createRequire(join4(resolve3(projectDir), "noop.js"));
|
|
2046
2104
|
return lexicons.map((lexicon) => {
|
|
2047
2105
|
const pkg = `@intentius/chant-lexicon-${lexicon}`;
|
|
2048
2106
|
const found = packageManifestFrom(req, pkg);
|
|
@@ -2052,7 +2110,7 @@ function resolveLexicons(projectDir, lexicons) {
|
|
|
2052
2110
|
function resolveChant(projectDir) {
|
|
2053
2111
|
const own = chantPackageFrom(createRequire(import.meta.url));
|
|
2054
2112
|
if (projectDir) {
|
|
2055
|
-
const fromProject = chantPackageFrom(createRequire(join4(
|
|
2113
|
+
const fromProject = chantPackageFrom(createRequire(join4(resolve3(projectDir), "noop.js")));
|
|
2056
2114
|
if (fromProject) return { source: fromProject.root === own?.root ? "behold" : "project", ...fromProject };
|
|
2057
2115
|
}
|
|
2058
2116
|
if (own) return { source: "behold", ...own };
|
|
@@ -2179,9 +2237,9 @@ async function graphPath(projectDir, opts = {}) {
|
|
|
2179
2237
|
}
|
|
2180
2238
|
if (info.stacks?.length) {
|
|
2181
2239
|
const picked = opts.stack ? info.stacks.find((s) => s.name === opts.stack) : void 0;
|
|
2182
|
-
return
|
|
2240
|
+
return resolve3(projectDir, (picked ?? info.stacks[0]).src);
|
|
2183
2241
|
}
|
|
2184
|
-
if (info.sourceDir) return
|
|
2242
|
+
if (info.sourceDir) return resolve3(projectDir, info.sourceDir);
|
|
2185
2243
|
return legacyGraphPath(projectDir);
|
|
2186
2244
|
}
|
|
2187
2245
|
function graphArgs(src, format, opts, components) {
|
|
@@ -2189,7 +2247,8 @@ function graphArgs(src, format, opts, components) {
|
|
|
2189
2247
|
}
|
|
2190
2248
|
async function graphIr(projectDir, opts = {}) {
|
|
2191
2249
|
const src = await graphPath(projectDir, opts);
|
|
2192
|
-
|
|
2250
|
+
const ir = await runChantJson(graphArgs(src, "ir", opts, false), projectDir, envOverridesFor(opts));
|
|
2251
|
+
return dropForeignDeclarations(ir, projectDir);
|
|
2193
2252
|
}
|
|
2194
2253
|
async function clusterRootGraphIr(projectDir, opts = {}) {
|
|
2195
2254
|
if (!existsSync3(join4(projectDir, "cluster"))) return void 0;
|
|
@@ -2230,7 +2289,21 @@ function parseCiPipeline(stdout) {
|
|
|
2230
2289
|
const script = Array.isArray(props?.script) ? props.script.map(String) : [];
|
|
2231
2290
|
return { ...j, script };
|
|
2232
2291
|
});
|
|
2233
|
-
|
|
2292
|
+
const fromJson = typeof parsed.env === "string" && parsed.env ? parsed.env : void 0;
|
|
2293
|
+
const env = fromJson ?? pipelineEnvFromYaml(doc);
|
|
2294
|
+
return { stages: parsed.stages, jobs, ...env ? { env } : {} };
|
|
2295
|
+
}
|
|
2296
|
+
function pipelineEnvFromYaml(doc) {
|
|
2297
|
+
const name = doc?.name;
|
|
2298
|
+
if (typeof name === "string") {
|
|
2299
|
+
const m = /^chant-components-(.+)$/.exec(name);
|
|
2300
|
+
if (m) return m[1];
|
|
2301
|
+
}
|
|
2302
|
+
const env = doc?.env;
|
|
2303
|
+
if (env && typeof env === "object" && typeof env.CHANT_ENV === "string") {
|
|
2304
|
+
return env.CHANT_ENV;
|
|
2305
|
+
}
|
|
2306
|
+
return void 0;
|
|
2234
2307
|
}
|
|
2235
2308
|
function ciPipeline(projectDir, opts = {}, forge = "gitlab") {
|
|
2236
2309
|
const args = ciPipelineArgs(opts, forge);
|
|
@@ -2423,7 +2496,7 @@ function synthesizeHelmReleases(ir, observed, owners) {
|
|
|
2423
2496
|
}
|
|
2424
2497
|
|
|
2425
2498
|
// src/gh-run.ts
|
|
2426
|
-
import { spawn as spawn2 } from "node:child_process";
|
|
2499
|
+
import { execFileSync, spawn as spawn2 } from "node:child_process";
|
|
2427
2500
|
import { readdirSync as readdirSync3, readFileSync as readFileSync6 } from "node:fs";
|
|
2428
2501
|
import { join as join6 } from "node:path";
|
|
2429
2502
|
|
|
@@ -2469,6 +2542,9 @@ function foldPipelineLine(state, line) {
|
|
|
2469
2542
|
}
|
|
2470
2543
|
return out;
|
|
2471
2544
|
}
|
|
2545
|
+
function losePipelineProgress(state) {
|
|
2546
|
+
return { ...state, status: "lost" };
|
|
2547
|
+
}
|
|
2472
2548
|
function finishPipelineProgress(state, exitCode) {
|
|
2473
2549
|
const terminal = exitCode === 0 ? "ok" : "failed";
|
|
2474
2550
|
let out = state;
|
|
@@ -2479,19 +2555,19 @@ function finishPipelineProgress(state, exitCode) {
|
|
|
2479
2555
|
}
|
|
2480
2556
|
|
|
2481
2557
|
// src/gh-run.ts
|
|
2482
|
-
var defaultGhExec = (args) => new Promise((
|
|
2558
|
+
var defaultGhExec = (args) => new Promise((resolve12) => {
|
|
2483
2559
|
let out = "";
|
|
2484
2560
|
let proc;
|
|
2485
2561
|
try {
|
|
2486
2562
|
proc = spawn2("gh", args, { stdio: ["ignore", "pipe", "pipe"] });
|
|
2487
2563
|
} catch {
|
|
2488
|
-
|
|
2564
|
+
resolve12({ code: 127, out: "" });
|
|
2489
2565
|
return;
|
|
2490
2566
|
}
|
|
2491
2567
|
proc.stdout.on("data", (d) => out += d);
|
|
2492
2568
|
proc.stderr.on("data", (d) => out += d);
|
|
2493
|
-
proc.on("error", () =>
|
|
2494
|
-
proc.on("close", (code) =>
|
|
2569
|
+
proc.on("error", () => resolve12({ code: 127, out }));
|
|
2570
|
+
proc.on("close", (code) => resolve12({ code: code ?? 1, out }));
|
|
2495
2571
|
});
|
|
2496
2572
|
async function ghReady(exec = defaultGhExec) {
|
|
2497
2573
|
const { code, out } = await exec(["auth", "status"]);
|
|
@@ -2512,8 +2588,16 @@ function parseWorkflow(file, text) {
|
|
|
2512
2588
|
const dispatchable = on === "workflow_dispatch" || Array.isArray(on) && on.includes("workflow_dispatch") || !!on && typeof on === "object" && "workflow_dispatch" in on || /^\s{2,}workflow_dispatch\s*:?\s*$/m.test(text);
|
|
2513
2589
|
return { file, jobIds: jobs, dispatchable, ...typeof d.name === "string" && d.name ? { name: d.name } : {} };
|
|
2514
2590
|
}
|
|
2591
|
+
function workflowsDir(projectDir) {
|
|
2592
|
+
let root = projectDir;
|
|
2593
|
+
try {
|
|
2594
|
+
root = execFileSync("git", ["-C", projectDir, "rev-parse", "--show-toplevel"], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim() || projectDir;
|
|
2595
|
+
} catch {
|
|
2596
|
+
}
|
|
2597
|
+
return join6(root, ".github", "workflows");
|
|
2598
|
+
}
|
|
2515
2599
|
function committedWorkflows(projectDir) {
|
|
2516
|
-
const dir =
|
|
2600
|
+
const dir = workflowsDir(projectDir);
|
|
2517
2601
|
let files;
|
|
2518
2602
|
try {
|
|
2519
2603
|
files = readdirSync3(dir).filter((f) => f.endsWith(".yml") || f.endsWith(".yaml"));
|
|
@@ -2544,9 +2628,16 @@ function resolveWorkflow(projectDir, pipeline, designated) {
|
|
|
2544
2628
|
if (named.length > 1) return { reason: `${named.length} committed workflows are named ${ciWorkflowName(pipeline.env)} (${named.map((w) => w.file).join(", ")}) \u2014 designate one in .behold.json's executor block` };
|
|
2545
2629
|
}
|
|
2546
2630
|
const wanted = new Set(pipeline.jobs.map((j) => j.jobName));
|
|
2547
|
-
const
|
|
2631
|
+
const otherEnv = (w) => {
|
|
2632
|
+
const m = w.name ? /^chant-components-(.+)$/.exec(w.name) : null;
|
|
2633
|
+
return !!m && !!pipeline.env && m[1] !== pipeline.env;
|
|
2634
|
+
};
|
|
2635
|
+
const scored = dispatchable.filter((w) => !otherEnv(w)).map((w) => ({ w, overlap: w.jobIds.filter((id) => wanted.has(id)).length })).filter((x) => x.overlap > 0).sort((a, b) => b.overlap - a.overlap);
|
|
2548
2636
|
if (scored.length === 0) {
|
|
2549
|
-
|
|
2637
|
+
const excluded = dispatchable.filter(otherEnv).map((w) => `${w.file} (${w.name})`);
|
|
2638
|
+
return {
|
|
2639
|
+
reason: "no committed workflow_dispatch workflow whose jobs match the generated pipeline \u2014 commit one (or add `workflow_dispatch:` to its `on:` block)" + (excluded.length ? `; ${excluded.join(", ")} ${excluded.length === 1 ? "is" : "are"} named for another env and never stand in` : "")
|
|
2640
|
+
};
|
|
2550
2641
|
}
|
|
2551
2642
|
if (scored.length > 1 && scored[1].overlap === scored[0].overlap) {
|
|
2552
2643
|
const tied = scored.filter((x) => x.overlap === scored[0].overlap).map((x) => x.w.file);
|
|
@@ -2592,6 +2683,7 @@ function runViewToProgress(pipeline, view) {
|
|
|
2592
2683
|
}
|
|
2593
2684
|
return state;
|
|
2594
2685
|
}
|
|
2686
|
+
var LOST_EXIT = 75;
|
|
2595
2687
|
async function dispatchAndFollow(workflow, pipeline, ref, deps) {
|
|
2596
2688
|
const exec = deps.exec ?? defaultGhExec;
|
|
2597
2689
|
const pollMs = deps.pollMs ?? 5e3;
|
|
@@ -2620,10 +2712,28 @@ async function dispatchAndFollow(workflow, pipeline, ref, deps) {
|
|
|
2620
2712
|
return 1;
|
|
2621
2713
|
}
|
|
2622
2714
|
deps.onLine(`\u25CF run ${runId} started \u2014 following`);
|
|
2715
|
+
deps.onAdopted?.({ runId });
|
|
2716
|
+
return followRun(runId, pipeline, deps);
|
|
2717
|
+
}
|
|
2718
|
+
async function followRun(runId, pipeline, deps) {
|
|
2719
|
+
const exec = deps.exec ?? defaultGhExec;
|
|
2720
|
+
const pollMs = deps.pollMs ?? 5e3;
|
|
2721
|
+
const sleep = deps.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
|
|
2722
|
+
const pollFailBudget = deps.pollFailBudget ?? 24;
|
|
2723
|
+
const followDeadline = Date.now() + (deps.followTimeoutMs ?? 6 * 60 * 60 * 1e3);
|
|
2623
2724
|
let urlSaid = false;
|
|
2725
|
+
let state = pipelineProgress(pipeline);
|
|
2726
|
+
let consecutiveFailures = 0;
|
|
2727
|
+
const lose = (why) => {
|
|
2728
|
+
deps.onLine(`\u2717 run ${runId}: ${why} \u2014 behold stopped following; the run itself may still be live${state.url ? ` at ${state.url}` : " on the forge"} (restart behold, or POST /api/ci/readopt, to re-adopt it)`);
|
|
2729
|
+
deps.onProgress(losePipelineProgress(state));
|
|
2730
|
+
deps.onConcluded?.({ verdict: "lost" });
|
|
2731
|
+
return LOST_EXIT;
|
|
2732
|
+
};
|
|
2624
2733
|
for (; ; ) {
|
|
2625
2734
|
const { code, out } = await exec(["run", "view", String(runId), "--json", "status,conclusion,jobs,url"]);
|
|
2626
2735
|
if (code === 0) {
|
|
2736
|
+
consecutiveFailures = 0;
|
|
2627
2737
|
let view;
|
|
2628
2738
|
try {
|
|
2629
2739
|
view = JSON.parse(out);
|
|
@@ -2633,16 +2743,27 @@ async function dispatchAndFollow(workflow, pipeline, ref, deps) {
|
|
|
2633
2743
|
if (view) {
|
|
2634
2744
|
if (view.url && !urlSaid) {
|
|
2635
2745
|
urlSaid = true;
|
|
2746
|
+
deps.onAdopted?.({ runId, url: view.url });
|
|
2636
2747
|
deps.onLine(`\u25CF ${view.url} \u2014 if the workflow's environment requires an approval, it is granted there, by a GitHub identity; behold holds none that could`);
|
|
2637
2748
|
}
|
|
2638
|
-
|
|
2749
|
+
state = runViewToProgress(pipeline, view);
|
|
2750
|
+
deps.onProgress(state);
|
|
2639
2751
|
if (view.status === "completed") {
|
|
2640
2752
|
deps.onLine(`\u25A0 run ${runId} ${view.conclusion ?? "completed"}`);
|
|
2641
|
-
|
|
2753
|
+
const ok = view.conclusion === "success";
|
|
2754
|
+
deps.onConcluded?.({ verdict: ok ? "ok" : "failed" });
|
|
2755
|
+
return ok ? 0 : 1;
|
|
2642
2756
|
}
|
|
2643
2757
|
}
|
|
2644
2758
|
} else {
|
|
2645
|
-
|
|
2759
|
+
consecutiveFailures++;
|
|
2760
|
+
deps.onLine(`\u26A0 gh run view ${runId} failed (exit ${code}) \u2014 retrying (${consecutiveFailures}/${pollFailBudget})`);
|
|
2761
|
+
if (consecutiveFailures >= pollFailBudget) {
|
|
2762
|
+
return lose(`${pollFailBudget} consecutive polls failed \u2014 the stream is dead, not flaky`);
|
|
2763
|
+
}
|
|
2764
|
+
}
|
|
2765
|
+
if (Date.now() >= followDeadline) {
|
|
2766
|
+
return lose("the follow deadline passed");
|
|
2646
2767
|
}
|
|
2647
2768
|
await sleep(pollMs);
|
|
2648
2769
|
}
|
|
@@ -2668,6 +2789,45 @@ function joinCiProgress(ir, state) {
|
|
|
2668
2789
|
return ir;
|
|
2669
2790
|
}
|
|
2670
2791
|
|
|
2792
|
+
// src/ci-run-store.ts
|
|
2793
|
+
import { existsSync as existsSync5, mkdirSync as mkdirSync2, readFileSync as readFileSync7, writeFileSync as writeFileSync2, rmSync } from "node:fs";
|
|
2794
|
+
import { homedir as homedir2 } from "node:os";
|
|
2795
|
+
import { join as join7, resolve as resolve4 } from "node:path";
|
|
2796
|
+
function slug(projectDir) {
|
|
2797
|
+
return resolve4(projectDir).replace(/[^A-Za-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(-80);
|
|
2798
|
+
}
|
|
2799
|
+
function defaultRoot() {
|
|
2800
|
+
return process.env.BEHOLD_CI_RUN_DIR ?? join7(homedir2(), ".behold", "ci-runs");
|
|
2801
|
+
}
|
|
2802
|
+
function dispatchedRunPath(projectDir, root = defaultRoot()) {
|
|
2803
|
+
return join7(root, `${slug(projectDir)}.json`);
|
|
2804
|
+
}
|
|
2805
|
+
function saveDispatchedRun(run3, root = defaultRoot()) {
|
|
2806
|
+
try {
|
|
2807
|
+
const path = dispatchedRunPath(run3.projectDir, root);
|
|
2808
|
+
mkdirSync2(join7(path, ".."), { recursive: true });
|
|
2809
|
+
writeFileSync2(path, JSON.stringify({ version: 1, ...run3 }, null, 2) + "\n");
|
|
2810
|
+
} catch {
|
|
2811
|
+
}
|
|
2812
|
+
}
|
|
2813
|
+
function readDispatchedRun(projectDir, root) {
|
|
2814
|
+
try {
|
|
2815
|
+
const path = dispatchedRunPath(projectDir, root);
|
|
2816
|
+
if (!existsSync5(path)) return void 0;
|
|
2817
|
+
const parsed = JSON.parse(readFileSync7(path, "utf8"));
|
|
2818
|
+
if (parsed?.version !== 1 || typeof parsed.runId !== "number") return void 0;
|
|
2819
|
+
if (resolve4(parsed.projectDir) !== resolve4(projectDir)) return void 0;
|
|
2820
|
+
return parsed;
|
|
2821
|
+
} catch {
|
|
2822
|
+
return void 0;
|
|
2823
|
+
}
|
|
2824
|
+
}
|
|
2825
|
+
function concludeDispatchedRun(projectDir, runId, verdict, root) {
|
|
2826
|
+
const current = readDispatchedRun(projectDir, root);
|
|
2827
|
+
if (!current || current.runId !== runId) return;
|
|
2828
|
+
saveDispatchedRun({ ...current, concluded: { verdict, at: (/* @__PURE__ */ new Date()).toISOString() } }, root);
|
|
2829
|
+
}
|
|
2830
|
+
|
|
2671
2831
|
// src/operator.ts
|
|
2672
2832
|
var ANSI = /\x1b\[[0-9;]*m/g;
|
|
2673
2833
|
var strip = (s) => s.replace(ANSI, "");
|
|
@@ -3740,26 +3900,26 @@ function applyHelmArtifacts(ir, observed) {
|
|
|
3740
3900
|
|
|
3741
3901
|
// src/carve-discovery.ts
|
|
3742
3902
|
import { readdirSync as readdirSync5, statSync as statSync4 } from "node:fs";
|
|
3743
|
-
import { join as
|
|
3903
|
+
import { join as join9 } from "node:path";
|
|
3744
3904
|
|
|
3745
3905
|
// src/member-ir.ts
|
|
3746
3906
|
import { createHash } from "node:crypto";
|
|
3747
3907
|
import { readdirSync as readdirSync4, statSync as statSync3 } from "node:fs";
|
|
3748
|
-
import { join as
|
|
3908
|
+
import { join as join8, relative as relative2, resolve as resolve5 } from "node:path";
|
|
3749
3909
|
var SKIP = /* @__PURE__ */ new Set(["node_modules", "dist", ".git"]);
|
|
3750
3910
|
function memberIrCacheSize(env = process.env) {
|
|
3751
3911
|
const override = Number.parseInt(env.BEHOLD_ESTATE_IR_CACHE ?? "", 10);
|
|
3752
3912
|
return Number.isInteger(override) && override >= 0 ? override : 64;
|
|
3753
3913
|
}
|
|
3754
3914
|
function memberSourceStamp(dir) {
|
|
3755
|
-
const root =
|
|
3915
|
+
const root = resolve5(dir);
|
|
3756
3916
|
const h = createHash("sha1");
|
|
3757
3917
|
let any = false;
|
|
3758
3918
|
const walk = (at) => {
|
|
3759
3919
|
const entries = readdirSync4(at, { withFileTypes: true }).sort((a, b) => a.name < b.name ? -1 : 1);
|
|
3760
3920
|
for (const e of entries) {
|
|
3761
3921
|
if (SKIP.has(e.name)) continue;
|
|
3762
|
-
const path =
|
|
3922
|
+
const path = join8(at, e.name);
|
|
3763
3923
|
if (e.isDirectory()) {
|
|
3764
3924
|
walk(path);
|
|
3765
3925
|
continue;
|
|
@@ -3781,7 +3941,7 @@ function memberSourceStamp(dir) {
|
|
|
3781
3941
|
function cacheKey(dir, opts) {
|
|
3782
3942
|
const chant = resolveChant(dir);
|
|
3783
3943
|
const canonical = JSON.stringify(opts, Object.keys(opts).sort());
|
|
3784
|
-
return `${
|
|
3944
|
+
return `${resolve5(dir)}\0${chant.bin}\0${chant.version ?? ""}\0${canonical}`;
|
|
3785
3945
|
}
|
|
3786
3946
|
var cache = /* @__PURE__ */ new Map();
|
|
3787
3947
|
var hits = 0;
|
|
@@ -3791,7 +3951,7 @@ function invalidateMember(dir) {
|
|
|
3791
3951
|
cache.clear();
|
|
3792
3952
|
return;
|
|
3793
3953
|
}
|
|
3794
|
-
const root =
|
|
3954
|
+
const root = resolve5(dir);
|
|
3795
3955
|
for (const [key, entry] of cache) if (entry.dir === root) cache.delete(key);
|
|
3796
3956
|
}
|
|
3797
3957
|
async function memberIr(dir, opts = {}) {
|
|
@@ -3810,7 +3970,7 @@ async function memberIr(dir, opts = {}) {
|
|
|
3810
3970
|
const ir = await graphIr(dir, opts);
|
|
3811
3971
|
if (stamp === void 0 || memberSourceStamp(dir) !== stamp) return ir;
|
|
3812
3972
|
cache.delete(key);
|
|
3813
|
-
cache.set(key, { dir:
|
|
3973
|
+
cache.set(key, { dir: resolve5(dir), stamp, ir });
|
|
3814
3974
|
for (const oldest of cache.keys()) {
|
|
3815
3975
|
if (cache.size <= size) break;
|
|
3816
3976
|
cache.delete(oldest);
|
|
@@ -3838,7 +3998,7 @@ function hasAnyCarveManifest(dir, io = fsIo) {
|
|
|
3838
3998
|
for (const name of entries) {
|
|
3839
3999
|
if (name.endsWith(CARVE_MANIFEST_SUFFIX)) return true;
|
|
3840
4000
|
if (name.startsWith(".") || SKIP2.has(name)) continue;
|
|
3841
|
-
const full =
|
|
4001
|
+
const full = join9(d, name);
|
|
3842
4002
|
if (io.isDirectory(full) && walk(full)) return true;
|
|
3843
4003
|
}
|
|
3844
4004
|
return false;
|
|
@@ -3879,7 +4039,7 @@ async function carveStatesUnder(dirs) {
|
|
|
3879
4039
|
// src/estate.ts
|
|
3880
4040
|
import { statSync as statSync5 } from "node:fs";
|
|
3881
4041
|
import { availableParallelism } from "node:os";
|
|
3882
|
-
import { join as
|
|
4042
|
+
import { join as join10, resolve as resolve6, sep as sep3 } from "node:path";
|
|
3883
4043
|
import { composeStacks, shortStackNames } from "@intentius/pinhole";
|
|
3884
4044
|
|
|
3885
4045
|
// src/zoom-notes.ts
|
|
@@ -4038,11 +4198,11 @@ function declaredSegments(path) {
|
|
|
4038
4198
|
function pathAlignment(declared, dir, isDir = realIsDir) {
|
|
4039
4199
|
const want = declaredSegments(declared);
|
|
4040
4200
|
if (!want) return 0;
|
|
4041
|
-
const root =
|
|
4042
|
-
const have = root.split(
|
|
4201
|
+
const root = resolve6(dir);
|
|
4202
|
+
const have = root.split(sep3).filter(Boolean);
|
|
4043
4203
|
for (let k = Math.min(want.length, have.length); k >= 1; k--) {
|
|
4044
4204
|
if (!have.slice(-k).every((s, i) => s === want[i])) continue;
|
|
4045
|
-
if (!isDir(
|
|
4205
|
+
if (!isDir(join10(root, ...want.slice(k)))) continue;
|
|
4046
4206
|
return k;
|
|
4047
4207
|
}
|
|
4048
4208
|
return 0;
|
|
@@ -4072,10 +4232,10 @@ function joinNamespaceBindings(members, isDir = realIsDir) {
|
|
|
4072
4232
|
if (!winner || tied) continue;
|
|
4073
4233
|
const prev = best.get(winner.dir);
|
|
4074
4234
|
if (prev === "ambiguous") continue;
|
|
4075
|
-
const
|
|
4076
|
-
if (!prev) best.set(winner.dir, { score: winner.score, join:
|
|
4235
|
+
const join23 = { dir: winner.dir, namespace: b.namespace, path: b.path, declaredBy: b.declaredBy };
|
|
4236
|
+
if (!prev) best.set(winner.dir, { score: winner.score, join: join23 });
|
|
4077
4237
|
else if (prev.join.namespace !== b.namespace) best.set(winner.dir, "ambiguous");
|
|
4078
|
-
else if (winner.score > prev.score) best.set(winner.dir, { score: winner.score, join:
|
|
4238
|
+
else if (winner.score > prev.score) best.set(winner.dir, { score: winner.score, join: join23 });
|
|
4079
4239
|
}
|
|
4080
4240
|
return [...best.values()].filter((v) => v !== "ambiguous").map((v) => v.join);
|
|
4081
4241
|
}
|
|
@@ -4793,8 +4953,8 @@ function projectHelmLogical(ir) {
|
|
|
4793
4953
|
}
|
|
4794
4954
|
|
|
4795
4955
|
// src/logical-kustomize.ts
|
|
4796
|
-
import { existsSync as
|
|
4797
|
-
import { join as
|
|
4956
|
+
import { existsSync as existsSync6 } from "node:fs";
|
|
4957
|
+
import { join as join11 } from "node:path";
|
|
4798
4958
|
function overlayBoxTitle(name) {
|
|
4799
4959
|
return `overlay ${name}`;
|
|
4800
4960
|
}
|
|
@@ -4814,7 +4974,7 @@ function kustomizationRoot(bases, dir, exists) {
|
|
|
4814
4974
|
let current = dir;
|
|
4815
4975
|
for (let i = 0; i < 4 && current; i++) {
|
|
4816
4976
|
for (const base of bases) {
|
|
4817
|
-
if (exists(
|
|
4977
|
+
if (exists(join11(base, current, "kustomization.yaml")) || exists(join11(base, current, "kustomization.yml"))) {
|
|
4818
4978
|
return current;
|
|
4819
4979
|
}
|
|
4820
4980
|
}
|
|
@@ -4823,7 +4983,7 @@ function kustomizationRoot(bases, dir, exists) {
|
|
|
4823
4983
|
}
|
|
4824
4984
|
return void 0;
|
|
4825
4985
|
}
|
|
4826
|
-
function projectKustomizeLogical(ir, sourceRoots, exists =
|
|
4986
|
+
function projectKustomizeLogical(ir, sourceRoots, exists = existsSync6) {
|
|
4827
4987
|
const bases = Array.isArray(sourceRoots) ? sourceRoots : [sourceRoots];
|
|
4828
4988
|
const k8s = ir.nodes.filter((n) => n.lexicon === "k8s");
|
|
4829
4989
|
if (k8s.length === 0) return { ir: { nodes: [], edges: [], groups: {} }, byContainer: {} };
|
|
@@ -5545,10 +5705,10 @@ import {
|
|
|
5545
5705
|
} from "@intentius/pinhole";
|
|
5546
5706
|
|
|
5547
5707
|
// src/icon-packs.ts
|
|
5548
|
-
import { readFileSync as
|
|
5549
|
-
import { dirname as dirname4, join as
|
|
5708
|
+
import { readFileSync as readFileSync8 } from "node:fs";
|
|
5709
|
+
import { dirname as dirname4, join as join12 } from "node:path";
|
|
5550
5710
|
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
5551
|
-
var ICON_ROOT =
|
|
5711
|
+
var ICON_ROOT = join12(dirname4(fileURLToPath2(import.meta.url)), "..", "web", "icons");
|
|
5552
5712
|
var cache3 = /* @__PURE__ */ new Map();
|
|
5553
5713
|
function unprefix(body) {
|
|
5554
5714
|
return body.replace(/\s(?:inkscape|sodipodi):[\w-]+\s*=\s*(["'])[\s\S]*?\1/g, "").replace(/\sxlink:href\s*=/g, " href=");
|
|
@@ -5570,7 +5730,7 @@ function plate(glyph) {
|
|
|
5570
5730
|
function loadIcon(rel) {
|
|
5571
5731
|
const hit = cache3.get(rel);
|
|
5572
5732
|
if (hit) return hit;
|
|
5573
|
-
const raw =
|
|
5733
|
+
const raw = readFileSync8(join12(ICON_ROOT, `${rel}.svg`), "utf8");
|
|
5574
5734
|
const open = raw.indexOf("<svg");
|
|
5575
5735
|
const openEnd = raw.indexOf(">", open);
|
|
5576
5736
|
const close = raw.lastIndexOf("</svg");
|
|
@@ -6735,12 +6895,12 @@ function radializeLayout(layout, groupOf, size = /* @__PURE__ */ new Map()) {
|
|
|
6735
6895
|
}
|
|
6736
6896
|
|
|
6737
6897
|
// src/carve-actions.ts
|
|
6738
|
-
import { existsSync as
|
|
6739
|
-
import { isAbsolute as
|
|
6898
|
+
import { existsSync as existsSync9, mkdirSync as mkdirSync4, readFileSync as readFileSync11, readdirSync as readdirSync6, statSync as statSync6 } from "node:fs";
|
|
6899
|
+
import { isAbsolute as isAbsolute3, join as join15, relative as relative3, resolve as resolve7, sep as sep4 } from "node:path";
|
|
6740
6900
|
|
|
6741
6901
|
// src/layout.ts
|
|
6742
|
-
import { existsSync as
|
|
6743
|
-
import { join as
|
|
6902
|
+
import { existsSync as existsSync7, mkdirSync as mkdirSync3, readFileSync as readFileSync9, renameSync, rmSync as rmSync2, writeFileSync as writeFileSync3, accessSync, constants } from "node:fs";
|
|
6903
|
+
import { join as join13 } from "node:path";
|
|
6744
6904
|
var LAYOUT_DIR = ".behold";
|
|
6745
6905
|
var LAYOUT_FILE = "layout.json";
|
|
6746
6906
|
var MAX_BODY_BYTES = 64 * 1024;
|
|
@@ -6750,12 +6910,12 @@ var MAX_LENSES = 64;
|
|
|
6750
6910
|
var MAX_ID_LENGTH = 512;
|
|
6751
6911
|
var MAX_LENS_LENGTH = 128;
|
|
6752
6912
|
var NUM = ["dx", "dy", "dw", "dh"];
|
|
6753
|
-
function
|
|
6913
|
+
function slug2(s) {
|
|
6754
6914
|
return String(s ?? "").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "unknown";
|
|
6755
6915
|
}
|
|
6756
6916
|
function normalizeLens(raw) {
|
|
6757
6917
|
if (typeof raw !== "string" || !raw.trim() || raw.length > MAX_LENS_LENGTH) return null;
|
|
6758
|
-
const parts = raw.split("+").map((p) =>
|
|
6918
|
+
const parts = raw.split("+").map((p) => slug2(p)).filter((p) => p && p !== "unknown");
|
|
6759
6919
|
if (!parts.length) return null;
|
|
6760
6920
|
const key = parts.join("+");
|
|
6761
6921
|
return key === "__proto__" || key === "constructor" || key === "prototype" ? null : key;
|
|
@@ -6778,13 +6938,13 @@ function normalizeDeltas(raw) {
|
|
|
6778
6938
|
function lensFromQuery(params) {
|
|
6779
6939
|
const zoom = params.get("components") === "1" ? "components" : params.get("logical") === "1" ? "logical" : params.get("runtime") === "1" ? "runtime" : { 1: "composites", 2: "resources", 3: "attributes" }[Number(params.get("detail") ?? 2)] ?? "resources";
|
|
6780
6940
|
const stack = params.get("stack");
|
|
6781
|
-
return [
|
|
6941
|
+
return [slug2(zoom), params.get("radial") === "1" ? "radial" : null, stack ? slug2(`stack-${stack}`) : null].filter(Boolean).join("+");
|
|
6782
6942
|
}
|
|
6783
6943
|
function layoutPath(projectDir) {
|
|
6784
|
-
return
|
|
6944
|
+
return join13(projectDir, LAYOUT_DIR, LAYOUT_FILE);
|
|
6785
6945
|
}
|
|
6786
6946
|
function unwritableReason(projectDir) {
|
|
6787
|
-
if (!projectDir || !
|
|
6947
|
+
if (!projectDir || !existsSync7(projectDir)) return `no such project directory: ${projectDir || "(none)"}`;
|
|
6788
6948
|
try {
|
|
6789
6949
|
accessSync(projectDir, constants.W_OK);
|
|
6790
6950
|
} catch {
|
|
@@ -6796,8 +6956,8 @@ function readLayoutFile(projectDir) {
|
|
|
6796
6956
|
const empty = { version: 1, lenses: {} };
|
|
6797
6957
|
const file = layoutPath(projectDir);
|
|
6798
6958
|
try {
|
|
6799
|
-
if (!
|
|
6800
|
-
const raw =
|
|
6959
|
+
if (!existsSync7(file)) return empty;
|
|
6960
|
+
const raw = readFileSync9(file, "utf8");
|
|
6801
6961
|
if (raw.length > MAX_FILE_BYTES) return empty;
|
|
6802
6962
|
const parsed = JSON.parse(raw);
|
|
6803
6963
|
if (!parsed || typeof parsed !== "object" || !parsed.lenses || typeof parsed.lenses !== "object") return empty;
|
|
@@ -6832,14 +6992,14 @@ function writeLens(projectDir, lens, deltas) {
|
|
|
6832
6992
|
if (Object.keys(lenses).length > MAX_LENSES) throw new LayoutTooLarge(`a layout sidecar holds at most ${MAX_LENSES} lenses`);
|
|
6833
6993
|
const file = layoutPath(projectDir);
|
|
6834
6994
|
if (!Object.keys(lenses).length) {
|
|
6835
|
-
|
|
6995
|
+
rmSync2(file, { force: true });
|
|
6836
6996
|
return { lens: key, deltas: clean, bytes: 0 };
|
|
6837
6997
|
}
|
|
6838
6998
|
const body = JSON.stringify({ version: 1, lenses }, null, 2) + "\n";
|
|
6839
6999
|
if (body.length > MAX_FILE_BYTES) throw new LayoutTooLarge(`a layout sidecar is capped at ${MAX_FILE_BYTES} bytes`);
|
|
6840
|
-
|
|
7000
|
+
mkdirSync3(join13(projectDir, LAYOUT_DIR), { recursive: true });
|
|
6841
7001
|
const tmp = `${file}.tmp`;
|
|
6842
|
-
|
|
7002
|
+
writeFileSync3(tmp, body, "utf8");
|
|
6843
7003
|
renameSync(tmp, file);
|
|
6844
7004
|
return { lens: key, deltas: clean, bytes: body.length };
|
|
6845
7005
|
}
|
|
@@ -6922,8 +7082,8 @@ function reanchorEdges(svg, moved) {
|
|
|
6922
7082
|
|
|
6923
7083
|
// src/carve-live.ts
|
|
6924
7084
|
import { spawn as spawn3 } from "node:child_process";
|
|
6925
|
-
import { readFileSync as
|
|
6926
|
-
import { join as
|
|
7085
|
+
import { readFileSync as readFileSync10, writeFileSync as writeFileSync4, existsSync as existsSync8 } from "node:fs";
|
|
7086
|
+
import { join as join14 } from "node:path";
|
|
6927
7087
|
|
|
6928
7088
|
// src/scratch.ts
|
|
6929
7089
|
var PROTECTED_INFRA = [
|
|
@@ -7011,9 +7171,9 @@ async function teardownScratchFloci() {
|
|
|
7011
7171
|
await capture("docker", ["rm", "-f", LIVE_CONTAINER], process.cwd()).catch(() => void 0);
|
|
7012
7172
|
}
|
|
7013
7173
|
async function applyIntoFloci(fromDir, step) {
|
|
7014
|
-
const template =
|
|
7015
|
-
if (!
|
|
7016
|
-
|
|
7174
|
+
const template = join14(fromDir, "floci-override.tf.disabled");
|
|
7175
|
+
if (!existsSync8(template)) return `no floci-override.tf.disabled in ${fromDir} \u2014 this estate wasn't authored for the live tier`;
|
|
7176
|
+
writeFileSync4(join14(fromDir, "floci_override.tf"), armOverride(readFileSync10(template, "utf8"), LIVE_PORT));
|
|
7017
7177
|
const init = await step("terraform", ["init", "-input=false", "-no-color"], fromDir);
|
|
7018
7178
|
if (init !== 0) return `terraform init exited ${init} (provider downloads need network)`;
|
|
7019
7179
|
const apply = await step(
|
|
@@ -7063,7 +7223,7 @@ async function runCarveObserve(demo, select) {
|
|
|
7063
7223
|
"run `behold demo carve --live` (needs docker + terraform)"
|
|
7064
7224
|
);
|
|
7065
7225
|
}
|
|
7066
|
-
if (!
|
|
7226
|
+
if (!existsSync9(join15(demo.out, "src"))) {
|
|
7067
7227
|
return refuse3("carve-action", "nothing has been emitted yet", "run Emit first \u2014 observe reads the carveout emit writes");
|
|
7068
7228
|
}
|
|
7069
7229
|
const args = ["lifecycle", "diff", "prod", "--live", "--json"];
|
|
@@ -7133,8 +7293,8 @@ var refuse3 = (code, error, remedy) => ({
|
|
|
7133
7293
|
});
|
|
7134
7294
|
var BUILD_CAVEAT = "The gate shown is `chant lint`, not `chant build`. chant#1637's fold is applied now: the bucket's versioning and public-access-block sub-resources come out as native props, not just folded-in mentions. `chant build` still fails, but on one rule only \u2014 WAW042, a TLS-deny bucket policy the source Terraform never declared. That's real drift the estate inherited, not a gap in the carve. example-carve/README.md, \"Why build still fails\", has the full statement.";
|
|
7135
7295
|
function insideDemo(root, p) {
|
|
7136
|
-
const rel = relative3(
|
|
7137
|
-
return rel !== "" && !rel.startsWith("..") && !
|
|
7296
|
+
const rel = relative3(resolve7(root), resolve7(p));
|
|
7297
|
+
return rel !== "" && !rel.startsWith("..") && !isAbsolute3(rel);
|
|
7138
7298
|
}
|
|
7139
7299
|
function selectFromReport(report, raw) {
|
|
7140
7300
|
if (typeof raw !== "string" || !raw || raw.length > 512) return null;
|
|
@@ -7144,7 +7304,7 @@ function carveWriteBlock(demo) {
|
|
|
7144
7304
|
if (!demo) {
|
|
7145
7305
|
return "this server isn't running a carve demo \u2014 the carve actions only exist inside a `behold demo carve` copy";
|
|
7146
7306
|
}
|
|
7147
|
-
if (!
|
|
7307
|
+
if (!existsSync9(demo.from)) return `the demo copy has no Terraform estate at ${demo.from}`;
|
|
7148
7308
|
if (!insideDemo(demo.root, demo.out)) return "the carve output directory is outside the demo copy";
|
|
7149
7309
|
return unwritableReason(demo.root);
|
|
7150
7310
|
}
|
|
@@ -7161,7 +7321,7 @@ function readArtifacts(demo, dir, filter) {
|
|
|
7161
7321
|
for (const name of entries) {
|
|
7162
7322
|
if (out.length >= MAX_ARTIFACTS) return;
|
|
7163
7323
|
if (name === "node_modules" || name.startsWith(".")) continue;
|
|
7164
|
-
const full =
|
|
7324
|
+
const full = join15(d, name);
|
|
7165
7325
|
let st;
|
|
7166
7326
|
try {
|
|
7167
7327
|
st = statSync6(full);
|
|
@@ -7172,11 +7332,11 @@ function readArtifacts(demo, dir, filter) {
|
|
|
7172
7332
|
walk(full, depth + 1);
|
|
7173
7333
|
continue;
|
|
7174
7334
|
}
|
|
7175
|
-
const rel = relative3(demo.root, full).split(
|
|
7335
|
+
const rel = relative3(demo.root, full).split(sep4).join("/");
|
|
7176
7336
|
if (!filter(rel)) continue;
|
|
7177
7337
|
let text = "";
|
|
7178
7338
|
try {
|
|
7179
|
-
text =
|
|
7339
|
+
text = readFileSync11(full, "utf8");
|
|
7180
7340
|
} catch {
|
|
7181
7341
|
continue;
|
|
7182
7342
|
}
|
|
@@ -7198,17 +7358,17 @@ function carveSlug(select) {
|
|
|
7198
7358
|
}
|
|
7199
7359
|
var clip = (s) => s.length > MAX_OUTPUT_BYTES ? s.slice(0, MAX_OUTPUT_BYTES) + "\n\u2026 (truncated)" : s;
|
|
7200
7360
|
function shortenIn(arg, root) {
|
|
7201
|
-
return arg.startsWith(root +
|
|
7361
|
+
return arg.startsWith(root + sep4) ? arg.slice(root.length + 1) : arg === root ? "." : arg;
|
|
7202
7362
|
}
|
|
7203
7363
|
function merge(r, root) {
|
|
7204
7364
|
const joined = stripAnsi([r.stdout, r.stderr].filter((s) => s.trim()).join("\n").trim());
|
|
7205
|
-
return clip(joined.split(root +
|
|
7365
|
+
return clip(joined.split(root + sep4).join("").split(root).join("."));
|
|
7206
7366
|
}
|
|
7207
7367
|
async function runCarveEmit(demo, select) {
|
|
7208
7368
|
const block = carveWriteBlock(demo);
|
|
7209
7369
|
if (block) return refuse3("read-only", block, "Start the walkthrough with `behold demo carve`.");
|
|
7210
|
-
|
|
7211
|
-
const reportFile =
|
|
7370
|
+
mkdirSync4(demo.out, { recursive: true });
|
|
7371
|
+
const reportFile = join15(demo.out, `${carveSlug(select)}-boundary.json`);
|
|
7212
7372
|
const args = [
|
|
7213
7373
|
"carve",
|
|
7214
7374
|
"emit",
|
|
@@ -7237,10 +7397,10 @@ async function runCarveEmit(demo, select) {
|
|
|
7237
7397
|
}
|
|
7238
7398
|
let boundary = null;
|
|
7239
7399
|
try {
|
|
7240
|
-
boundary = JSON.parse(
|
|
7400
|
+
boundary = JSON.parse(readFileSync11(reportFile, "utf8"));
|
|
7241
7401
|
} catch {
|
|
7242
7402
|
}
|
|
7243
|
-
const lintPath = relative3(demo.project,
|
|
7403
|
+
const lintPath = relative3(demo.project, join15(demo.out, "src")).split(sep4).join("/");
|
|
7244
7404
|
const lintArgs = ["lint", lintPath];
|
|
7245
7405
|
const lint = await runChantRaw(lintArgs, demo.project).catch((err) => ({
|
|
7246
7406
|
code: 127,
|
|
@@ -7265,7 +7425,7 @@ async function runCarveEmit(demo, select) {
|
|
|
7265
7425
|
async function runCarveBridge(demo, select) {
|
|
7266
7426
|
const block = carveWriteBlock(demo);
|
|
7267
7427
|
if (block) return refuse3("read-only", block, "Start the walkthrough with `behold demo carve`.");
|
|
7268
|
-
|
|
7428
|
+
mkdirSync4(demo.out, { recursive: true });
|
|
7269
7429
|
const args = [
|
|
7270
7430
|
"carve",
|
|
7271
7431
|
"bridge",
|
|
@@ -7290,21 +7450,21 @@ async function runCarveBridge(demo, select) {
|
|
|
7290
7450
|
"Run the Emit step first \u2014 bridge reads the carve manifest emit leaves in the output directory."
|
|
7291
7451
|
);
|
|
7292
7452
|
}
|
|
7293
|
-
const
|
|
7453
|
+
const slug4 = carveSlug(select);
|
|
7294
7454
|
const all = readArtifacts(demo, demo.out, (rel) => /\.(tf|md|patch)$/.test(rel));
|
|
7295
7455
|
return {
|
|
7296
7456
|
ok: true,
|
|
7297
7457
|
select,
|
|
7298
7458
|
command: `chant ${args.map((a) => shortenIn(a, demo.root)).join(" ")}`,
|
|
7299
7459
|
output,
|
|
7300
|
-
runbook: all.find((a) => a.path.endsWith(`${
|
|
7301
|
-
proposals: all.filter((a) => !a.path.endsWith(`${
|
|
7460
|
+
runbook: all.find((a) => a.path.endsWith(`${slug4}-runbook.md`)) ?? null,
|
|
7461
|
+
proposals: all.filter((a) => !a.path.endsWith(`${slug4}-runbook.md`))
|
|
7302
7462
|
};
|
|
7303
7463
|
}
|
|
7304
7464
|
|
|
7305
7465
|
// src/ops.ts
|
|
7306
|
-
import { readdirSync as readdirSync7, readFileSync as
|
|
7307
|
-
import { join as
|
|
7466
|
+
import { readdirSync as readdirSync7, readFileSync as readFileSync12, existsSync as existsSync10 } from "node:fs";
|
|
7467
|
+
import { join as join16 } from "node:path";
|
|
7308
7468
|
var APPLY_TARGET_LEXICON = {
|
|
7309
7469
|
cloudformation: "aws",
|
|
7310
7470
|
kubectl: "k8s",
|
|
@@ -7326,11 +7486,11 @@ function discoverOps(projectDir) {
|
|
|
7326
7486
|
const seen = /* @__PURE__ */ new Set();
|
|
7327
7487
|
const out = [];
|
|
7328
7488
|
for (const sub of ["ops", "src", "."]) {
|
|
7329
|
-
const dir =
|
|
7330
|
-
if (!
|
|
7489
|
+
const dir = join16(projectDir, sub);
|
|
7490
|
+
if (!existsSync10(dir)) continue;
|
|
7331
7491
|
for (const f of readdirSync7(dir)) {
|
|
7332
7492
|
if (!f.endsWith(".op.ts")) continue;
|
|
7333
|
-
const content =
|
|
7493
|
+
const content = readFileSync12(join16(dir, f), "utf8");
|
|
7334
7494
|
const name = content.match(/name:\s*["'`]([^"'`]+)["'`]/)?.[1];
|
|
7335
7495
|
if (!name || seen.has(name)) continue;
|
|
7336
7496
|
seen.add(name);
|
|
@@ -7346,25 +7506,25 @@ function discoverOps(projectDir) {
|
|
|
7346
7506
|
...gate ? { gate } : {},
|
|
7347
7507
|
...env ? { env } : {},
|
|
7348
7508
|
...substrate ? { substrate } : {},
|
|
7349
|
-
...
|
|
7509
|
+
...existsSync10(irPath) ? { irPath } : {}
|
|
7350
7510
|
});
|
|
7351
7511
|
}
|
|
7352
7512
|
}
|
|
7353
7513
|
return out.sort((a, b) => a.name.localeCompare(b.name));
|
|
7354
7514
|
}
|
|
7355
7515
|
function opIrPath(projectDir, name) {
|
|
7356
|
-
return
|
|
7516
|
+
return join16(projectDir, "dist", "ops", name, "op.json");
|
|
7357
7517
|
}
|
|
7358
7518
|
function discoverOpIrs(projectDirs) {
|
|
7359
7519
|
const seen = /* @__PURE__ */ new Set();
|
|
7360
7520
|
const out = [];
|
|
7361
7521
|
for (const dir of projectDirs) {
|
|
7362
|
-
const root =
|
|
7363
|
-
if (!
|
|
7522
|
+
const root = join16(dir, "dist", "ops");
|
|
7523
|
+
if (!existsSync10(root)) continue;
|
|
7364
7524
|
for (const name of readdirSync7(root)) {
|
|
7365
7525
|
if (seen.has(name)) continue;
|
|
7366
7526
|
const path = opIrPath(dir, name);
|
|
7367
|
-
if (!
|
|
7527
|
+
if (!existsSync10(path)) continue;
|
|
7368
7528
|
seen.add(name);
|
|
7369
7529
|
out.push({ name, dir, path });
|
|
7370
7530
|
}
|
|
@@ -7599,11 +7759,11 @@ var RAN = { ok: "ok", fail: "failed", skipped: "skipped" };
|
|
|
7599
7759
|
function tookLabel(ms) {
|
|
7600
7760
|
return ms < 1e3 ? `${ms}ms` : `${(ms / 1e3).toFixed(1)}s`;
|
|
7601
7761
|
}
|
|
7602
|
-
function paintPlayhead(ir,
|
|
7603
|
-
if (!
|
|
7762
|
+
function paintPlayhead(ir, join23, gate) {
|
|
7763
|
+
if (!join23.byNode.size && !join23.at) return ir;
|
|
7604
7764
|
const nodes = ir.nodes.map((node) => {
|
|
7605
|
-
const run3 =
|
|
7606
|
-
const here =
|
|
7765
|
+
const run3 = join23.byNode.get(node.id);
|
|
7766
|
+
const here = join23.at === node.id;
|
|
7607
7767
|
if (!run3 && !here) return node;
|
|
7608
7768
|
const isGate = node.attrs?._step === "gate";
|
|
7609
7769
|
const attrs = { ...node.attrs };
|
|
@@ -7618,7 +7778,7 @@ function paintPlayhead(ir, join22, gate) {
|
|
|
7618
7778
|
if (here) {
|
|
7619
7779
|
attrs._playhead = true;
|
|
7620
7780
|
attrs._status = isGate && gate ? "warn" : "accent";
|
|
7621
|
-
attrs.run = isGate && gate ? `waiting for approval since ${gate.since}` :
|
|
7781
|
+
attrs.run = isGate && gate ? `waiting for approval since ${gate.since}` : join23.atReason ?? "here";
|
|
7622
7782
|
if (isGate && gate?.description) attrs.gateDescription = gate.description;
|
|
7623
7783
|
}
|
|
7624
7784
|
return { ...node, attrs };
|
|
@@ -7640,11 +7800,11 @@ function pendingGateCard(op, gate, ir) {
|
|
|
7640
7800
|
}
|
|
7641
7801
|
};
|
|
7642
7802
|
}
|
|
7643
|
-
function playheadNote(state,
|
|
7803
|
+
function playheadNote(state, join23) {
|
|
7644
7804
|
if (!state.op) {
|
|
7645
7805
|
return "No run state: nothing has been run from this behold session, so every step paints as declared.";
|
|
7646
7806
|
}
|
|
7647
|
-
const settled =
|
|
7807
|
+
const settled = join23 ? join23.byNode.size : state.records.length;
|
|
7648
7808
|
const bits = [];
|
|
7649
7809
|
const head = state.status === "idle" ? `Run state for ${state.op}: no per-step records \u2014 this behold session didn't start the run, so only the gate query answers` : `Run: ${state.op} (${state.mode === "temporal" ? "durable" : "local executor"}) \u2014 ${settled} step${settled === 1 ? "" : "s"} settled`;
|
|
7650
7810
|
if (state.status === "running") {
|
|
@@ -7660,9 +7820,9 @@ function playheadNote(state, join22) {
|
|
|
7660
7820
|
} else if (state.gateNote) {
|
|
7661
7821
|
bits.push(`gate state unavailable \u2014 ${state.gateNote}`);
|
|
7662
7822
|
}
|
|
7663
|
-
if (
|
|
7823
|
+
if (join23?.unplaced.length) {
|
|
7664
7824
|
bits.push(
|
|
7665
|
-
`${
|
|
7825
|
+
`${join23.unplaced.length} record${join23.unplaced.length === 1 ? "" : "s"} matched no declared step and ${join23.unplaced.length === 1 ? "is" : "are"} not painted`
|
|
7666
7826
|
);
|
|
7667
7827
|
}
|
|
7668
7828
|
if (!bits.length) return `${head}.`;
|
|
@@ -8328,24 +8488,24 @@ var OpRunner = class {
|
|
|
8328
8488
|
|
|
8329
8489
|
// src/substrates.ts
|
|
8330
8490
|
import { spawn as spawn4 } from "node:child_process";
|
|
8331
|
-
import { existsSync as
|
|
8332
|
-
import { join as
|
|
8491
|
+
import { existsSync as existsSync11, readFileSync as readFileSync13 } from "node:fs";
|
|
8492
|
+
import { join as join17 } from "node:path";
|
|
8333
8493
|
import { platform } from "node:os";
|
|
8334
8494
|
var DOCKER_DOWN_DETAIL = "docker is down";
|
|
8335
8495
|
function probe(cmd, args) {
|
|
8336
|
-
return new Promise((
|
|
8496
|
+
return new Promise((resolve12) => {
|
|
8337
8497
|
let out = "";
|
|
8338
8498
|
let proc;
|
|
8339
8499
|
try {
|
|
8340
8500
|
proc = spawn4(cmd, args, { stdio: ["ignore", "pipe", "pipe"] });
|
|
8341
8501
|
} catch {
|
|
8342
|
-
|
|
8502
|
+
resolve12({ code: 127, out: "" });
|
|
8343
8503
|
return;
|
|
8344
8504
|
}
|
|
8345
8505
|
proc.stdout.on("data", (d) => out += d);
|
|
8346
8506
|
proc.stderr.on("data", (d) => out += d);
|
|
8347
|
-
proc.on("error", () =>
|
|
8348
|
-
proc.on("close", (code) =>
|
|
8507
|
+
proc.on("error", () => resolve12({ code: 127, out }));
|
|
8508
|
+
proc.on("close", (code) => resolve12({ code: code ?? 1, out }));
|
|
8349
8509
|
});
|
|
8350
8510
|
}
|
|
8351
8511
|
async function dockerAvailable(run3) {
|
|
@@ -8358,11 +8518,11 @@ async function dockerRunning(run3, nameFilter) {
|
|
|
8358
8518
|
return out.split(/\r?\n/).map((s) => s.trim()).filter(Boolean);
|
|
8359
8519
|
}
|
|
8360
8520
|
function scriptBringUp(projectDir, relPath, label) {
|
|
8361
|
-
return
|
|
8521
|
+
return existsSync11(join17(projectDir, relPath)) ? { label, cmd: "bash", args: [relPath] } : void 0;
|
|
8362
8522
|
}
|
|
8363
8523
|
function projectLexicons(projectDir) {
|
|
8364
8524
|
try {
|
|
8365
|
-
const src =
|
|
8525
|
+
const src = readFileSync13(join17(projectDir, "chant.config.ts"), "utf-8");
|
|
8366
8526
|
const m = src.match(/lexicons\s*:\s*\[([^\]]*)\]/);
|
|
8367
8527
|
if (!m) return [];
|
|
8368
8528
|
return [...m[1].matchAll(/["']([^"']+)["']/g)].map((x) => x[1]);
|
|
@@ -8414,7 +8574,7 @@ async function detectSubstrates(projectDir, preview = false, boundContext, probe
|
|
|
8414
8574
|
["forgejo", "Forgejo", ".forgejo", "test/forgejo-runtime-e2e.sh"]
|
|
8415
8575
|
];
|
|
8416
8576
|
for (const [name, label, marker, script] of forges) {
|
|
8417
|
-
if (!
|
|
8577
|
+
if (!existsSync11(join17(projectDir, marker))) continue;
|
|
8418
8578
|
const c = docker ? await dockerRunning(run3, name) : [];
|
|
8419
8579
|
const d = dep(c.length > 0, "container up", "on-demand (pipeline run)");
|
|
8420
8580
|
subs.push({
|
|
@@ -8448,7 +8608,7 @@ async function detectSubstrates(projectDir, preview = false, boundContext, probe
|
|
|
8448
8608
|
detail: endpoint ? `targeting ${endpoint}` : "real Fly (FLY_FLAPS_BASE_URL unset)"
|
|
8449
8609
|
});
|
|
8450
8610
|
}
|
|
8451
|
-
if (
|
|
8611
|
+
if (existsSync11(join17(projectDir, ".github", "workflows"))) {
|
|
8452
8612
|
const gh = await run3("gh", ["auth", "status"]);
|
|
8453
8613
|
const ready = gh.code === 0;
|
|
8454
8614
|
subs.push({
|
|
@@ -8461,7 +8621,7 @@ async function detectSubstrates(projectDir, preview = false, boundContext, probe
|
|
|
8461
8621
|
if (lexicons.includes("temporal")) {
|
|
8462
8622
|
let hasProfiles = false;
|
|
8463
8623
|
try {
|
|
8464
|
-
hasProfiles = /temporal\s*:\s*\{[\s\S]{0,400}?profiles\s*:/.test(
|
|
8624
|
+
hasProfiles = /temporal\s*:\s*\{[\s\S]{0,400}?profiles\s*:/.test(readFileSync13(join17(projectDir, "chant.config.ts"), "utf-8"));
|
|
8465
8625
|
} catch {
|
|
8466
8626
|
}
|
|
8467
8627
|
subs.push({
|
|
@@ -8669,8 +8829,8 @@ function addEstateMemberEdges(ir, members, isDir = realIsDir2) {
|
|
|
8669
8829
|
}
|
|
8670
8830
|
|
|
8671
8831
|
// src/events.ts
|
|
8672
|
-
import { watch, existsSync as
|
|
8673
|
-
import { join as
|
|
8832
|
+
import { watch, existsSync as existsSync12 } from "node:fs";
|
|
8833
|
+
import { join as join18 } from "node:path";
|
|
8674
8834
|
var Broadcaster = class {
|
|
8675
8835
|
listeners = /* @__PURE__ */ new Set();
|
|
8676
8836
|
subscribe(fn) {
|
|
@@ -8688,7 +8848,7 @@ var Broadcaster = class {
|
|
|
8688
8848
|
};
|
|
8689
8849
|
var IGNORE = /(^|[\\/])(node_modules|dist|\.git)([\\/]|$)/;
|
|
8690
8850
|
function watchSource(projectDir, onChange, debounceMs = 200) {
|
|
8691
|
-
const dir =
|
|
8851
|
+
const dir = existsSync12(join18(projectDir, "src")) ? join18(projectDir, "src") : projectDir;
|
|
8692
8852
|
let timer;
|
|
8693
8853
|
const watcher = watch(dir, { recursive: true }, (_event, file) => {
|
|
8694
8854
|
const name = typeof file === "string" ? file : "";
|
|
@@ -8976,13 +9136,13 @@ async function emulatorDown(projectDir) {
|
|
|
8976
9136
|
}
|
|
8977
9137
|
|
|
8978
9138
|
// src/demos.ts
|
|
8979
|
-
import { readFileSync as
|
|
8980
|
-
import { join as
|
|
9139
|
+
import { readFileSync as readFileSync14, existsSync as existsSync13, cpSync } from "node:fs";
|
|
9140
|
+
import { join as join19, relative as relative4, resolve as resolve8, sep as sep5 } from "node:path";
|
|
8981
9141
|
import { spawn as spawn5, spawnSync } from "node:child_process";
|
|
8982
9142
|
function loadDemoRegistry(pkgRoot2) {
|
|
8983
9143
|
let raw;
|
|
8984
9144
|
try {
|
|
8985
|
-
raw = JSON.parse(
|
|
9145
|
+
raw = JSON.parse(readFileSync14(join19(pkgRoot2, "demos.json"), "utf8"));
|
|
8986
9146
|
} catch {
|
|
8987
9147
|
return [];
|
|
8988
9148
|
}
|
|
@@ -9022,22 +9182,22 @@ function fetchesFromNetwork(entry) {
|
|
|
9022
9182
|
return entry.source === "git";
|
|
9023
9183
|
}
|
|
9024
9184
|
function demoTargetDir(entry, cwd = process.cwd()) {
|
|
9025
|
-
const legacy =
|
|
9026
|
-
if (entry.name === "writes" &&
|
|
9027
|
-
return
|
|
9185
|
+
const legacy = resolve8(cwd, "behold-demo");
|
|
9186
|
+
if (entry.name === "writes" && existsSync13(legacy)) return legacy;
|
|
9187
|
+
return resolve8(cwd, "behold-demos", entry.name);
|
|
9028
9188
|
}
|
|
9029
9189
|
async function loadDemo(entry, opts) {
|
|
9030
9190
|
const { pkgRoot: pkgRoot2, target } = opts;
|
|
9031
9191
|
const say = (line) => opts.log?.(`behold demo ${entry.name} \u2192 ${line}`);
|
|
9032
|
-
if (!
|
|
9192
|
+
if (!existsSync13(target)) {
|
|
9033
9193
|
if (entry.source === "bundled") {
|
|
9034
|
-
const bundled =
|
|
9035
|
-
if (!
|
|
9194
|
+
const bundled = join19(pkgRoot2, entry.dir);
|
|
9195
|
+
if (!existsSync13(bundled)) return { ok: false, error: `this install has no bundled ${entry.dir}` };
|
|
9036
9196
|
say(`copying to ${target} (it's yours \u2014 edit it)`);
|
|
9037
9197
|
try {
|
|
9038
9198
|
cpSync(bundled, target, {
|
|
9039
9199
|
recursive: true,
|
|
9040
|
-
filter: (src) => !relative4(bundled, src).split(
|
|
9200
|
+
filter: (src) => !relative4(bundled, src).split(sep5).includes("node_modules")
|
|
9041
9201
|
});
|
|
9042
9202
|
} catch (err) {
|
|
9043
9203
|
return { ok: false, error: `copy failed: ${err instanceof Error ? err.message : String(err)}` };
|
|
@@ -9051,7 +9211,7 @@ async function loadDemo(entry, opts) {
|
|
|
9051
9211
|
} else {
|
|
9052
9212
|
say(`reusing ${target}`);
|
|
9053
9213
|
}
|
|
9054
|
-
if (
|
|
9214
|
+
if (existsSync13(join19(target, "package.json")) && !existsSync13(join19(target, "node_modules"))) {
|
|
9055
9215
|
say("npm install\u2026");
|
|
9056
9216
|
if (await runStep("npm", ["install"], { cwd: target, shell: process.platform === "win32" }) !== 0) {
|
|
9057
9217
|
return { ok: false, error: `npm install failed in ${target}` };
|
|
@@ -9063,7 +9223,7 @@ async function loadDemo(entry, opts) {
|
|
|
9063
9223
|
return { ok: false, error: `setup failed (${entry.setup})` };
|
|
9064
9224
|
}
|
|
9065
9225
|
}
|
|
9066
|
-
return { ok: true, serveDirs: entry.serve.dirs?.length ? entry.serve.dirs.map((d) =>
|
|
9226
|
+
return { ok: true, serveDirs: entry.serve.dirs?.length ? entry.serve.dirs.map((d) => join19(target, d)) : [target] };
|
|
9067
9227
|
}
|
|
9068
9228
|
function runStep(cmd, args, opts = {}) {
|
|
9069
9229
|
return new Promise((res) => {
|
|
@@ -9074,8 +9234,8 @@ function runStep(cmd, args, opts = {}) {
|
|
|
9074
9234
|
}
|
|
9075
9235
|
|
|
9076
9236
|
// src/server.ts
|
|
9077
|
-
var pkgRoot =
|
|
9078
|
-
var webRoot =
|
|
9237
|
+
var pkgRoot = join20(dirname5(fileURLToPath3(import.meta.url)), "..");
|
|
9238
|
+
var webRoot = join20(pkgRoot, "web");
|
|
9079
9239
|
var execFileP = async (cmd, args) => (await promisify3(execFile3)(cmd, args, { encoding: "utf8", timeout: 1e4 })).stdout;
|
|
9080
9240
|
function optsFromQuery(url, tierEnvVar, projectDir) {
|
|
9081
9241
|
const q = url.searchParams;
|
|
@@ -9125,7 +9285,7 @@ function tierFailure(tier, message) {
|
|
|
9125
9285
|
function beholdVersion() {
|
|
9126
9286
|
try {
|
|
9127
9287
|
const here = dirname5(fileURLToPath3(import.meta.url));
|
|
9128
|
-
return JSON.parse(
|
|
9288
|
+
return JSON.parse(readFileSync15(join20(here, "..", "package.json"), "utf8")).version ?? "unknown";
|
|
9129
9289
|
} catch {
|
|
9130
9290
|
return "unknown";
|
|
9131
9291
|
}
|
|
@@ -9170,7 +9330,7 @@ async function captureFrame(projectDir, env, frames, broadcaster) {
|
|
|
9170
9330
|
}
|
|
9171
9331
|
}
|
|
9172
9332
|
function carveRoutes(app, reportPath, demo) {
|
|
9173
|
-
const load = () => readCarveReport(reportPath, (p) =>
|
|
9333
|
+
const load = () => readCarveReport(reportPath, (p) => readFileSync15(p, "utf8"));
|
|
9174
9334
|
const carveDirs = [dirname5(reportPath), ...demo ? [demo.out] : []];
|
|
9175
9335
|
const carveStates = () => readCarveStates(carveDirs, nodeCarveIo, 0);
|
|
9176
9336
|
const emittedNodes = async (states) => {
|
|
@@ -9189,8 +9349,8 @@ function carveRoutes(app, reportPath, demo) {
|
|
|
9189
9349
|
project: demo.project,
|
|
9190
9350
|
out: demo.out,
|
|
9191
9351
|
// Paths the UI can print without leaking the operator's whole home dir.
|
|
9192
|
-
outLabel: relative5(demo.root, demo.out).split(
|
|
9193
|
-
fromLabel: relative5(demo.root, demo.from).split(
|
|
9352
|
+
outLabel: relative5(demo.root, demo.out).split(sep6).join("/"),
|
|
9353
|
+
fromLabel: relative5(demo.root, demo.from).split(sep6).join("/"),
|
|
9194
9354
|
runnable: !block,
|
|
9195
9355
|
...block ? { reason: block } : {},
|
|
9196
9356
|
// The live tier, when this boot armed it: the scratch endpoint the
|
|
@@ -9207,7 +9367,7 @@ function carveRoutes(app, reportPath, demo) {
|
|
|
9207
9367
|
let appGraph;
|
|
9208
9368
|
const appGraphOnce = () => {
|
|
9209
9369
|
if (!demo) return Promise.resolve(null);
|
|
9210
|
-
appGraph ??= graphIr(demo.project).then((ir) => ir.nodes.length ? { ir, label: relative5(demo.root, demo.project).split(
|
|
9370
|
+
appGraph ??= graphIr(demo.project).then((ir) => ir.nodes.length ? { ir, label: relative5(demo.root, demo.project).split(sep6).join("/") } : null).catch(() => null);
|
|
9211
9371
|
return appGraph;
|
|
9212
9372
|
};
|
|
9213
9373
|
app.get("/api/graph", async (c) => {
|
|
@@ -9223,7 +9383,7 @@ function carveRoutes(app, reportPath, demo) {
|
|
|
9223
9383
|
return bandGraduated(split.tf, split.graduated);
|
|
9224
9384
|
};
|
|
9225
9385
|
const { svg, ir } = appSide ? renderCarveEstate(tfIr, appSide.ir, {
|
|
9226
|
-
tfTitle: `${relative5(demo.root, demo.from).split(
|
|
9386
|
+
tfTitle: `${relative5(demo.root, demo.from).split(sep6).join("/")} \u2014 terraform`,
|
|
9227
9387
|
appTitle: `${appSide.label} \u2014 chant`,
|
|
9228
9388
|
carved: states,
|
|
9229
9389
|
shorten,
|
|
@@ -9243,7 +9403,9 @@ function carveRoutes(app, reportPath, demo) {
|
|
|
9243
9403
|
carve: true,
|
|
9244
9404
|
// A demo whose own advisor run failed says so on the statusbar, not
|
|
9245
9405
|
// only in the terminal the viewer isn't looking at.
|
|
9246
|
-
note: carveNote(parsed.report, tfIr) + carveStateNote(carveProgress(states.values(), tfIr.nodes.length), states) + (demo?.degraded ? ` Degraded: ${demo.degraded}` : "")
|
|
9406
|
+
note: carveNote(parsed.report, tfIr) + carveStateNote(carveProgress(states.values(), tfIr.nodes.length), states) + (demo?.degraded ? ` Degraded: ${demo.degraded}` : "") + // chant#2058: the app box shows only the app's own declarations;
|
|
9407
|
+
// what the git-root Op discovery added from elsewhere is named.
|
|
9408
|
+
(appSide && foreignNote(appSide.ir) ? ` ${foreignNote(appSide.ir)}.` : "")
|
|
9247
9409
|
}
|
|
9248
9410
|
});
|
|
9249
9411
|
});
|
|
@@ -9275,7 +9437,7 @@ function carveRoutes(app, reportPath, demo) {
|
|
|
9275
9437
|
}
|
|
9276
9438
|
const tfIr = carveReportToIr(parsed.report);
|
|
9277
9439
|
const html = renderCarveMorph(tfIr, appSide.ir, [select], {
|
|
9278
|
-
tfTitle: `${relative5(demo.root, demo.from).split(
|
|
9440
|
+
tfTitle: `${relative5(demo.root, demo.from).split(sep6).join("/")} \u2014 terraform`,
|
|
9279
9441
|
appTitle: `${appSide.label} \u2014 chant`,
|
|
9280
9442
|
title: `carve \u2014 ${select}`,
|
|
9281
9443
|
carved: carveStates(),
|
|
@@ -9369,6 +9531,36 @@ function carveRoutes(app, reportPath, demo) {
|
|
|
9369
9531
|
app.get("/api/resources", (c) => c.json({ byComponent: {} }));
|
|
9370
9532
|
app.get("/api/ci", (c) => c.json({ stages: [], jobs: [], forge: null }));
|
|
9371
9533
|
}
|
|
9534
|
+
function followLimits(env = process.env) {
|
|
9535
|
+
const timeout = Number.parseInt(env.BEHOLD_CI_FOLLOW_TIMEOUT_MS ?? "", 10);
|
|
9536
|
+
const budget = Number.parseInt(env.BEHOLD_CI_POLL_FAIL_BUDGET ?? "", 10);
|
|
9537
|
+
return {
|
|
9538
|
+
...Number.isInteger(timeout) && timeout >= 0 ? { followTimeoutMs: timeout } : {},
|
|
9539
|
+
...Number.isInteger(budget) && budget > 0 ? { pollFailBudget: budget } : {}
|
|
9540
|
+
};
|
|
9541
|
+
}
|
|
9542
|
+
async function readoptDispatchedRun(cfg, runner) {
|
|
9543
|
+
const run3 = readDispatchedRun(cfg.projectDir);
|
|
9544
|
+
if (!run3 || run3.concluded && run3.concluded.verdict !== "lost") return { outcome: "none" };
|
|
9545
|
+
const ready = await ghReady();
|
|
9546
|
+
if (!ready.ok) return { outcome: "refused", reason: ready.reason ?? "gh is not ready", run: run3 };
|
|
9547
|
+
const pipeline = await ciPipeline(cfg.projectDir, run3.env ? { env: run3.env } : {}, "github").catch(() => void 0);
|
|
9548
|
+
if (!pipeline || pipeline.jobs.length === 0) {
|
|
9549
|
+
return { outcome: "refused", reason: "no generated GitHub pipeline to paint the run onto \u2014 `chant build --components --generate github` first", run: run3 };
|
|
9550
|
+
}
|
|
9551
|
+
const started = runner.track(`GitHub Actions ${run3.workflow} (re-adopted run ${run3.runId})`, (io) => {
|
|
9552
|
+
io.line(`\u25CF re-adopting run ${run3.runId} (${run3.workflow}, dispatched ${run3.dispatchedAt}) \u2014 \`gh run view\` is the durable read`);
|
|
9553
|
+
return followRun(run3.runId, pipeline, {
|
|
9554
|
+
...followLimits(),
|
|
9555
|
+
onLine: io.line,
|
|
9556
|
+
onProgress: io.progress,
|
|
9557
|
+
onAdopted: ({ runId, url }) => saveDispatchedRun({ ...run3, runId, ...url ? { url } : {} }),
|
|
9558
|
+
onConcluded: ({ verdict }) => concludeDispatchedRun(cfg.projectDir, run3.runId, verdict)
|
|
9559
|
+
});
|
|
9560
|
+
});
|
|
9561
|
+
if (!started) return { outcome: "refused", reason: `busy \u2014 ${runner.running} is running`, run: run3 };
|
|
9562
|
+
return { outcome: "readopted", run: run3 };
|
|
9563
|
+
}
|
|
9372
9564
|
function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffer(), runner = new OpRunner({
|
|
9373
9565
|
projectDir: cfg.projectDir,
|
|
9374
9566
|
broadcaster,
|
|
@@ -9486,9 +9678,9 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
|
|
|
9486
9678
|
return c.json({ started: true, name, ran: label });
|
|
9487
9679
|
});
|
|
9488
9680
|
app.post("/api/local/reset", (c) => {
|
|
9489
|
-
const down =
|
|
9490
|
-
const up =
|
|
9491
|
-
if (!
|
|
9681
|
+
const down = join20(cfg.projectDir, "scripts/local/local-down.sh");
|
|
9682
|
+
const up = join20(cfg.projectDir, "scripts/local/local-up.sh");
|
|
9683
|
+
if (!existsSync14(down) || !existsSync14(up)) {
|
|
9492
9684
|
return c.json({ error: "no local-down.sh / local-up.sh in scripts/local \u2014 reset is only for local emulator projects" }, 400);
|
|
9493
9685
|
}
|
|
9494
9686
|
if (!runner.bringUp("reset local emulator", "bash", ["-c", "bash scripts/local/local-down.sh && bash scripts/local/local-up.sh"], cfg.projectDir)) {
|
|
@@ -9509,7 +9701,7 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
|
|
|
9509
9701
|
const dirs = [];
|
|
9510
9702
|
const declared = [];
|
|
9511
9703
|
for (const f of discoverOpIrs(estateDirs)) {
|
|
9512
|
-
const parsed = readOpIr(f.path, (p) =>
|
|
9704
|
+
const parsed = readOpIr(f.path, (p) => readFileSync15(p, "utf8"));
|
|
9513
9705
|
if (!parsed.ok) continue;
|
|
9514
9706
|
const mine = declaredConvergeOps([parsed.ir]);
|
|
9515
9707
|
if (!mine.length) continue;
|
|
@@ -9679,9 +9871,9 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
|
|
|
9679
9871
|
app.post("/api/project/open", async (c) => {
|
|
9680
9872
|
if (cfg.previewMode) return c.json({ error: "switching projects is locked in preview mode" }, 403);
|
|
9681
9873
|
const body = await c.req.json().catch(() => ({}));
|
|
9682
|
-
const dir = typeof body.dir === "string" && body.dir.trim() ?
|
|
9683
|
-
if (!dir || !
|
|
9684
|
-
if (!
|
|
9874
|
+
const dir = typeof body.dir === "string" && body.dir.trim() ? resolve9(body.dir.trim()) : "";
|
|
9875
|
+
if (!dir || !existsSync14(dir)) return c.json({ error: `no such directory: ${dir || "(no dir given)"}` }, 400);
|
|
9876
|
+
if (!existsSync14(join20(dir, "chant.config.ts"))) {
|
|
9685
9877
|
return c.json({ error: `${dir} doesn't look like a chant project \u2014 no chant.config.ts` }, 400);
|
|
9686
9878
|
}
|
|
9687
9879
|
switchServedProject([dir]);
|
|
@@ -9701,7 +9893,7 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
|
|
|
9701
9893
|
};
|
|
9702
9894
|
app.post("/api/project/reveal", async (c) => {
|
|
9703
9895
|
const body = await c.req.json().catch(() => ({}));
|
|
9704
|
-
const dir = typeof body.dir === "string" && body.dir.trim() ?
|
|
9896
|
+
const dir = typeof body.dir === "string" && body.dir.trim() ? resolve9(body.dir.trim()) : cfg.projectDir;
|
|
9705
9897
|
const known = /* @__PURE__ */ new Set([cfg.projectDir, ...cfg.projectDirs ?? [], ...listRecents().map((r) => r.dir)]);
|
|
9706
9898
|
if (!known.has(dir)) return c.json({ error: "not a served or recent project directory" }, 400);
|
|
9707
9899
|
const opener = process.platform === "darwin" ? "open" : process.platform === "win32" ? "explorer" : "xdg-open";
|
|
@@ -9725,7 +9917,7 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
|
|
|
9725
9917
|
fetches: fetchesFromNetwork(e),
|
|
9726
9918
|
...e.repo ? { repo: e.repo } : {},
|
|
9727
9919
|
target,
|
|
9728
|
-
loaded:
|
|
9920
|
+
loaded: existsSync14(target),
|
|
9729
9921
|
satisfiable: missing.length === 0,
|
|
9730
9922
|
// #254: the carve walkthrough runs fine here — it just isn't a project
|
|
9731
9923
|
// to switch INTO. Carve mode claims `/api/graph` and `/api/project` at
|
|
@@ -9769,7 +9961,7 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
|
|
|
9769
9961
|
const loaded = await loadDemo(entry, { pkgRoot, target, log: (line) => process.stdout.write(line + "\n") });
|
|
9770
9962
|
if (!loaded.ok) return c.json({ error: loaded.error }, 500);
|
|
9771
9963
|
const dirs = loaded.serveDirs;
|
|
9772
|
-
if (!
|
|
9964
|
+
if (!existsSync14(join20(dirs[0], "chant.config.ts"))) {
|
|
9773
9965
|
return c.json({ error: `${entry.name} loaded to ${target} but ${dirs[0]} has no chant.config.ts` }, 500);
|
|
9774
9966
|
}
|
|
9775
9967
|
if (entry.serve.local) {
|
|
@@ -9904,7 +10096,9 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
|
|
|
9904
10096
|
{ method: "POST", path: "/api/rollback", desc: "open a rollback PR: ?to=<sha>" },
|
|
9905
10097
|
{ method: "POST", path: "/api/substrates/:name/up", desc: "bring a substrate up" },
|
|
9906
10098
|
{ method: "POST", path: "/api/local/reset", desc: "reset the local emulator" },
|
|
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" }
|
|
10099
|
+
{ method: "POST", path: "/api/ci/dispatch", desc: "dispatch the GitHub Actions pipeline for ?env= via the operator's gh \u2014 the designated workflow when .behold.json names one" },
|
|
10100
|
+
{ method: "GET", path: "/api/ci/run", desc: "the persisted dispatched-run record (id, verdict) \u2014 what survives a behold restart" },
|
|
10101
|
+
{ method: "POST", path: "/api/ci/readopt", desc: "re-adopt the persisted unconcluded run: follow it by its saved id (also attempted automatically at boot)" }
|
|
9908
10102
|
]
|
|
9909
10103
|
})
|
|
9910
10104
|
);
|
|
@@ -9926,7 +10120,7 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
|
|
|
9926
10120
|
}
|
|
9927
10121
|
const parsedOps = [];
|
|
9928
10122
|
for (const f of files) {
|
|
9929
|
-
const parsed = readOpIr(f.path, (p) =>
|
|
10123
|
+
const parsed = readOpIr(f.path, (p) => readFileSync15(p, "utf8"));
|
|
9930
10124
|
if (!parsed.ok) return c.json(parsed.refusal, 422);
|
|
9931
10125
|
parsedOps.push(parsed.ir);
|
|
9932
10126
|
}
|
|
@@ -9939,8 +10133,8 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
|
|
|
9939
10133
|
}
|
|
9940
10134
|
const runState = runner.runState;
|
|
9941
10135
|
const ranOp = runState.op ? parsedOps.find((o) => o.name === runState.op) : void 0;
|
|
9942
|
-
const
|
|
9943
|
-
const opsIr =
|
|
10136
|
+
const join23 = ranOp ? joinRun(ranOp, runState.records, runState.gate) : null;
|
|
10137
|
+
const opsIr = join23 ? paintPlayhead(declaredIr, join23, runState.gate) : declaredIr;
|
|
9944
10138
|
const { svg: svg2 } = renderGraph(opsIr, { boxes: "byStack" });
|
|
9945
10139
|
const operator = runner.noteDeclaredConvergeOps(declaredConvergeOps(parsedOps));
|
|
9946
10140
|
return c.json({
|
|
@@ -9953,7 +10147,7 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
|
|
|
9953
10147
|
target: null,
|
|
9954
10148
|
mode: "ops",
|
|
9955
10149
|
ops: parsedOps.length,
|
|
9956
|
-
note: `${opsNote(parsedOps, opsIr)} ${playheadNote(runState,
|
|
10150
|
+
note: `${opsNote(parsedOps, opsIr)} ${playheadNote(runState, join23)} ${operatorNote(operator)}`,
|
|
9957
10151
|
// The operator strip's own model: the ConvergeOps this project
|
|
9958
10152
|
// declares, plus whatever the last status read said. `declared`
|
|
9959
10153
|
// without a `strip` is the honest starting state — a loop exists and
|
|
@@ -10043,7 +10237,7 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
|
|
|
10043
10237
|
srcCompositeEdgesAttached = 0;
|
|
10044
10238
|
}
|
|
10045
10239
|
}
|
|
10046
|
-
if (!multi && !components && !logical && !opts.lens && ir.nodes.length === 0 && !
|
|
10240
|
+
if (!multi && !components && !logical && !opts.lens && ir.nodes.length === 0 && !existsSync14(join20(cfg.projectDir, "chant.config.ts"))) {
|
|
10047
10241
|
return c.json(noProjectError(cfg.projectDir), 404);
|
|
10048
10242
|
}
|
|
10049
10243
|
const radial = new URL(c.req.url).searchParams.get("radial") === "1";
|
|
@@ -10106,13 +10300,36 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
|
|
|
10106
10300
|
if ("reason" in resolved) return c.json({ error: resolved.reason, ...designated ? { code: "executor-forge" } : {} }, 400);
|
|
10107
10301
|
const workflow = resolved.workflow;
|
|
10108
10302
|
const ref = await execFileP("git", ["-C", cfg.projectDir, "branch", "--show-current"]).then((out) => out.trim() || "main").catch(() => "main");
|
|
10303
|
+
const base = { projectDir: cfg.projectDir, ...env ? { env } : {}, workflow: workflow.file, ref, dispatchedAt: (/* @__PURE__ */ new Date()).toISOString() };
|
|
10304
|
+
let adoptedId;
|
|
10109
10305
|
const started = runner.track(
|
|
10110
10306
|
`GitHub Actions ${workflow.file}`,
|
|
10111
|
-
(io) => dispatchAndFollow(workflow, pipeline, ref, {
|
|
10307
|
+
(io) => dispatchAndFollow(workflow, pipeline, ref, {
|
|
10308
|
+
...followLimits(),
|
|
10309
|
+
onLine: io.line,
|
|
10310
|
+
onProgress: io.progress,
|
|
10311
|
+
onAdopted: ({ runId, url }) => {
|
|
10312
|
+
adoptedId = runId;
|
|
10313
|
+
saveDispatchedRun({ ...base, runId, ...url ? { url } : {} });
|
|
10314
|
+
},
|
|
10315
|
+
onConcluded: ({ verdict }) => {
|
|
10316
|
+
if (adoptedId !== void 0) concludeDispatchedRun(cfg.projectDir, adoptedId, verdict);
|
|
10317
|
+
}
|
|
10318
|
+
})
|
|
10112
10319
|
);
|
|
10113
10320
|
if (!started) return c.json({ error: `busy \u2014 ${runner.running} is running` }, 409);
|
|
10114
10321
|
return c.json({ started: true, workflow: workflow.file, ref, jobs: pipeline.jobs.length, ...env ? { env } : {}, designated: Boolean(designated) });
|
|
10115
10322
|
});
|
|
10323
|
+
app.get("/api/ci/run", (c) => {
|
|
10324
|
+
const run3 = readDispatchedRun(cfg.projectDir);
|
|
10325
|
+
return c.json({ run: run3 ?? null });
|
|
10326
|
+
});
|
|
10327
|
+
app.post("/api/ci/readopt", async (c) => {
|
|
10328
|
+
const result = await readoptDispatchedRun(cfg, runner);
|
|
10329
|
+
if (result.outcome === "none") return c.json({ error: "no unconcluded dispatched run is persisted for this project" }, 404);
|
|
10330
|
+
if (result.outcome === "refused") return c.json({ error: result.reason, run: result.run }, 409);
|
|
10331
|
+
return c.json({ readopted: true, runId: result.run.runId, workflow: result.run.workflow });
|
|
10332
|
+
});
|
|
10116
10333
|
app.get("/api/resources", async (c) => {
|
|
10117
10334
|
const opts = optsFromQuery(new URL(c.req.url), tierEnvVar, cfg.projectDir);
|
|
10118
10335
|
const env = opts.env ?? cfg.env;
|
|
@@ -10397,7 +10614,7 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
|
|
|
10397
10614
|
});
|
|
10398
10615
|
const rel = relative5(process.cwd(), webRoot) || ".";
|
|
10399
10616
|
app.use("/*", serveStatic({ root: rel }));
|
|
10400
|
-
app.get("/", serveStatic({ path:
|
|
10617
|
+
app.get("/", serveStatic({ path: join20(rel, "index.html") }));
|
|
10401
10618
|
return app;
|
|
10402
10619
|
}
|
|
10403
10620
|
async function bootLocalEmulators(dir, who) {
|
|
@@ -10426,7 +10643,7 @@ async function bootLocalEmulators(dir, who) {
|
|
|
10426
10643
|
async function startServer(cfg) {
|
|
10427
10644
|
if (cfg.local) cfg.emulators = await bootLocalEmulators(cfg.projectDir, "behold serve --local");
|
|
10428
10645
|
for (const dir of cfg.projectDirs ?? [cfg.projectDir]) {
|
|
10429
|
-
if (
|
|
10646
|
+
if (existsSync14(join20(dir, "chant.config.ts"))) addRecent(dir);
|
|
10430
10647
|
}
|
|
10431
10648
|
const broadcaster = new Broadcaster();
|
|
10432
10649
|
const frames = new FrameBuffer();
|
|
@@ -10437,6 +10654,17 @@ async function startServer(cfg) {
|
|
|
10437
10654
|
});
|
|
10438
10655
|
const app = createApp(cfg, broadcaster, frames, runner);
|
|
10439
10656
|
const autoSync = cfg.autoSync ?? "off";
|
|
10657
|
+
if (!cfg.carveReport) {
|
|
10658
|
+
void readoptDispatchedRun(cfg, runner).then((result) => {
|
|
10659
|
+
if (result.outcome === "readopted") {
|
|
10660
|
+
process.stdout.write(` re-adopted dispatched run ${result.run.runId} (${result.run.workflow}) \u2014 following via gh
|
|
10661
|
+
`);
|
|
10662
|
+
} else if (result.outcome === "refused") {
|
|
10663
|
+
process.stdout.write(` a dispatched run (${result.run.runId}, ${result.run.workflow}) is persisted but not re-adopted: ${result.reason}
|
|
10664
|
+
`);
|
|
10665
|
+
}
|
|
10666
|
+
}).catch(() => void 0);
|
|
10667
|
+
}
|
|
10440
10668
|
const capture2 = () => captureFrame(cfg.projectDir, cfg.env, frames, broadcaster);
|
|
10441
10669
|
const onEstateChange = (memberDir) => {
|
|
10442
10670
|
broadcaster.emit("changed", memberDir ?? "");
|
|
@@ -10573,8 +10801,8 @@ async function startServer(cfg) {
|
|
|
10573
10801
|
}
|
|
10574
10802
|
|
|
10575
10803
|
// src/export.ts
|
|
10576
|
-
import { mkdirSync as
|
|
10577
|
-
import { join as
|
|
10804
|
+
import { mkdirSync as mkdirSync5, writeFileSync as writeFileSync5, copyFileSync, readFileSync as readFileSync16, readdirSync as readdirSync8 } from "node:fs";
|
|
10805
|
+
import { join as join21, dirname as dirname6, basename as basename2 } from "node:path";
|
|
10578
10806
|
import { fileURLToPath as fileURLToPath4 } from "node:url";
|
|
10579
10807
|
var LENS_PARAMS = ["components", "detail", "env", "logical", "ops", "radial", "tier"];
|
|
10580
10808
|
function canonicalKey(path, params) {
|
|
@@ -10582,7 +10810,7 @@ function canonicalKey(path, params) {
|
|
|
10582
10810
|
const q = LENS_PARAMS.filter((k) => params.has(k) && !(flat && (k === "detail" || k === "radial"))).map((k) => `${k}=${params.get(k)}`).join("&");
|
|
10583
10811
|
return q ? `${path}?${q}` : path;
|
|
10584
10812
|
}
|
|
10585
|
-
function
|
|
10813
|
+
function slug3(key) {
|
|
10586
10814
|
const base = key.replace(/^\//, "").replace(/[^a-zA-Z0-9=_.-]+/g, "_").slice(0, 120);
|
|
10587
10815
|
return `${base}.json`;
|
|
10588
10816
|
}
|
|
@@ -10623,7 +10851,7 @@ function captureKeys(axes) {
|
|
|
10623
10851
|
return [...keys];
|
|
10624
10852
|
}
|
|
10625
10853
|
function webDir() {
|
|
10626
|
-
return
|
|
10854
|
+
return join21(dirname6(fileURLToPath4(import.meta.url)), "..", "web");
|
|
10627
10855
|
}
|
|
10628
10856
|
function workerName(project, override) {
|
|
10629
10857
|
const raw = override ?? `behold-${basename2(project)}`;
|
|
@@ -10634,16 +10862,16 @@ async function runExport(cfg, outDir, opts = {}) {
|
|
|
10634
10862
|
const app = createApp({ ...cfg, layoutWrites: false });
|
|
10635
10863
|
const proj = await (await app.request("/api/project")).json();
|
|
10636
10864
|
const axes = { environments: proj.environments ?? [], tiers: proj.tiers ?? [], ...proj.ops ? { ops: proj.ops } : {} };
|
|
10637
|
-
const snapDir =
|
|
10638
|
-
|
|
10865
|
+
const snapDir = join21(outDir, "snapshots");
|
|
10866
|
+
mkdirSync5(snapDir, { recursive: true });
|
|
10639
10867
|
const keyToFile = {};
|
|
10640
10868
|
let ok = 0;
|
|
10641
10869
|
let failed = 0;
|
|
10642
10870
|
for (const key of captureKeys(axes)) {
|
|
10643
10871
|
const res = await app.request(`${key}${key.includes("?") ? "&" : "?"}layout=1`);
|
|
10644
10872
|
const body = await res.text();
|
|
10645
|
-
const file =
|
|
10646
|
-
|
|
10873
|
+
const file = slug3(key);
|
|
10874
|
+
writeFileSync5(join21(snapDir, file), body);
|
|
10647
10875
|
keyToFile[key] = `snapshots/${file}`;
|
|
10648
10876
|
if (res.ok) ok++;
|
|
10649
10877
|
else failed++;
|
|
@@ -10655,21 +10883,21 @@ async function runExport(cfg, outDir, opts = {}) {
|
|
|
10655
10883
|
axes,
|
|
10656
10884
|
keyToFile
|
|
10657
10885
|
};
|
|
10658
|
-
|
|
10659
|
-
const html =
|
|
10886
|
+
writeFileSync5(join21(outDir, "manifest.json"), JSON.stringify(manifest, null, 2));
|
|
10887
|
+
const html = readFileSync16(join21(webDir(), "index.html"), "utf8").replace(
|
|
10660
10888
|
/<\/head>/i,
|
|
10661
10889
|
` <script>window.__BEHOLD_STATIC__ = true;</script>
|
|
10662
10890
|
</head>`
|
|
10663
10891
|
);
|
|
10664
|
-
|
|
10892
|
+
writeFileSync5(join21(outDir, "index.html"), html);
|
|
10665
10893
|
for (const f of readdirSync8(webDir())) {
|
|
10666
10894
|
if (f === "index.html") continue;
|
|
10667
|
-
copyFileSync(
|
|
10895
|
+
copyFileSync(join21(webDir(), f), join21(outDir, f));
|
|
10668
10896
|
}
|
|
10669
|
-
|
|
10897
|
+
writeFileSync5(join21(outDir, "README.md"), BUNDLE_README);
|
|
10670
10898
|
const name = workerName(cfg.projectDir, opts.name);
|
|
10671
|
-
|
|
10672
|
-
|
|
10899
|
+
writeFileSync5(
|
|
10900
|
+
join21(outDir, "wrangler.jsonc"),
|
|
10673
10901
|
JSON.stringify(
|
|
10674
10902
|
{ $schema: "node_modules/wrangler/config-schema.json", name, compatibility_date: "2025-06-01", assets: { directory: "." } },
|
|
10675
10903
|
null,
|
|
@@ -10717,7 +10945,7 @@ It's just files \u2014 GitHub Pages, S3, nginx, or Cloudflare Pages
|
|
|
10717
10945
|
`;
|
|
10718
10946
|
|
|
10719
10947
|
// src/doctor.ts
|
|
10720
|
-
import { relative as relative6, resolve as
|
|
10948
|
+
import { relative as relative6, resolve as resolve10 } from "node:path";
|
|
10721
10949
|
var list2 = (xs) => xs.join(", ");
|
|
10722
10950
|
var EMPTY_KUBECONFIG = { contexts: /* @__PURE__ */ new Map(), servers: /* @__PURE__ */ new Map() };
|
|
10723
10951
|
function summarize(xs, max = 4) {
|
|
@@ -10866,7 +11094,7 @@ function opsCheck(root, ops, chantSource, estate) {
|
|
|
10866
11094
|
return { name: "ops", status: "pass", detail: `${ops.length} committed: ${list2(names)}; ${mcp}` };
|
|
10867
11095
|
}
|
|
10868
11096
|
async function diagnose(dir, probes = {}) {
|
|
10869
|
-
const root =
|
|
11097
|
+
const root = resolve10(dir);
|
|
10870
11098
|
const shape = detectProjectShape(root);
|
|
10871
11099
|
const behold = beholdVersion();
|
|
10872
11100
|
if (shape.kind === "none") {
|
|
@@ -10886,7 +11114,7 @@ async function diagnose(dir, probes = {}) {
|
|
|
10886
11114
|
};
|
|
10887
11115
|
}
|
|
10888
11116
|
const estate = shape.kind === "estate";
|
|
10889
|
-
const members = (shape.members ?? []).map((m) =>
|
|
11117
|
+
const members = (shape.members ?? []).map((m) => resolve10(root, m));
|
|
10890
11118
|
const targets = estate ? members : [root];
|
|
10891
11119
|
const primary = targets[0];
|
|
10892
11120
|
const projectCheck = estate ? {
|
|
@@ -11120,7 +11348,7 @@ ${USAGE}`);
|
|
|
11120
11348
|
process.stderr.write("behold serve: --auto-sync needs --env and --poll (it acts on polled drift)\n");
|
|
11121
11349
|
process.exit(2);
|
|
11122
11350
|
}
|
|
11123
|
-
const dirs = projectDirs.map((d) =>
|
|
11351
|
+
const dirs = projectDirs.map((d) => resolve11(d));
|
|
11124
11352
|
for (const d of dirs) warnIfNotChantProject(d);
|
|
11125
11353
|
await startServer({
|
|
11126
11354
|
projectDir: dirs[0],
|
|
@@ -11139,7 +11367,7 @@ function warnIfNotChantProject(dir) {
|
|
|
11139
11367
|
if (shape.kind === "estate") {
|
|
11140
11368
|
process.stderr.write(
|
|
11141
11369
|
`behold: warning \u2014 ${dir} is an estate root, not a chant project itself.
|
|
11142
|
-
Serve its members composed: behold serve ${shape.members.map((m) =>
|
|
11370
|
+
Serve its members composed: behold serve ${shape.members.map((m) => join22(dir, m)).join(" ")}
|
|
11143
11371
|
`
|
|
11144
11372
|
);
|
|
11145
11373
|
return;
|
|
@@ -11174,8 +11402,8 @@ async function runCarve(rest) {
|
|
|
11174
11402
|
);
|
|
11175
11403
|
process.exit(2);
|
|
11176
11404
|
}
|
|
11177
|
-
const reportPath =
|
|
11178
|
-
const parsed = readCarveReport(reportPath, (p) =>
|
|
11405
|
+
const reportPath = resolve11(fileArg);
|
|
11406
|
+
const parsed = readCarveReport(reportPath, (p) => readFileSync17(p, "utf8"));
|
|
11179
11407
|
if (!parsed.ok) {
|
|
11180
11408
|
process.stderr.write(`behold carve: ${parsed.refusal.error}
|
|
11181
11409
|
${parsed.refusal.remedy}
|
|
@@ -11202,8 +11430,8 @@ async function runDoctor(rest) {
|
|
|
11202
11430
|
}
|
|
11203
11431
|
}
|
|
11204
11432
|
const dir = dirArg ?? ".";
|
|
11205
|
-
if (!
|
|
11206
|
-
process.stderr.write(`behold doctor: no such directory: ${
|
|
11433
|
+
if (!existsSync15(resolve11(dir))) {
|
|
11434
|
+
process.stderr.write(`behold doctor: no such directory: ${resolve11(dir)}
|
|
11207
11435
|
`);
|
|
11208
11436
|
process.exit(2);
|
|
11209
11437
|
}
|
|
@@ -11212,7 +11440,7 @@ async function runDoctor(rest) {
|
|
|
11212
11440
|
if (!report.ok) process.exitCode = 1;
|
|
11213
11441
|
}
|
|
11214
11442
|
async function runDemo(rest) {
|
|
11215
|
-
const pkgRoot2 =
|
|
11443
|
+
const pkgRoot2 = join22(dirname7(fileURLToPath5(import.meta.url)), "..");
|
|
11216
11444
|
const registry = loadDemoRegistry(pkgRoot2);
|
|
11217
11445
|
let port = 4600;
|
|
11218
11446
|
let live = false;
|
|
@@ -11266,7 +11494,7 @@ async function runDemo(rest) {
|
|
|
11266
11494
|
`);
|
|
11267
11495
|
process.exit(2);
|
|
11268
11496
|
}
|
|
11269
|
-
const target = dirArg ?
|
|
11497
|
+
const target = dirArg ? resolve11(dirArg) : demoTargetDir(entry);
|
|
11270
11498
|
const loaded = await loadDemo(entry, { pkgRoot: pkgRoot2, target, log: (line) => process.stdout.write(line + "\n") });
|
|
11271
11499
|
if (!loaded.ok) {
|
|
11272
11500
|
process.stderr.write(`behold demo ${entry.name}: ${loaded.error}
|
|
@@ -11297,7 +11525,7 @@ function spawnStep(cmd, args, cwd) {
|
|
|
11297
11525
|
});
|
|
11298
11526
|
}
|
|
11299
11527
|
async function serveCarveDemo(target, carve, port, live = false) {
|
|
11300
|
-
const at = (rel) =>
|
|
11528
|
+
const at = (rel) => resolve11(target, rel);
|
|
11301
11529
|
const project = at(carve.project);
|
|
11302
11530
|
const from = at(carve.from);
|
|
11303
11531
|
const state = carve.state ? at(carve.state) : void 0;
|
|
@@ -11321,7 +11549,7 @@ async function serveCarveDemo(target, carve, port, live = false) {
|
|
|
11321
11549
|
process.exit(1);
|
|
11322
11550
|
}
|
|
11323
11551
|
process.stdout.write("behold demo carve --live \u2192 terraform init + apply (the starred resources, into the scratch Floci)\u2026\n");
|
|
11324
|
-
if (state)
|
|
11552
|
+
if (state) rmSync3(state, { force: true });
|
|
11325
11553
|
const applyErr = await applyIntoFloci(from, spawnStep);
|
|
11326
11554
|
if (applyErr) {
|
|
11327
11555
|
await teardownScratchFloci();
|
|
@@ -11337,7 +11565,7 @@ async function serveCarveDemo(target, carve, port, live = false) {
|
|
|
11337
11565
|
};
|
|
11338
11566
|
process.stdout.write("behold demo carve --live \u2192 the tfstate is real now; the advisor reads what terraform wrote.\n");
|
|
11339
11567
|
}
|
|
11340
|
-
if (
|
|
11568
|
+
if (existsSync15(join22(project, "package.json")) && !existsSync15(join22(project, "node_modules"))) {
|
|
11341
11569
|
process.stdout.write(`behold demo carve \u2192 npm install in ${carve.project}/ (the chant this walkthrough shells)\u2026
|
|
11342
11570
|
`);
|
|
11343
11571
|
const code = await spawnStep("npm", ["install"], project);
|
|
@@ -11348,7 +11576,7 @@ async function serveCarveDemo(target, carve, port, live = false) {
|
|
|
11348
11576
|
}
|
|
11349
11577
|
}
|
|
11350
11578
|
let degraded;
|
|
11351
|
-
if (!
|
|
11579
|
+
if (!existsSync15(join22(target, "node_modules", "@cdktf", "hcl2json"))) {
|
|
11352
11580
|
process.stdout.write("behold demo carve \u2192 npm install @cdktf/hcl2json (chant's HCL parser, ~2MB, once)\u2026\n");
|
|
11353
11581
|
const code = await spawnStep("npm", ["install", "--no-save", "--no-package-lock", "@cdktf/hcl2json"], target);
|
|
11354
11582
|
if (code !== 0) degraded = "couldn't install @cdktf/hcl2json (chant's HCL parser) \u2014 no network?";
|
|
@@ -11364,7 +11592,7 @@ async function serveCarveDemo(target, carve, port, live = false) {
|
|
|
11364
11592
|
);
|
|
11365
11593
|
if (code !== 0) degraded = `chant carve advise exited ${code}`;
|
|
11366
11594
|
}
|
|
11367
|
-
const serving = !degraded &&
|
|
11595
|
+
const serving = !degraded && existsSync15(report) ? report : committed;
|
|
11368
11596
|
if (degraded) {
|
|
11369
11597
|
process.stderr.write(
|
|
11370
11598
|
`behold demo carve: ${degraded}
|
|
@@ -11372,7 +11600,7 @@ async function serveCarveDemo(target, carve, port, live = false) {
|
|
|
11372
11600
|
`
|
|
11373
11601
|
);
|
|
11374
11602
|
}
|
|
11375
|
-
if (!
|
|
11603
|
+
if (!existsSync15(serving)) {
|
|
11376
11604
|
process.stderr.write(`behold demo carve: no carve report at ${serving}
|
|
11377
11605
|
`);
|
|
11378
11606
|
process.exit(2);
|
|
@@ -11417,8 +11645,8 @@ async function runPreview(rest) {
|
|
|
11417
11645
|
process.stderr.write("behold preview: --port must be a number\n");
|
|
11418
11646
|
process.exit(2);
|
|
11419
11647
|
}
|
|
11420
|
-
const projectDir =
|
|
11421
|
-
if (!
|
|
11648
|
+
const projectDir = resolve11(dirArg ?? process.cwd());
|
|
11649
|
+
if (!existsSync15(projectDir)) {
|
|
11422
11650
|
process.stderr.write(`behold preview: project not found at ${projectDir}
|
|
11423
11651
|
`);
|
|
11424
11652
|
process.exit(2);
|
|
@@ -11438,26 +11666,26 @@ async function runPreview(rest) {
|
|
|
11438
11666
|
await startServer({ projectDir, port, env: "local", previewMode: true });
|
|
11439
11667
|
}
|
|
11440
11668
|
async function runExportCmd(rest) {
|
|
11441
|
-
let outDir =
|
|
11669
|
+
let outDir = resolve11("behold-export");
|
|
11442
11670
|
let env;
|
|
11443
11671
|
let name;
|
|
11444
11672
|
let dirArg;
|
|
11445
11673
|
let emulator = false;
|
|
11446
11674
|
for (let i = 0; i < rest.length; i++) {
|
|
11447
11675
|
const a = rest[i];
|
|
11448
|
-
if (a === "--out") outDir =
|
|
11676
|
+
if (a === "--out") outDir = resolve11(rest[++i]);
|
|
11449
11677
|
else if (a === "--env") env = rest[++i];
|
|
11450
11678
|
else if (a === "--name") name = rest[++i];
|
|
11451
11679
|
else if (a === "--emulator") emulator = true;
|
|
11452
11680
|
else if (a === "-h" || a === "--help") return void process.stdout.write(USAGE);
|
|
11453
11681
|
else if (!a.startsWith("-")) dirArg = a;
|
|
11454
11682
|
}
|
|
11455
|
-
const projectDir =
|
|
11683
|
+
const projectDir = resolve11(dirArg ?? process.cwd());
|
|
11456
11684
|
if (emulator) {
|
|
11457
11685
|
injectEmulatorEnv(env);
|
|
11458
11686
|
env ??= "local";
|
|
11459
11687
|
}
|
|
11460
|
-
if (!
|
|
11688
|
+
if (!existsSync15(projectDir)) {
|
|
11461
11689
|
process.stderr.write(`behold export: project not found at ${projectDir}
|
|
11462
11690
|
`);
|
|
11463
11691
|
process.exit(2);
|