@intentius/behold 0.9.0 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -1038,10 +1038,10 @@ var init_src = __esm({
1038
1038
  });
1039
1039
 
1040
1040
  // src/cli.ts
1041
- import { resolve as resolve7, dirname as dirname6, join as join17 } from "node:path";
1042
- import { realpathSync, existsSync as existsSync13, readFileSync as readFileSync14 } from "node:fs";
1041
+ import { resolve as resolve7, dirname as dirname6, join as join18 } from "node:path";
1042
+ import { realpathSync, existsSync as existsSync14, readFileSync as readFileSync15, rmSync as rmSync2 } from "node:fs";
1043
1043
  import { fileURLToPath as fileURLToPath5 } from "node:url";
1044
- import { spawn as spawn5 } from "node:child_process";
1044
+ import { spawn as spawn6, spawnSync as spawnSync2 } from "node:child_process";
1045
1045
 
1046
1046
  // src/server.ts
1047
1047
  import { Hono } from "hono";
@@ -1120,8 +1120,8 @@ import { serve } from "@hono/node-server";
1120
1120
  import { fileURLToPath as fileURLToPath3 } from "node:url";
1121
1121
  import { execFile as execFile3 } from "node:child_process";
1122
1122
  import { promisify as promisify3 } from "node:util";
1123
- import { existsSync as existsSync12, readFileSync as readFileSync12 } from "node:fs";
1124
- import { dirname as dirname4, join as join15, relative as relative3, resolve as resolve5, sep as sep4 } from "node:path";
1123
+ import { existsSync as existsSync13, readFileSync as readFileSync13 } from "node:fs";
1124
+ import { dirname as dirname4, join as join16, relative as relative3, resolve as resolve5, sep as sep4 } from "node:path";
1125
1125
 
1126
1126
  // src/recents.ts
1127
1127
  import { readFileSync, writeFileSync, mkdirSync, existsSync } from "node:fs";
@@ -3872,7 +3872,14 @@ function summarizePlan(plan, byComponent, nonResource) {
3872
3872
  }
3873
3873
 
3874
3874
  // src/render.ts
3875
- import { layoutIr, layoutArchitecture, renderSvg, cardSizes, registerPack } from "@intentius/pinhole";
3875
+ import {
3876
+ layoutIr,
3877
+ layoutArchitecture,
3878
+ renderSvg,
3879
+ renderMorphHtml,
3880
+ cardSizes,
3881
+ registerPack
3882
+ } from "@intentius/pinhole";
3876
3883
 
3877
3884
  // src/icon-packs.ts
3878
3885
  import { readFileSync as readFileSync6 } from "node:fs";
