@intentius/behold 0.28.0 → 0.29.1

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/dist/cli.js CHANGED
@@ -5828,10 +5828,10 @@ var require_utils = __commonJS({
5828
5828
  REGEX_SPECIAL_CHARS_GLOBAL
5829
5829
  } = require_constants();
5830
5830
  exports.isObject = (val) => val !== null && typeof val === "object" && !Array.isArray(val);
5831
- exports.hasRegexChars = (str15) => REGEX_SPECIAL_CHARS.test(str15);
5832
- exports.isRegexChar = (str15) => str15.length === 1 && exports.hasRegexChars(str15);
5833
- exports.escapeRegex = (str15) => str15.replace(REGEX_SPECIAL_CHARS_GLOBAL, "\\$1");
5834
- exports.toPosixSlashes = (str15) => str15.replace(REGEX_BACKSLASH, "/");
5831
+ exports.hasRegexChars = (str16) => REGEX_SPECIAL_CHARS.test(str16);
5832
+ exports.isRegexChar = (str16) => str16.length === 1 && exports.hasRegexChars(str16);
5833
+ exports.escapeRegex = (str16) => str16.replace(REGEX_SPECIAL_CHARS_GLOBAL, "\\$1");
5834
+ exports.toPosixSlashes = (str16) => str16.replace(REGEX_BACKSLASH, "/");
5835
5835
  exports.isWindows = () => {
5836
5836
  if (typeof navigator !== "undefined" && navigator.platform) {
5837
5837
  const platform2 = navigator.platform.toLowerCase();
@@ -5842,8 +5842,8 @@ var require_utils = __commonJS({
5842
5842
  }
5843
5843
  return false;
5844
5844
  };
5845
- exports.removeBackslashes = (str15) => {
5846
- return str15.replace(REGEX_REMOVE_BACKSLASH, (match) => {
5845
+ exports.removeBackslashes = (str16) => {
5846
+ return str16.replace(REGEX_REMOVE_BACKSLASH, (match) => {
5847
5847
  return match === "\\" ? "" : match;
5848
5848
  });
5849
5849
  };
@@ -5933,7 +5933,7 @@ var require_scan = __commonJS({
5933
5933
  const slashes = [];
5934
5934
  const tokens = [];
5935
5935
  const parts = [];
5936
- let str15 = input;
5936
+ let str16 = input;
5937
5937
  let index = -1;
5938
5938
  let start = 0;
5939
5939
  let lastIndex = 0;
@@ -5952,10 +5952,10 @@ var require_scan = __commonJS({
5952
5952
  let code;
5953
5953
  let token = { value: "", depth: 0, isGlob: false };
5954
5954
  const eos = () => index >= length;
5955
- const peek = () => str15.charCodeAt(index + 1);
5955
+ const peek = () => str16.charCodeAt(index + 1);
5956
5956
  const advance = () => {
5957
5957
  prev = code;
5958
- return str15.charCodeAt(++index);
5958
+ return str16.charCodeAt(++index);
5959
5959
  };
5960
5960
  while (index < length) {
5961
5961
  code = advance();
@@ -6123,24 +6123,24 @@ var require_scan = __commonJS({
6123
6123
  isExtglob = false;
6124
6124
  isGlob = false;
6125
6125
  }
6126
- let base = str15;
6126
+ let base = str16;
6127
6127
  let prefix = "";
6128
6128
  let glob = "";
6129
6129
  if (start > 0) {
6130
- prefix = str15.slice(0, start);
6131
- str15 = str15.slice(start);
6130
+ prefix = str16.slice(0, start);
6131
+ str16 = str16.slice(start);
6132
6132
  lastIndex -= start;
6133
6133
  }
6134
6134
  if (base && isGlob === true && lastIndex > 0) {
6135
- base = str15.slice(0, lastIndex);
6136
- glob = str15.slice(lastIndex);
6135
+ base = str16.slice(0, lastIndex);
6136
+ glob = str16.slice(lastIndex);
6137
6137
  } else if (isGlob === true) {
6138
6138
  base = "";
6139
- glob = str15;
6139
+ glob = str16;
6140
6140
  } else {
6141
- base = str15;
6141
+ base = str16;
6142
6142
  }
6143
- if (base && base !== "" && base !== "/" && base !== str15) {
6143
+ if (base && base !== "" && base !== "/" && base !== str16) {
6144
6144
  if (isPathSeparator(base.charCodeAt(base.length - 1))) {
6145
6145
  base = base.slice(0, -1);
6146
6146
  }
@@ -7187,8 +7187,8 @@ var require_parse = __commonJS({
7187
7187
  if (opts2.noglobstar === true) return star;
7188
7188
  return `(${capture2}(?:(?!${START_ANCHOR}${opts2.dot ? DOTS_SLASH : DOT_LITERAL}).)*?)`;
7189
7189
  };
7190
- const create = (str15) => {
7191
- switch (str15) {
7190
+ const create = (str16) => {
7191
+ switch (str16) {
7192
7192
  case "*":
7193
7193
  return `${nodot}${ONE_CHAR}${star}`;
7194
7194
  case ".*":
@@ -7206,7 +7206,7 @@ var require_parse = __commonJS({
7206
7206
  case "**/.*":
7207
7207
  return `(?:${nodot}${globstar(opts)}${SLASH_LITERAL})?${DOT_LITERAL}${ONE_CHAR}${star}`;
7208
7208
  default: {
7209
- const match = /^(.*?)\.(\w+)$/.exec(str15);
7209
+ const match = /^(.*?)\.(\w+)$/.exec(str16);
7210
7210
  if (!match) return;
7211
7211
  const source3 = create(match[1]);
7212
7212
  if (!source3) return;
@@ -7237,9 +7237,9 @@ var require_picomatch = __commonJS({
7237
7237
  var picomatch3 = (glob, options2, returnState = false) => {
7238
7238
  if (Array.isArray(glob)) {
7239
7239
  const fns = glob.map((input) => picomatch3(input, options2, returnState));
7240
- const arrayMatcher = (str15) => {
7240
+ const arrayMatcher = (str16) => {
7241
7241
  for (const isMatch of fns) {
7242
- const state2 = isMatch(str15);
7242
+ const state2 = isMatch(str16);
7243
7243
  if (state2) return state2;
7244
7244
  }
7245
7245
  return false;
@@ -7315,7 +7315,7 @@ var require_picomatch = __commonJS({
7315
7315
  const regex = glob instanceof RegExp ? glob : picomatch3.makeRe(glob, options2);
7316
7316
  return regex.test(utils.basename(input, { windows: posix5 }));
7317
7317
  };
7318
- picomatch3.isMatch = (str15, patterns, options2) => picomatch3(patterns, options2)(str15);
7318
+ picomatch3.isMatch = (str16, patterns, options2) => picomatch3(patterns, options2)(str16);
7319
7319
  picomatch3.parse = (pattern, options2) => {
7320
7320
  if (Array.isArray(pattern)) return pattern.map((p) => picomatch3.parse(p, options2));
7321
7321
  return parse(pattern, { ...options2, fastpaths: false });
@@ -8904,8 +8904,8 @@ __export(tree_exports, {
8904
8904
  workingTree: () => workingTree
8905
8905
  });
8906
8906
  import { execFileSync as execFileSync2 } from "node:child_process";
8907
- import { readdirSync as readdirSync10, readFileSync as readFileSync18, statSync as statSync8 } from "node:fs";
8908
- import { join as join25 } from "node:path";
8907
+ import { readdirSync as readdirSync10, readFileSync as readFileSync19, statSync as statSync8 } from "node:fs";
8908
+ import { join as join26 } from "node:path";
8909
8909
  function joinPath(...parts) {
8910
8910
  return parts.filter((p) => p !== "" && p !== ".").join("/");
8911
8911
  }
@@ -8914,7 +8914,7 @@ function workingTree(root) {
8914
8914
  label: "",
8915
8915
  stat(path) {
8916
8916
  try {
8917
- const s = statSync8(join25(root, path));
8917
+ const s = statSync8(join26(root, path));
8918
8918
  return s.isDirectory() ? "dir" : s.isFile() ? "file" : void 0;
8919
8919
  } catch {
8920
8920
  return void 0;
@@ -8923,7 +8923,7 @@ function workingTree(root) {
8923
8923
  list(path) {
8924
8924
  let dirents;
8925
8925
  try {
8926
- dirents = readdirSync10(join25(root, path), { withFileTypes: true });
8926
+ dirents = readdirSync10(join26(root, path), { withFileTypes: true });
8927
8927
  } catch {
8928
8928
  return void 0;
8929
8929
  }
@@ -8932,7 +8932,7 @@ function workingTree(root) {
8932
8932
  let type = d.isDirectory() ? "dir" : d.isFile() ? "file" : void 0;
8933
8933
  if (!type && d.isSymbolicLink()) {
8934
8934
  try {
8935
- const s = statSync8(join25(root, path, d.name));
8935
+ const s = statSync8(join26(root, path, d.name));
8936
8936
  type = s.isDirectory() ? "dir" : s.isFile() ? "file" : void 0;
8937
8937
  } catch {
8938
8938
  }
@@ -8942,10 +8942,10 @@ function workingTree(root) {
8942
8942
  return out;
8943
8943
  },
8944
8944
  read(path) {
8945
- return readFileSync18(join25(root, path), "utf-8");
8945
+ return readFileSync19(join26(root, path), "utf-8");
8946
8946
  },
8947
8947
  bytes(path) {
8948
- return readFileSync18(join25(root, path));
8948
+ return readFileSync19(join26(root, path));
8949
8949
  }
8950
8950
  };
8951
8951
  }
@@ -9547,12 +9547,12 @@ var init_identity_gate_rule = __esm({
9547
9547
  });
9548
9548
 
9549
9549
  // node_modules/@intentius/chant/src/workspace/class-data.ts
9550
- import { existsSync as existsSync17, readFileSync as readFileSync19, statSync as statSync9 } from "node:fs";
9551
- import { dirname as dirname7, join as join26, posix, resolve as resolve14 } from "node:path";
9550
+ import { existsSync as existsSync17, readFileSync as readFileSync20, statSync as statSync9 } from "node:fs";
9551
+ import { dirname as dirname7, join as join27, posix, resolve as resolve14 } from "node:path";
9552
9552
  function diskFiles(dir) {
9553
9553
  return (path) => {
9554
9554
  try {
9555
- return readFileSync19(join26(dir, ...path.split("/")), "utf-8");
9555
+ return readFileSync20(join27(dir, ...path.split("/")), "utf-8");
9556
9556
  } catch {
9557
9557
  return void 0;
9558
9558
  }
@@ -9560,9 +9560,9 @@ function diskFiles(dir) {
9560
9560
  }
9561
9561
  function findInstalledPackage(name, fromDir) {
9562
9562
  for (let dir = resolve14(fromDir); ; dir = dirname7(dir)) {
9563
- const candidate = join26(dir, "node_modules", ...name.split("/"));
9563
+ const candidate = join27(dir, "node_modules", ...name.split("/"));
9564
9564
  try {
9565
- if (statSync9(candidate).isDirectory() && existsSync17(join26(candidate, "package.json"))) return candidate;
9565
+ if (statSync9(candidate).isDirectory() && existsSync17(join27(candidate, "package.json"))) return candidate;
9566
9566
  } catch {
9567
9567
  }
9568
9568
  if (dirname7(dir) === dir) return void 0;
@@ -10012,10 +10012,10 @@ var init_jsonc = __esm({
10012
10012
  });
10013
10013
 
10014
10014
  // node_modules/@intentius/chant/src/package-version.ts
10015
- import { readFileSync as readFileSync20 } from "node:fs";
10015
+ import { readFileSync as readFileSync21 } from "node:fs";
10016
10016
  import { fileURLToPath as fileURLToPath4 } from "node:url";
10017
10017
  function readChantPackageVersion() {
10018
- return JSON.parse(readFileSync20(fileURLToPath4(new URL("../package.json", import.meta.url)), "utf-8")).version;
10018
+ return JSON.parse(readFileSync21(fileURLToPath4(new URL("../package.json", import.meta.url)), "utf-8")).version;
10019
10019
  }
10020
10020
  var init_package_version = __esm({
10021
10021
  "node_modules/@intentius/chant/src/package-version.ts"() {
@@ -10814,8 +10814,8 @@ var init_declaration = __esm({
10814
10814
  });
10815
10815
 
10816
10816
  // src/cli.ts
10817
- import { resolve as resolve33, dirname as dirname21, join as join48 } from "node:path";
10818
- import { realpathSync as realpathSync3, existsSync as existsSync30, readFileSync as readFileSync36, rmSync as rmSync4 } from "node:fs";
10817
+ import { resolve as resolve33, dirname as dirname21, join as join50 } from "node:path";
10818
+ import { realpathSync as realpathSync3, existsSync as existsSync30, readFileSync as readFileSync38, rmSync as rmSync4 } from "node:fs";
10819
10819
  import { fileURLToPath as fileURLToPath12 } from "node:url";
10820
10820
  import { spawn as spawn9, spawnSync as spawnSync4 } from "node:child_process";
10821
10821
 
@@ -10873,8 +10873,8 @@ import { serve } from "@hono/node-server";
10873
10873
  import { fileURLToPath as fileURLToPath10 } from "node:url";
10874
10874
  import { execFile as execFile5 } from "node:child_process";
10875
10875
  import { promisify as promisify3 } from "node:util";
10876
- import { existsSync as existsSync27, readFileSync as readFileSync32 } from "node:fs";
10877
- import { basename as basename5, dirname as dirname18, join as join44, relative as relative10, resolve as resolve30, sep as sep9 } from "node:path";
10876
+ import { existsSync as existsSync27, readFileSync as readFileSync34 } from "node:fs";
10877
+ import { basename as basename5, dirname as dirname18, join as join46, relative as relative10, resolve as resolve30, sep as sep9 } from "node:path";
10878
10878
 
10879
10879
  // src/recents.ts
10880
10880
  import { readFileSync, writeFileSync, mkdirSync, existsSync } from "node:fs";
@@ -13280,10 +13280,10 @@ function joinNamespaceBindings(members, isDir = realIsDir) {
13280
13280
  if (!winner || tied) continue;
13281
13281
  const prev = best.get(winner.dir);
13282
13282
  if (prev === "ambiguous") continue;
13283
- const join49 = { dir: winner.dir, namespace: b.namespace, path: b.path, declaredBy: b.declaredBy };
13284
- if (!prev) best.set(winner.dir, { score: winner.score, join: join49 });
13283
+ const join51 = { dir: winner.dir, namespace: b.namespace, path: b.path, declaredBy: b.declaredBy };
13284
+ if (!prev) best.set(winner.dir, { score: winner.score, join: join51 });
13285
13285
  else if (prev.join.namespace !== b.namespace) best.set(winner.dir, "ambiguous");
13286
- else if (winner.score > prev.score) best.set(winner.dir, { score: winner.score, join: join49 });
13286
+ else if (winner.score > prev.score) best.set(winner.dir, { score: winner.score, join: join51 });
13287
13287
  }
13288
13288
  return [...best.values()].filter((v) => v !== "ambiguous").map((v) => v.join);
13289
13289
  }
@@ -17490,11 +17490,11 @@ var RAN = { ok: "ok", fail: "failed", skipped: "skipped" };
17490
17490
  function tookLabel(ms) {
17491
17491
  return ms < 1e3 ? `${ms}ms` : `${(ms / 1e3).toFixed(1)}s`;
17492
17492
  }
17493
- function paintPlayhead(ir, join49, gate) {
17494
- if (!join49.byNode.size && !join49.at) return ir;
17493
+ function paintPlayhead(ir, join51, gate) {
17494
+ if (!join51.byNode.size && !join51.at) return ir;
17495
17495
  const nodes = ir.nodes.map((node) => {
17496
- const run3 = join49.byNode.get(node.id);
17497
- const here = join49.at === node.id;
17496
+ const run3 = join51.byNode.get(node.id);
17497
+ const here = join51.at === node.id;
17498
17498
  if (!run3 && !here) return node;
17499
17499
  const isGate = node.attrs?._step === "gate";
17500
17500
  const attrs = { ...node.attrs };
@@ -17509,7 +17509,7 @@ function paintPlayhead(ir, join49, gate) {
17509
17509
  if (here) {
17510
17510
  attrs._playhead = true;
17511
17511
  attrs._status = isGate && gate ? "warn" : "accent";
17512
- attrs.run = isGate && gate ? `waiting for approval since ${gate.since}` : join49.atReason ?? "here";
17512
+ attrs.run = isGate && gate ? `waiting for approval since ${gate.since}` : join51.atReason ?? "here";
17513
17513
  if (isGate && gate?.description) attrs.gateDescription = gate.description;
17514
17514
  }
17515
17515
  return { ...node, attrs };
@@ -17531,11 +17531,11 @@ function pendingGateCard(op, gate, ir) {
17531
17531
  }
17532
17532
  };
17533
17533
  }
17534
- function playheadNote(state, join49) {
17534
+ function playheadNote(state, join51) {
17535
17535
  if (!state.op) {
17536
17536
  return "No run state: nothing has been run from this behold session, so every step paints as declared.";
17537
17537
  }
17538
- const settled = join49 ? join49.byNode.size : state.records.length;
17538
+ const settled = join51 ? join51.byNode.size : state.records.length;
17539
17539
  const bits = [];
17540
17540
  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`;
17541
17541
  if (state.status === "running") {
@@ -17551,9 +17551,9 @@ function playheadNote(state, join49) {
17551
17551
  } else if (state.gateNote) {
17552
17552
  bits.push(`gate state unavailable \u2014 ${state.gateNote}`);
17553
17553
  }
17554
- if (join49?.unplaced.length) {
17554
+ if (join51?.unplaced.length) {
17555
17555
  bits.push(
17556
- `${join49.unplaced.length} record${join49.unplaced.length === 1 ? "" : "s"} matched no declared step and ${join49.unplaced.length === 1 ? "is" : "are"} not painted`
17556
+ `${join51.unplaced.length} record${join51.unplaced.length === 1 ? "" : "s"} matched no declared step and ${join51.unplaced.length === 1 ? "is" : "are"} not painted`
17557
17557
  );
17558
17558
  }
17559
17559
  if (!bits.length) return `${head}.`;
@@ -18223,10 +18223,90 @@ var OpRunner = class {
18223
18223
  }
18224
18224
  };
18225
18225
 
18226
+ // src/public-mode.ts
18227
+ import { readFileSync as readFileSync15 } from "node:fs";
18228
+ import { join as join20 } from "node:path";
18229
+ var ReadOnlyRunner = class {
18230
+ running = null;
18231
+ applyProgress = initialApplyProgress;
18232
+ runState = initialRunState;
18233
+ operatorState = initialOperatorState;
18234
+ retarget() {
18235
+ }
18236
+ noteGate() {
18237
+ return initialRunState;
18238
+ }
18239
+ noteDeclaredConvergeOps() {
18240
+ return initialOperatorState;
18241
+ }
18242
+ noteOperator() {
18243
+ return initialOperatorState;
18244
+ }
18245
+ trigger() {
18246
+ return false;
18247
+ }
18248
+ run() {
18249
+ return false;
18250
+ }
18251
+ apply() {
18252
+ return false;
18253
+ }
18254
+ pipeline() {
18255
+ return false;
18256
+ }
18257
+ bringUp() {
18258
+ return false;
18259
+ }
18260
+ track() {
18261
+ return false;
18262
+ }
18263
+ };
18264
+ var REFUSED_READS = [
18265
+ [/^\/api\/ops\/[^/]+\/status$/, "the op tools are off"],
18266
+ [/^\/api\/operator(\/|$)/, "the op tools are off"],
18267
+ [/^\/api\/overlay$/, "it reads the live estate with behold's own credentials"],
18268
+ [/^\/api\/reconcile$/, "it plans against the live estate with behold's own credentials"],
18269
+ [/^\/api\/diff(\/|$)/, "it reads the live estate with behold's own credentials"],
18270
+ [/^\/api\/components\/compare$/, "it reads the live estate with behold's own credentials"],
18271
+ [/^\/api\/resources$/, "it runs chant against the project"],
18272
+ [/^\/api\/ci(\/|$)/, "it reads the pipeline and the forge run"],
18273
+ [/^\/api\/doctor$/, "it probes this machine's tools"],
18274
+ [/^\/api\/substrates$/, "it probes this machine's tools"],
18275
+ [/^\/api\/history$/, "it runs git against the checkout"],
18276
+ [/^\/api\/workspace\/(gates|why)$/, "it runs chant against the workspace"],
18277
+ [/^\/api\/choudoufu\/moves$/, "it runs choudoufu"],
18278
+ [/^\/choudoufu\/morph$/, "it runs choudoufu"],
18279
+ [/^\/api\/sql-yodeler\/lifecycle$/, "a public view shows a terragucci repo"]
18280
+ ];
18281
+ var LIVE_PARAMS = ["env", "target", "consistent", "plan", "live"];
18282
+ function publicReadRefusal(path, query) {
18283
+ for (const [re, why] of REFUSED_READS) if (re.test(path)) return why;
18284
+ if (path === "/api/graph" && LIVE_PARAMS.some((p) => query.has(p))) {
18285
+ return "a public view draws the source graph only; a live read uses behold's own credentials";
18286
+ }
18287
+ return void 0;
18288
+ }
18289
+ var PUBLIC_FLAG = "<script>window.__BEHOLD_PUBLIC__ = true;</script>";
18290
+ function publicPage(webRoot2) {
18291
+ const html = readFileSync15(join20(webRoot2, "index.html"), "utf8");
18292
+ return html.replace("</head>", ` ${PUBLIC_FLAG}
18293
+ </head>`);
18294
+ }
18295
+ function publicRoutes(app, webRoot2) {
18296
+ app.use("*", async (c, next) => {
18297
+ const why = publicReadRefusal(c.req.path, new URL(c.req.url).searchParams);
18298
+ if (why) return c.json({ error: `behold is serving a public, read-only view: ${c.req.path} is refused, because ${why}`, code: "public" }, 403);
18299
+ await next();
18300
+ });
18301
+ const page = () => new Response(publicPage(webRoot2), { headers: { "content-type": "text/html; charset=utf-8", "cache-control": "no-cache" } });
18302
+ app.get("/", () => page());
18303
+ app.get("/index.html", () => page());
18304
+ }
18305
+
18226
18306
  // src/substrates.ts
18227
18307
  import { spawn as spawn5 } from "node:child_process";
18228
- import { existsSync as existsSync14, readFileSync as readFileSync15 } from "node:fs";
18229
- import { join as join20 } from "node:path";
18308
+ import { existsSync as existsSync14, readFileSync as readFileSync16 } from "node:fs";
18309
+ import { join as join21 } from "node:path";
18230
18310
  import { platform } from "node:os";
18231
18311
  var DOCKER_DOWN_DETAIL = "docker is down";
18232
18312
  function probe(cmd, args) {
@@ -18255,11 +18335,11 @@ async function dockerRunning(run3, nameFilter) {
18255
18335
  return out.split(/\r?\n/).map((s) => s.trim()).filter(Boolean);
18256
18336
  }
18257
18337
  function scriptBringUp(projectDir, relPath, label) {
18258
- return existsSync14(join20(projectDir, relPath)) ? { label, cmd: "bash", args: [relPath] } : void 0;
18338
+ return existsSync14(join21(projectDir, relPath)) ? { label, cmd: "bash", args: [relPath] } : void 0;
18259
18339
  }
18260
18340
  function projectLexicons(projectDir) {
18261
18341
  try {
18262
- const src = readFileSync15(join20(projectDir, "chant.config.ts"), "utf-8");
18342
+ const src = readFileSync16(join21(projectDir, "chant.config.ts"), "utf-8");
18263
18343
  const m = src.match(/lexicons\s*:\s*\[([^\]]*)\]/);
18264
18344
  if (!m) return [];
18265
18345
  return [...m[1].matchAll(/["']([^"']+)["']/g)].map((x) => x[1]);
@@ -18311,7 +18391,7 @@ async function detectSubstrates(projectDir, preview = false, boundContext, probe
18311
18391
  ["forgejo", "Forgejo", ".forgejo", "test/forgejo-runtime-e2e.sh"]
18312
18392
  ];
18313
18393
  for (const [name, label, marker, script] of forges) {
18314
- if (!existsSync14(join20(projectDir, marker))) continue;
18394
+ if (!existsSync14(join21(projectDir, marker))) continue;
18315
18395
  const c = docker ? await dockerRunning(run3, name) : [];
18316
18396
  const d = dep(c.length > 0, "container up", "on-demand (pipeline run)");
18317
18397
  subs.push({
@@ -18345,7 +18425,7 @@ async function detectSubstrates(projectDir, preview = false, boundContext, probe
18345
18425
  detail: endpoint ? `targeting ${endpoint}` : "real Fly (FLY_FLAPS_BASE_URL unset)"
18346
18426
  });
18347
18427
  }
18348
- if (existsSync14(join20(projectDir, ".github", "workflows"))) {
18428
+ if (existsSync14(join21(projectDir, ".github", "workflows"))) {
18349
18429
  const gh = await run3("gh", ["auth", "status"]);
18350
18430
  const ready = gh.code === 0;
18351
18431
  subs.push({
@@ -18358,7 +18438,7 @@ async function detectSubstrates(projectDir, preview = false, boundContext, probe
18358
18438
  if (lexicons.includes("temporal")) {
18359
18439
  let hasProfiles = false;
18360
18440
  try {
18361
- hasProfiles = /temporal\s*:\s*\{[\s\S]{0,400}?profiles\s*:/.test(readFileSync15(join20(projectDir, "chant.config.ts"), "utf-8"));
18441
+ hasProfiles = /temporal\s*:\s*\{[\s\S]{0,400}?profiles\s*:/.test(readFileSync16(join21(projectDir, "chant.config.ts"), "utf-8"));
18362
18442
  } catch {
18363
18443
  }
18364
18444
  subs.push({
@@ -18501,8 +18581,8 @@ function statusVocabulary(kinds) {
18501
18581
  }
18502
18582
 
18503
18583
  // src/behaviour.ts
18504
- import { readFileSync as readFileSync16 } from "node:fs";
18505
- import { join as join21 } from "node:path";
18584
+ import { readFileSync as readFileSync17 } from "node:fs";
18585
+ import { join as join22 } from "node:path";
18506
18586
  var rec5 = (v) => v && typeof v === "object" && !Array.isArray(v) ? v : void 0;
18507
18587
  var str6 = (v) => typeof v === "string" && v.trim().length > 0 ? v : void 0;
18508
18588
  var num = (v) => typeof v === "number" && Number.isFinite(v) ? v : void 0;
@@ -18609,7 +18689,7 @@ var FIRST_TOTAL_SHOWN = "two members stated an estate total; the first is shown"
18609
18689
  var behaviourFileName = (env) => `behaviour.${env}.json`;
18610
18690
  var realRead = (path) => {
18611
18691
  try {
18612
- return readFileSync16(path, "utf8");
18692
+ return readFileSync17(path, "utf8");
18613
18693
  } catch {
18614
18694
  return void 0;
18615
18695
  }
@@ -18667,7 +18747,7 @@ function attachBehaviour(ir, members, env, readFile2 = realRead) {
18667
18747
  }
18668
18748
  const file = behaviourFileName(env);
18669
18749
  looked.push(`${label}/${file}`);
18670
- const raw = readFile2(join21(m.dir, file));
18750
+ const raw = readFile2(join22(m.dir, file));
18671
18751
  if (raw === void 0) {
18672
18752
  sources.set(label, "none");
18673
18753
  continue;
@@ -19103,7 +19183,7 @@ init_choudoufu_live();
19103
19183
  init_choudoufu_member();
19104
19184
  import { mkdtempSync, rmSync as rmSync3 } from "node:fs";
19105
19185
  import { tmpdir as tmpdir2 } from "node:os";
19106
- import { join as join22 } from "node:path";
19186
+ import { join as join23 } from "node:path";
19107
19187
  var isRecord10 = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
19108
19188
  var refuse7 = (error, remedy) => ({ ok: false, refusal: { error, code: "choudoufu-live-check", remedy } });
19109
19189
  var CREDS2 = "Give behold the ambient read-only AWS credentials the estate's `plan` needs (AWS_PROFILE, or AWS_ENDPOINT_URL for an emulator), then re-check live with plan.";
@@ -19177,8 +19257,8 @@ var planArgv = (out) => ({
19177
19257
  show: ["show", "-json", out]
19178
19258
  });
19179
19259
  async function readChoudoufuPlan(dir, run3 = captureChoudoufu) {
19180
- const scratch = mkdtempSync(join22(tmpdir2(), "behold-choudoufu-plan-"));
19181
- const file = join22(scratch, "plan.tfplan");
19260
+ const scratch = mkdtempSync(join23(tmpdir2(), "behold-choudoufu-plan-"));
19261
+ const file = join23(scratch, "plan.tfplan");
19182
19262
  const argv = planArgv(file);
19183
19263
  try {
19184
19264
  const planRun = await run3(argv.plan, dir);
@@ -19210,8 +19290,8 @@ init_choudoufu_refs();
19210
19290
  // src/choudoufu-moves.ts
19211
19291
  init_choudoufu_member();
19212
19292
  init_choudoufu_live();
19213
- import { existsSync as existsSync15, readFileSync as readFileSync17 } from "node:fs";
19214
- import { join as join23, relative as relative5 } from "node:path";
19293
+ import { existsSync as existsSync15, readFileSync as readFileSync18 } from "node:fs";
19294
+ import { join as join24, relative as relative5 } from "node:path";
19215
19295
  var isRecord11 = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
19216
19296
  var refuse8 = (error, remedy) => ({ ok: false, refusal: { error, code: "choudoufu-live-check", remedy } });
19217
19297
  var HOW_TO_GET_ONE2 = "A plan is the live-mv workbench's carve.json: `{from, estates[], moves[{address, from, to}]}` \u2014 the workbench's plan phase writes one, or write it by hand.";
@@ -19231,7 +19311,7 @@ function parseCarvePlan(value) {
19231
19311
  }
19232
19312
  };
19233
19313
  }
19234
- function readCarvePlan(path, readFile2 = (p) => readFileSync17(p, "utf8")) {
19314
+ function readCarvePlan(path, readFile2 = (p) => readFileSync18(p, "utf8")) {
19235
19315
  let text;
19236
19316
  try {
19237
19317
  text = readFile2(path);
@@ -19247,7 +19327,7 @@ function readCarvePlan(path, readFile2 = (p) => readFileSync17(p, "utf8")) {
19247
19327
  function discoverCarvePlans(memberDirs, base) {
19248
19328
  const out = [];
19249
19329
  for (const dir of memberDirs) {
19250
- const p = join23(dir, "carve.json");
19330
+ const p = join24(dir, "carve.json");
19251
19331
  if (existsSync15(p)) out.push(relative5(base, p));
19252
19332
  }
19253
19333
  return out;
@@ -19478,24 +19558,24 @@ function getRuntime() {
19478
19558
 
19479
19559
  // node_modules/@intentius/chant/src/lifecycle/member-ledger.ts
19480
19560
  import { existsSync as existsSync18 } from "node:fs";
19481
- import { dirname as dirname8, join as join27, relative as relative6, resolve as resolve15, sep as sep6 } from "node:path";
19561
+ import { dirname as dirname8, join as join28, relative as relative6, resolve as resolve15, sep as sep6 } from "node:path";
19482
19562
 
19483
19563
  // node_modules/@intentius/chant/src/project-root.ts
19484
19564
  import { existsSync as existsSync16 } from "fs";
19485
- import { dirname as dirname6, join as join24, resolve as resolve13 } from "path";
19565
+ import { dirname as dirname6, join as join25, resolve as resolve13 } from "path";
19486
19566
  var WORKSPACE_DECLARATION_FILES = ["chant.workspace.json", "chant.workspace.jsonc"];
19487
19567
  function findWorkspaceRoot(startDir) {
19488
19568
  const start = resolve13(startDir);
19489
19569
  let top;
19490
19570
  for (let dir = start; ; dir = dirname6(dir)) {
19491
- if (existsSync16(join24(dir, ".git"))) {
19571
+ if (existsSync16(join25(dir, ".git"))) {
19492
19572
  top = dir;
19493
19573
  break;
19494
19574
  }
19495
19575
  if (dirname6(dir) === dir) break;
19496
19576
  }
19497
19577
  for (let dir = start; ; dir = dirname6(dir)) {
19498
- const [json2, jsonc] = WORKSPACE_DECLARATION_FILES.map((name) => join24(dir, name));
19578
+ const [json2, jsonc] = WORKSPACE_DECLARATION_FILES.map((name) => join25(dir, name));
19499
19579
  const hasJson = existsSync16(json2);
19500
19580
  const hasJsonc = existsSync16(jsonc);
19501
19581
  if (hasJson && hasJsonc) return { dir, file: json2, conflicting: jsonc };
@@ -19557,7 +19637,7 @@ async function ledgerIn(root, dir) {
19557
19637
  }
19558
19638
  let outer = FLAT;
19559
19639
  const parent = dirname8(root);
19560
- if (parent !== root && !existsSync18(join27(root, ".git"))) {
19640
+ if (parent !== root && !existsSync18(join28(root, ".git"))) {
19561
19641
  const outerFound = findWorkspaceRoot(parent);
19562
19642
  if (outerFound) outer = await ledgerIn(outerFound.dir, root);
19563
19643
  }
@@ -20112,7 +20192,7 @@ function decideOverride(input) {
20112
20192
 
20113
20193
  // src/terragucci-repo.ts
20114
20194
  import { existsSync as existsSync19 } from "node:fs";
20115
- import { dirname as dirname9, join as join28, resolve as resolve16 } from "node:path";
20195
+ import { dirname as dirname9, join as join29, resolve as resolve16 } from "node:path";
20116
20196
  var TERRAGUCCI_CONFIG_NAMES = ["terragucci.yml", "terragucci.yaml", "terragucci.json", "terragucci.ts"];
20117
20197
  function terragucciConfig(dir) {
20118
20198
  return walkUp(dir, TERRAGUCCI_CONFIG_NAMES);
@@ -20121,10 +20201,10 @@ function walkUp(dir, names) {
20121
20201
  let at2 = resolve16(dir);
20122
20202
  for (; ; ) {
20123
20203
  for (const name of names) {
20124
- const file = join28(at2, name);
20204
+ const file = join29(at2, name);
20125
20205
  if (existsSync19(file)) return file;
20126
20206
  }
20127
- if (existsSync19(join28(at2, ".git"))) return void 0;
20207
+ if (existsSync19(join29(at2, ".git"))) return void 0;
20128
20208
  const up = dirname9(at2);
20129
20209
  if (up === at2) return void 0;
20130
20210
  at2 = up;
@@ -20529,6 +20609,9 @@ function guardRequest(req, cfg) {
20529
20609
  return { ok: false, status: 421, error: `behold doesn't answer to host ${name}. Start it with --allow-host ${name} if a proxy of yours sends that name.` };
20530
20610
  }
20531
20611
  if (SAFE.has(req.method.toUpperCase())) return { ok: true };
20612
+ if (cfg.public) {
20613
+ return { ok: false, status: 405, error: `behold is serving a public, read-only view: a ${req.method.toUpperCase()} is refused (GET, HEAD and OPTIONS only)` };
20614
+ }
20532
20615
  if ((req.secFetchSite || "").toLowerCase() === "cross-site") {
20533
20616
  return { ok: false, status: 403, error: "a cross-site page can't write to behold" };
20534
20617
  }
@@ -20924,7 +21007,7 @@ init_foreign();
20924
21007
 
20925
21008
  // src/events.ts
20926
21009
  import { watch, existsSync as existsSync20 } from "node:fs";
20927
- import { join as join29 } from "node:path";
21010
+ import { join as join30 } from "node:path";
20928
21011
  var Broadcaster = class {
20929
21012
  listeners = /* @__PURE__ */ new Set();
20930
21013
  subscribe(fn) {
@@ -20942,7 +21025,7 @@ var Broadcaster = class {
20942
21025
  };
20943
21026
  var IGNORE = /(^|[\\/])(node_modules|dist|\.git)([\\/]|$)/;
20944
21027
  function watchSource(projectDir, onChange, debounceMs = 200) {
20945
- const dir = existsSync20(join29(projectDir, "src")) ? join29(projectDir, "src") : projectDir;
21028
+ const dir = existsSync20(join30(projectDir, "src")) ? join30(projectDir, "src") : projectDir;
20946
21029
  let timer;
20947
21030
  const watcher = watch(dir, { recursive: true }, (_event, file) => {
20948
21031
  const name = typeof file === "string" ? file : "";
@@ -21245,13 +21328,13 @@ async function emulatorDown(projectDir) {
21245
21328
 
21246
21329
  // src/terragucci-view.ts
21247
21330
  import { execFileSync as execFileSync3 } from "node:child_process";
21248
- import { dirname as dirname13, join as join37, relative as relative8, sep as sep7 } from "node:path";
21331
+ import { dirname as dirname13, join as join39, relative as relative8, sep as sep7 } from "node:path";
21249
21332
  init_terraform_member();
21250
21333
 
21251
21334
  // src/terragucci-reports.ts
21252
- import { readFileSync as readFileSync22, statSync as statSync10 } from "node:fs";
21335
+ import { readFileSync as readFileSync23, statSync as statSync10 } from "node:fs";
21253
21336
  import { createRequire as createRequire4 } from "node:module";
21254
- import { dirname as dirname10, join as join32, resolve as resolve20 } from "node:path";
21337
+ import { dirname as dirname10, join as join33, resolve as resolve20 } from "node:path";
21255
21338
  import { fileURLToPath as fileURLToPath5 } from "node:url";
21256
21339
  import { Ajv2020 } from "ajv/dist/2020.js";
21257
21340
 
@@ -21260,12 +21343,12 @@ import { execFile as execFile4 } from "node:child_process";
21260
21343
  import { createHash as createHash8 } from "node:crypto";
21261
21344
  import { existsSync as existsSync21, mkdirSync as mkdirSync6, readdirSync as readdirSync11 } from "node:fs";
21262
21345
  import { tmpdir as tmpdir3 } from "node:os";
21263
- import { basename as basename3, isAbsolute as isAbsolute4, join as join31, resolve as resolve19 } from "node:path";
21346
+ import { basename as basename3, isAbsolute as isAbsolute4, join as join32, resolve as resolve19 } from "node:path";
21264
21347
 
21265
21348
  // node_modules/@intentius/chant/src/gate-decision.ts
21266
21349
  import { spawnSync as spawnSync2 } from "node:child_process";
21267
- import { readFileSync as readFileSync21 } from "node:fs";
21268
- import { join as join30, posix as posix3 } from "node:path";
21350
+ import { readFileSync as readFileSync22 } from "node:fs";
21351
+ import { join as join31, posix as posix3 } from "node:path";
21269
21352
 
21270
21353
  // node_modules/@intentius/chant/src/lifecycle/gate-applied.ts
21271
21354
  function appliedGatePath(op) {
@@ -21374,7 +21457,7 @@ function pinnedClassesAt(repo, base, pins, packages) {
21374
21457
  }
21375
21458
  let installed2;
21376
21459
  try {
21377
- installed2 = JSON.parse(readFileSync21(join30(dir, "package.json"), "utf-8")).version;
21460
+ installed2 = JSON.parse(readFileSync22(join31(dir, "package.json"), "utf-8")).version;
21378
21461
  } catch {
21379
21462
  installed2 = void 0;
21380
21463
  }
@@ -21969,7 +22052,7 @@ var realGit = (args, o = {}) => new Promise((res) => {
21969
22052
  });
21970
22053
  var redact = (s) => s.replace(/(\b[a-z][a-z0-9+.-]*:\/\/)[^/@\s]+@/gi, "$1");
21971
22054
  function cacheDir(origin, root = tmpdir3()) {
21972
- return join31(root, `${CACHE_PREFIX}${createHash8("sha256").update(origin).digest("hex").slice(0, 16)}`);
22055
+ return join32(root, `${CACHE_PREFIX}${createHash8("sha256").update(origin).digest("hex").slice(0, 16)}`);
21973
22056
  }
21974
22057
  var inflight = /* @__PURE__ */ new Map();
21975
22058
  function serially(key, fn) {
@@ -21988,7 +22071,7 @@ function fetchable(url, checkout) {
21988
22071
  async function fetchIntoCache(git3, origin, cache6) {
21989
22072
  assertScratch(basename3(cache6));
21990
22073
  return serially(cache6, async () => {
21991
- if (!existsSync21(join31(cache6, "HEAD"))) {
22074
+ if (!existsSync21(join32(cache6, "HEAD"))) {
21992
22075
  if (existsSync21(cache6) && readdirSync11(cache6).length > 0) return { ok: false, error: `${cache6} exists and is not behold's lifecycle cache; behold leaves it alone` };
21993
22076
  mkdirSync6(cache6, { recursive: true });
21994
22077
  const init = await git3(["init", "--bare", "-q", cache6]);
@@ -22176,7 +22259,7 @@ function structural(name, doc) {
22176
22259
  var compiledSchemas = /* @__PURE__ */ new Map();
22177
22260
  function schemaStamp(dist) {
22178
22261
  try {
22179
- const files = [join32(dist, "..", "package.json"), ...Object.values(SCHEMA_FILES).map((f) => join32(dist, f))];
22262
+ const files = [join33(dist, "..", "package.json"), ...Object.values(SCHEMA_FILES).map((f) => join33(dist, f))];
22180
22263
  return [dist, ...files.map((f) => {
22181
22264
  const st = statSync10(f);
22182
22265
  return `${st.mtimeMs}:${st.size}`;
@@ -22186,10 +22269,10 @@ function schemaStamp(dist) {
22186
22269
  }
22187
22270
  }
22188
22271
  function compileSchemas(dist) {
22189
- const version = JSON.parse(readFileSync22(join32(dist, "..", "package.json"), "utf8")).version ?? "";
22272
+ const version = JSON.parse(readFileSync23(join33(dist, "..", "package.json"), "utf8")).version ?? "";
22190
22273
  const ajv = new Ajv2020({ strict: false, allErrors: false });
22191
22274
  const compiled4 = Object.fromEntries(
22192
- Object.keys(SCHEMA_FILES).map((n) => [n, ajv.compile(JSON.parse(readFileSync22(join32(dist, SCHEMA_FILES[n]), "utf8")))])
22275
+ Object.keys(SCHEMA_FILES).map((n) => [n, ajv.compile(JSON.parse(readFileSync23(join33(dist, SCHEMA_FILES[n]), "utf8")))])
22193
22276
  );
22194
22277
  return {
22195
22278
  validation: { by: "schema", terragucci: version },
@@ -22206,7 +22289,7 @@ function terragucciValidator(dirs = []) {
22206
22289
  for (const from of [...dirs, dirname10(fileURLToPath5(import.meta.url))]) {
22207
22290
  let entry;
22208
22291
  try {
22209
- entry = createRequire4(join32(resolve20(from), "noop.js")).resolve("@intentius/terragucci/report.schema.json");
22292
+ entry = createRequire4(join33(resolve20(from), "noop.js")).resolve("@intentius/terragucci/report.schema.json");
22210
22293
  } catch {
22211
22294
  continue;
22212
22295
  }
@@ -22330,6 +22413,10 @@ async function readTerragucci(source2, opts = {}) {
22330
22413
  }
22331
22414
  }
22332
22415
  }
22416
+ let pullRequest;
22417
+ const prRow = rows.find((r) => r.stage === "tf-plan" && r.pull_request);
22418
+ const prDoc = prRow ? await report(prRow) : void 0;
22419
+ if (prRow && prDoc) pullRequest = pullRequestPlan(prDoc, under(prRow.path, "report.html"));
22333
22420
  const estateDoc = await readDoc(source2, "estate.json", "estate", v);
22334
22421
  const estateProject = estateDoc?.projects.find((p) => p.project === project);
22335
22422
  const waiting = [];
@@ -22375,6 +22462,7 @@ async function readTerragucci(source2, opts = {}) {
22375
22462
  reports: cache6.size,
22376
22463
  roots,
22377
22464
  waiting,
22465
+ ...pullRequest ? { pull_request: pullRequest } : {},
22378
22466
  ...estateDoc && estateProject ? {
22379
22467
  estate: {
22380
22468
  generated: estateDoc.generated,
@@ -22387,6 +22475,57 @@ async function readTerragucci(source2, opts = {}) {
22387
22475
  } : {}
22388
22476
  };
22389
22477
  }
22478
+ function pullRequestPlan(doc, report) {
22479
+ return {
22480
+ run: {
22481
+ commit: doc.run.commit,
22482
+ finished: doc.run.finished,
22483
+ pull_request: doc.run.pull_request ?? "",
22484
+ ...doc.run.pull_request_url ? { pull_request_url: doc.run.pull_request_url } : {},
22485
+ ...doc.run.job_url ? { job_url: doc.run.job_url } : {},
22486
+ report
22487
+ },
22488
+ reads: doc.roots.flatMap(
22489
+ (r) => (r.reads ?? []).map((x) => ({
22490
+ root: r.path,
22491
+ upstream: x.upstream,
22492
+ data: x.data,
22493
+ outputs: x.outputs,
22494
+ ...x.unknown?.length ? { unknown: [...x.unknown] } : {},
22495
+ ...x.why ? { why: x.why } : {},
22496
+ ...x.composed ? { composed: true } : {}
22497
+ }))
22498
+ ),
22499
+ roots: doc.roots.map((r) => r.path),
22500
+ waves: doc.waves.map((w) => ({
22501
+ number: w.number,
22502
+ roots: w.roots,
22503
+ ...w.state ? { state: w.state } : {},
22504
+ // A wave that plans again has no digest a review binds until it does.
22505
+ set_digest: w.replans_after?.length ? null : w.set_digest,
22506
+ ...w.review_digest !== void 0 ? { review_digest: w.replans_after?.length ? null : w.review_digest } : {},
22507
+ ...w.waits !== void 0 ? { waits: w.waits } : {},
22508
+ ...w.reads?.length ? { reads: w.reads } : {},
22509
+ ...w.replans_after?.length ? { replans_after: w.replans_after } : {},
22510
+ ...w.cost ? { cost: w.cost } : {},
22511
+ // #568 step 3: each member's changes come from its own slice, under its own name.
22512
+ ...w.composed ? {
22513
+ composed: {
22514
+ ...w.composed,
22515
+ members: w.composed.members.map((m) => {
22516
+ const counts = doc.roots.find((r) => r.path === m.root)?.counts;
22517
+ return { ...m, ...counts ? { counts } : {} };
22518
+ })
22519
+ }
22520
+ } : {}
22521
+ })),
22522
+ ...doc.compose ? { compose: doc.compose } : {},
22523
+ ...doc.totals ? { totals: doc.totals } : {},
22524
+ ...doc.totals_roots !== void 0 ? { totals_roots: doc.totals_roots } : {},
22525
+ ...doc.cost ? { cost: doc.cost } : {},
22526
+ ...doc.blast ? { blast: doc.blast } : {}
22527
+ };
22528
+ }
22390
22529
 
22391
22530
  // src/terragucci-overlay.ts
22392
22531
  function blockPath(address) {
@@ -22491,11 +22630,11 @@ function joinTerragucci(read, nodes, roots) {
22491
22630
  // src/terragucci-source.ts
22492
22631
  import { spawn as spawn7 } from "node:child_process";
22493
22632
  import { readFile } from "node:fs/promises";
22494
- import { join as join33, resolve as resolve21 } from "node:path";
22633
+ import { join as join34, resolve as resolve21 } from "node:path";
22495
22634
 
22496
22635
  // src/s3-object.ts
22497
22636
  import { createHash as createHash9, createHmac } from "node:crypto";
22498
- import { readFileSync as readFileSync23 } from "node:fs";
22637
+ import { readFileSync as readFileSync24 } from "node:fs";
22499
22638
  var S3Error = class extends Error {
22500
22639
  /** The HTTP status S3 answered, when it answered. */
22501
22640
  status;
@@ -22542,7 +22681,7 @@ function s3FromEnv(bucket, env = process.env) {
22542
22681
  async function assumeRole(w, fetchFn) {
22543
22682
  let token;
22544
22683
  try {
22545
- token = readFileSync23(w.tokenFile, "utf8").trim();
22684
+ token = readFileSync24(w.tokenFile, "utf8").trim();
22546
22685
  } catch (e) {
22547
22686
  throw new S3Error(`cannot read the OIDC token in ${w.tokenFile}: ${e.message}`);
22548
22687
  }
@@ -22739,7 +22878,7 @@ function terragucciSource(spec2, opts = {}) {
22739
22878
  location: dir,
22740
22879
  async read(key) {
22741
22880
  try {
22742
- return await readFile(join33(dir, ...trim(key).split("/")), "utf8");
22881
+ return await readFile(join34(dir, ...trim(key).split("/")), "utf8");
22743
22882
  } catch (e) {
22744
22883
  if (e.code === "ENOENT") return void 0;
22745
22884
  throw e;
@@ -22749,9 +22888,9 @@ function terragucciSource(spec2, opts = {}) {
22749
22888
  }
22750
22889
 
22751
22890
  // src/terragucci-timeline.ts
22752
- import { readFileSync as readFileSync24, statSync as statSync11 } from "node:fs";
22891
+ import { readFileSync as readFileSync25, statSync as statSync11 } from "node:fs";
22753
22892
  import { createRequire as createRequire5 } from "node:module";
22754
- import { dirname as dirname11, join as join34, resolve as resolve22 } from "node:path";
22893
+ import { dirname as dirname11, join as join35, resolve as resolve22 } from "node:path";
22755
22894
  import { fileURLToPath as fileURLToPath6 } from "node:url";
22756
22895
  import { Ajv2020 as Ajv20202 } from "ajv/dist/2020.js";
22757
22896
  var AUDIT_SCHEMA = "terragucci.audit/v1";
@@ -22792,12 +22931,12 @@ function auditValidator(dirs = []) {
22792
22931
  for (const from of [...dirs, dirname11(fileURLToPath6(import.meta.url))]) {
22793
22932
  let dist;
22794
22933
  try {
22795
- dist = dirname11(createRequire5(join34(resolve22(from), "noop.js")).resolve("@intentius/terragucci/report.schema.json"));
22934
+ dist = dirname11(createRequire5(join35(resolve22(from), "noop.js")).resolve("@intentius/terragucci/report.schema.json"));
22796
22935
  } catch {
22797
22936
  continue;
22798
22937
  }
22799
- const pkg = join34(dist, "..", "package.json");
22800
- const file = join34(dist, AUDIT_SCHEMA_FILE);
22938
+ const pkg = join35(dist, "..", "package.json");
22939
+ const file = join35(dist, AUDIT_SCHEMA_FILE);
22801
22940
  let stamp;
22802
22941
  try {
22803
22942
  stamp = [dist, ...[pkg, file].map((f) => {
@@ -22811,8 +22950,8 @@ function auditValidator(dirs = []) {
22811
22950
  if (hit) return hit;
22812
22951
  let v;
22813
22952
  try {
22814
- const version = JSON.parse(readFileSync24(pkg, "utf8")).version ?? "";
22815
- v = compileAuditSchema(JSON.parse(readFileSync24(file, "utf8")), version);
22953
+ const version = JSON.parse(readFileSync25(pkg, "utf8")).version ?? "";
22954
+ v = compileAuditSchema(JSON.parse(readFileSync25(file, "utf8")), version);
22816
22955
  } catch {
22817
22956
  continue;
22818
22957
  }
@@ -23008,7 +23147,7 @@ function terragucciTimelineRoute(app, source2, opts = {}) {
23008
23147
  // src/terragucci-poll.ts
23009
23148
  import { createHash as createHash10 } from "node:crypto";
23010
23149
  import { stat } from "node:fs/promises";
23011
- import { join as join35, resolve as resolve23 } from "node:path";
23150
+ import { join as join36, resolve as resolve23 } from "node:path";
23012
23151
  import { streamSSE } from "hono/streaming";
23013
23152
  var TERRAGUCCI_POLL_SECS = 30;
23014
23153
  var INDEX_KEY = "index.json";
@@ -23059,7 +23198,7 @@ function indexProbe(spec2, opts = {}) {
23059
23198
  return verdict2(prev, etag);
23060
23199
  };
23061
23200
  }
23062
- const file = join35(resolve23(opts.cwd ?? process.cwd(), spec2), name);
23201
+ const file = join36(resolve23(opts.cwd ?? process.cwd(), spec2), name);
23063
23202
  return async (prev) => {
23064
23203
  try {
23065
23204
  const s = await stat(file);
@@ -23214,9 +23353,9 @@ function terragucciLaneRoutes(app, o, base, forget) {
23214
23353
  }
23215
23354
 
23216
23355
  // src/terragucci-progress.ts
23217
- import { readFileSync as readFileSync25, statSync as statSync12 } from "node:fs";
23356
+ import { readFileSync as readFileSync26, statSync as statSync12 } from "node:fs";
23218
23357
  import { createRequire as createRequire6 } from "node:module";
23219
- import { dirname as dirname12, join as join36, resolve as resolve24 } from "node:path";
23358
+ import { dirname as dirname12, join as join37, resolve as resolve24 } from "node:path";
23220
23359
  import { fileURLToPath as fileURLToPath7 } from "node:url";
23221
23360
  import { Ajv2020 as Ajv20203 } from "ajv/dist/2020.js";
23222
23361
  var RUN_SCHEMA = "terragucci.run/v1";
@@ -23261,12 +23400,12 @@ function runValidator(dirs = []) {
23261
23400
  for (const from of [...dirs, dirname12(fileURLToPath7(import.meta.url))]) {
23262
23401
  let dist;
23263
23402
  try {
23264
- dist = dirname12(createRequire6(join36(resolve24(from), "noop.js")).resolve("@intentius/terragucci/report.schema.json"));
23403
+ dist = dirname12(createRequire6(join37(resolve24(from), "noop.js")).resolve("@intentius/terragucci/report.schema.json"));
23265
23404
  } catch {
23266
23405
  continue;
23267
23406
  }
23268
- const pkg = join36(dist, "..", "package.json");
23269
- const file = join36(dist, RUN_SCHEMA_FILE);
23407
+ const pkg = join37(dist, "..", "package.json");
23408
+ const file = join37(dist, RUN_SCHEMA_FILE);
23270
23409
  let stamp;
23271
23410
  try {
23272
23411
  stamp = [dist, ...[pkg, file].map((f) => {
@@ -23280,8 +23419,8 @@ function runValidator(dirs = []) {
23280
23419
  if (hit) return hit;
23281
23420
  let v;
23282
23421
  try {
23283
- const version = JSON.parse(readFileSync25(pkg, "utf8")).version ?? "";
23284
- v = compileRunSchema(JSON.parse(readFileSync25(file, "utf8")), version);
23422
+ const version = JSON.parse(readFileSync26(pkg, "utf8")).version ?? "";
23423
+ v = compileRunSchema(JSON.parse(readFileSync26(file, "utf8")), version);
23285
23424
  } catch {
23286
23425
  continue;
23287
23426
  }
@@ -23307,7 +23446,10 @@ function runKey(row) {
23307
23446
  var zero = () => ({ done: 0, "in-flight": 0, waiting: 0, "not-applied": 0 });
23308
23447
  var MARK_ORDER = ["not-applied", "in-flight", "waiting", "done"];
23309
23448
  function unsettled(run3) {
23310
- return run3.waves.some((w) => w.state === "applying" || (w.progress?.resources ?? []).some((r) => r.status === "in-flight" || r.status === "waiting"));
23449
+ if (run3.waves.some((w) => w.state === "applying" || (w.progress?.resources ?? []).some((r) => r.status === "in-flight" || r.status === "waiting"))) return true;
23450
+ if (run3.waves.some((w) => w.state === "waiting")) return true;
23451
+ if (run3.waves.some((w) => w.state === "failed" || w.state === "refused")) return false;
23452
+ return run3.waves.some((w) => w.state === "applied") && run3.waves.some((w) => w.state === "not-started");
23311
23453
  }
23312
23454
  function joinProgress(run3, nodes, roots) {
23313
23455
  const byRoot = /* @__PURE__ */ new Map();
@@ -23350,12 +23492,36 @@ function joinProgress(run3, nodes, roots) {
23350
23492
  }
23351
23493
  waves.push({ number: w.number, state: w.state, gate: w.gate, read: w.progress.read, counts, resources });
23352
23494
  }
23353
- return { waves, cards, unmatched, watching: unsettled(run3) };
23495
+ const states = run3.waves.map((w) => ({
23496
+ number: w.number,
23497
+ state: w.state,
23498
+ gate: w.gate,
23499
+ roots: [...w.roots],
23500
+ reads: [...w.reads ?? []],
23501
+ ...w.approval ? { approval: w.approval } : {},
23502
+ ...w.digest !== void 0 ? { digest: w.digest } : {},
23503
+ // The run view writes the approve line while the wave waits; behold's own form when it does not.
23504
+ ...w.state === "waiting" ? { command: w.command ?? terragucciApproveCommand(w.number, w.digest ?? null) } : {},
23505
+ ...w.report ? { report: `${w.report}/report.html` } : {},
23506
+ ...w.changed ? { changed: [...w.changed] } : {},
23507
+ ...w.totals ? { totals: { ...w.totals } } : {},
23508
+ ...w.totals_roots !== void 0 ? { totals_roots: w.totals_roots } : {},
23509
+ ...w.composed ? {
23510
+ composed: {
23511
+ roots: [...w.composed.roots],
23512
+ ...w.composed.digest !== void 0 ? { digest: w.composed.digest } : {},
23513
+ ...w.composed.unknown ? { unknown: [...w.composed.unknown] } : {},
23514
+ ...w.composed.members ? { members: w.composed.members.map((m) => ({ ...m })) } : {}
23515
+ }
23516
+ } : {}
23517
+ }));
23518
+ const reads2 = (run3.roots ?? []).map((r) => ({ root: r.root, wave: r.wave, reads: [...r.reads ?? []], ...r.composed ? { composed: { ...r.composed } } : {} }));
23519
+ return { states, reads: reads2, waves, cards, unmatched, watching: unsettled(run3) };
23354
23520
  }
23355
23521
  var ProgressGraphError = class extends Error {
23356
23522
  };
23357
23523
  var FILES = "/api/terragucci/file?key=";
23358
- var progressKey = (a) => JSON.stringify([a.run, a.waves, a.unmatched]);
23524
+ var progressKey = (a) => JSON.stringify([a.run, a.waves, a.unmatched, a.states, a.reads]);
23359
23525
  var TerragucciProgress = class {
23360
23526
  constructor(o) {
23361
23527
  this.o = o;
@@ -23410,6 +23576,8 @@ var TerragucciProgress = class {
23410
23576
  ...this.base(project, v?.run.validation),
23411
23577
  run: null,
23412
23578
  absent: `index.json lists no tf-apply run of ${project} yet, so there is no run view to read.`,
23579
+ states: [],
23580
+ reads: [],
23413
23581
  waves: [],
23414
23582
  cards: {},
23415
23583
  unmatched: [],
@@ -23446,6 +23614,8 @@ var TerragucciProgress = class {
23446
23614
  run: { commit, key, present: false },
23447
23615
  ...unreadable ? { unreadable: true } : {},
23448
23616
  absent: unreadable ?? `${src.location} has no ${key}: terragucci writes the run view when reports.bucket is set, and a wave's progress per resource only for a choudoufu wave (terragucci 0.4.7 and newer). This says nothing about whether the apply moved.`,
23617
+ states: [],
23618
+ reads: [],
23449
23619
  waves: [],
23450
23620
  cards: {},
23451
23621
  unmatched: [],
@@ -23516,6 +23686,90 @@ function terragucciProgressRoute(app, progress) {
23516
23686
  });
23517
23687
  }
23518
23688
 
23689
+ // src/terragucci-demo.ts
23690
+ import { readFileSync as readFileSync27 } from "node:fs";
23691
+ import { join as join38 } from "node:path";
23692
+ var DEMO_KEY = "demo.json";
23693
+ var DEMO_READ_TTL_MS = 2e3;
23694
+ var DEMO_STEPS = ["pull-request", "planned", "merged", "applying", "waiting", "approved", "failed", "resumed", "applied", "reset", "stalled"];
23695
+ var UTC = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z$/;
23696
+ var isRecord17 = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
23697
+ var str13 = (v) => typeof v === "string";
23698
+ var int = (v) => typeof v === "number" && Number.isInteger(v);
23699
+ function demoProblem(doc) {
23700
+ if (!isRecord17(doc)) return "it is not an object";
23701
+ if (!int(doc.cycle)) return "cycle is not an integer";
23702
+ if (!str13(doc.scenario) || !doc.scenario) return "scenario is not a string";
23703
+ if (!str13(doc.title)) return "title is not a string";
23704
+ if (!str13(doc.step) || !DEMO_STEPS.includes(doc.step)) return `step is not one of ${DEMO_STEPS.join(", ")}`;
23705
+ if (!str13(doc.label)) return "label is not a string";
23706
+ if (!str13(doc.started_at) || !UTC.test(doc.started_at)) return "started_at is not a UTC time (YYYY-MM-DDTHH:MM:SSZ)";
23707
+ if (doc.next !== null) {
23708
+ if (!isRecord17(doc.next)) return "next is neither an object nor null";
23709
+ if (!str13(doc.next.action)) return "next.action is not a string";
23710
+ if (doc.next.at !== null && !(str13(doc.next.at) && UTC.test(doc.next.at))) return "next.at is neither a UTC time nor null";
23711
+ }
23712
+ if (doc.pull !== null && !int(doc.pull)) return "pull is neither an integer nor null";
23713
+ if (doc.commit !== null && !str13(doc.commit)) return "commit is neither a string nor null";
23714
+ return void 0;
23715
+ }
23716
+ async function readDemo(source2, now = /* @__PURE__ */ new Date()) {
23717
+ const base = { source: source2.location, read: now.toISOString() };
23718
+ let body;
23719
+ try {
23720
+ body = await source2.read(DEMO_KEY);
23721
+ } catch (e) {
23722
+ if (e instanceof TerragucciDenied) return { ...base, demo: null, error: deniedWords(e, "the demo's narration"), code: "terragucci-demo" };
23723
+ return { ...base, demo: null, error: `behold could not read ${DEMO_KEY} from ${source2.location}: ${e instanceof Error ? e.message : String(e)}`, code: "terragucci-demo" };
23724
+ }
23725
+ if (body === void 0) {
23726
+ return { ...base, demo: null, absent: `${source2.location} has no ${DEMO_KEY}: the demo's loop driver writes it at every step (terragucci's stack/demo.sh), beside index.json.` };
23727
+ }
23728
+ let doc;
23729
+ try {
23730
+ doc = JSON.parse(body);
23731
+ } catch {
23732
+ return { ...base, demo: null, error: `${DEMO_KEY} in ${source2.location} is not JSON.`, code: "terragucci-demo" };
23733
+ }
23734
+ const problem = demoProblem(doc);
23735
+ if (problem) return { ...base, demo: null, error: `${DEMO_KEY} in ${source2.location} is not the demo's format: ${problem}.`, code: "terragucci-demo" };
23736
+ const d = doc;
23737
+ return {
23738
+ ...base,
23739
+ demo: {
23740
+ cycle: d.cycle,
23741
+ scenario: d.scenario,
23742
+ title: d.title,
23743
+ step: d.step,
23744
+ label: d.label,
23745
+ started_at: d.started_at,
23746
+ next: d.next ? { action: d.next.action, at: d.next.at } : null,
23747
+ pull: d.pull,
23748
+ commit: d.commit
23749
+ }
23750
+ };
23751
+ }
23752
+ function terragucciDemoRoute(app, source2, opts = {}) {
23753
+ let cached;
23754
+ app.get("/api/terragucci/demo", async (c) => {
23755
+ const at2 = Date.now();
23756
+ if (!cached || at2 - cached.at >= DEMO_READ_TTL_MS) cached = { at: at2, read: readDemo(source2, opts.now ? opts.now() : /* @__PURE__ */ new Date()) };
23757
+ const a = await cached.read;
23758
+ return c.json(a, "error" in a ? 502 : 200, { "cache-control": "no-store" });
23759
+ });
23760
+ }
23761
+ function kioskRoute(app, webRoot2, enabled) {
23762
+ app.get("/kiosk", (c) => {
23763
+ if (!enabled) {
23764
+ return c.html(
23765
+ `<!doctype html><meta charset=utf-8><title>behold kiosk</title><script type="module">import { initTheme } from "/theme.js"; initTheme();</script><body style="font:16px system-ui;background:var(--bg,#1e1e2e);color:var(--fg,#cdd6f4);padding:2rem"><h3>The kiosk follows a terragucci demo</h3><p style="color:var(--muted,#777a8c)">Start behold with <code>--terragucci &lt;reports source&gt;</code>; the kiosk reads <code>demo.json</code> there.</p></body>`,
23766
+ 404
23767
+ );
23768
+ }
23769
+ return new Response(readFileSync27(join38(webRoot2, "kiosk.html"), "utf8"), { headers: { "content-type": "text/html; charset=utf-8", "cache-control": "no-cache" } });
23770
+ });
23771
+ }
23772
+
23519
23773
  // src/terragucci-view.ts
23520
23774
  var TERRAGUCCI_READ_TTL_MS = 3e4;
23521
23775
  function repoBase(dirs) {
@@ -23554,7 +23808,7 @@ function checkoutRoots(dirs, base = repoBase(dirs)) {
23554
23808
  for (const m of estateMembers(dirs)) {
23555
23809
  if (m.kind !== "terraform") continue;
23556
23810
  for (const r of discoverTerraformRoots(m.dir).roots) {
23557
- const path = relative8(base, join37(m.dir, r.dir)).split(sep7).join("/") || ".";
23811
+ const path = relative8(base, join39(m.dir, r.dir)).split(sep7).join("/") || ".";
23558
23812
  out.push({ member: m.name, name: r.name, path });
23559
23813
  }
23560
23814
  }
@@ -23630,9 +23884,12 @@ function terragucciRoutes(app, opts, dirs) {
23630
23884
  // Where a key opens: this server, which reads it from the same source.
23631
23885
  files: "/api/terragucci/file?key=",
23632
23886
  ...r.estate ? { estate: r.estate } : {},
23887
+ // #568: the pull request stage, read from the newest tf-plan of a pull request.
23888
+ ...r.pull_request ? { pull_request: r.pull_request } : {},
23633
23889
  ...marks
23634
23890
  });
23635
23891
  });
23892
+ terragucciDemoRoute(app, source2, opts.now ? { now: opts.now } : {});
23636
23893
  terragucciTimelineRoute(app, source2, {
23637
23894
  ...opts.project ? { project: opts.project } : {},
23638
23895
  checkoutProject: () => checkoutProject(repoBase(dirs())),
@@ -23697,7 +23954,7 @@ var heldByOf = (v) => {
23697
23954
  return h && typeof h === "object" && typeof h.hook === "string" && typeof h.moment === "string" ? { hook: h.hook, moment: h.moment, ...typeof h.target === "string" ? { target: h.target } : {} } : void 0;
23698
23955
  };
23699
23956
  var modeOf = (v) => v === "ledger" || v === "pr-review" || v === "sealed" ? v : void 0;
23700
- var str13 = (v) => typeof v === "string" && v !== "" ? v : void 0;
23957
+ var str14 = (v) => typeof v === "string" && v !== "" ? v : void 0;
23701
23958
  var strings3 = (v) => Array.isArray(v) ? v.filter((x) => typeof x === "string") : void 0;
23702
23959
  var time3 = (iso) => Date.parse(iso) || 0;
23703
23960
  function rowOf(line) {
@@ -23713,14 +23970,14 @@ function rowOf(line) {
23713
23970
  }
23714
23971
  function sealOf(r) {
23715
23972
  const s = r.seal;
23716
- const signed = s && s.signature ? { sealed: true, ...str13(s.signer) ? { signer: str13(s.signer) } : {} } : void 0;
23973
+ const signed = s && s.signature ? { sealed: true, ...str14(s.signer) ? { signer: str14(s.signer) } : {} } : void 0;
23717
23974
  if (r.review || r.via === "pr-review") return { seal: "review", ...signed };
23718
23975
  return signed ? { seal: "sealed", ...signed } : { seal: "unsigned" };
23719
23976
  }
23720
23977
  function reviewOf(r) {
23721
23978
  const rv = r.review ?? void 0;
23722
23979
  const pr = typeof rv?.pr === "number" ? rv.pr : typeof r.pr === "number" ? r.pr : void 0;
23723
- const head = str13(rv?.head) ?? str13(r.head);
23980
+ const head = str14(rv?.head) ?? str14(r.head);
23724
23981
  const approvers = strings3(rv?.approvers) ?? strings3(r.reviewers);
23725
23982
  if (pr === void 0 && head === void 0 && approvers === void 0) return void 0;
23726
23983
  return { ...pr !== void 0 ? { pr } : {}, ...head ? { head } : {}, ...approvers ? { approvers } : {} };
@@ -23746,7 +24003,7 @@ function decisionOf(d) {
23746
24003
  const r = v;
23747
24004
  if (!r || typeof r !== "object" || typeof r.wave !== "number" || typeof r.name !== "string" || typeof r.op !== "string" || typeof r.gate !== "string") return void 0;
23748
24005
  const outcomes2 = Array.isArray(r.outcomes) ? r.outcomes.filter((o) => !!o && typeof o === "object" && typeof o.moment === "string" && typeof o.name === "string" && typeof o.exit === "number" && typeof o.outcome === "string") : void 0;
23749
- return { wave: r.wave, name: r.name, op: r.op, gate: r.gate, ...str13(r.digest) ? { digest: str13(r.digest) } : {}, ...strings3(r.heldBy) ? { heldBy: strings3(r.heldBy) } : {}, ...r.heldOn !== void 0 ? { heldOn: r.heldOn } : {}, ...outcomes2 ? { outcomes: outcomes2 } : {} };
24006
+ return { wave: r.wave, name: r.name, op: r.op, gate: r.gate, ...str14(r.digest) ? { digest: str14(r.digest) } : {}, ...strings3(r.heldBy) ? { heldBy: strings3(r.heldBy) } : {}, ...r.heldOn !== void 0 ? { heldOn: r.heldOn } : {}, ...outcomes2 ? { outcomes: outcomes2 } : {} };
23750
24007
  }
23751
24008
  function deriveGateAudit(input) {
23752
24009
  const isOverride = input.isOverride ?? isOverrideGate2;
@@ -23771,17 +24028,17 @@ function deriveGateAudit(input) {
23771
24028
  const requests = [];
23772
24029
  for (const p of parsed) {
23773
24030
  if (!p.pending || p.line.removed) continue;
23774
- const digest = str13(p.r.planDigest);
24031
+ const digest = str14(p.r.planDigest);
23775
24032
  denials.set(`${String(p.r.gate)}
23776
24033
  ${digest ?? ""}`, p.r);
23777
- requests.push({ op: ledger.op, gate: String(p.r.gate), at: time3(String(p.r.timestamp)), ...digest ? { digest } : {}, ...str13(p.r.environment) ? { environment: str13(p.r.environment) } : {} });
24034
+ requests.push({ op: ledger.op, gate: String(p.r.gate), at: time3(String(p.r.timestamp)), ...digest ? { digest } : {}, ...str14(p.r.environment) ? { environment: str14(p.r.environment) } : {} });
23778
24035
  }
23779
24036
  for (const { line, n, r, pending } of parsed) {
23780
24037
  const gate = String(r.gate);
23781
24038
  const override = isOverride(ledger.op, gate);
23782
24039
  const removed = line.removed;
23783
24040
  if (pending && removed) continue;
23784
- const digest = str13(r.planDigest);
24041
+ const digest = str14(r.planDigest);
23785
24042
  const author = removed ?? line.added;
23786
24043
  const source2 = {
23787
24044
  path,
@@ -23800,7 +24057,7 @@ ${digest ?? ""}`, p.r);
23800
24057
  ...base,
23801
24058
  kind: override ? "override-requested" : "approval-requested",
23802
24059
  at: String(r.timestamp),
23803
- ...drop({ runId: str13(r.runId), expiresAt: str13(r.expiresAt), description: str13(r.description), url: str13(r.url), rules: strings3(r.rules), wave: waveOf(r.wave), heldBy: heldByOf(r.heldBy), approvalMode: modeOf(r.approvalMode) }),
24060
+ ...drop({ runId: str14(r.runId), expiresAt: str14(r.expiresAt), description: str14(r.description), url: str14(r.url), rules: strings3(r.rules), wave: waveOf(r.wave), heldBy: heldByOf(r.heldBy), approvalMode: modeOf(r.approvalMode) }),
23804
24061
  detail: extra2
23805
24062
  }));
23806
24063
  continue;
@@ -23828,13 +24085,13 @@ ${digest ?? ""}`) : void 0;
23828
24085
  at: String(r.timestamp),
23829
24086
  by: String(r.resolvedBy),
23830
24087
  seal,
23831
- ...drop({ sealed, signer, committedBy: line.added?.by, relayedBy: str13(r.relayedBy), approver: approverOf(r), note: str13(r.note), url: str13(r.url), review: reviewOf(r), rules }),
24088
+ ...drop({ sealed, signer, committedBy: line.added?.by, relayedBy: str14(r.relayedBy), approver: approverOf(r), note: str14(r.note), url: str14(r.url), review: reviewOf(r), rules }),
23832
24089
  detail: extra2
23833
24090
  });
23834
24091
  entries.push(entry);
23835
24092
  if (!override) {
23836
24093
  const at2 = time3(entry.at);
23837
- const env = str13(r.environment);
24094
+ const env = str14(r.environment);
23838
24095
  const asked = requests.some((q) => q.gate === gate && q.at <= at2 && compatible(q.digest, digest) && (q.environment === void 0 || env === void 0 || q.environment === env));
23839
24096
  if (!asked) {
23840
24097
  gaps.push({ kind: "approval-without-request", op: ledger.op, gate, ...digest ? { digest } : {}, detail: `${path} holds an approval of ${gate} by ${entry.by} at ${entry.at} with no pending fact of that gate${digest ? " and digest" : ""} before it` });
@@ -24109,8 +24366,8 @@ function lifecycleHistoryRoute(app, base, opts = {}) {
24109
24366
  }
24110
24367
 
24111
24368
  // src/lifecycle-releases.ts
24112
- import { existsSync as existsSync22, readFileSync as readFileSync26 } from "node:fs";
24113
- import { join as join38 } from "node:path";
24369
+ import { existsSync as existsSync22, readFileSync as readFileSync28 } from "node:fs";
24370
+ import { join as join40 } from "node:path";
24114
24371
 
24115
24372
  // node_modules/@intentius/chant/src/lifecycle/legacy-digest.ts
24116
24373
  import { resolve as resolve25 } from "node:path";
@@ -24223,10 +24480,10 @@ function memberNote(l) {
24223
24480
  // src/lifecycle-releases.ts
24224
24481
  var WAVES_SPEC_FILE = "yodel-waves.json";
24225
24482
  function waveOrder(checkout) {
24226
- const p = join38(checkout, WAVES_SPEC_FILE);
24483
+ const p = join40(checkout, WAVES_SPEC_FILE);
24227
24484
  if (!existsSync22(p)) return void 0;
24228
24485
  try {
24229
- const doc = JSON.parse(readFileSync26(p, "utf8"));
24486
+ const doc = JSON.parse(readFileSync28(p, "utf8"));
24230
24487
  const names = (doc.waves ?? []).map((w) => w?.name).filter((n) => typeof n === "string" && n.length > 0);
24231
24488
  return names.length ? [...new Set(names)] : void 0;
24232
24489
  } catch {
@@ -24234,7 +24491,7 @@ function waveOrder(checkout) {
24234
24491
  }
24235
24492
  }
24236
24493
  function hasReleaseLedgerHint(checkout) {
24237
- return existsSync22(join38(checkout, "yodel.config.ts")) || existsSync22(join38(checkout, WAVES_SPEC_FILE));
24494
+ return existsSync22(join40(checkout, "yodel.config.ts")) || existsSync22(join40(checkout, WAVES_SPEC_FILE));
24238
24495
  }
24239
24496
  function orderEnvironments(envs, waves) {
24240
24497
  const seen = new Set(envs);
@@ -24276,7 +24533,7 @@ function releaseCells(ledgers, order, sources) {
24276
24533
  }
24277
24534
  async function copyCheckoutRef(git3, checkout, cache6) {
24278
24535
  assertScratch(cache6.split(/[\\/]/).pop());
24279
- if (!existsSync22(join38(cache6, "HEAD"))) {
24536
+ if (!existsSync22(join40(cache6, "HEAD"))) {
24280
24537
  const init = await git3(["init", "--bare", "-q", cache6]);
24281
24538
  if (init.code !== 0) return false;
24282
24539
  }
@@ -24379,8 +24636,8 @@ function releasesRoutes(app, base, opts = {}) {
24379
24636
  }
24380
24637
 
24381
24638
  // src/sql-yodeler-lane.ts
24382
- import { existsSync as existsSync23, readdirSync as readdirSync12, readFileSync as readFileSync27 } from "node:fs";
24383
- import { join as join39 } from "node:path";
24639
+ import { existsSync as existsSync23, readdirSync as readdirSync12, readFileSync as readFileSync29 } from "node:fs";
24640
+ import { join as join41 } from "node:path";
24384
24641
  var YODEL_APPLY_OP = "yodel-apply";
24385
24642
  var YODEL_HEAD_PLANS_DIR = headPlansPath(YODEL_APPLY_OP, "").dir;
24386
24643
  function parseYodelWaves(text, path = WAVES_SPEC_FILE) {
@@ -24412,16 +24669,16 @@ function parseYodelWaves(text, path = WAVES_SPEC_FILE) {
24412
24669
  return { path, waves };
24413
24670
  }
24414
24671
  function readYodelWaves(checkout) {
24415
- const p = join39(checkout, WAVES_SPEC_FILE);
24672
+ const p = join41(checkout, WAVES_SPEC_FILE);
24416
24673
  if (!existsSync23(p)) return void 0;
24417
24674
  try {
24418
- return parseYodelWaves(readFileSync27(p, "utf8"));
24675
+ return parseYodelWaves(readFileSync29(p, "utf8"));
24419
24676
  } catch (e) {
24420
24677
  return { path: WAVES_SPEC_FILE, waves: [], error: e instanceof Error ? e.message : String(e) };
24421
24678
  }
24422
24679
  }
24423
24680
  function migrateOpEnvironments(checkout) {
24424
- const dir = join39(checkout, "ops");
24681
+ const dir = join41(checkout, "ops");
24425
24682
  if (!existsSync23(dir)) return [];
24426
24683
  try {
24427
24684
  return readdirSync12(dir).map((f) => /^migrate-(.+)\.op\.ts$/.exec(f)?.[1]).filter((e) => !!e).sort();
@@ -24544,16 +24801,16 @@ function sqlYodelerLaneRoute(app, checkout, opts = {}) {
24544
24801
 
24545
24802
  // src/terragucci-estate.ts
24546
24803
  init_yaml();
24547
- import { existsSync as existsSync24, readFileSync as readFileSync28, statSync as statSync13 } from "node:fs";
24804
+ import { existsSync as existsSync24, readFileSync as readFileSync30, statSync as statSync13 } from "node:fs";
24548
24805
  import { createRequire as createRequire7 } from "node:module";
24549
- import { dirname as dirname14, extname, join as join40, resolve as resolve26 } from "node:path";
24806
+ import { dirname as dirname14, extname, join as join42, resolve as resolve26 } from "node:path";
24550
24807
  import { fileURLToPath as fileURLToPath8 } from "node:url";
24551
24808
  import { Ajv2020 as Ajv20204 } from "ajv/dist/2020.js";
24552
24809
  var RUN_SCHEMA2 = "terragucci.run/v1";
24553
24810
  function configProjects(file) {
24554
24811
  let text;
24555
24812
  try {
24556
- text = readFileSync28(file, "utf8");
24813
+ text = readFileSync30(file, "utf8");
24557
24814
  } catch {
24558
24815
  return void 0;
24559
24816
  }
@@ -24581,21 +24838,21 @@ function controlRepo(dirs, roots = () => checkoutRoots(dirs).length) {
24581
24838
  }
24582
24839
  return n === 0 ? { config, projects } : void 0;
24583
24840
  }
24584
- var isRecord17 = (v) => v !== null && typeof v === "object" && !Array.isArray(v);
24585
- var str14 = (v) => typeof v === "string";
24586
- var int = (v) => typeof v === "number" && Number.isInteger(v);
24841
+ var isRecord18 = (v) => v !== null && typeof v === "object" && !Array.isArray(v);
24842
+ var str15 = (v) => typeof v === "string";
24843
+ var int2 = (v) => typeof v === "number" && Number.isInteger(v);
24587
24844
  function structuralRun2(doc) {
24588
- if (!isRecord17(doc)) return "it is not a JSON object";
24845
+ if (!isRecord18(doc)) return "it is not a JSON object";
24589
24846
  if (doc.schema !== RUN_SCHEMA2) return `its schema is ${JSON.stringify(doc.schema)}, not ${RUN_SCHEMA2}`;
24590
- if (!str14(doc.project) || !str14(doc.commit) || !str14(doc.updated)) return "it lacks project, commit or updated";
24847
+ if (!str15(doc.project) || !str15(doc.commit) || !str15(doc.updated)) return "it lacks project, commit or updated";
24591
24848
  if (!Array.isArray(doc.roots) || !Array.isArray(doc.waves)) return "it has no roots or waves array";
24592
24849
  for (const [i, r] of doc.roots.entries()) {
24593
- if (!isRecord17(r) || !str14(r.root) || !int(r.wave) || !Array.isArray(r.reads) || !r.reads.every(str14)) return `root ${i} lacks root, wave or reads`;
24594
- if (r.state !== void 0 && !(isRecord17(r.state) && str14(r.state.key))) return `root ${String(r.root)}'s state has no key`;
24595
- if (r.external !== void 0 && !(Array.isArray(r.external) && r.external.every((x) => isRecord17(x) && str14(x.data) && str14(x.key)))) return `root ${String(r.root)}'s external reads lack data or key`;
24850
+ if (!isRecord18(r) || !str15(r.root) || !int2(r.wave) || !Array.isArray(r.reads) || !r.reads.every(str15)) return `root ${i} lacks root, wave or reads`;
24851
+ if (r.state !== void 0 && !(isRecord18(r.state) && str15(r.state.key))) return `root ${String(r.root)}'s state has no key`;
24852
+ if (r.external !== void 0 && !(Array.isArray(r.external) && r.external.every((x) => isRecord18(x) && str15(x.data) && str15(x.key)))) return `root ${String(r.root)}'s external reads lack data or key`;
24596
24853
  }
24597
24854
  for (const [i, w] of doc.waves.entries()) {
24598
- if (!isRecord17(w) || !int(w.number) || !Array.isArray(w.roots) || !Array.isArray(w.reads) || !str14(w.state) || !str14(w.gate)) return `wave ${i} lacks number, roots, reads, state or gate`;
24855
+ if (!isRecord18(w) || !int2(w.number) || !Array.isArray(w.roots) || !Array.isArray(w.reads) || !str15(w.state) || !str15(w.gate)) return `wave ${i} lacks number, roots, reads, state or gate`;
24599
24856
  }
24600
24857
  return void 0;
24601
24858
  }
@@ -24604,16 +24861,16 @@ function runViewValidator(dirs = []) {
24604
24861
  for (const from of [...dirs, dirname14(fileURLToPath8(import.meta.url))]) {
24605
24862
  let entry;
24606
24863
  try {
24607
- entry = createRequire7(join40(resolve26(from), "noop.js")).resolve("@intentius/terragucci/report.schema.json");
24864
+ entry = createRequire7(join42(resolve26(from), "noop.js")).resolve("@intentius/terragucci/report.schema.json");
24608
24865
  } catch {
24609
24866
  continue;
24610
24867
  }
24611
24868
  const dist = dirname14(entry);
24612
- const schema = join40(dist, "run.schema.json");
24869
+ const schema = join42(dist, "run.schema.json");
24613
24870
  let stamp;
24614
24871
  try {
24615
24872
  const st = statSync13(schema);
24616
- const pkg = statSync13(join40(dist, "..", "package.json"));
24873
+ const pkg = statSync13(join42(dist, "..", "package.json"));
24617
24874
  stamp = `${schema}\0${st.mtimeMs}:${st.size}\0${pkg.mtimeMs}`;
24618
24875
  } catch {
24619
24876
  continue;
@@ -24622,12 +24879,12 @@ function runViewValidator(dirs = []) {
24622
24879
  if (hit) return hit;
24623
24880
  let v;
24624
24881
  try {
24625
- const version = JSON.parse(readFileSync28(join40(dist, "..", "package.json"), "utf8")).version ?? "";
24626
- const validate = new Ajv20204({ strict: false, allErrors: false }).compile(JSON.parse(readFileSync28(schema, "utf8")));
24882
+ const version = JSON.parse(readFileSync30(join42(dist, "..", "package.json"), "utf8")).version ?? "";
24883
+ const validate = new Ajv20204({ strict: false, allErrors: false }).compile(JSON.parse(readFileSync30(schema, "utf8")));
24627
24884
  v = {
24628
24885
  validation: { by: "schema", terragucci: version },
24629
24886
  check(doc) {
24630
- const id = isRecord17(doc) ? doc.schema : void 0;
24887
+ const id = isRecord18(doc) ? doc.schema : void 0;
24631
24888
  if (id !== RUN_SCHEMA2) return `its schema is ${JSON.stringify(id)}, not ${RUN_SCHEMA2}`;
24632
24889
  if (validate(doc)) return void 0;
24633
24890
  const e = validate.errors?.[0];
@@ -24678,8 +24935,8 @@ function controlEdges(views) {
24678
24935
  function siblingCheckout(project, base, projectOf = checkoutProject) {
24679
24936
  const name = project.split("/").filter(Boolean).pop();
24680
24937
  if (!name) return void 0;
24681
- const dir = join40(dirname14(resolve26(base)), name);
24682
- if (dir === resolve26(base) || !existsSync24(join40(dir, ".git"))) return void 0;
24938
+ const dir = join42(dirname14(resolve26(base)), name);
24939
+ if (dir === resolve26(base) || !existsSync24(join42(dir, ".git"))) return void 0;
24683
24940
  return projectOf(dir) === project ? dir : void 0;
24684
24941
  }
24685
24942
  var shellWord = (s) => /^[A-Za-z0-9_./:@%+=,-]+$/.test(s) ? s : `'${s.replace(/'/g, `'\\''`)}'`;
@@ -24886,9 +25143,9 @@ function controlRepoInfo(dirs) {
24886
25143
  }
24887
25144
 
24888
25145
  // src/yodel-report.ts
24889
- import { readdirSync as readdirSync13, readFileSync as readFileSync29, statSync as statSync14 } from "node:fs";
25146
+ import { readdirSync as readdirSync13, readFileSync as readFileSync31, statSync as statSync14 } from "node:fs";
24890
25147
  import { createRequire as createRequire8 } from "node:module";
24891
- import { basename as basename4, dirname as dirname15, join as join41, resolve as resolve27 } from "node:path";
25148
+ import { basename as basename4, dirname as dirname15, join as join43, resolve as resolve27 } from "node:path";
24892
25149
  import { fileURLToPath as fileURLToPath9 } from "node:url";
24893
25150
  import { Ajv2020 as Ajv20205 } from "ajv/dist/2020.js";
24894
25151
 
@@ -25317,13 +25574,13 @@ var installed = /* @__PURE__ */ new Map();
25317
25574
  function packageRoot(from) {
25318
25575
  let entry;
25319
25576
  try {
25320
- entry = createRequire8(join41(resolve27(from), "noop.js")).resolve("@intentius/sql-yodeler");
25577
+ entry = createRequire8(join43(resolve27(from), "noop.js")).resolve("@intentius/sql-yodeler");
25321
25578
  } catch {
25322
25579
  return void 0;
25323
25580
  }
25324
25581
  for (let d = dirname15(entry); d !== dirname15(d); d = dirname15(d)) {
25325
25582
  try {
25326
- const pkg = JSON.parse(readFileSync29(join41(d, "package.json"), "utf8"));
25583
+ const pkg = JSON.parse(readFileSync31(join43(d, "package.json"), "utf8"));
25327
25584
  if (pkg.name === "@intentius/sql-yodeler") return d;
25328
25585
  } catch {
25329
25586
  }
@@ -25335,11 +25592,11 @@ function yodelValidator(dirs = []) {
25335
25592
  const root = packageRoot(from);
25336
25593
  if (!root) continue;
25337
25594
  try {
25338
- const version = JSON.parse(readFileSync29(join41(root, "package.json"), "utf8")).version ?? "";
25595
+ const version = JSON.parse(readFileSync31(join43(root, "package.json"), "utf8")).version ?? "";
25339
25596
  const key = `${root}\0${version}`;
25340
25597
  let v = installed.get(key);
25341
25598
  if (!v) {
25342
- const load2 = (n) => JSON.parse(readFileSync29(join41(root, "schemas", `${n}.schema.json`), "utf8"));
25599
+ const load2 = (n) => JSON.parse(readFileSync31(join43(root, "schemas", `${n}.schema.json`), "utf8"));
25343
25600
  v = compile({ common: load2("common"), report: load2("report"), drift: load2("drift") }, { by: "schema", from: "package", sqlYodeler: version });
25344
25601
  installed.set(key, v);
25345
25602
  }
@@ -25350,9 +25607,9 @@ function yodelValidator(dirs = []) {
25350
25607
  pinned ??= compile({ common: common_schema_default, report: report_schema_default, drift: drift_schema_default }, { by: "schema", from: "pinned", commit: PINNED_COMMIT.slice(0, 7) });
25351
25608
  return pinned;
25352
25609
  }
25353
- var isRecord18 = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
25354
- var looksLikeReport = (d) => isRecord18(d) && "generatedAt" in d && "environments" in d;
25355
- var looksLikeDrift = (d) => isRecord18(d) && "environment" in d && ("drifted" in d || "changed" in d);
25610
+ var isRecord19 = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
25611
+ var looksLikeReport = (d) => isRecord19(d) && "generatedAt" in d && "environments" in d;
25612
+ var looksLikeDrift = (d) => isRecord19(d) && "environment" in d && ("drifted" in d || "changed" in d);
25356
25613
  function filesOf(source2) {
25357
25614
  let st;
25358
25615
  try {
@@ -25361,7 +25618,7 @@ function filesOf(source2) {
25361
25618
  return refuse14(`--yodel-report ${source2}: no such file or directory`, "Point --yodel-report at the file `yodel report --json` wrote, or the directory holding it.");
25362
25619
  }
25363
25620
  const dir = st.isDirectory() ? source2 : dirname15(source2);
25364
- const files = readdirSync13(dir).filter((f) => f.endsWith(".json")).sort().map((f) => join41(dir, f));
25621
+ const files = readdirSync13(dir).filter((f) => f.endsWith(".json")).sort().map((f) => join43(dir, f));
25365
25622
  return st.isDirectory() ? { dir, files } : { dir, files, named: resolve27(source2) };
25366
25623
  }
25367
25624
  function readYodelReport(source2, validator2) {
@@ -25373,7 +25630,7 @@ function readYodelReport(source2, validator2) {
25373
25630
  for (const file of files) {
25374
25631
  let doc;
25375
25632
  try {
25376
- doc = JSON.parse(readFileSync29(file, "utf8"));
25633
+ doc = JSON.parse(readFileSync31(file, "utf8"));
25377
25634
  } catch (e) {
25378
25635
  if (file === named) refuse14(`${basename4(file)} is not JSON: ${e instanceof Error ? e.message : String(e)}`, remedy);
25379
25636
  ignored.push(basename4(file));
@@ -25460,24 +25717,24 @@ function yodelReportRoute(app, source2, dirs) {
25460
25717
 
25461
25718
  // src/demo-pins.ts
25462
25719
  init_terraform_member();
25463
- import { existsSync as existsSync25, lstatSync as lstatSync2, mkdirSync as mkdirSync7, readFileSync as readFileSync30, readlinkSync as readlinkSync2, symlinkSync as symlinkSync2, writeFileSync as writeFileSync6 } from "node:fs";
25464
- import { dirname as dirname16, join as join42, resolve as resolve28 } from "node:path";
25465
- var isRecord19 = (v) => v !== null && typeof v === "object" && !Array.isArray(v);
25720
+ import { existsSync as existsSync25, lstatSync as lstatSync2, mkdirSync as mkdirSync7, readFileSync as readFileSync32, readlinkSync as readlinkSync2, symlinkSync as symlinkSync2, writeFileSync as writeFileSync6 } from "node:fs";
25721
+ import { dirname as dirname16, join as join44, resolve as resolve28 } from "node:path";
25722
+ var isRecord20 = (v) => v !== null && typeof v === "object" && !Array.isArray(v);
25466
25723
  function linkDemoPins(work, probes = {}) {
25467
- const file = join42(work, "chant.workspace.json");
25724
+ const file = join44(work, "chant.workspace.json");
25468
25725
  if (!existsSync25(file)) return { ok: true, linked: [] };
25469
25726
  let decl;
25470
25727
  try {
25471
- decl = JSON.parse(readFileSync30(file, "utf8"));
25728
+ decl = JSON.parse(readFileSync32(file, "utf8"));
25472
25729
  } catch {
25473
25730
  return { ok: true, linked: [] };
25474
25731
  }
25475
- if (!isRecord19(decl) || !Array.isArray(decl.pins)) return { ok: true, linked: [] };
25732
+ if (!isRecord20(decl) || !Array.isArray(decl.pins)) return { ok: true, linked: [] };
25476
25733
  const find = probes.resolve ?? ((pkg) => resolvedPackage(pkg));
25477
25734
  const linked = [];
25478
25735
  let rewrite = false;
25479
25736
  for (const pin of decl.pins) {
25480
- if (!isRecord19(pin) || typeof pin.package !== "string") continue;
25737
+ if (!isRecord20(pin) || typeof pin.package !== "string") continue;
25481
25738
  const pkg = pin.package;
25482
25739
  if (pkg === TERRAFORM_LEXICON_PKG) {
25483
25740
  const refusal2 = (probes.reader ?? terraformReaderState)().refusal;
@@ -25491,7 +25748,7 @@ function linkDemoPins(work, probes = {}) {
25491
25748
  remedy: `Install ${pkg} beside behold, then load the demo again.`
25492
25749
  };
25493
25750
  }
25494
- const link = join42(work, "node_modules", ...pkg.split("/"));
25751
+ const link = join44(work, "node_modules", ...pkg.split("/"));
25495
25752
  if (!present(link)) {
25496
25753
  mkdirSync7(dirname16(link), { recursive: true });
25497
25754
  symlinkSync2(found.dir, link, "junction");
@@ -25526,13 +25783,13 @@ function linksTo(link, target) {
25526
25783
  }
25527
25784
 
25528
25785
  // src/demos.ts
25529
- import { readFileSync as readFileSync31, existsSync as existsSync26, cpSync, mkdirSync as mkdirSync8 } from "node:fs";
25530
- import { dirname as dirname17, isAbsolute as isAbsolute5, join as join43, relative as relative9, resolve as resolve29, sep as sep8 } from "node:path";
25786
+ import { readFileSync as readFileSync33, existsSync as existsSync26, cpSync, mkdirSync as mkdirSync8 } from "node:fs";
25787
+ import { dirname as dirname17, isAbsolute as isAbsolute5, join as join45, relative as relative9, resolve as resolve29, sep as sep8 } from "node:path";
25531
25788
  import { spawn as spawn8, spawnSync as spawnSync3 } from "node:child_process";
25532
25789
  function readCatalog(file, catalog) {
25533
25790
  let raw;
25534
25791
  try {
25535
- raw = JSON.parse(readFileSync31(file, "utf8"));
25792
+ raw = JSON.parse(readFileSync33(file, "utf8"));
25536
25793
  } catch {
25537
25794
  return [];
25538
25795
  }
@@ -25572,10 +25829,10 @@ function validEntry(e) {
25572
25829
  return true;
25573
25830
  }
25574
25831
  function loadDemoRegistry(pkgRoot3) {
25575
- const merged = readCatalog(join43(pkgRoot3, "demos.json"), "demos");
25832
+ const merged = readCatalog(join45(pkgRoot3, "demos.json"), "demos");
25576
25833
  const seen = new Set(merged.map((e) => e.name));
25577
25834
  const named = process.env.BEHOLD_WORKBENCH;
25578
- for (const e of [...readCatalog(join43(pkgRoot3, "workbench.json"), "workbench"), ...named ? readCatalog(resolve29(named), "workbench") : []]) {
25835
+ for (const e of [...readCatalog(join45(pkgRoot3, "workbench.json"), "workbench"), ...named ? readCatalog(resolve29(named), "workbench") : []]) {
25579
25836
  if (seen.has(e.name)) {
25580
25837
  process.stderr.write(`behold: workbench entry "${e.name}" collides with a demo already in the catalog \u2014 dropped
25581
25838
  `);
@@ -25622,7 +25879,7 @@ async function loadDemo(entry, opts) {
25622
25879
  say(`serving ${work} where it is \u2014 nothing copied`);
25623
25880
  } else if (!existsSync26(target)) {
25624
25881
  if (entry.source === "bundled" || entry.source === "local" && local) {
25625
- const from = entry.source === "bundled" ? join43(pkgRoot3, entry.dir) : local;
25882
+ const from = entry.source === "bundled" ? join45(pkgRoot3, entry.dir) : local;
25626
25883
  if (!existsSync26(from)) return { ok: false, error: entry.source === "bundled" ? `this install has no bundled ${entry.dir}` : `${entry.path} is not checked out (looked in ${from})` };
25627
25884
  say(`copying to ${target} (it's yours \u2014 edit it)`);
25628
25885
  try {
@@ -25654,7 +25911,7 @@ async function loadDemo(entry, opts) {
25654
25911
  if (!pins.ok) return { ok: false, error: `${pins.error} ${pins.remedy}` };
25655
25912
  for (const p of pins.linked) say(`linked ${p.pkg} ${p.version} into the copy${p.was ? ` (the declaration pinned ${p.was}; now ${p.version})` : ""}`);
25656
25913
  }
25657
- if (!inPlace && existsSync26(join43(work, "package.json")) && !existsSync26(join43(work, "node_modules"))) {
25914
+ if (!inPlace && existsSync26(join45(work, "package.json")) && !existsSync26(join45(work, "node_modules"))) {
25658
25915
  say("npm install\u2026");
25659
25916
  if (await runStep("npm", ["install"], { cwd: work, shell: process.platform === "win32" }) !== 0) {
25660
25917
  return { ok: false, error: `npm install failed in ${work}` };
@@ -25671,7 +25928,7 @@ async function loadDemo(entry, opts) {
25671
25928
  return { ok: false, error: `setup failed (${entry.setup})` };
25672
25929
  }
25673
25930
  }
25674
- return { ok: true, serveDirs: entry.serve.dirs?.length ? entry.serve.dirs.map((d) => join43(work, d)) : [work] };
25931
+ return { ok: true, serveDirs: entry.serve.dirs?.length ? entry.serve.dirs.map((d) => join45(work, d)) : [work] };
25675
25932
  }
25676
25933
  function runStep(cmd, args, opts = {}) {
25677
25934
  return new Promise((res) => {
@@ -25682,8 +25939,8 @@ function runStep(cmd, args, opts = {}) {
25682
25939
  }
25683
25940
 
25684
25941
  // src/server.ts
25685
- var pkgRoot = join44(dirname18(fileURLToPath10(import.meta.url)), "..");
25686
- var webRoot = join44(pkgRoot, "web");
25942
+ var pkgRoot = join46(dirname18(fileURLToPath10(import.meta.url)), "..");
25943
+ var webRoot = join46(pkgRoot, "web");
25687
25944
  var execFileP = async (cmd, args) => (await promisify3(execFile5)(cmd, args, { encoding: "utf8", timeout: 1e4 })).stdout;
25688
25945
  function optsFromQuery(url, tierEnvVar, projectDir) {
25689
25946
  const q = url.searchParams;
@@ -25734,7 +25991,7 @@ function tierFailure(tier, message2) {
25734
25991
  function beholdVersion() {
25735
25992
  try {
25736
25993
  const here = dirname18(fileURLToPath10(import.meta.url));
25737
- return JSON.parse(readFileSync32(join44(here, "..", "package.json"), "utf8")).version ?? "unknown";
25994
+ return JSON.parse(readFileSync34(join46(here, "..", "package.json"), "utf8")).version ?? "unknown";
25738
25995
  } catch {
25739
25996
  return "unknown";
25740
25997
  }
@@ -25781,7 +26038,7 @@ async function captureFrame(projectDir, env, frames, broadcaster) {
25781
26038
  }
25782
26039
  }
25783
26040
  function carveRoutes(app, reportPath, demo) {
25784
- const load2 = () => readCarveReport(reportPath, (p) => readFileSync32(p, "utf8"));
26041
+ const load2 = () => readCarveReport(reportPath, (p) => readFileSync34(p, "utf8"));
25785
26042
  const carveDirs = [dirname18(reportPath), ...demo ? [demo.out] : []];
25786
26043
  const carveStates = () => readCarveStates(carveDirs, nodeCarveIo, 0);
25787
26044
  const emittedNodes = async (states) => {
@@ -26017,13 +26274,18 @@ function cardPrefixesOf(ir) {
26017
26274
  const byStack = ir.groups.byStack;
26018
26275
  return byStack && Object.keys(byStack).length ? byStack : void 0;
26019
26276
  }
26020
- function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffer(), runner = new OpRunner({
26021
- projectDir: cfg.projectDir,
26022
- broadcaster,
26023
- onDone: (opEnv) => captureFrame(cfg.projectDir, opEnv ?? cfg.env, frames, broadcaster)
26024
- })) {
26277
+ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffer(), given) {
26278
+ const runner = cfg.public ? new ReadOnlyRunner() : given ?? new OpRunner({
26279
+ projectDir: cfg.projectDir,
26280
+ broadcaster,
26281
+ onDone: (opEnv) => captureFrame(cfg.projectDir, opEnv ?? cfg.env, frames, broadcaster)
26282
+ });
26025
26283
  const app = new Hono();
26026
- const guard = { allowedHosts: allowedHostsFrom(cfg.allowedHosts ?? []), ...cfg.host ? { boundHost: cfg.host } : {} };
26284
+ const guard = {
26285
+ allowedHosts: allowedHostsFrom(cfg.allowedHosts ?? []),
26286
+ ...cfg.host ? { boundHost: cfg.host } : {},
26287
+ ...cfg.public ? { public: true } : {}
26288
+ };
26027
26289
  app.use("*", async (c, next) => {
26028
26290
  const verdict3 = guardRequest(
26029
26291
  {
@@ -26034,9 +26296,13 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
26034
26296
  },
26035
26297
  guard
26036
26298
  );
26037
- if (!verdict3.ok) return c.json({ error: verdict3.error, code: "request-refused" }, verdict3.status);
26299
+ if (!verdict3.ok) {
26300
+ if (verdict3.status === 405) c.header("allow", "GET, HEAD, OPTIONS");
26301
+ return c.json({ error: verdict3.error, code: verdict3.status === 405 ? "public" : "request-refused" }, verdict3.status);
26302
+ }
26038
26303
  await next();
26039
26304
  });
26305
+ if (cfg.public) publicRoutes(app, webRoot);
26040
26306
  setChoudoufuRunner(cfg.choudoufu?.run);
26041
26307
  setEstateLexiconRead(cfg.choudoufu ? cfg.choudoufu.lexicon ?? null : void 0);
26042
26308
  setServedWorkspace(cfg.workspace);
@@ -26094,6 +26360,7 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
26094
26360
  };
26095
26361
  const estateSourceRoots = async (opts) => (await Promise.all((cfg.projectDirs ?? []).map(async (dir) => [await graphPath(dir, opts), dir]))).flat();
26096
26362
  app.get("/healthz", (c) => c.json({ ok: true, projectDir: cfg.projectDir, env: cfg.env ?? null, frames: frames.size }));
26363
+ kioskRoute(app, webRoot, !!cfg.terragucci);
26097
26364
  app.get("/api/frames", (c) => c.json({ frames: frames.summaries() }));
26098
26365
  app.get("/lanes", (c) => {
26099
26366
  const all = frames.all();
@@ -26177,8 +26444,8 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
26177
26444
  return c.json({ started: true, name, ran: label });
26178
26445
  });
26179
26446
  app.post("/api/local/reset", (c) => {
26180
- const down = join44(cfg.projectDir, "scripts/local/local-down.sh");
26181
- const up = join44(cfg.projectDir, "scripts/local/local-up.sh");
26447
+ const down = join46(cfg.projectDir, "scripts/local/local-down.sh");
26448
+ const up = join46(cfg.projectDir, "scripts/local/local-up.sh");
26182
26449
  if (!existsSync27(down) || !existsSync27(up)) {
26183
26450
  return c.json({ error: "no local-down.sh / local-up.sh in scripts/local \u2014 reset is only for local emulator projects" }, 400);
26184
26451
  }
@@ -26200,7 +26467,7 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
26200
26467
  const dirs = [];
26201
26468
  const declared = [];
26202
26469
  for (const f of discoverOpIrs(estateDirs)) {
26203
- const parsed = readOpIr(f.path, (p) => readFileSync32(p, "utf8"));
26470
+ const parsed = readOpIr(f.path, (p) => readFileSync34(p, "utf8"));
26204
26471
  if (!parsed.ok) continue;
26205
26472
  const mine = declaredConvergeOps([parsed.ir]);
26206
26473
  if (!mine.length) continue;
@@ -26380,7 +26647,7 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
26380
26647
  });
26381
26648
  app.get("/api/project", async (c) => {
26382
26649
  const { environments: declaredEnvs, lexicons, stacks, k8sProfiles } = await detectProject(cfg.projectDir);
26383
- const environments = declaredEnvs.length === 0 && (cfg.projectDirs ?? [cfg.projectDir]).some((d) => memberKindOf(d) === "choudoufu") ? ["live"] : declaredEnvs;
26650
+ const environments = cfg.public ? [] : declaredEnvs.length === 0 && (cfg.projectDirs ?? [cfg.projectDir]).some((d) => memberKindOf(d) === "choudoufu") ? ["live"] : declaredEnvs;
26384
26651
  const k8sTarget = lexicons.includes("k8s") ? resolveK8sTarget(k8sProfiles, cfg.env, await loadKubeconfig()) : void 0;
26385
26652
  const axes = deployAxes(tierEnvVar, lexicons, k8sTarget);
26386
26653
  const emittedOps = discoverOpIrs(estateDirs).length;
@@ -26414,6 +26681,8 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
26414
26681
  currentEnv: cfg.env ?? null,
26415
26682
  // v0.1.0 preview: the SPA hides git/PR ops + arbitrary-project affordances.
26416
26683
  ...cfg.previewMode ? { previewMode: true } : {},
26684
+ // #569: a public view; the SPA hides every control that writes.
26685
+ ...cfg.public ? { publicMode: true } : {},
26417
26686
  // #489: a terragucci repo is applied by its pipeline; the SPA offers no
26418
26687
  // write at all, and the write routes refuse with the same words.
26419
26688
  ...tgConfig() ? { terragucci: { config: tgConfig(), ...terragucciRefusal(tgConfig(), "deploying from behold") } } : {},
@@ -26478,7 +26747,8 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
26478
26747
  // true is a flag the SPA can read as "absent means no".
26479
26748
  memberKinds,
26480
26749
  ...runtimeCapable ? { runtimeCapable: true } : {},
26481
- targets: deployTargets(lexicons, k8sTarget),
26750
+ // #569: a public view offers no target: `?target=` is refused there.
26751
+ targets: cfg.public ? [] : deployTargets(lexicons, k8sTarget),
26482
26752
  // Where the k8s binding came from, so the SPA never implies behold chose
26483
26753
  // it (#106). Absent for a project with no k8s lexicon or no resolvable
26484
26754
  // context.
@@ -26491,7 +26761,7 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
26491
26761
  const body = await c.req.json().catch(() => ({}));
26492
26762
  const dir = typeof body.dir === "string" && body.dir.trim() ? resolve30(body.dir.trim()) : "";
26493
26763
  if (!dir || !existsSync27(dir)) return c.json({ error: `no such directory: ${dir || "(no dir given)"}` }, 400);
26494
- if (!existsSync27(join44(dir, "chant.config.ts"))) {
26764
+ if (!existsSync27(join46(dir, "chant.config.ts"))) {
26495
26765
  return c.json({ error: `${dir} doesn't look like a chant project \u2014 no chant.config.ts` }, 400);
26496
26766
  }
26497
26767
  switchServedProject([dir]);
@@ -26604,6 +26874,7 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
26604
26874
  const layoutDir = () => cfg.workspace?.root ?? cfg.projectDir;
26605
26875
  const layoutWriteBlock = () => {
26606
26876
  if (cfg.previewMode) return "the layout sidecar is read-only in preview mode";
26877
+ if (cfg.public) return "a public view doesn't write to the project";
26607
26878
  if (cfg.carveReport) return "a carve report isn't a project \u2014 there's nowhere to keep a hand layout";
26608
26879
  if (cfg.layoutWrites === false) return "a static export captures a snapshot \u2014 it doesn't write to the project";
26609
26880
  return unwritableReason(layoutDir());
@@ -26794,7 +27065,7 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
26794
27065
  }
26795
27066
  const parsedOps = [];
26796
27067
  for (const f of files) {
26797
- const parsed = readOpIr(f.path, (p) => readFileSync32(p, "utf8"));
27068
+ const parsed = readOpIr(f.path, (p) => readFileSync34(p, "utf8"));
26798
27069
  if (!parsed.ok) return c.json(parsed.refusal, 422);
26799
27070
  parsedOps.push(parsed.ir);
26800
27071
  }
@@ -26807,8 +27078,8 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
26807
27078
  }
26808
27079
  const runState = runner.runState;
26809
27080
  const ranOp = runState.op ? parsedOps.find((o) => o.name === runState.op) : void 0;
26810
- const join49 = ranOp ? joinRun(ranOp, runState.records, runState.gate) : null;
26811
- const opsIr = join49 ? paintPlayhead(declaredIr, join49, runState.gate) : declaredIr;
27081
+ const join51 = ranOp ? joinRun(ranOp, runState.records, runState.gate) : null;
27082
+ const opsIr = join51 ? paintPlayhead(declaredIr, join51, runState.gate) : declaredIr;
26812
27083
  const { svg: svg2 } = renderGraph(opsIr, { boxes: "byStack" });
26813
27084
  const operator = runner.noteDeclaredConvergeOps(declaredConvergeOps(parsedOps));
26814
27085
  return c.json({
@@ -26821,7 +27092,7 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
26821
27092
  target: null,
26822
27093
  mode: "ops",
26823
27094
  ops: parsedOps.length,
26824
- note: `${opsNote(parsedOps, opsIr)} ${playheadNote(runState, join49)} ${operatorNote(operator)}`,
27095
+ note: `${opsNote(parsedOps, opsIr)} ${playheadNote(runState, join51)} ${operatorNote(operator)}`,
26825
27096
  // The operator strip's own model: the ConvergeOps this project
26826
27097
  // declares, plus whatever the last status read said. `declared`
26827
27098
  // without a `strip` is the honest starting state — a loop exists and
@@ -26932,7 +27203,7 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
26932
27203
  srcCompositeEdgesAttached = 0;
26933
27204
  }
26934
27205
  }
26935
- if (!multi && !components && !logical && !opts.lens && !ownKindVia && ir.nodes.length === 0 && !existsSync27(join44(cfg.projectDir, "chant.config.ts"))) {
27206
+ if (!multi && !components && !logical && !opts.lens && !ownKindVia && ir.nodes.length === 0 && !existsSync27(join46(cfg.projectDir, "chant.config.ts"))) {
26936
27207
  return c.json(noProjectError(cfg.projectDir), 404);
26937
27208
  }
26938
27209
  const radial = new URL(c.req.url).searchParams.get("radial") === "1";
@@ -27569,7 +27840,7 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
27569
27840
  if (!c.req.path.startsWith("/api/") && c.res.ok && !c.res.headers.has("cache-control")) c.res.headers.set("cache-control", "no-cache");
27570
27841
  });
27571
27842
  app.use("/*", serveStatic({ root: rel }));
27572
- app.get("/", serveStatic({ path: join44(rel, "index.html") }));
27843
+ app.get("/", serveStatic({ path: join46(rel, "index.html") }));
27573
27844
  return app;
27574
27845
  }
27575
27846
  async function bootLocalEmulators(dir, who) {
@@ -27598,11 +27869,11 @@ async function bootLocalEmulators(dir, who) {
27598
27869
  async function startServer(cfg) {
27599
27870
  if (cfg.local) cfg.emulators = await bootLocalEmulators(cfg.projectDir, "behold serve --local");
27600
27871
  for (const dir of cfg.projectDirs ?? [cfg.projectDir]) {
27601
- if (existsSync27(join44(dir, "chant.config.ts"))) addRecent(dir);
27872
+ if (existsSync27(join46(dir, "chant.config.ts"))) addRecent(dir);
27602
27873
  }
27603
27874
  const broadcaster = new Broadcaster();
27604
27875
  const frames = new FrameBuffer();
27605
- const runner = new OpRunner({
27876
+ const runner = cfg.public ? new ReadOnlyRunner() : new OpRunner({
27606
27877
  projectDir: cfg.projectDir,
27607
27878
  broadcaster,
27608
27879
  onDone: (opEnv) => captureFrame(cfg.projectDir, opEnv ?? cfg.env, frames, broadcaster)
@@ -27610,7 +27881,7 @@ async function startServer(cfg) {
27610
27881
  if (!cfg.host && process.env.BEHOLD_HOST) cfg.host = process.env.BEHOLD_HOST;
27611
27882
  const app = createApp(cfg, broadcaster, frames, runner);
27612
27883
  const autoSync = cfg.autoSync ?? "off";
27613
- if (!cfg.carveReport && !servedProductRepo(cfg.projectDirs ?? [cfg.projectDir])) {
27884
+ if (!cfg.public && !cfg.carveReport && !servedProductRepo(cfg.projectDirs ?? [cfg.projectDir])) {
27614
27885
  void readoptDispatchedRun(cfg, runner).then((result) => {
27615
27886
  if (result.outcome === "readopted") {
27616
27887
  process.stdout.write(` re-adopted dispatched run ${result.run.runId} (${result.run.workflow}) \u2014 following via gh
@@ -27634,7 +27905,7 @@ async function startServer(cfg) {
27634
27905
  const memberTag = (dir) => (cfg.projectDirs?.length ?? 0) > 1 ? ` [${basename5(dir)}]` : "";
27635
27906
  const onPollDrift = (dir, movedLexicons) => {
27636
27907
  broadcaster.emit("changed", dir);
27637
- if (autoSync === "off") return;
27908
+ if (autoSync === "off" || cfg.public) return;
27638
27909
  const guarded = servedProductRepo(cfg.projectDirs ?? [cfg.projectDir]);
27639
27910
  if (guarded) {
27640
27911
  broadcaster.emit("op", `\u27F3 auto-sync (${autoSync})${memberTag(dir)} declined: ${productRefusal(guarded, "auto-sync").error}`);
@@ -27743,7 +28014,8 @@ async function startServer(cfg) {
27743
28014
  `behold \u2192 http://localhost:${info.port}
27744
28015
  project: ${cfg.projectDir}${cfg.env ? ` env: ${cfg.env}` : ""}${auto}${localTag}
27745
28016
  read-only, watching for edits${poll}. lanes: /lanes. Ctrl-C to stop.
27746
- `
28017
+ ` + (cfg.public ? ` public: GET, HEAD and OPTIONS only; no op runner; answers ${["localhost", ...allowedHostsFrom(cfg.allowedHosts ?? [])].join(", ")}.
28018
+ ` : "")
27747
28019
  );
27748
28020
  void detectProject(cfg.projectDir).then(({ environments, lexicons }) => {
27749
28021
  const envs = environments.length ? environments.join(", ") : "(none declared \u2014 env picker shows only source)";
@@ -27771,8 +28043,8 @@ init_chant();
27771
28043
 
27772
28044
  // src/workspace-convert.ts
27773
28045
  init_chant();
27774
- import { existsSync as existsSync28, mkdirSync as mkdirSync9, readFileSync as readFileSync33, renameSync as renameSync2, writeFileSync as writeFileSync7 } from "node:fs";
27775
- import { basename as basename6, dirname as dirname19, join as join45, relative as relative11, resolve as resolve31, sep as sep10 } from "node:path";
28046
+ import { existsSync as existsSync28, mkdirSync as mkdirSync9, readFileSync as readFileSync35, renameSync as renameSync2, writeFileSync as writeFileSync7 } from "node:fs";
28047
+ import { basename as basename6, dirname as dirname19, join as join47, relative as relative11, resolve as resolve31, sep as sep10 } from "node:path";
27776
28048
  import { shortStackNames as shortStackNames2 } from "@intentius/pinhole";
27777
28049
  init_project();
27778
28050
  init_terraform_member();
@@ -27790,10 +28062,10 @@ function unique(name, taken) {
27790
28062
  var posix4 = (p) => p.split(sep10).join("/");
27791
28063
  function installedVersion(pkg, dir) {
27792
28064
  for (let at2 = resolve31(dir); ; at2 = dirname19(at2)) {
27793
- const manifest = join45(at2, "node_modules", ...pkg.split("/"), "package.json");
28065
+ const manifest = join47(at2, "node_modules", ...pkg.split("/"), "package.json");
27794
28066
  if (existsSync28(manifest)) {
27795
28067
  try {
27796
- const v = JSON.parse(readFileSync33(manifest, "utf8")).version;
28068
+ const v = JSON.parse(readFileSync35(manifest, "utf8")).version;
27797
28069
  return typeof v === "string" ? v : void 0;
27798
28070
  } catch {
27799
28071
  return void 0;
@@ -27804,7 +28076,7 @@ function installedVersion(pkg, dir) {
27804
28076
  }
27805
28077
  function planConversion(rootArg) {
27806
28078
  const root = resolve31(rootArg);
27807
- const existing = DECLARATION_FILES.find((f) => existsSync28(join45(root, f)));
28079
+ const existing = DECLARATION_FILES.find((f) => existsSync28(join47(root, f)));
27808
28080
  if (existing) return { ok: false, error: `${root} already has ${existing}; there is nothing to convert.` };
27809
28081
  const shape = detectProjectShape(root);
27810
28082
  if (shape.kind !== "estate" || !shape.members?.length || shape.membersFrom === "itself") {
@@ -27825,10 +28097,10 @@ function planConversion(rootArg) {
27825
28097
  const scan = discoverTerraformRoots(abs[i]);
27826
28098
  for (const r of scan.roots) {
27827
28099
  const name2 = unique(declarationName(r.name), taken);
27828
- members.push({ name: name2, dir: r.dir === "." ? dir : posix4(join45(dir, r.dir)), kind: "terraform" });
28100
+ members.push({ name: name2, dir: r.dir === "." ? dir : posix4(join47(dir, r.dir)), kind: "terraform" });
27829
28101
  moves.push({ from: `${short}/${r.name}/`, to: `${name2}/${name2}/` });
27830
28102
  }
27831
- for (const s of scan.skipped) notes.push(`${posix4(join45(dir, s.dir))} is not declared: ${s.why}.`);
28103
+ for (const s of scan.skipped) notes.push(`${posix4(join47(dir, s.dir))} is not declared: ${s.why}.`);
27832
28104
  if (scan.roots.length > 1) notes.push(`${dir} held ${scan.roots.length} Terraform roots; each is its own member now, since a chant terraform member is one root.`);
27833
28105
  return;
27834
28106
  }
@@ -27871,7 +28143,7 @@ function moveLayoutIds(file, moves) {
27871
28143
  return { version: 1, lenses };
27872
28144
  }
27873
28145
  function applyConversion(plan) {
27874
- const declarationFile = join45(plan.root, DECLARATION_FILES[0]);
28146
+ const declarationFile = join47(plan.root, DECLARATION_FILES[0]);
27875
28147
  if (existsSync28(declarationFile)) throw new Error(`${declarationFile} appeared while converting; nothing was written`);
27876
28148
  writeFileSync7(declarationFile, `${JSON.stringify(plan.declaration, null, 2)}
27877
28149
  `);
@@ -27882,7 +28154,7 @@ function applyConversion(plan) {
27882
28154
  else if (existsSync28(target)) layout = { skipped: `${target} already exists; the layout in ${plan.layoutFrom} was left alone` };
27883
28155
  else {
27884
28156
  const moved = moveLayoutIds(readLayoutFile(dirname19(dirname19(plan.layoutFrom))), plan.moves);
27885
- mkdirSync9(join45(plan.root, LAYOUT_DIR), { recursive: true });
28157
+ mkdirSync9(join47(plan.root, LAYOUT_DIR), { recursive: true });
27886
28158
  const tmp = `${target}.tmp`;
27887
28159
  writeFileSync7(tmp, `${JSON.stringify(moved, null, 2)}
27888
28160
  `);
@@ -27893,9 +28165,9 @@ function applyConversion(plan) {
27893
28165
  }
27894
28166
 
27895
28167
  // src/export.ts
27896
- import { mkdirSync as mkdirSync10, writeFileSync as writeFileSync8, copyFileSync, cpSync as cpSync2, readFileSync as readFileSync34, readdirSync as readdirSync14, existsSync as existsSync29, realpathSync as realpathSync2 } from "node:fs";
28168
+ import { mkdirSync as mkdirSync10, writeFileSync as writeFileSync8, copyFileSync, cpSync as cpSync2, readFileSync as readFileSync36, readdirSync as readdirSync14, existsSync as existsSync29, realpathSync as realpathSync2 } from "node:fs";
27897
28169
  import { homedir as homedir3 } from "node:os";
27898
- import { join as join46, dirname as dirname20, basename as basename7, relative as relative12, resolve as resolve32, sep as sep11 } from "node:path";
28170
+ import { join as join48, dirname as dirname20, basename as basename7, relative as relative12, resolve as resolve32, sep as sep11 } from "node:path";
27899
28171
  import { createHash as createHash11 } from "node:crypto";
27900
28172
  import { fileURLToPath as fileURLToPath11 } from "node:url";
27901
28173
  var LENS_PARAMS = ["collapse", "components", "detail", "env", "logical", "ops", "radial", "stacks", "tier"];
@@ -27952,24 +28224,24 @@ function captureKeys(axes) {
27952
28224
  return [...keys];
27953
28225
  }
27954
28226
  function webDir() {
27955
- return join46(dirname20(fileURLToPath11(import.meta.url)), "..", "web");
28227
+ return join48(dirname20(fileURLToPath11(import.meta.url)), "..", "web");
27956
28228
  }
27957
28229
  function pkgRoot2() {
27958
- return join46(dirname20(fileURLToPath11(import.meta.url)), "..");
28230
+ return join48(dirname20(fileURLToPath11(import.meta.url)), "..");
27959
28231
  }
27960
28232
  var NOTICE_FILES = ["LICENSE", "THIRD_PARTY.md"];
27961
28233
  function copyNotices(outDir, root = pkgRoot2()) {
27962
28234
  const copied = [];
27963
28235
  for (const f of NOTICE_FILES) {
27964
- if (!existsSync29(join46(root, f))) continue;
27965
- copyFileSync(join46(root, f), join46(outDir, f));
28236
+ if (!existsSync29(join48(root, f))) continue;
28237
+ copyFileSync(join48(root, f), join48(outDir, f));
27966
28238
  copied.push(f);
27967
28239
  }
27968
- const lic = join46(root, "licenses");
28240
+ const lic = join48(root, "licenses");
27969
28241
  if (existsSync29(lic)) {
27970
- mkdirSync10(join46(outDir, "licenses"), { recursive: true });
28242
+ mkdirSync10(join48(outDir, "licenses"), { recursive: true });
27971
28243
  for (const f of readdirSync14(lic)) {
27972
- copyFileSync(join46(lic, f), join46(outDir, "licenses", f));
28244
+ copyFileSync(join48(lic, f), join48(outDir, "licenses", f));
27973
28245
  copied.push(`licenses/${f}`);
27974
28246
  }
27975
28247
  }
@@ -27994,7 +28266,7 @@ async function runExport(cfg, outDir, opts = {}) {
27994
28266
  ...proj.sqlYodeler ? { sqlYodeler: true } : {}
27995
28267
  };
27996
28268
  const scrub = exportScrubber(cfg);
27997
- const snapDir = join46(outDir, "snapshots");
28269
+ const snapDir = join48(outDir, "snapshots");
27998
28270
  mkdirSync10(snapDir, { recursive: true });
27999
28271
  const keyToFile = {};
28000
28272
  let ok = 0;
@@ -28003,7 +28275,7 @@ async function runExport(cfg, outDir, opts = {}) {
28003
28275
  const res = await app.request(`${key}${key.includes("?") ? "&" : "?"}layout=1`);
28004
28276
  const body = scrub(shapeSnapshot(key, await res.text(), opts));
28005
28277
  const file = slug3(key, body);
28006
- writeFileSync8(join46(snapDir, file), body);
28278
+ writeFileSync8(join48(snapDir, file), body);
28007
28279
  keyToFile[key] = `snapshots/${file}`;
28008
28280
  if (res.ok) ok++;
28009
28281
  else failed++;
@@ -28025,29 +28297,29 @@ async function runExport(cfg, outDir, opts = {}) {
28025
28297
  axes,
28026
28298
  keyToFile
28027
28299
  };
28028
- writeFileSync8(join46(outDir, "manifest.json"), JSON.stringify(manifest, null, 2));
28029
- const html = readFileSync34(join46(webDir(), "index.html"), "utf8").replace(
28300
+ writeFileSync8(join48(outDir, "manifest.json"), JSON.stringify(manifest, null, 2));
28301
+ const html = readFileSync36(join48(webDir(), "index.html"), "utf8").replace(
28030
28302
  /<\/head>/i,
28031
28303
  ` <script>window.__BEHOLD_STATIC__ = true;</script>
28032
28304
  </head>`
28033
28305
  );
28034
- writeFileSync8(join46(outDir, "index.html"), html);
28306
+ writeFileSync8(join48(outDir, "index.html"), html);
28035
28307
  for (const f of readdirSync14(webDir())) {
28036
28308
  if (f === "index.html" || f.endsWith(".test.js")) continue;
28037
- cpSync2(join46(webDir(), f), join46(outDir, f), { recursive: true });
28309
+ cpSync2(join48(webDir(), f), join48(outDir, f), { recursive: true });
28038
28310
  }
28039
- writeFileSync8(join46(outDir, "README.md"), BUNDLE_README);
28311
+ writeFileSync8(join48(outDir, "README.md"), BUNDLE_README);
28040
28312
  copyNotices(outDir);
28041
28313
  const name = workerName(cfg.projectDir, opts.name);
28042
28314
  writeFileSync8(
28043
- join46(outDir, "wrangler.jsonc"),
28315
+ join48(outDir, "wrangler.jsonc"),
28044
28316
  JSON.stringify(
28045
28317
  { $schema: "node_modules/wrangler/config-schema.json", name, compatibility_date: "2025-06-01", assets: { directory: "." } },
28046
28318
  null,
28047
28319
  2
28048
28320
  ) + "\n"
28049
28321
  );
28050
- writeFileSync8(join46(outDir, "manifest.json"), JSON.stringify({ ...manifest, files: bundleFiles(outDir).filter((f) => f !== "manifest.json") }, null, 2));
28322
+ writeFileSync8(join48(outDir, "manifest.json"), JSON.stringify({ ...manifest, files: bundleFiles(outDir).filter((f) => f !== "manifest.json") }, null, 2));
28051
28323
  process.stdout.write(
28052
28324
  `behold export \u2192 ${outDir}
28053
28325
  ${ok} snapshots${failed ? ` (${failed} endpoint error(s) captured as-is)` : ""}
@@ -28056,11 +28328,11 @@ async function runExport(cfg, outDir, opts = {}) {
28056
28328
  `
28057
28329
  );
28058
28330
  }
28059
- var isRecord20 = (v) => v !== null && typeof v === "object" && !Array.isArray(v);
28331
+ var isRecord21 = (v) => v !== null && typeof v === "object" && !Array.isArray(v);
28060
28332
  function dropSource(v) {
28061
28333
  if (Array.isArray(v)) return v.forEach(dropSource);
28062
- if (!isRecord20(v)) return;
28063
- if (isRecord20(v.attrs)) delete v.attrs.source;
28334
+ if (!isRecord21(v)) return;
28335
+ if (isRecord21(v.attrs)) delete v.attrs.source;
28064
28336
  for (const x of Object.values(v)) dropSource(x);
28065
28337
  }
28066
28338
  function shapeSnapshot(key, body, opts = {}) {
@@ -28070,7 +28342,7 @@ function shapeSnapshot(key, body, opts = {}) {
28070
28342
  } catch {
28071
28343
  return body;
28072
28344
  }
28073
- if (!isRecord20(doc)) return body;
28345
+ if (!isRecord21(doc)) return body;
28074
28346
  if (key === "/api/project") {
28075
28347
  delete doc.recents;
28076
28348
  delete doc.approver;
@@ -28140,11 +28412,11 @@ function manifestFiles(text) {
28140
28412
  } catch {
28141
28413
  return [];
28142
28414
  }
28143
- const map = isRecord20(doc) && isRecord20(doc.keyToFile) ? doc.keyToFile : {};
28415
+ const map = isRecord21(doc) && isRecord21(doc.keyToFile) ? doc.keyToFile : {};
28144
28416
  return Object.values(map).filter((f) => typeof f === "string" && /^snapshots\/[A-Za-z0-9=_.-]+$/.test(f) && !f.includes(".."));
28145
28417
  }
28146
28418
  function bundleFiles(outDir) {
28147
- return readdirSync14(outDir, { recursive: true, withFileTypes: true }).filter((d) => d.isFile()).map((d) => relative12(outDir, join46(d.parentPath, d.name)).split(sep11).join("/")).sort();
28419
+ return readdirSync14(outDir, { recursive: true, withFileTypes: true }).filter((d) => d.isFile()).map((d) => relative12(outDir, join48(d.parentPath, d.name)).split(sep11).join("/")).sort();
28148
28420
  }
28149
28421
  async function publishBundle(outDir, target, client) {
28150
28422
  const files = bundleFiles(outDir);
@@ -28160,7 +28432,7 @@ async function publishBundle(outDir, target, client) {
28160
28432
  }
28161
28433
  const last = ["manifest.json", "index.html"];
28162
28434
  const order = [...files.filter((f) => !last.includes(f)), ...last.filter((f) => files.includes(f))];
28163
- for (const f of order) await client.put(at2(f), readFileSync34(join46(outDir, f)), contentType(f), CONTENT_ADDRESSED.test(f) ? IMMUTABLE : NO_CACHE);
28435
+ for (const f of order) await client.put(at2(f), readFileSync36(join48(outDir, f)), contentType(f), CONTENT_ADDRESSED.test(f) ? IMMUTABLE : NO_CACHE);
28164
28436
  const wrote = new Set(files);
28165
28437
  const removed = [];
28166
28438
  for (const f of previous) {
@@ -28212,8 +28484,8 @@ It's just files \u2014 GitHub Pages, S3, nginx, or Cloudflare Pages
28212
28484
 
28213
28485
  // src/publish-snapshots.ts
28214
28486
  import { execFileSync as execFileSync4 } from "node:child_process";
28215
- import { readFileSync as readFileSync35 } from "node:fs";
28216
- import { join as join47 } from "node:path";
28487
+ import { readFileSync as readFileSync37 } from "node:fs";
28488
+ import { join as join49 } from "node:path";
28217
28489
  var DEFAULT_KEEP = 30;
28218
28490
  var SNAPSHOT_REPORTS_BASE = "../../../";
28219
28491
  var COMMIT = /^[0-9a-f]{7,64}$/;
@@ -28238,12 +28510,12 @@ function resolveCommit2(dir, given) {
28238
28510
  return { sha };
28239
28511
  }
28240
28512
  }
28241
- var isRecord21 = (v) => v !== null && typeof v === "object" && !Array.isArray(v);
28513
+ var isRecord22 = (v) => v !== null && typeof v === "object" && !Array.isArray(v);
28242
28514
  function parseHistory(text) {
28243
28515
  const doc = JSON.parse(text);
28244
28516
  if (!Array.isArray(doc)) throw new Error("history.json is not an array");
28245
- const str15 = (v) => typeof v === "string" ? v : "";
28246
- return doc.filter((e) => isRecord21(e) && typeof e.commit === "string" && COMMIT.test(e.commit)).map((e) => ({ commit: e.commit, at: str15(e.at), project: str15(e.project), generated: str15(e.generated) }));
28517
+ const str16 = (v) => typeof v === "string" ? v : "";
28518
+ return doc.filter((e) => isRecord22(e) && typeof e.commit === "string" && COMMIT.test(e.commit)).map((e) => ({ commit: e.commit, at: str16(e.at), project: str16(e.project), generated: str16(e.generated) }));
28247
28519
  }
28248
28520
  function nextHistory(old, entry, keep) {
28249
28521
  const all = [entry, ...old.filter((e) => e.commit !== entry.commit)];
@@ -28255,9 +28527,9 @@ var BUNDLE_PATH = /^[A-Za-z0-9=_.@+-]+(\/[A-Za-z0-9=_.@+-]+)*$/;
28255
28527
  var insideBundle = (f) => typeof f === "string" && BUNDLE_PATH.test(f) && !f.split("/").some((p) => p === "." || p === "..");
28256
28528
  function commitFiles(manifestText) {
28257
28529
  const doc = JSON.parse(manifestText);
28258
- if (!isRecord21(doc)) return ["manifest.json"];
28530
+ if (!isRecord22(doc)) return ["manifest.json"];
28259
28531
  const listed = Array.isArray(doc.files) ? doc.files.filter(insideBundle) : [];
28260
- const snaps = isRecord21(doc.keyToFile) ? Object.values(doc.keyToFile).filter(insideBundle) : [];
28532
+ const snaps = isRecord22(doc.keyToFile) ? Object.values(doc.keyToFile).filter(insideBundle) : [];
28261
28533
  const rest = [.../* @__PURE__ */ new Set([...listed, ...snaps])].filter((f) => f !== "index.html" && f !== "manifest.json").sort();
28262
28534
  return ["index.html", ...rest, "manifest.json"];
28263
28535
  }
@@ -28301,7 +28573,7 @@ async function publishSnapshot(outDir, target, commit, client, opts = {}) {
28301
28573
  const keep = opts.keep ?? DEFAULT_KEEP;
28302
28574
  if (!Number.isInteger(keep) || keep < 1) throw new Error(`--keep takes a whole number of at least 1, not ${keep}`);
28303
28575
  const at2 = (f) => `${target.prefix}/${f}`;
28304
- const local = JSON.parse(readFileSync35(join47(outDir, "manifest.json"), "utf8"));
28576
+ const local = JSON.parse(readFileSync37(join49(outDir, "manifest.json"), "utf8"));
28305
28577
  const generated = local.capturedAt ?? (/* @__PURE__ */ new Date()).toISOString();
28306
28578
  const entry = { commit: sha, at: commit.at ?? generated, project: opts.project ?? local.projectDir ?? "", generated };
28307
28579
  const bundle = await publishBundle(outDir, { bucket: target.bucket, prefix: at2(sha) }, client);
@@ -28369,6 +28641,7 @@ Usage:
28369
28641
  [--yodel-report <file|dir>]
28370
28642
  [--publish s3://<bucket>/<prefix>/views/<name> [--commit <sha>] [--keep <n>]]
28371
28643
  behold serve <project-dir\u2026> [--port <n>] [--host <addr>] [--allow-host <name,\u2026>] [--hud <url>] [--env <name>] [--poll <secs>] [--local]
28644
+ [--public] [--terragucci <src> [--terragucci-project <p>] [--terragucci-poll <secs>]]
28372
28645
  [--yodel-report <file|dir>]
28373
28646
  behold carve <report.json> [--port <n>]
28374
28647
 
@@ -28434,6 +28707,19 @@ Options:
28434
28707
  --allow-host <n,\u2026> serve only: names besides loopback behold answers to, and
28435
28708
  whose pages may write (BEHOLD_ALLOWED_HOSTS too). For a
28436
28709
  proxy that frames behold, or --host 0.0.0.0.
28710
+ --public serve only: a read-only view anyone may load, for a
28711
+ public host (#569). behold builds no op runner, refuses
28712
+ every method but GET, HEAD and OPTIONS (405), and refuses
28713
+ (403) every read that runs a command or uses your
28714
+ credentials and isn't part of the view: the op tools,
28715
+ the live overlay and its plan, diff, reconcile, CI,
28716
+ doctor, substrates, history, workspace gates and why,
28717
+ choudoufu moves. The page shows no control that writes
28718
+ or asks for those. One project directory only; not with
28719
+ --env, --poll, --local or --auto-sync. It still binds
28720
+ 127.0.0.1: put a proxy in front and pass the public name
28721
+ with --allow-host. With --terragucci, the poll defaults
28722
+ to 10 s.
28437
28723
  --hud <url> serve only: where hud reviews this workspace's records
28438
28724
  (BEHOLD_HUD_URL too). A proposed decision in a member's
28439
28725
  or a card's "why" links to its review there (#471).
@@ -28452,7 +28738,8 @@ Options:
28452
28738
  serve with --terragucci only: while a page is open, ask
28453
28739
  whether index.json changed (If-None-Match, or its mtime)
28454
28740
  and fetch chant/lifecycle (the gates and locks) every
28455
- <secs> (default 30), and push what moved to the page.
28741
+ <secs> (default 30, or 10 with --public), and push what
28742
+ moved to the page.
28456
28743
  0 turns it off. The fetch goes into behold's own cache
28457
28744
  (<tmpdir>/behold-lifecycle-*), never into the checkout.
28458
28745
  --yodel-report <file|dir>
@@ -28573,6 +28860,7 @@ ${USAGE}`);
28573
28860
  let terragucciProject;
28574
28861
  let terragucciPoll;
28575
28862
  let yodelReport;
28863
+ let isPublic = false;
28576
28864
  const allowHosts = [];
28577
28865
  for (let i = 0; i < rest.length; i++) {
28578
28866
  const a = rest[i];
@@ -28587,6 +28875,7 @@ ${USAGE}`);
28587
28875
  else if (a === "--env") env = rest[++i];
28588
28876
  else if (a === "--poll") pollSecs = Number(rest[++i]);
28589
28877
  else if (a === "--local") local = true;
28878
+ else if (a === "--public") isPublic = true;
28590
28879
  else if (a === "--auto-sync") {
28591
28880
  const m = rest[++i];
28592
28881
  if (!m || !isAutoSyncMode(m)) {
@@ -28646,6 +28935,23 @@ ${USAGE}`);
28646
28935
  process.stderr.write("behold serve: --terragucci-poll needs --terragucci\n");
28647
28936
  process.exit(2);
28648
28937
  }
28938
+ if (isPublic && projectDirs.length > 1) {
28939
+ process.stderr.write("behold serve: --public serves one project directory\n");
28940
+ process.exit(2);
28941
+ }
28942
+ if (isPublic && local) {
28943
+ process.stderr.write("behold serve: --public can't --local (it boots emulators and deploys to them)\n");
28944
+ process.exit(2);
28945
+ }
28946
+ if (isPublic && (env || pollSecs !== void 0)) {
28947
+ process.stderr.write("behold serve: --public can't --env or --poll (a live read uses your credentials)\n");
28948
+ process.exit(2);
28949
+ }
28950
+ if (isPublic && autoSync !== "off") {
28951
+ process.stderr.write("behold serve: --public can't --auto-sync (it runs Ops)\n");
28952
+ process.exit(2);
28953
+ }
28954
+ if (isPublic && terragucci !== void 0 && terragucciPoll === void 0) terragucciPoll = PUBLIC_TERRAGUCCI_POLL_SECS;
28649
28955
  if (yodelReport !== void 0 && !yodelReport) {
28650
28956
  process.stderr.write("behold serve: --yodel-report needs the file `yodel report --json` wrote, or the directory holding it\n");
28651
28957
  process.exit(2);
@@ -28661,11 +28967,13 @@ ${USAGE}`);
28661
28967
  ...local ? { local: true } : {},
28662
28968
  ...host ? { host } : {},
28663
28969
  ...allowHosts.length ? { allowedHosts: allowHosts } : {},
28970
+ ...isPublic ? { public: true, layoutWrites: false } : {},
28664
28971
  ...target.workspace && hud ? { hud } : {},
28665
28972
  ...yodelReport ? { yodelReport: resolve33(yodelReport) } : {},
28666
28973
  ...terragucci ? { terragucci: { source: terragucci, ...terragucciProject ? { project: terragucciProject } : {}, ...terragucciPoll !== void 0 ? { pollSecs: terragucciPoll } : {} } } : {}
28667
28974
  });
28668
28975
  }
28976
+ var PUBLIC_TERRAGUCCI_POLL_SECS = 10;
28669
28977
  async function serveTarget(dirs, verb) {
28670
28978
  const workspace = dirs.length === 1 && hasDeclaration(dirs[0]) ? await declaredWorkspace(dirs[0], verb) : void 0;
28671
28979
  if (workspace && workspace.members.length > 0) {
@@ -28717,7 +29025,7 @@ function warnIfNotChantProject(dir) {
28717
29025
  if (shape.kind === "estate") {
28718
29026
  process.stderr.write(
28719
29027
  `behold: warning \u2014 ${dir} is an estate root, not a chant project itself.
28720
- Serve its members composed: behold serve ${shape.members.map((m) => join48(dir, m.dir)).join(" ")}
29028
+ Serve its members composed: behold serve ${shape.members.map((m) => join50(dir, m.dir)).join(" ")}
28721
29029
  `
28722
29030
  );
28723
29031
  return;
@@ -28753,7 +29061,7 @@ async function runCarve(rest) {
28753
29061
  process.exit(2);
28754
29062
  }
28755
29063
  const reportPath = resolve33(fileArg);
28756
- const parsed = readCarveReport(reportPath, (p) => readFileSync36(p, "utf8"));
29064
+ const parsed = readCarveReport(reportPath, (p) => readFileSync38(p, "utf8"));
28757
29065
  if (!parsed.ok) {
28758
29066
  process.stderr.write(`behold carve: ${parsed.refusal.error}
28759
29067
  ${parsed.refusal.remedy}
@@ -28819,7 +29127,7 @@ ${check.stdout || check.stderr}
28819
29127
  if (check.code !== 0) process.exitCode = 1;
28820
29128
  }
28821
29129
  async function runDemo(rest) {
28822
- const pkgRoot3 = join48(dirname21(fileURLToPath12(import.meta.url)), "..");
29130
+ const pkgRoot3 = join50(dirname21(fileURLToPath12(import.meta.url)), "..");
28823
29131
  const registry2 = loadDemoRegistry(pkgRoot3);
28824
29132
  let port = 4600;
28825
29133
  let live2 = false;
@@ -28954,7 +29262,7 @@ async function serveCarveDemo(target, carve, port, live2 = false) {
28954
29262
  };
28955
29263
  process.stdout.write("behold demo carve --live \u2192 the tfstate is real now; the advisor reads what terraform wrote.\n");
28956
29264
  }
28957
- if (existsSync30(join48(project, "package.json")) && !existsSync30(join48(project, "node_modules"))) {
29265
+ if (existsSync30(join50(project, "package.json")) && !existsSync30(join50(project, "node_modules"))) {
28958
29266
  process.stdout.write(`behold demo carve \u2192 npm install in ${carve.project}/ (the chant this walkthrough shells)\u2026
28959
29267
  `);
28960
29268
  const code = await spawnStep("npm", ["install"], project);
@@ -28965,7 +29273,7 @@ async function serveCarveDemo(target, carve, port, live2 = false) {
28965
29273
  }
28966
29274
  }
28967
29275
  let degraded;
28968
- if (!existsSync30(join48(target, "node_modules", ...HCL_PARSER_PKG.split("/")))) {
29276
+ if (!existsSync30(join50(target, "node_modules", ...HCL_PARSER_PKG.split("/")))) {
28969
29277
  process.stdout.write(`behold demo carve \u2192 npm install ${HCL_PARSER_PKG} (chant's HCL parser, ~2MB, once)\u2026
28970
29278
  `);
28971
29279
  const code = await spawnStep("npm", ["install", "--no-save", "--no-package-lock", HCL_PARSER_PKG], target);