@@ -4052,6 +4059,7 @@ function scoreArithmetic(r) {
4052
4059
  const terms = [];
4053
4060
  if (p.inbound) terms.push(`- 12x${b.inbound ?? 0} inbound`);
4054
4061
  if (p.outbound) terms.push(`- 4x${b.outbound ?? 0} outbound`);
4062
+ if (p.outputs) terms.push(`- 4x${b.outputs ?? 0} output`);
4055
4063
  if (p.tier) terms.push(`- 15x${(b.tier ?? 1) - 1} tier${b.tier}`);
4056
4064
  if (p.dynamic) terms.push(`- 10 dynamic`);
4057
4065
  if (p.instances) terms.push(`- 3x${(b.instances ?? 1) - 1} instances`);
@@ -4066,6 +4074,7 @@ function boundaryWorkOf(r) {
4066
4074
  const parts = [];
4067
4075
  if (b.inbound) parts.push(`${b.inbound} inbound (a data-source patch each)`);
4068
4076
  if (b.outbound) parts.push(`${b.outbound} outbound (a deferred input each)`);
4077
+ if (b.outputs) parts.push(`${b.outputs} output block(s) reading it (a one-line rewrite each)`);
4069
4078
  if ((b.tier ?? 1) > 1) parts.push(`tier ${b.tier} map`);
4070
4079
  if (b.hasDynamic) parts.push("count/for_each/data present");
4071
4080
  if ((b.instances ?? 1) > 1) parts.push(`${b.instances} instances`);
@@ -4106,6 +4115,7 @@ function carveReportToIr(report) {
4106
4115
  tier: b.tier === null || b.tier === void 0 ? "none" : b.tier,
4107
4116
  inbound: b.inbound ?? 0,
4108
4117
  outbound: b.outbound ?? 0,
4118
+ outputs: b.outputs ?? 0,
4109
4119
  instances: b.instances ?? 1,
4110
4120
  dynamic: b.hasDynamic ?? false,
4111
4121
  // The predicted diff, when the report carries the edge lists: who needs
@@ -4186,16 +4196,32 @@ function renderGraph(ir, opts = {}) {
4186
4196
  return { svg };
4187
4197
  }
4188
4198
  function renderBanded(ir, opts = {}) {
4189
- const bands = ir.groups.byStack ?? {};
4199
+ const plan = bandedPlan(ir, ir.groups.byStack ?? {});
4200
+ const height = plan.height;
4201
+ const layout = {
4202
+ width: plan.width,
4203
+ height,
4204
+ nodes: plan.placed.map((p) => ({ id: p.id, x: p.x, y: height - p.y }))
4205
+ };
4206
+ const svg = renderSvg(ir, layout, {
4207
+ fit: true,
4208
+ hideTitle: true,
4209
+ groups: plan.boxes.map((b) => ({ ...b, y: height - b.y })),
4210
+ ...opts.theme ? { theme: opts.theme } : {}
4211
+ });
4212
+ return { svg };
4213
+ }
4214
+ var GAP = 28;
4215
+ var PAD = 24;
4216
+ var TITLE = 34;
4217
+ var BAND_GAP = 26;
4218
+ var MEMBER_GAP = 48;
4219
+ function bandedPlan(ir, bands) {
4190
4220
  const size = footprints(ir);
4191
4221
  const dims = (id) => size.get(id) ?? { w: NODE_W, h: NODE_H };
4192
4222
  const ids = ir.nodes.map((n) => n.id);
4193
4223
  const cellW = Math.max(NODE_W, ...ids.map((id) => dims(id).w));
4194
4224
  const cellH = Math.max(NODE_H, ...ids.map((id) => dims(id).h));
4195
- const GAP = 28;
4196
- const PAD = 24;
4197
- const TITLE = 34;
4198
- const BAND_GAP = 26;
4199
4225
  const cols = Math.max(1, Math.round(Math.sqrt(ids.length * (cellH + GAP) * 2 / (cellW + GAP))));
4200
4226
  const contentW = cols * cellW + (cols - 1) * GAP;
4201
4227
  const panelW = contentW + PAD * 2;
@@ -4224,19 +4250,95 @@ function renderBanded(ir, opts = {}) {
4224
4250
  boxes.push({ title, x: panelW / 2, y: top + panelH / 2, w: panelW, h: panelH, ...status ? { status } : {} });
4225
4251
  top += panelH + BAND_GAP;
4226
4252
  }
4227
- const height = Math.max(1, top - BAND_GAP);
4228
- const layout = {
4229
- width: panelW,
4230
- height,
4231
- nodes: placed.map((p) => ({ id: p.id, x: p.x, y: height - p.y }))
4232
- };
4233
- const svg = renderSvg(ir, layout, {
4253
+ return { placed, boxes, width: panelW, height: Math.max(1, top - BAND_GAP) };
4254
+ }
4255
+ function renderCarveEstate(tfIr, appIr, opts) {
4256
+ const comp = composeCarveEstate(tfIr, namespaceAppIr(appIr, opts.appTitle), opts);
4257
+ const svg = renderSvg(comp.ir, comp.layout, {
4234
4258
  fit: true,
4235
4259
  hideTitle: true,
4236
- groups: boxes.map((b) => ({ ...b, y: height - b.y })),
4260
+ groups: comp.boxes,
4237
4261
  ...opts.theme ? { theme: opts.theme } : {}
4238
4262
  });
4239
- return { svg };
4263
+ return { svg, ir: comp.ir };
4264
+ }
4265
+ function namespaceAppIr(appIr, appTitle) {
4266
+ const prefix = `${appTitle.split(" ")[0]}/`;
4267
+ const appId = (id) => `${prefix}${id}`;
4268
+ return {
4269
+ ...appIr,
4270
+ nodes: appIr.nodes.map((n) => ({ ...n, id: appId(n.id) })),
4271
+ edges: appIr.edges.map((e) => ({ ...e, from: appId(e.from), to: appId(e.to) })),
4272
+ groups: {}
4273
+ };
4274
+ }
4275
+ function composeCarveEstate(tfIr, appNs, opts) {
4276
+ const tfPlan = bandedPlan(tfIr, tfIr.groups.byStack ?? {});
4277
+ const appPlan = bandedPlan(appNs, { "carved so far": appNs.nodes.map((n) => n.id) });
4278
+ const memberH = (plan) => TITLE + PAD + plan.height + PAD;
4279
+ const memberW = (plan) => plan.width + PAD * 2;
4280
+ const appX0 = memberW(tfPlan) + MEMBER_GAP;
4281
+ const width = appX0 + memberW(appPlan);
4282
+ const height = Math.max(memberH(tfPlan), memberH(appPlan));
4283
+ const offset = (plan, x0) => ({
4284
+ placed: plan.placed.map((p) => ({ id: p.id, x: x0 + PAD + p.x, y: TITLE + PAD + p.y })),
4285
+ boxes: plan.boxes.map((b) => ({ ...b, x: x0 + PAD + b.x, y: TITLE + PAD + b.y }))
4286
+ });
4287
+ const tf = offset(tfPlan, 0);
4288
+ const app = offset(appPlan, appX0);
4289
+ const boxes = [
4290
+ { title: opts.tfTitle, x: memberW(tfPlan) / 2, y: memberH(tfPlan) / 2, w: memberW(tfPlan), h: memberH(tfPlan) },
4291
+ { title: opts.appTitle, x: appX0 + memberW(appPlan) / 2, y: memberH(appPlan) / 2, w: memberW(appPlan), h: memberH(appPlan) },
4292
+ ...tf.boxes,
4293
+ ...app.boxes
4294
+ ];
4295
+ const ir = {
4296
+ nodes: [...tfIr.nodes, ...appNs.nodes],
4297
+ edges: [...tfIr.edges, ...appNs.edges],
4298
+ groups: {
4299
+ byStack: {
4300
+ ...tfIr.groups.byStack ?? {},
4301
+ [opts.appTitle]: appNs.nodes.map((n) => n.id)
4302
+ }
4303
+ }
4304
+ };
4305
+ return {
4306
+ ir,
4307
+ layout: {
4308
+ width,
4309
+ height,
4310
+ nodes: [...tf.placed, ...app.placed].map((p) => ({ id: p.id, x: p.x, y: height - p.y }))
4311
+ },
4312
+ boxes: boxes.map((b) => ({ ...b, y: height - b.y }))
4313
+ };
4314
+ }
4315
+ function renderCarveMorph(tfIr, appIr, carved, opts) {
4316
+ const appNs = namespaceAppIr(appIr, opts.appTitle);
4317
+ const before = composeCarveEstate(tfIr, appNs, opts);
4318
+ const gone = new Set(carved);
4319
+ const bands = tfIr.groups.byStack ?? {};
4320
+ const bandsAfter = {};
4321
+ for (const [band, members] of Object.entries(bands)) {
4322
+ const left = members.filter((id) => !gone.has(id));
4323
+ if (left.length) bandsAfter[band] = left;
4324
+ }
4325
+ const tfAfter = {
4326
+ nodes: tfIr.nodes.filter((n) => !gone.has(n.id)),
4327
+ edges: tfIr.edges.filter((e) => !gone.has(e.from) && !gone.has(e.to)),
4328
+ groups: { byStack: bandsAfter }
4329
+ };
4330
+ const carvedNodes = tfIr.nodes.filter((n) => gone.has(n.id)).map((n) => ({ ...n, attrs: { ...n.attrs, _status: "good", carve: "carved \u2192 chant" } }));
4331
+ const appAfter = { ...appNs, nodes: [...appNs.nodes, ...carvedNodes] };
4332
+ const after = composeCarveEstate(tfAfter, appAfter, opts);
4333
+ const view = (name, comp) => ({
4334
+ name,
4335
+ ir: comp.ir,
4336
+ layout: comp.layout,
4337
+ groups: comp.boxes
4338
+ });
4339
+ return renderMorphHtml([view("Terraform owns it", before), view("chant owns it", after)], {
4340
+ title: opts.title ?? `carve morph \u2014 ${carved.join(", ")}`
4341
+ });
4240
4342
  }
4241
4343
  var NODE_W = 175;
4242
4344
  var NODE_H = 104;
@@ -4403,8 +4505,8 @@ function radializeLayout(layout, groupOf, size = /* @__PURE__ */ new Map()) {
4403
4505
  }
4404
4506
 
4405
4507
  // src/carve-actions.ts
4406
- import { existsSync as existsSync7, mkdirSync as mkdirSync3, readFileSync as readFileSync8, readdirSync as readdirSync3, statSync as statSync2 } from "node:fs";
4407
- import { isAbsolute, join as join9, relative, resolve as resolve2, sep } from "node:path";
4508
+ import { existsSync as existsSync8, mkdirSync as mkdirSync3, readFileSync as readFileSync9, readdirSync as readdirSync3, statSync as statSync2 } from "node:fs";
4509
+ import { isAbsolute, join as join10, relative, resolve as resolve2, sep } from "node:path";
4408
4510
 
4409
4511
  // src/layout.ts
4410
4512
  import { existsSync as existsSync6, mkdirSync as mkdirSync2, readFileSync as readFileSync7, renameSync, rmSync, writeFileSync as writeFileSync2, accessSync, constants } from "node:fs";
@@ -4588,7 +4690,112 @@ function reanchorEdges(svg, moved) {
4588
4690
  return out + svg.slice(cursor);
4589
4691
  }
4590
4692
 
4693
+ // src/carve-live.ts
4694
+ import { spawn as spawn3 } from "node:child_process";
4695
+ import { readFileSync as readFileSync8, writeFileSync as writeFileSync3, existsSync as existsSync7 } from "node:fs";
4696
+ import { join as join9 } from "node:path";
4697
+ var LIVE_CONTAINER = "behold-carve-floci";
4698
+ var LIVE_PORT = 4602;
4699
+ var LIVE_IMAGE = "floci/floci:1.5.34";
4700
+ var LIVE_TARGETS = [
4701
+ "aws_s3_bucket.assets",
4702
+ "aws_s3_bucket_versioning.assets",
4703
+ "aws_s3_bucket_public_access_block.assets",
4704
+ "aws_cloudwatch_log_group.worker"
4705
+ ];
4706
+ function armOverride(disabledText, port) {
4707
+ return disabledText.replaceAll("localhost:4566", `localhost:${port}`);
4708
+ }
4709
+ function parsePlanOutput(stdout, exitCode) {
4710
+ const planLine = stdout.split("\n").reverse().find((l) => l.startsWith("Plan:") || l.includes("No changes.")) ?? `terraform plan exited ${exitCode}`;
4711
+ const destroyMatch = planLine.match(/(\d+) to destroy/);
4712
+ return {
4713
+ planLine: planLine.trim(),
4714
+ changes: exitCode === 2,
4715
+ noDestroy: !destroyMatch || destroyMatch[1] === "0"
4716
+ };
4717
+ }
4718
+ function capture(cmd, args, cwd) {
4719
+ return new Promise((res) => {
4720
+ const child = spawn3(cmd, args, { stdio: ["ignore", "pipe", "pipe"], cwd, shell: process.platform === "win32" });
4721
+ let out = "";
4722
+ child.stdout?.on("data", (d) => out += d.toString());
4723
+ child.stderr?.on("data", (d) => out += d.toString());
4724
+ child.on("error", () => res({ code: -1, stdout: out }));
4725
+ child.on("close", (code) => res({ code: code ?? 1, stdout: out }));
4726
+ });
4727
+ }
4728
+ async function bootScratchFloci() {
4729
+ const ps = await capture("docker", ["ps", "-a", "--format", "{{.Names}}"], process.cwd());
4730
+ if (ps.code !== 0) return "docker isn't answering \u2014 is the daemon up?";
4731
+ if (ps.stdout.split("\n").includes(LIVE_CONTAINER)) {
4732
+ return `container ${LIVE_CONTAINER} already exists \u2014 a previous run didn't tear down. \`docker rm -f ${LIVE_CONTAINER}\` and re-run.`;
4733
+ }
4734
+ const run3 = await capture(
4735
+ "docker",
4736
+ ["run", "-d", "--rm", "-p", `${LIVE_PORT}:4566`, "--name", LIVE_CONTAINER, LIVE_IMAGE],
4737
+ process.cwd()
4738
+ );
4739
+ if (run3.code !== 0) return `docker run ${LIVE_IMAGE} failed (${run3.code})`;
4740
+ for (let i = 0; i < 30; i++) {
4741
+ try {
4742
+ const health = await fetch(`http://localhost:${LIVE_PORT}/_localstack/health`);
4743
+ if (health.ok && (await health.text()).includes("cloudformation")) return void 0;
4744
+ } catch {
4745
+ }
4746
+ await new Promise((r) => setTimeout(r, 1e3));
4747
+ }
4748
+ await teardownScratchFloci();
4749
+ return `Floci never reported healthy on :${LIVE_PORT} after 30s`;
4750
+ }
4751
+ async function teardownScratchFloci() {
4752
+ await capture("docker", ["rm", "-f", LIVE_CONTAINER], process.cwd()).catch(() => void 0);
4753
+ }
4754
+ async function applyIntoFloci(fromDir, step) {
4755
+ const template = join9(fromDir, "floci-override.tf.disabled");
4756
+ if (!existsSync7(template)) return `no floci-override.tf.disabled in ${fromDir} \u2014 this estate wasn't authored for the live tier`;
4757
+ writeFileSync3(join9(fromDir, "floci_override.tf"), armOverride(readFileSync8(template, "utf8"), LIVE_PORT));
4758
+ const init = await step("terraform", ["init", "-input=false", "-no-color"], fromDir);
4759
+ if (init !== 0) return `terraform init exited ${init} (provider downloads need network)`;
4760
+ const apply = await step(
4761
+ "terraform",
4762
+ ["apply", "-input=false", "-auto-approve", "-no-color", ...LIVE_TARGETS.map((t) => `-target=${t}`)],
4763
+ fromDir
4764
+ );
4765
+ if (apply !== 0) return `terraform apply exited ${apply}`;
4766
+ return void 0;
4767
+ }
4768
+ async function runLivePlan(fromDir) {
4769
+ const r = await capture("terraform", ["plan", "-input=false", "-no-color", "-detailed-exitcode", ...LIVE_TARGETS.map((t) => `-target=${t}`)], fromDir);
4770
+ if (r.code !== 0 && r.code !== 2) {
4771
+ const tail = r.stdout.trim().split("\n").slice(-8).join("\n");
4772
+ return { error: `terraform plan failed (exit ${r.code}):
4773
+ ${tail}`, exitCode: r.code };
4774
+ }
4775
+ return { ...parsePlanOutput(r.stdout, r.code), exitCode: r.code };
4776
+ }
4777
+
4591
4778
  // src/carve-actions.ts
4779
+ async function runCarvePlan(demo) {
4780
+ if (!demo?.live) {
4781
+ return refuse2(
4782
+ "carve-action",
4783
+ "terraform plan is the live tier's beat, and this server isn't serving one",
4784
+ "run `behold demo carve --live` (needs docker + terraform)"
4785
+ );
4786
+ }
4787
+ const r = await runLivePlan(demo.from);
4788
+ if ("error" in r) {
4789
+ return refuse2("carve-action", r.error, "Is the scratch Floci still up? `docker ps` should list " + demo.live.container + ".");
4790
+ }
4791
+ return {
4792
+ ok: true,
4793
+ command: "terraform plan -detailed-exitcode (targeted, in the demo copy)",
4794
+ planLine: r.planLine,
4795
+ changes: r.changes,
4796
+ noDestroy: r.noDestroy
4797
+ };
4798
+ }
4592
4799
  var MAX_ARTIFACT_BYTES = 64 * 1024;
4593
4800
  var MAX_ARTIFACTS = 24;
4594
4801
  var MAX_OUTPUT_BYTES = 32 * 1024;
@@ -4596,7 +4803,7 @@ var refuse2 = (code, error, remedy) => ({
4596
4803
  ok: false,
4597
4804
  refusal: { error, code, remedy }
4598
4805
  });
4599
- var BUILD_CAVEAT = 'The gate shown is `chant lint`, not `chant build`. `carve emit` folds the bucket\'s versioning and public-access-block sub-resources into the carve set but does not yet carry them as native props, so `chant build` fails two AWS policy rules on source the advisor scored 88 (chant#1637). example-carve/README.md, "Known rough edge in emit", has the full statement.';
4806
+ var BUILD_CAVEAT = "The gate shown is `chant lint`, not `chant build`. chant#1637's fold is applied now: the bucket's versioning and public-access-block sub-resources come out as native props, not just folded-in mentions. `chant build` still fails, but on one rule only \u2014 WAW042, a TLS-deny bucket policy the source Terraform never declared. That's real drift the estate inherited, not a gap in the carve. example-carve/README.md, \"Why build still fails\", has the full statement.";
4600
4807
  function insideDemo(root, p) {
4601
4808
  const rel = relative(resolve2(root), resolve2(p));
4602
4809
  return rel !== "" && !rel.startsWith("..") && !isAbsolute(rel);
@@ -4609,7 +4816,7 @@ function carveWriteBlock(demo) {
4609
4816
  if (!demo) {
4610
4817
  return "this server isn't running a carve demo \u2014 the carve actions only exist inside a `behold demo carve` copy";
4611
4818
  }
4612
- if (!existsSync7(demo.from)) return `the demo copy has no Terraform estate at ${demo.from}`;
4819
+ if (!existsSync8(demo.from)) return `the demo copy has no Terraform estate at ${demo.from}`;
4613
4820
  if (!insideDemo(demo.root, demo.out)) return "the carve output directory is outside the demo copy";
4614
4821
  return unwritableReason(demo.root);
4615
4822
  }
@@ -4626,7 +4833,7 @@ function readArtifacts(demo, dir, filter) {
4626
4833
  for (const name of entries) {
4627
4834
  if (out.length >= MAX_ARTIFACTS) return;
4628
4835
  if (name === "node_modules" || name.startsWith(".")) continue;
4629
- const full = join9(d, name);
4836
+ const full = join10(d, name);
4630
4837
  let st;
4631
4838
  try {
4632
4839
  st = statSync2(full);
@@ -4641,7 +4848,7 @@ function readArtifacts(demo, dir, filter) {
4641
4848
  if (!filter(rel)) continue;
4642
4849
  let text = "";
4643
4850
  try {
4644
- text = readFileSync8(full, "utf8");
4851
+ text = readFileSync9(full, "utf8");
4645
4852
  } catch {
4646
4853
  continue;
4647
4854
  }
@@ -4673,7 +4880,7 @@ async function runCarveEmit(demo, select) {
4673
4880
  const block = carveWriteBlock(demo);
4674
4881
  if (block) return refuse2("read-only", block, "Start the walkthrough with `behold demo carve`.");
4675
4882
  mkdirSync3(demo.out, { recursive: true });
4676
- const reportFile = join9(demo.out, `${carveSlug(select)}-boundary.json`);
4883
+ const reportFile = join10(demo.out, `${carveSlug(select)}-boundary.json`);
4677
4884
  const args = [
4678
4885
  "carve",
4679
4886
  "emit",
@@ -4702,10 +4909,10 @@ async function runCarveEmit(demo, select) {
4702
4909
  }
4703
4910
  let boundary = null;
4704
4911
  try {
4705
- boundary = JSON.parse(readFileSync8(reportFile, "utf8"));
4912
+ boundary = JSON.parse(readFileSync9(reportFile, "utf8"));
4706
4913
  } catch {
4707
4914
  }
4708
- const lintPath = relative(demo.project, join9(demo.out, "src")).split(sep).join("/");
4915
+ const lintPath = relative(demo.project, join10(demo.out, "src")).split(sep).join("/");
4709
4916
  const lintArgs = ["lint", lintPath];
4710
4917
  const lint = await runChantRaw(lintArgs, demo.project).catch((err) => ({
4711
4918
  code: 127,
@@ -4768,8 +4975,8 @@ async function runCarveBridge(demo, select) {
4768
4975
  }
4769
4976
 
4770
4977
  // src/ops.ts
4771
- import { readdirSync as readdirSync4, readFileSync as readFileSync9, existsSync as existsSync8 } from "node:fs";
4772
- import { join as join10 } from "node:path";
4978
+ import { readdirSync as readdirSync4, readFileSync as readFileSync10, existsSync as existsSync9 } from "node:fs";
4979
+ import { join as join11 } from "node:path";
4773
4980
  var APPLY_TARGET_LEXICON = {
4774
4981
  cloudformation: "aws",
4775
4982
  kubectl: "k8s",
@@ -4791,11 +4998,11 @@ function discoverOps(projectDir) {
4791
4998
  const seen = /* @__PURE__ */ new Set();
4792
4999
  const out = [];
4793
5000
  for (const sub of ["ops", "src", "."]) {
4794
- const dir = join10(projectDir, sub);
4795
- if (!existsSync8(dir)) continue;
5001
+ const dir = join11(projectDir, sub);
5002
+ if (!existsSync9(dir)) continue;
4796
5003
  for (const f of readdirSync4(dir)) {
4797
5004
  if (!f.endsWith(".op.ts")) continue;
4798
- const content = readFileSync9(join10(dir, f), "utf8");
5005
+ const content = readFileSync10(join11(dir, f), "utf8");
4799
5006
  const name = content.match(/name:\s*["'`]([^"'`]+)["'`]/)?.[1];
4800
5007
  if (!name || seen.has(name)) continue;
4801
5008
  seen.add(name);
@@ -5320,16 +5527,17 @@ var OpRunner = class {
5320
5527
  };
5321
5528
 
5322
5529
  // src/substrates.ts
5323
- import { spawn as spawn3 } from "node:child_process";
5324
- import { existsSync as existsSync9, readFileSync as readFileSync10 } from "node:fs";
5325
- import { join as join11 } from "node:path";
5530
+ import { spawn as spawn4 } from "node:child_process";
5531
+ import { existsSync as existsSync10, readFileSync as readFileSync11 } from "node:fs";
5532
+ import { join as join12 } from "node:path";
5326
5533
  import { platform } from "node:os";
5534
+ var DOCKER_DOWN_DETAIL = "docker is down";
5327
5535
  function probe(cmd, args) {
5328
5536
  return new Promise((resolve8) => {
5329
5537
  let out = "";
5330
5538
  let proc;
5331
5539
  try {
5332
- proc = spawn3(cmd, args, { stdio: ["ignore", "pipe", "pipe"] });
5540
+ proc = spawn4(cmd, args, { stdio: ["ignore", "pipe", "pipe"] });
5333
5541
  } catch {
5334
5542
  resolve8({ code: 127, out: "" });
5335
5543
  return;
@@ -5340,21 +5548,21 @@ function probe(cmd, args) {
5340
5548
  proc.on("close", (code) => resolve8({ code: code ?? 1, out }));
5341
5549
  });
5342
5550
  }
5343
- async function dockerAvailable() {
5344
- const { code } = await probe("docker", ["info", "--format", "{{.ServerVersion}}"]);
5551
+ async function dockerAvailable(run3) {
5552
+ const { code } = await run3("docker", ["info", "--format", "{{.ServerVersion}}"]);
5345
5553
  return code === 0;
5346
5554
  }
5347
- async function dockerRunning(nameFilter) {
5348
- const { code, out } = await probe("docker", ["ps", "--filter", `name=${nameFilter}`, "--format", "{{.Names}}"]);
5555
+ async function dockerRunning(run3, nameFilter) {
5556
+ const { code, out } = await run3("docker", ["ps", "--filter", `name=${nameFilter}`, "--format", "{{.Names}}"]);
5349
5557
  if (code !== 0) return [];
5350
5558
  return out.split(/\r?\n/).map((s) => s.trim()).filter(Boolean);
5351
5559
  }
5352
5560
  function scriptBringUp(projectDir, relPath, label) {
5353
- return existsSync9(join11(projectDir, relPath)) ? { label, cmd: "bash", args: [relPath] } : void 0;
5561
+ return existsSync10(join12(projectDir, relPath)) ? { label, cmd: "bash", args: [relPath] } : void 0;
5354
5562
  }
5355
5563
  function projectLexicons(projectDir) {
5356
5564
  try {
5357
- const src = readFileSync10(join11(projectDir, "chant.config.ts"), "utf-8");
5565
+ const src = readFileSync11(join12(projectDir, "chant.config.ts"), "utf-8");
5358
5566
  const m = src.match(/lexicons\s*:\s*\[([^\]]*)\]/);
5359
5567
  if (!m) return [];
5360
5568
  return [...m[1].matchAll(/["']([^"']+)["']/g)].map((x) => x[1]);
@@ -5362,10 +5570,11 @@ function projectLexicons(projectDir) {
5362
5570
  return [];
5363
5571
  }
5364
5572
  }
5365
- async function detectSubstrates(projectDir, preview = false, boundContext) {
5573
+ async function detectSubstrates(projectDir, preview = false, boundContext, probes = {}) {
5574
+ const run3 = probes.probe ?? probe;
5366
5575
  const subs = [];
5367
5576
  const lexicons = projectLexicons(projectDir);
5368
- const docker = await dockerAvailable();
5577
+ const docker = await dockerAvailable(run3);
5369
5578
  subs.push({
5370
5579
  name: "docker",
5371
5580
  label: "Docker",
@@ -5373,9 +5582,10 @@ async function detectSubstrates(projectDir, preview = false, boundContext) {
5373
5582
  detail: docker ? "daemon running" : "daemon not running",
5374
5583
  bringUp: docker || platform() !== "darwin" ? void 0 : { label: "open -a Docker", cmd: "open", args: ["-a", "Docker"] }
5375
5584
  });
5376
- const dep = (up, upDetail, offDetail) => !docker ? { status: "blocked", detail: "waiting on Docker" } : { status: up ? "up" : offDetail === "on-demand (pipeline run)" ? "on-demand" : "down", detail: up ? upDetail : offDetail };
5585
+ const DOCKER_BLOCKED = { status: "blocked", detail: DOCKER_DOWN_DETAIL };
5586
+ const dep = (up, upDetail, offDetail) => !docker ? DOCKER_BLOCKED : { status: up ? "up" : offDetail === "on-demand (pipeline run)" ? "on-demand" : "down", detail: up ? upDetail : offDetail };
5377
5587
  if (lexicons.includes("aws")) {
5378
- const floci = docker ? await dockerRunning("^floci$|^chant-floci$") : [];
5588
+ const floci = docker ? await dockerRunning(run3, "^floci$|^chant-floci$") : [];
5379
5589
  const d = dep(floci.length > 0, "container up on :4566", "not running");
5380
5590
  subs.push({
5381
5591
  name: "floci",
@@ -5390,7 +5600,7 @@ async function detectSubstrates(projectDir, preview = false, boundContext) {
5390
5600
  ];
5391
5601
  for (const [lexicon, name, label, pattern, port] of emulators) {
5392
5602
  if (!lexicons.includes(lexicon)) continue;
5393
- const running = docker ? await dockerRunning(pattern) : [];
5603
+ const running = docker ? await dockerRunning(run3, pattern) : [];
5394
5604
  subs.push({
5395
5605
  name,
5396
5606
  label,
@@ -5404,8 +5614,8 @@ async function detectSubstrates(projectDir, preview = false, boundContext) {
5404
5614
  ["forgejo", "Forgejo", ".forgejo", "test/forgejo-runtime-e2e.sh"]
5405
5615
  ];
5406
5616
  for (const [name, label, marker, script] of forges) {
5407
- if (!existsSync9(join11(projectDir, marker))) continue;
5408
- const c = docker ? await dockerRunning(name) : [];
5617
+ if (!existsSync10(join12(projectDir, marker))) continue;
5618
+ const c = docker ? await dockerRunning(run3, name) : [];
5409
5619
  const d = dep(c.length > 0, "container up", "on-demand (pipeline run)");
5410
5620
  subs.push({
5411
5621
  name,
@@ -5415,15 +5625,19 @@ async function detectSubstrates(projectDir, preview = false, boundContext) {
5415
5625
  });
5416
5626
  }
5417
5627
  if (lexicons.includes("k8s")) {
5418
- const k3d = await probe("k3d", ["cluster", "list", "--no-headers"]);
5419
- const clusters = k3d.code === 0 ? k3d.out.split(/\r?\n/).map((s) => s.trim()).filter(Boolean) : [];
5420
- subs.push({
5421
- name: "k3d",
5422
- label: "k3d",
5423
- status: k3d.code === 127 ? "unknown" : clusters.length ? "up" : "down",
5424
- detail: k3d.code === 127 ? "k3d not installed" : clusters.length ? `${clusters.length} cluster(s)` : "no clusters",
5425
- bringUp: docker && k3d.code !== 127 && !clusters.length ? scriptBringUp(projectDir, "scripts/local/local-up.sh", "local-up") : void 0
5426
- });
5628
+ if (!docker) {
5629
+ subs.push({ name: "k3d", label: "k3d", ...DOCKER_BLOCKED });
5630
+ } else {
5631
+ const k3d = await run3("k3d", ["cluster", "list", "--no-headers"]);
5632
+ const clusters = k3d.code === 0 ? k3d.out.split(/\r?\n/).map((s) => s.trim()).filter(Boolean) : [];
5633
+ subs.push({
5634
+ name: "k3d",
5635
+ label: "k3d",
5636
+ status: k3d.code === 127 ? "unknown" : clusters.length ? "up" : "down",
5637
+ detail: k3d.code === 127 ? "k3d not installed" : clusters.length ? `${clusters.length} cluster(s)` : "no clusters",
5638
+ bringUp: k3d.code !== 127 && !clusters.length ? scriptBringUp(projectDir, "scripts/local/local-up.sh", "local-up") : void 0
5639
+ });
5640
+ }
5427
5641
  }
5428
5642
  if (lexicons.includes("fly")) {
5429
5643
  const endpoint = process.env.FLY_FLAPS_BASE_URL;
@@ -5434,8 +5648,8 @@ async function detectSubstrates(projectDir, preview = false, boundContext) {
5434
5648
  detail: endpoint ? `targeting ${endpoint}` : "real Fly (FLY_FLAPS_BASE_URL unset)"
5435
5649
  });
5436
5650
  }
5437
- if (existsSync9(join11(projectDir, ".github", "workflows"))) {
5438
- const gh = await probe("gh", ["auth", "status"]);
5651
+ if (existsSync10(join12(projectDir, ".github", "workflows"))) {
5652
+ const gh = await run3("gh", ["auth", "status"]);
5439
5653
  const ready = gh.code === 0;
5440
5654
  subs.push({
5441
5655
  name: "github",
@@ -5447,7 +5661,7 @@ async function detectSubstrates(projectDir, preview = false, boundContext) {
5447
5661
  if (lexicons.includes("temporal")) {
5448
5662
  let hasProfiles = false;
5449
5663
  try {
5450
- hasProfiles = /temporal\s*:\s*\{[\s\S]{0,400}?profiles\s*:/.test(readFileSync10(join11(projectDir, "chant.config.ts"), "utf-8"));
5664
+ hasProfiles = /temporal\s*:\s*\{[\s\S]{0,400}?profiles\s*:/.test(readFileSync11(join12(projectDir, "chant.config.ts"), "utf-8"));
5451
5665
  } catch {
5452
5666
  }
5453
5667
  subs.push({
@@ -5458,7 +5672,7 @@ async function detectSubstrates(projectDir, preview = false, boundContext) {
5458
5672
  });
5459
5673
  }
5460
5674
  if (lexicons.includes("helm")) {
5461
- const helm = await probe("helm", ["version", "--short"]);
5675
+ const helm = await run3("helm", ["version", "--short"]);
5462
5676
  const ambient = helm.code === 0 && !boundContext ? await ambientContext() : void 0;
5463
5677
  const ctx = boundContext ?? ambient ?? "";
5464
5678
  subs.push({
@@ -5552,7 +5766,7 @@ async function openRollbackBranches(projectDir, env) {
5552
5766
 
5553
5767
  // src/estate.ts
5554
5768
  import { statSync as statSync3 } from "node:fs";
5555
- import { join as join12, resolve as resolve3, sep as sep2 } from "node:path";
5769
+ import { join as join13, resolve as resolve3, sep as sep2 } from "node:path";
5556
5770
  import { composeStacks, shortStackNames } from "@intentius/pinhole";
5557
5771
  async function composeEstate(projectDirs, opts = {}) {
5558
5772
  const names = shortStackNames(projectDirs);
@@ -5612,7 +5826,7 @@ function pathAlignment(declared, dir, isDir = realIsDir) {
5612
5826
  const have = root.split(sep2).filter(Boolean);
5613
5827
  for (let k = Math.min(want.length, have.length); k >= 1; k--) {
5614
5828
  if (!have.slice(-k).every((s, i) => s === want[i])) continue;
5615
- if (!isDir(join12(root, ...want.slice(k)))) continue;
5829
+ if (!isDir(join13(root, ...want.slice(k)))) continue;
5616
5830
  return k;
5617
5831
  }
5618
5832
  return 0;
@@ -5642,10 +5856,10 @@ function joinNamespaceBindings(members, isDir = realIsDir) {
5642
5856
  if (!winner || tied) continue;
5643
5857
  const prev = best.get(winner.dir);
5644
5858
  if (prev === "ambiguous") continue;
5645
- const join18 = { dir: winner.dir, namespace: b.namespace, path: b.path, declaredBy: b.declaredBy };
5646
- if (!prev) best.set(winner.dir, { score: winner.score, join: join18 });
5859
+ const join19 = { dir: winner.dir, namespace: b.namespace, path: b.path, declaredBy: b.declaredBy };
5860
+ if (!prev) best.set(winner.dir, { score: winner.score, join: join19 });
5647
5861
  else if (prev.join.namespace !== b.namespace) best.set(winner.dir, "ambiguous");
5648
- else if (winner.score > prev.score) best.set(winner.dir, { score: winner.score, join: join18 });
5862
+ else if (winner.score > prev.score) best.set(winner.dir, { score: winner.score, join: join19 });
5649
5863
  }
5650
5864
  return [...best.values()].filter((v) => v !== "ambiguous").map((v) => v.join);
5651
5865
  }
@@ -5707,8 +5921,8 @@ async function composeEstateOverlay(projectDirs, opts, classify) {
5707
5921
  }
5708
5922
 
5709
5923
  // src/events.ts
5710
- import { watch, existsSync as existsSync10 } from "node:fs";
5711
- import { join as join13 } from "node:path";
5924
+ import { watch, existsSync as existsSync11 } from "node:fs";
5925
+ import { join as join14 } from "node:path";
5712
5926
  var Broadcaster = class {
5713
5927
  listeners = /* @__PURE__ */ new Set();
5714
5928
  subscribe(fn) {
@@ -5726,7 +5940,7 @@ var Broadcaster = class {
5726
5940
  };
5727
5941
  var IGNORE = /(^|[\\/])(node_modules|dist|\.git)([\\/]|$)/;
5728
5942
  function watchSource(projectDir, onChange, debounceMs = 200) {
5729
- const dir = existsSync10(join13(projectDir, "src")) ? join13(projectDir, "src") : projectDir;
5943
+ const dir = existsSync11(join14(projectDir, "src")) ? join14(projectDir, "src") : projectDir;
5730
5944
  let timer;
5731
5945
  const watcher = watch(dir, { recursive: true }, (_event, file) => {
5732
5946
  const name = typeof file === "string" ? file : "";
@@ -5830,7 +6044,7 @@ var FrameBuffer = class {
5830
6044
  };
5831
6045
 
5832
6046
  // src/lanes.ts
5833
- import { renderMorphHtml, layoutIr as layoutIr2 } from "@intentius/pinhole";
6047
+ import { renderMorphHtml as renderMorphHtml2, layoutIr as layoutIr2 } from "@intentius/pinhole";
5834
6048
  function safeJson(value) {
5835
6049
  return JSON.stringify(value).replace(/</g, "\\u003c");
5836
6050
  }
@@ -5957,7 +6171,7 @@ function renderLanes(frames, summaries) {
5957
6171
  ir: f.ir,
5958
6172
  layout: layoutIr2(f.ir)
5959
6173
  }));
5960
- const doc = renderMorphHtml(views, { title: "Deployment lanes" });
6174
+ const doc = renderMorphHtml2(views, { title: "Deployment lanes" });
5961
6175
  const laneFrames = frames.map((f, i) => ({
5962
6176
  t: summaries[i].t,
5963
6177
  name: new Date(summaries[i].t).toISOString().slice(11, 19),
@@ -6004,13 +6218,13 @@ async function emulatorDown(projectDir) {
6004
6218
  }
6005
6219
 
6006
6220
  // src/demos.ts
6007
- import { readFileSync as readFileSync11, existsSync as existsSync11, cpSync } from "node:fs";
6008
- import { join as join14, relative as relative2, resolve as resolve4, sep as sep3 } from "node:path";
6009
- import { spawn as spawn4, spawnSync } from "node:child_process";
6221
+ import { readFileSync as readFileSync12, existsSync as existsSync12, cpSync } from "node:fs";
6222
+ import { join as join15, relative as relative2, resolve as resolve4, sep as sep3 } from "node:path";
6223
+ import { spawn as spawn5, spawnSync } from "node:child_process";
6010
6224
  function loadDemoRegistry(pkgRoot2) {
6011
6225
  let raw;
6012
6226
  try {
6013
- raw = JSON.parse(readFileSync11(join14(pkgRoot2, "demos.json"), "utf8"));
6227
+ raw = JSON.parse(readFileSync12(join15(pkgRoot2, "demos.json"), "utf8"));
6014
6228
  } catch {
6015
6229
  return [];
6016
6230
  }
@@ -6051,16 +6265,16 @@ function fetchesFromNetwork(entry) {
6051
6265
  }
6052
6266
  function demoTargetDir(entry, cwd = process.cwd()) {
6053
6267
  const legacy = resolve4(cwd, "behold-demo");
6054
- if (entry.name === "writes" && existsSync11(legacy)) return legacy;
6268
+ if (entry.name === "writes" && existsSync12(legacy)) return legacy;
6055
6269
  return resolve4(cwd, "behold-demos", entry.name);
6056
6270
  }
6057
6271
  async function loadDemo(entry, opts) {
6058
6272
  const { pkgRoot: pkgRoot2, target } = opts;
6059
6273
  const say = (line) => opts.log?.(`behold demo ${entry.name} \u2192 ${line}`);
6060
- if (!existsSync11(target)) {
6274
+ if (!existsSync12(target)) {
6061
6275
  if (entry.source === "bundled") {
6062
- const bundled = join14(pkgRoot2, entry.dir);
6063
- if (!existsSync11(bundled)) return { ok: false, error: `this install has no bundled ${entry.dir}` };
6276
+ const bundled = join15(pkgRoot2, entry.dir);
6277
+ if (!existsSync12(bundled)) return { ok: false, error: `this install has no bundled ${entry.dir}` };
6064
6278
  say(`copying to ${target} (it's yours \u2014 edit it)`);
6065
6279
  try {
6066
6280
  cpSync(bundled, target, {
@@ -6079,7 +6293,7 @@ async function loadDemo(entry, opts) {
6079
6293
  } else {
6080
6294
  say(`reusing ${target}`);
6081
6295
  }
6082
- if (existsSync11(join14(target, "package.json")) && !existsSync11(join14(target, "node_modules"))) {
6296
+ if (existsSync12(join15(target, "package.json")) && !existsSync12(join15(target, "node_modules"))) {
6083
6297
  say("npm install\u2026");
6084
6298
  if (await runStep("npm", ["install"], { cwd: target, shell: process.platform === "win32" }) !== 0) {
6085
6299
  return { ok: false, error: `npm install failed in ${target}` };
@@ -6091,19 +6305,19 @@ async function loadDemo(entry, opts) {
6091
6305
  return { ok: false, error: `setup failed (${entry.setup})` };
6092
6306
  }
6093
6307
  }
6094
- return { ok: true, serveDirs: entry.serve.dirs?.length ? entry.serve.dirs.map((d) => join14(target, d)) : [target] };
6308
+ return { ok: true, serveDirs: entry.serve.dirs?.length ? entry.serve.dirs.map((d) => join15(target, d)) : [target] };
6095
6309
  }
6096
6310
  function runStep(cmd, args, opts = {}) {
6097
6311
  return new Promise((res) => {
6098
- const child = spawn4(cmd, args, { stdio: "inherit", cwd: opts.cwd, shell: opts.shell ?? false });
6312
+ const child = spawn5(cmd, args, { stdio: "inherit", cwd: opts.cwd, shell: opts.shell ?? false });
6099
6313
  child.on("error", () => res(-1));
6100
6314
  child.on("close", (code) => res(code ?? 1));
6101
6315
  });
6102
6316
  }
6103
6317
 
6104
6318
  // src/server.ts
6105
- var pkgRoot = join15(dirname4(fileURLToPath3(import.meta.url)), "..");
6106
- var webRoot = join15(pkgRoot, "web");
6319
+ var pkgRoot = join16(dirname4(fileURLToPath3(import.meta.url)), "..");
6320
+ var webRoot = join16(pkgRoot, "web");
6107
6321
  var execFileP = async (cmd, args) => (await promisify3(execFile3)(cmd, args, { encoding: "utf8", timeout: 1e4 })).stdout;
6108
6322
  function optsFromQuery(url, tierEnvVar, projectDir) {
6109
6323
  const q = url.searchParams;
@@ -6153,7 +6367,7 @@ function tierFailure(tier, message) {
6153
6367
  function beholdVersion() {
6154
6368
  try {
6155
6369
  const here = dirname4(fileURLToPath3(import.meta.url));
6156
- return JSON.parse(readFileSync12(join15(here, "..", "package.json"), "utf8")).version ?? "unknown";
6370
+ return JSON.parse(readFileSync13(join16(here, "..", "package.json"), "utf8")).version ?? "unknown";
6157
6371
  } catch {
6158
6372
  return "unknown";
6159
6373
  }
@@ -6198,7 +6412,7 @@ async function captureFrame(projectDir, env, frames, broadcaster) {
6198
6412
  }
6199
6413
  }
6200
6414
  function carveRoutes(app, reportPath, demo) {
6201
- const load = () => readCarveReport(reportPath, (p) => readFileSync12(p, "utf8"));
6415
+ const load = () => readCarveReport(reportPath, (p) => readFileSync13(p, "utf8"));
6202
6416
  const demoBlock = () => carveWriteBlock(demo);
6203
6417
  const demoInfo = () => {
6204
6418
  if (!demo) return null;
@@ -6214,6 +6428,9 @@ function carveRoutes(app, reportPath, demo) {
6214
6428
  fromLabel: relative3(demo.root, demo.from).split(sep4).join("/"),
6215
6429
  runnable: !block,
6216
6430
  ...block ? { reason: block } : {},
6431
+ // The live tier, when this boot armed it: the scratch endpoint the
6432
+ // stepper names, and the fact the plan button exists at all.
6433
+ ...demo.live ? { live: { endpoint: demo.live.endpoint, container: demo.live.container, applied: demo.live.applied } } : {},
6217
6434
  ...demo.degraded ? { degraded: demo.degraded } : {},
6218
6435
  buildCaveat: BUILD_CAVEAT
6219
6436
  };
@@ -6222,11 +6439,21 @@ function carveRoutes(app, reportPath, demo) {
6222
6439
  const parsed = load();
6223
6440
  return parsed.ok ? c.json(parsed.report) : c.json(parsed.refusal, 422);
6224
6441
  });
6225
- app.get("/api/graph", (c) => {
6442
+ let appGraph;
6443
+ const appGraphOnce = () => {
6444
+ if (!demo) return Promise.resolve(null);
6445
+ appGraph ??= graphIr(demo.project).then((ir) => ir.nodes.length ? { ir, label: relative3(demo.root, demo.project).split(sep4).join("/") } : null).catch(() => null);
6446
+ return appGraph;
6447
+ };
6448
+ app.get("/api/graph", async (c) => {
6226
6449
  const parsed = load();
6227
6450
  if (!parsed.ok) return c.json(parsed.refusal, 422);
6228
- const ir = carveReportToIr(parsed.report);
6229
- const { svg } = renderBanded(ir);
6451
+ const tfIr = carveReportToIr(parsed.report);
6452
+ const appSide = await appGraphOnce();
6453
+ const { svg, ir } = appSide ? renderCarveEstate(tfIr, appSide.ir, {
6454
+ tfTitle: `${relative3(demo.root, demo.from).split(sep4).join("/")} \u2014 terraform`,
6455
+ appTitle: `${appSide.label} \u2014 chant`
6456
+ }) : { ...renderBanded(tfIr), ir: tfIr };
6230
6457
  return c.json({
6231
6458
  ir,
6232
6459
  svg,
@@ -6238,10 +6465,44 @@ function carveRoutes(app, reportPath, demo) {
6238
6465
  carve: true,
6239
6466
  // A demo whose own advisor run failed says so on the statusbar, not
6240
6467
  // only in the terminal the viewer isn't looking at.
6241
- note: carveNote(parsed.report, ir) + (demo?.degraded ? ` Degraded: ${demo.degraded}` : "")
6468
+ note: carveNote(parsed.report, tfIr) + (demo?.degraded ? ` Degraded: ${demo.degraded}` : "")
6242
6469
  }
6243
6470
  });
6244
6471
  });
6472
+ app.get("/carve/morph", async (c) => {
6473
+ const parsed = load();
6474
+ if (!parsed.ok) return c.json(parsed.refusal, 422);
6475
+ const select = c.req.query("select") ?? "";
6476
+ const ranked = (parsed.report.resources ?? []).some((r) => r.address === select);
6477
+ if (!ranked) {
6478
+ return c.json(
6479
+ {
6480
+ error: `"${select}" is not a ranked address in this report`,
6481
+ code: "carve_bad_select",
6482
+ remedy: "pass ?select=<address> naming a resource the report ranks \u2014 GET /api/carve lists them"
6483
+ },
6484
+ 400
6485
+ );
6486
+ }
6487
+ const appSide = await appGraphOnce();
6488
+ if (!appSide) {
6489
+ return c.json(
6490
+ {
6491
+ error: "the morph needs the demo estate's chant project, and this server isn't serving a demo copy",
6492
+ code: "carve_no_demo",
6493
+ remedy: "run `behold demo carve` \u2014 the bundled estate has the chant box the card moves into"
6494
+ },
6495
+ 404
6496
+ );
6497
+ }
6498
+ const tfIr = carveReportToIr(parsed.report);
6499
+ const html = renderCarveMorph(tfIr, appSide.ir, [select], {
6500
+ tfTitle: `${relative3(demo.root, demo.from).split(sep4).join("/")} \u2014 terraform`,
6501
+ appTitle: `${appSide.label} \u2014 chant`,
6502
+ title: `carve \u2014 ${select}`
6503
+ });
6504
+ return c.html(html);
6505
+ });
6245
6506
  app.get("/api/project", (c) => {
6246
6507
  const parsed = load();
6247
6508
  return c.json({
@@ -6304,6 +6565,17 @@ function carveRoutes(app, reportPath, demo) {
6304
6565
  };
6305
6566
  app.post("/api/carve/emit", (c) => runStep2(c, runCarveEmit));
6306
6567
  app.post("/api/carve/bridge", (c) => runStep2(c, runCarveBridge));
6568
+ app.post("/api/carve/plan", async (c) => {
6569
+ const block = demoBlock();
6570
+ if (block) {
6571
+ return c.json({ error: block, code: "read-only", remedy: "The live plan runs inside a demo copy \u2014 `behold demo carve --live`." }, 403);
6572
+ }
6573
+ if (!(c.req.header("content-type") ?? "").includes("application/json")) {
6574
+ return c.json({ error: "send application/json", code: "carve-action", remedy: "POST {} with content-type application/json" }, 415);
6575
+ }
6576
+ const result = await runCarvePlan(demo);
6577
+ return result.ok ? c.json(result) : c.json(result.refusal, 422);
6578
+ });
6307
6579
  app.get("/api/substrates", (c) => c.json({ substrates: [] }));
6308
6580
  app.get("/api/history", (c) => c.json({ commits: [] }));
6309
6581
  app.get("/api/resources", (c) => c.json({ byComponent: {} }));
@@ -6400,9 +6672,9 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
6400
6672
  return c.json({ started: true, name, ran: label });
6401
6673
  });
6402
6674
  app.post("/api/local/reset", (c) => {
6403
- const down = join15(cfg.projectDir, "scripts/local/local-down.sh");
6404
- const up = join15(cfg.projectDir, "scripts/local/local-up.sh");
6405
- if (!existsSync12(down) || !existsSync12(up)) {
6675
+ const down = join16(cfg.projectDir, "scripts/local/local-down.sh");
6676
+ const up = join16(cfg.projectDir, "scripts/local/local-up.sh");
6677
+ if (!existsSync13(down) || !existsSync13(up)) {
6406
6678
  return c.json({ error: "no local-down.sh / local-up.sh in scripts/local \u2014 reset is only for local emulator projects" }, 400);
6407
6679
  }
6408
6680
  if (!runner.bringUp("reset local emulator", "bash", ["-c", "bash scripts/local/local-down.sh && bash scripts/local/local-up.sh"], cfg.projectDir)) {
@@ -6480,8 +6752,8 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
6480
6752
  if (cfg.previewMode) return c.json({ error: "switching projects is locked in preview mode" }, 403);
6481
6753
  const body = await c.req.json().catch(() => ({}));
6482
6754
  const dir = typeof body.dir === "string" && body.dir.trim() ? resolve5(body.dir.trim()) : "";
6483
- if (!dir || !existsSync12(dir)) return c.json({ error: `no such directory: ${dir || "(no dir given)"}` }, 400);
6484
- if (!existsSync12(join15(dir, "chant.config.ts"))) {
6755
+ if (!dir || !existsSync13(dir)) return c.json({ error: `no such directory: ${dir || "(no dir given)"}` }, 400);
6756
+ if (!existsSync13(join16(dir, "chant.config.ts"))) {
6485
6757
  return c.json({ error: `${dir} doesn't look like a chant project \u2014 no chant.config.ts` }, 400);
6486
6758
  }
6487
6759
  switchServedProject([dir]);
@@ -6525,7 +6797,7 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
6525
6797
  fetches: fetchesFromNetwork(e),
6526
6798
  ...e.repo ? { repo: e.repo } : {},
6527
6799
  target,
6528
- loaded: existsSync12(target),
6800
+ loaded: existsSync13(target),
6529
6801
  satisfiable: missing.length === 0,
6530
6802
  // #254: the carve walkthrough runs fine here — it just isn't a project
6531
6803
  // to switch INTO. Carve mode claims `/api/graph` and `/api/project` at
@@ -6569,7 +6841,7 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
6569
6841
  const loaded = await loadDemo(entry, { pkgRoot, target, log: (line) => process.stdout.write(line + "\n") });
6570
6842
  if (!loaded.ok) return c.json({ error: loaded.error }, 500);
6571
6843
  const dirs = loaded.serveDirs;
6572
- if (!existsSync12(join15(dirs[0], "chant.config.ts"))) {
6844
+ if (!existsSync13(join16(dirs[0], "chant.config.ts"))) {
6573
6845
  return c.json({ error: `${entry.name} loaded to ${target} but ${dirs[0]} has no chant.config.ts` }, 500);
6574
6846
  }
6575
6847
  if (entry.serve.local) {
@@ -6775,7 +7047,7 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
6775
7047
  srcCompositeEdgesAttached = 0;
6776
7048
  }
6777
7049
  }
6778
- if (!multi && !components && !logical && !opts.lens && ir.nodes.length === 0 && !existsSync12(join15(cfg.projectDir, "chant.config.ts"))) {
7050
+ if (!multi && !components && !logical && !opts.lens && ir.nodes.length === 0 && !existsSync13(join16(cfg.projectDir, "chant.config.ts"))) {
6779
7051
  return c.json(noProjectError(cfg.projectDir), 404);
6780
7052
  }
6781
7053
  const radial = new URL(c.req.url).searchParams.get("radial") === "1";
@@ -7115,7 +7387,7 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
7115
7387
  });
7116
7388
  const rel = relative3(process.cwd(), webRoot) || ".";
7117
7389
  app.use("/*", serveStatic({ root: rel }));
7118
- app.get("/", serveStatic({ path: join15(rel, "index.html") }));
7390
+ app.get("/", serveStatic({ path: join16(rel, "index.html") }));
7119
7391
  return app;
7120
7392
  }
7121
7393
  async function bootLocalEmulators(dir, who) {
@@ -7144,7 +7416,7 @@ async function bootLocalEmulators(dir, who) {
7144
7416
  async function startServer(cfg) {
7145
7417
  if (cfg.local) cfg.emulators = await bootLocalEmulators(cfg.projectDir, "behold serve --local");
7146
7418
  for (const dir of cfg.projectDirs ?? [cfg.projectDir]) {
7147
- if (existsSync12(join15(dir, "chant.config.ts"))) addRecent(dir);
7419
+ if (existsSync13(join16(dir, "chant.config.ts"))) addRecent(dir);
7148
7420
  }
7149
7421
  const broadcaster = new Broadcaster();
7150
7422
  const frames = new FrameBuffer();
@@ -7155,10 +7427,10 @@ async function startServer(cfg) {
7155
7427
  });
7156
7428
  const app = createApp(cfg, broadcaster, frames, runner);
7157
7429
  const autoSync = cfg.autoSync ?? "off";
7158
- const capture = () => captureFrame(cfg.projectDir, cfg.env, frames, broadcaster);
7430
+ const capture2 = () => captureFrame(cfg.projectDir, cfg.env, frames, broadcaster);
7159
7431
  const onEstateChange = () => {
7160
7432
  broadcaster.emit("changed");
7161
- void capture();
7433
+ void capture2();
7162
7434
  };
7163
7435
  const onPollDrift = (movedLexicons) => {
7164
7436
  onEstateChange();
@@ -7205,18 +7477,24 @@ async function startServer(cfg) {
7205
7477
  };
7206
7478
  process.stdout.write(` switched \u2192 ${dir}
7207
7479
  `);
7208
- void capture();
7480
+ void capture2();
7209
7481
  };
7210
- if (!carve) void capture();
7482
+ if (!carve) void capture2();
7211
7483
  let shuttingDown = false;
7212
7484
  const shutdown = () => {
7213
7485
  if (shuttingDown) return;
7214
7486
  shuttingDown = true;
7215
7487
  stopWatch();
7216
7488
  stopPoll();
7217
- const done = cfg.local && cfg.emulators && cfg.emulators.length ? emulatorDown(cfg.projectDir).catch((err) => process.stderr.write(`emulator down: ${err instanceof Error ? err.message : String(err)}
7218
- `)) : Promise.resolve();
7219
- void done.finally(() => process.exit(0));
7489
+ const downs = [];
7490
+ if (cfg.local && cfg.emulators && cfg.emulators.length) {
7491
+ downs.push(
7492
+ emulatorDown(cfg.projectDir).catch((err) => process.stderr.write(`emulator down: ${err instanceof Error ? err.message : String(err)}
7493
+ `))
7494
+ );
7495
+ }
7496
+ if (cfg.carveDemo?.live) downs.push(teardownScratchFloci().catch(() => void 0));
7497
+ void Promise.all(downs).finally(() => process.exit(0));
7220
7498
  };
7221
7499
  process.on("SIGINT", shutdown);
7222
7500
  process.on("SIGTERM", shutdown);
@@ -7228,7 +7506,8 @@ async function startServer(cfg) {
7228
7506
  green = carve now, amber = boundary work, grey = leave in Terraform.
7229
7507
  ` + (cfg.carveDemo ? ` walkthrough: the panel's Carve tab \u2014 advise \u2192 pick \u2192 emit \u2192 bridge \u2192 handoff \u2192 done.
7230
7508
  Emit and bridge write only into ${cfg.carveDemo.out}; your Terraform is never edited.
7231
- ` + (cfg.carveDemo.degraded ? ` degraded: ${cfg.carveDemo.degraded}
7509
+ ` + (cfg.carveDemo.live ? ` live: scratch Floci at ${cfg.carveDemo.live.endpoint} \u2014 deleted on Ctrl-C.
7510
+ ` : "") + (cfg.carveDemo.degraded ? ` degraded: ${cfg.carveDemo.degraded}
7232
7511
  ` : "") : ` Read-only advisory: behold emits nothing and touches no Terraform. Ctrl-C to stop.
7233
7512
  `)
7234
7513
  );
@@ -7265,8 +7544,8 @@ async function startServer(cfg) {
7265
7544
  }
7266
7545
 
7267
7546
  // src/export.ts
7268
- import { mkdirSync as mkdirSync4, writeFileSync as writeFileSync3, copyFileSync, readFileSync as readFileSync13, readdirSync as readdirSync5 } from "node:fs";
7269
- import { join as join16, dirname as dirname5, basename } from "node:path";
7547
+ import { mkdirSync as mkdirSync4, writeFileSync as writeFileSync4, copyFileSync, readFileSync as readFileSync14, readdirSync as readdirSync5 } from "node:fs";
7548
+ import { join as join17, dirname as dirname5, basename } from "node:path";
7270
7549
  import { fileURLToPath as fileURLToPath4 } from "node:url";
7271
7550
  var LENS_PARAMS = ["components", "detail", "env", "logical", "radial", "tier"];
7272
7551
  function canonicalKey(path, params) {
@@ -7314,7 +7593,7 @@ function captureKeys(axes) {
7314
7593
  return [...keys];
7315
7594
  }
7316
7595
  function webDir() {
7317
- return join16(dirname5(fileURLToPath4(import.meta.url)), "..", "web");
7596
+ return join17(dirname5(fileURLToPath4(import.meta.url)), "..", "web");
7318
7597
  }
7319
7598
  function workerName(project, override) {
7320
7599
  const raw = override ?? `behold-${basename(project)}`;
@@ -7325,7 +7604,7 @@ async function runExport(cfg, outDir, opts = {}) {
7325
7604
  const app = createApp({ ...cfg, layoutWrites: false });
7326
7605
  const proj = await (await app.request("/api/project")).json();
7327
7606
  const axes = { environments: proj.environments ?? [], tiers: proj.tiers ?? [] };
7328
- const snapDir = join16(outDir, "snapshots");
7607
+ const snapDir = join17(outDir, "snapshots");
7329
7608
  mkdirSync4(snapDir, { recursive: true });
7330
7609
  const keyToFile = {};
7331
7610
  let ok = 0;
@@ -7334,7 +7613,7 @@ async function runExport(cfg, outDir, opts = {}) {
7334
7613
  const res = await app.request(`${key}${key.includes("?") ? "&" : "?"}layout=1`);
7335
7614
  const body = await res.text();
7336
7615
  const file = slug2(key);
7337
- writeFileSync3(join16(snapDir, file), body);
7616
+ writeFileSync4(join17(snapDir, file), body);
7338
7617
  keyToFile[key] = `snapshots/${file}`;
7339
7618
  if (res.ok) ok++;
7340
7619
  else failed++;
@@ -7346,21 +7625,21 @@ async function runExport(cfg, outDir, opts = {}) {
7346
7625
  axes,
7347
7626
  keyToFile
7348
7627
  };
7349
- writeFileSync3(join16(outDir, "manifest.json"), JSON.stringify(manifest, null, 2));
7350
- const html = readFileSync13(join16(webDir(), "index.html"), "utf8").replace(
7628
+ writeFileSync4(join17(outDir, "manifest.json"), JSON.stringify(manifest, null, 2));
7629
+ const html = readFileSync14(join17(webDir(), "index.html"), "utf8").replace(
7351
7630
  /<\/head>/i,
7352
7631
  ` <script>window.__BEHOLD_STATIC__ = true;</script>
7353
7632
  </head>`
7354
7633
  );
7355
- writeFileSync3(join16(outDir, "index.html"), html);
7634
+ writeFileSync4(join17(outDir, "index.html"), html);
7356
7635
  for (const f of readdirSync5(webDir())) {
7357
7636
  if (f === "index.html") continue;
7358
- copyFileSync(join16(webDir(), f), join16(outDir, f));
7637
+ copyFileSync(join17(webDir(), f), join17(outDir, f));
7359
7638
  }
7360
- writeFileSync3(join16(outDir, "README.md"), BUNDLE_README);
7639
+ writeFileSync4(join17(outDir, "README.md"), BUNDLE_README);
7361
7640
  const name = workerName(cfg.projectDir, opts.name);
7362
- writeFileSync3(
7363
- join16(outDir, "wrangler.jsonc"),
7641
+ writeFileSync4(
7642
+ join17(outDir, "wrangler.jsonc"),
7364
7643
  JSON.stringify(
7365
7644
  { $schema: "node_modules/wrangler/config-schema.json", name, compatibility_date: "2025-06-01", assets: { directory: "." } },
7366
7645
  null,
@@ -7627,6 +7906,7 @@ var USAGE = `behold \u2014 a live control plane on chant (read-only core)
7627
7906
 
7628
7907
  Usage:
7629
7908
  behold demo [name] [target-dir] [--port <n>] [--list]
7909
+ behold demo carve [--live] [--port <n>]
7630
7910
  behold doctor [project-dir] [--json]
7631
7911
  behold preview [project-dir] [--port <n>] [--emulator]
7632
7912
  behold export [project-dir] [--out <dir>] [--env <name>] [--name <worker>] [--emulator]
@@ -7659,7 +7939,10 @@ Usage:
7659
7939
  up on a throwaway k3d cluster instead. \`behold demo carve\` is the
7660
7940
  odd one out: no cluster, no Docker, no cloud \u2014 a half-migrated
7661
7941
  Terraform/chant estate plus the six-step carve walkthrough on the
7662
- panel's Carve tab. Needs Docker (and per-demo tools --list names).
7942
+ panel's Carve tab. Its \`--live\` tier flips that: docker + terraform
7943
+ on PATH, a scratch Floci booted and deleted on exit, the starred
7944
+ resources REALLY applied, and a real \`terraform plan\` button at
7945
+ Handoff. Needs Docker (and per-demo tools --list names).
7663
7946
  Loaded demos land in the panel's recents, so switching between them
7664
7947
  is the Scope tab \u2014 which lists this whole catalog too (#268), one
7665
7948
  click from any served project.
@@ -7826,7 +8109,7 @@ function warnIfNotChantProject(dir) {
7826
8109
  if (shape.kind === "estate") {
7827
8110
  process.stderr.write(
7828
8111
  `behold: warning \u2014 ${dir} is an estate root, not a chant project itself.
7829
- Serve its members composed: behold serve ${shape.members.map((m) => join17(dir, m)).join(" ")}
8112
+ Serve its members composed: behold serve ${shape.members.map((m) => join18(dir, m)).join(" ")}
7830
8113
  `
7831
8114
  );
7832
8115
  return;
@@ -7862,7 +8145,7 @@ async function runCarve(rest) {
7862
8145
  process.exit(2);
7863
8146
  }
7864
8147
  const reportPath = resolve7(fileArg);
7865
- const parsed = readCarveReport(reportPath, (p) => readFileSync14(p, "utf8"));
8148
+ const parsed = readCarveReport(reportPath, (p) => readFileSync15(p, "utf8"));
7866
8149
  if (!parsed.ok) {
7867
8150
  process.stderr.write(`behold carve: ${parsed.refusal.error}
7868
8151
  ${parsed.refusal.remedy}
@@ -7889,7 +8172,7 @@ async function runDoctor(rest) {
7889
8172
  }
7890
8173
  }
7891
8174
  const dir = dirArg ?? ".";
7892
- if (!existsSync13(resolve7(dir))) {
8175
+ if (!existsSync14(resolve7(dir))) {
7893
8176
  process.stderr.write(`behold doctor: no such directory: ${resolve7(dir)}
7894
8177
  `);
7895
8178
  process.exit(2);
@@ -7899,14 +8182,16 @@ async function runDoctor(rest) {
7899
8182
  if (!report.ok) process.exitCode = 1;
7900
8183
  }
7901
8184
  async function runDemo(rest) {
7902
- const pkgRoot2 = join17(dirname6(fileURLToPath5(import.meta.url)), "..");
8185
+ const pkgRoot2 = join18(dirname6(fileURLToPath5(import.meta.url)), "..");
7903
8186
  const registry = loadDemoRegistry(pkgRoot2);
7904
8187
  let port = 4600;
8188
+ let live = false;
7905
8189
  let name;
7906
8190
  let dirArg;
7907
8191
  for (let i = 0; i < rest.length; i++) {
7908
8192
  const a = rest[i];
7909
8193
  if (a === "--port") port = Number(rest[++i]);
8194
+ else if (a === "--live") live = true;
7910
8195
  else if (a === "--list") {
7911
8196
  if (!registry.length) {
7912
8197
  process.stdout.write("behold demo: no catalog in this install (demos.json missing)\n");
@@ -7959,9 +8244,14 @@ async function runDemo(rest) {
7959
8244
  process.exit(1);
7960
8245
  }
7961
8246
  if (entry.serve.carve) {
7962
- await serveCarveDemo(target, entry.serve.carve, port);
8247
+ await serveCarveDemo(target, entry.serve.carve, port, live);
7963
8248
  return;
7964
8249
  }
8250
+ if (live) {
8251
+ process.stderr.write(`behold demo ${entry.name}: --live is the carve demo's tier \u2014 only \`behold demo carve --live\` takes it.
8252
+ `);
8253
+ process.exit(2);
8254
+ }
7965
8255
  process.stdout.write(`behold demo ${entry.name} \u2192 serving. Blue = declared; Deploy turns it green.
7966
8256
  `);
7967
8257
  const serveArgs = ["serve", ...loaded.serveDirs, "--port", String(port)];
@@ -7971,18 +8261,53 @@ async function runDemo(rest) {
7971
8261
  }
7972
8262
  function spawnStep(cmd, args, cwd) {
7973
8263
  return new Promise((res) => {
7974
- const child = spawn5(cmd, args, { stdio: "inherit", cwd, shell: process.platform === "win32" });
8264
+ const child = spawn6(cmd, args, { stdio: "inherit", cwd, shell: process.platform === "win32" });
7975
8265
  child.on("error", () => res(-1));
7976
8266
  child.on("close", (code) => res(code ?? 1));
7977
8267
  });
7978
8268
  }
7979
- async function serveCarveDemo(target, carve, port) {
8269
+ async function serveCarveDemo(target, carve, port, live = false) {
7980
8270
  const at = (rel) => resolve7(target, rel);
7981
8271
  const project = at(carve.project);
7982
8272
  const from = at(carve.from);
7983
8273
  const state = carve.state ? at(carve.state) : void 0;
7984
8274
  const committed = at(carve.report);
7985
- if (existsSync13(join17(project, "package.json")) && !existsSync13(join17(project, "node_modules"))) {
8275
+ let liveInfo;
8276
+ if (live) {
8277
+ for (const bin of ["docker", "terraform"]) {
8278
+ const probe2 = spawnSync2(process.platform === "win32" ? "where" : "which", [bin], { stdio: "ignore" });
8279
+ if (probe2.status !== 0) {
8280
+ process.stderr.write(`behold demo carve --live: needs ${bin} on PATH (the offline tier doesn't \u2014 drop --live).
8281
+ `);
8282
+ process.exit(2);
8283
+ }
8284
+ }
8285
+ process.stdout.write(`behold demo carve --live \u2192 scratch Floci (${LIVE_CONTAINER}, :${LIVE_PORT}, deleted on exit)\u2026
8286
+ `);
8287
+ const bootErr = await bootScratchFloci();
8288
+ if (bootErr) {
8289
+ process.stderr.write(`behold demo carve --live: ${bootErr}
8290
+ `);
8291
+ process.exit(1);
8292
+ }
8293
+ process.stdout.write("behold demo carve --live \u2192 terraform init + apply (the starred resources, into the scratch Floci)\u2026\n");
8294
+ if (state) rmSync2(state, { force: true });
8295
+ const applyErr = await applyIntoFloci(from, spawnStep);
8296
+ if (applyErr) {
8297
+ await teardownScratchFloci();
8298
+ process.stderr.write(`behold demo carve --live: ${applyErr}
8299
+ `);
8300
+ process.exit(1);
8301
+ }
8302
+ liveInfo = {
8303
+ container: LIVE_CONTAINER,
8304
+ port: LIVE_PORT,
8305
+ endpoint: `http://localhost:${LIVE_PORT}`,
8306
+ applied: LIVE_TARGETS
8307
+ };
8308
+ process.stdout.write("behold demo carve --live \u2192 the tfstate is real now; the advisor reads what terraform wrote.\n");
8309
+ }
8310
+ if (existsSync14(join18(project, "package.json")) && !existsSync14(join18(project, "node_modules"))) {
7986
8311
  process.stdout.write(`behold demo carve \u2192 npm install in ${carve.project}/ (the chant this walkthrough shells)\u2026
7987
8312
  `);
7988
8313
  const code = await spawnStep("npm", ["install"], project);
@@ -7993,7 +8318,7 @@ async function serveCarveDemo(target, carve, port) {
7993
8318
  }
7994
8319
  }
7995
8320
  let degraded;
7996
- if (!existsSync13(join17(target, "node_modules", "@cdktf", "hcl2json"))) {
8321
+ if (!existsSync14(join18(target, "node_modules", "@cdktf", "hcl2json"))) {
7997
8322
  process.stdout.write("behold demo carve \u2192 npm install @cdktf/hcl2json (chant's HCL parser, ~2MB, once)\u2026\n");
7998
8323
  const code = await spawnStep("npm", ["install", "--no-save", "--no-package-lock", "@cdktf/hcl2json"], target);
7999
8324
  if (code !== 0) degraded = "couldn't install @cdktf/hcl2json (chant's HCL parser) \u2014 no network?";
@@ -8009,7 +8334,7 @@ async function serveCarveDemo(target, carve, port) {
8009
8334
  );
8010
8335
  if (code !== 0) degraded = `chant carve advise exited ${code}`;
8011
8336
  }
8012
- const serving = !degraded && existsSync13(report) ? report : committed;
8337
+ const serving = !degraded && existsSync14(report) ? report : committed;
8013
8338
  if (degraded) {
8014
8339
  process.stderr.write(
8015
8340
  `behold demo carve: ${degraded}
@@ -8017,7 +8342,7 @@ async function serveCarveDemo(target, carve, port) {
8017
8342
  `
8018
8343
  );
8019
8344
  }
8020
- if (!existsSync13(serving)) {
8345
+ if (!existsSync14(serving)) {
8021
8346
  process.stderr.write(`behold demo carve: no carve report at ${serving}
8022
8347
  `);
8023
8348
  process.exit(2);
@@ -8034,6 +8359,7 @@ async function serveCarveDemo(target, carve, port) {
8034
8359
  ...state ? { state } : {},
8035
8360
  project,
8036
8361
  out: at(carve.out),
8362
+ ...liveInfo ? { live: liveInfo } : {},
8037
8363
  ...degraded ? { degraded: `${degraded} \u2014 showing the committed report shipped with the demo.` } : {}
8038
8364
  },
8039
8365
  port
@@ -8062,7 +8388,7 @@ async function runPreview(rest) {
8062
8388
  process.exit(2);
8063
8389
  }
8064
8390
  const projectDir = resolve7(dirArg ?? process.cwd());
8065
- if (!existsSync13(projectDir)) {
8391
+ if (!existsSync14(projectDir)) {
8066
8392
  process.stderr.write(`behold preview: project not found at ${projectDir}
8067
8393
  `);
8068
8394
  process.exit(2);
@@ -8101,7 +8427,7 @@ async function runExportCmd(rest) {
8101
8427
  injectEmulatorEnv(env);
8102
8428
  env ??= "local";
8103
8429
  }
8104
- if (!existsSync13(projectDir)) {
8430
+ if (!existsSync14(projectDir)) {
8105
8431
  process.stderr.write(`behold export: project not found at ${projectDir}
8106
8432
  `);
8107
8433
  process.exit(2);