@intentius/behold 0.9.1 → 0.10.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
@@ -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";
@@ -4189,16 +4196,32 @@ function renderGraph(ir, opts = {}) {
4189
4196
  return { svg };
4190
4197
  }
4191
4198
  function renderBanded(ir, opts = {}) {
4192
- 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) {
4193
4220
  const size = footprints(ir);
4194
4221
  const dims = (id) => size.get(id) ?? { w: NODE_W, h: NODE_H };
4195
4222
  const ids = ir.nodes.map((n) => n.id);
4196
4223
  const cellW = Math.max(NODE_W, ...ids.map((id) => dims(id).w));
4197
4224
  const cellH = Math.max(NODE_H, ...ids.map((id) => dims(id).h));
4198
- const GAP = 28;
4199
- const PAD = 24;
4200
- const TITLE = 34;
4201
- const BAND_GAP = 26;
4202
4225
  const cols = Math.max(1, Math.round(Math.sqrt(ids.length * (cellH + GAP) * 2 / (cellW + GAP))));
4203
4226
  const contentW = cols * cellW + (cols - 1) * GAP;
4204
4227
  const panelW = contentW + PAD * 2;
@@ -4227,19 +4250,95 @@ function renderBanded(ir, opts = {}) {
4227
4250
  boxes.push({ title, x: panelW / 2, y: top + panelH / 2, w: panelW, h: panelH, ...status ? { status } : {} });
4228
4251
  top += panelH + BAND_GAP;
4229
4252
  }
4230
- const height = Math.max(1, top - BAND_GAP);
4231
- const layout = {
4232
- width: panelW,
4233
- height,
4234
- nodes: placed.map((p) => ({ id: p.id, x: p.x, y: height - p.y }))
4235
- };
4236
- 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, {
4237
4258
  fit: true,
4238
4259
  hideTitle: true,
4239
- groups: boxes.map((b) => ({ ...b, y: height - b.y })),
4260
+ groups: comp.boxes,
4240
4261
  ...opts.theme ? { theme: opts.theme } : {}
4241
4262
  });
4242
- 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
+ });
4243
4342
  }
4244
4343
  var NODE_W = 175;
4245
4344
  var NODE_H = 104;
@@ -4406,8 +4505,8 @@ function radializeLayout(layout, groupOf, size = /* @__PURE__ */ new Map()) {
4406
4505
  }
4407
4506
 
4408
4507
  // src/carve-actions.ts
4409
- import { existsSync as existsSync7, mkdirSync as mkdirSync3, readFileSync as readFileSync8, readdirSync as readdirSync3, statSync as statSync2 } from "node:fs";
4410
- 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";
4411
4510
 
4412
4511
  // src/layout.ts
4413
4512
  import { existsSync as existsSync6, mkdirSync as mkdirSync2, readFileSync as readFileSync7, renameSync, rmSync, writeFileSync as writeFileSync2, accessSync, constants } from "node:fs";
@@ -4591,7 +4690,112 @@ function reanchorEdges(svg, moved) {
4591
4690
  return out + svg.slice(cursor);
4592
4691
  }
4593
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
+
4594
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
+ }
4595
4799
  var MAX_ARTIFACT_BYTES = 64 * 1024;
4596
4800
  var MAX_ARTIFACTS = 24;
4597
4801
  var MAX_OUTPUT_BYTES = 32 * 1024;
@@ -4612,7 +4816,7 @@ function carveWriteBlock(demo) {
4612
4816
  if (!demo) {
4613
4817
  return "this server isn't running a carve demo \u2014 the carve actions only exist inside a `behold demo carve` copy";
4614
4818
  }
4615
- 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}`;
4616
4820
  if (!insideDemo(demo.root, demo.out)) return "the carve output directory is outside the demo copy";
4617
4821
  return unwritableReason(demo.root);
4618
4822
  }
@@ -4629,7 +4833,7 @@ function readArtifacts(demo, dir, filter) {
4629
4833
  for (const name of entries) {
4630
4834
  if (out.length >= MAX_ARTIFACTS) return;
4631
4835
  if (name === "node_modules" || name.startsWith(".")) continue;
4632
- const full = join9(d, name);
4836
+ const full = join10(d, name);
4633
4837
  let st;
4634
4838
  try {
4635
4839
  st = statSync2(full);
@@ -4644,7 +4848,7 @@ function readArtifacts(demo, dir, filter) {
4644
4848
  if (!filter(rel)) continue;
4645
4849
  let text = "";
4646
4850
  try {
4647
- text = readFileSync8(full, "utf8");
4851
+ text = readFileSync9(full, "utf8");
4648
4852
  } catch {
4649
4853
  continue;
4650
4854
  }
@@ -4676,7 +4880,7 @@ async function runCarveEmit(demo, select) {
4676
4880
  const block = carveWriteBlock(demo);
4677
4881
  if (block) return refuse2("read-only", block, "Start the walkthrough with `behold demo carve`.");
4678
4882
  mkdirSync3(demo.out, { recursive: true });
4679
- const reportFile = join9(demo.out, `${carveSlug(select)}-boundary.json`);
4883
+ const reportFile = join10(demo.out, `${carveSlug(select)}-boundary.json`);
4680
4884
  const args = [
4681
4885
  "carve",
4682
4886
  "emit",
@@ -4705,10 +4909,10 @@ async function runCarveEmit(demo, select) {
4705
4909
  }
4706
4910
  let boundary = null;
4707
4911
  try {
4708
- boundary = JSON.parse(readFileSync8(reportFile, "utf8"));
4912
+ boundary = JSON.parse(readFileSync9(reportFile, "utf8"));
4709
4913
  } catch {
4710
4914
  }
4711
- 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("/");
4712
4916
  const lintArgs = ["lint", lintPath];
4713
4917
  const lint = await runChantRaw(lintArgs, demo.project).catch((err) => ({
4714
4918
  code: 127,
@@ -4771,8 +4975,8 @@ async function runCarveBridge(demo, select) {
4771
4975
  }
4772
4976
 
4773
4977
  // src/ops.ts
4774
- import { readdirSync as readdirSync4, readFileSync as readFileSync9, existsSync as existsSync8 } from "node:fs";
4775
- 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";
4776
4980
  var APPLY_TARGET_LEXICON = {
4777
4981
  cloudformation: "aws",
4778
4982
  kubectl: "k8s",
@@ -4794,11 +4998,11 @@ function discoverOps(projectDir) {
4794
4998
  const seen = /* @__PURE__ */ new Set();
4795
4999
  const out = [];
4796
5000
  for (const sub of ["ops", "src", "."]) {
4797
- const dir = join10(projectDir, sub);
4798
- if (!existsSync8(dir)) continue;
5001
+ const dir = join11(projectDir, sub);
5002
+ if (!existsSync9(dir)) continue;
4799
5003
  for (const f of readdirSync4(dir)) {
4800
5004
  if (!f.endsWith(".op.ts")) continue;
4801
- const content = readFileSync9(join10(dir, f), "utf8");
5005
+ const content = readFileSync10(join11(dir, f), "utf8");
4802
5006
  const name = content.match(/name:\s*["'`]([^"'`]+)["'`]/)?.[1];
4803
5007
  if (!name || seen.has(name)) continue;
4804
5008
  seen.add(name);
@@ -4936,10 +5140,16 @@ var ARGO_SYNC = {
4936
5140
  Unknown: "unknown"
4937
5141
  };
4938
5142
  var ARGO_ERROR_CONDITION = /Error$/;
5143
+ var ARGO_ERROR_STRING = /^([A-Za-z]*Error)(?:=[^:]*)?: (.+)$/;
4939
5144
  function argoErrorMessage(tree) {
4940
5145
  const conditions = tree?.conditions;
4941
5146
  if (!Array.isArray(conditions)) return void 0;
4942
5147
  for (const c of conditions) {
5148
+ if (typeof c === "string") {
5149
+ const m = ARGO_ERROR_STRING.exec(c);
5150
+ if (m) return m[2];
5151
+ continue;
5152
+ }
4943
5153
  const type = str2(rec2(c)?.type);
4944
5154
  if (!type || !ARGO_ERROR_CONDITION.test(type)) continue;
4945
5155
  const message = str2(rec2(c)?.message);
@@ -5323,9 +5533,9 @@ var OpRunner = class {
5323
5533
  };
5324
5534
 
5325
5535
  // src/substrates.ts
5326
- import { spawn as spawn3 } from "node:child_process";
5327
- import { existsSync as existsSync9, readFileSync as readFileSync10 } from "node:fs";
5328
- import { join as join11 } from "node:path";
5536
+ import { spawn as spawn4 } from "node:child_process";
5537
+ import { existsSync as existsSync10, readFileSync as readFileSync11 } from "node:fs";
5538
+ import { join as join12 } from "node:path";
5329
5539
  import { platform } from "node:os";
5330
5540
  var DOCKER_DOWN_DETAIL = "docker is down";
5331
5541
  function probe(cmd, args) {
@@ -5333,7 +5543,7 @@ function probe(cmd, args) {
5333
5543
  let out = "";
5334
5544
  let proc;
5335
5545
  try {
5336
- proc = spawn3(cmd, args, { stdio: ["ignore", "pipe", "pipe"] });
5546
+ proc = spawn4(cmd, args, { stdio: ["ignore", "pipe", "pipe"] });
5337
5547
  } catch {
5338
5548
  resolve8({ code: 127, out: "" });
5339
5549
  return;
@@ -5354,11 +5564,11 @@ async function dockerRunning(run3, nameFilter) {
5354
5564
  return out.split(/\r?\n/).map((s) => s.trim()).filter(Boolean);
5355
5565
  }
5356
5566
  function scriptBringUp(projectDir, relPath, label) {
5357
- return existsSync9(join11(projectDir, relPath)) ? { label, cmd: "bash", args: [relPath] } : void 0;
5567
+ return existsSync10(join12(projectDir, relPath)) ? { label, cmd: "bash", args: [relPath] } : void 0;
5358
5568
  }
5359
5569
  function projectLexicons(projectDir) {
5360
5570
  try {
5361
- const src = readFileSync10(join11(projectDir, "chant.config.ts"), "utf-8");
5571
+ const src = readFileSync11(join12(projectDir, "chant.config.ts"), "utf-8");
5362
5572
  const m = src.match(/lexicons\s*:\s*\[([^\]]*)\]/);
5363
5573
  if (!m) return [];
5364
5574
  return [...m[1].matchAll(/["']([^"']+)["']/g)].map((x) => x[1]);
@@ -5410,7 +5620,7 @@ async function detectSubstrates(projectDir, preview = false, boundContext, probe
5410
5620
  ["forgejo", "Forgejo", ".forgejo", "test/forgejo-runtime-e2e.sh"]
5411
5621
  ];
5412
5622
  for (const [name, label, marker, script] of forges) {
5413
- if (!existsSync9(join11(projectDir, marker))) continue;
5623
+ if (!existsSync10(join12(projectDir, marker))) continue;
5414
5624
  const c = docker ? await dockerRunning(run3, name) : [];
5415
5625
  const d = dep(c.length > 0, "container up", "on-demand (pipeline run)");
5416
5626
  subs.push({
@@ -5444,7 +5654,7 @@ async function detectSubstrates(projectDir, preview = false, boundContext, probe
5444
5654
  detail: endpoint ? `targeting ${endpoint}` : "real Fly (FLY_FLAPS_BASE_URL unset)"
5445
5655
  });
5446
5656
  }
5447
- if (existsSync9(join11(projectDir, ".github", "workflows"))) {
5657
+ if (existsSync10(join12(projectDir, ".github", "workflows"))) {
5448
5658
  const gh = await run3("gh", ["auth", "status"]);
5449
5659
  const ready = gh.code === 0;
5450
5660
  subs.push({
@@ -5457,7 +5667,7 @@ async function detectSubstrates(projectDir, preview = false, boundContext, probe
5457
5667
  if (lexicons.includes("temporal")) {
5458
5668
  let hasProfiles = false;
5459
5669
  try {
5460
- hasProfiles = /temporal\s*:\s*\{[\s\S]{0,400}?profiles\s*:/.test(readFileSync10(join11(projectDir, "chant.config.ts"), "utf-8"));
5670
+ hasProfiles = /temporal\s*:\s*\{[\s\S]{0,400}?profiles\s*:/.test(readFileSync11(join12(projectDir, "chant.config.ts"), "utf-8"));
5461
5671
  } catch {
5462
5672
  }
5463
5673
  subs.push({
@@ -5562,7 +5772,7 @@ async function openRollbackBranches(projectDir, env) {
5562
5772
 
5563
5773
  // src/estate.ts
5564
5774
  import { statSync as statSync3 } from "node:fs";
5565
- import { join as join12, resolve as resolve3, sep as sep2 } from "node:path";
5775
+ import { join as join13, resolve as resolve3, sep as sep2 } from "node:path";
5566
5776
  import { composeStacks, shortStackNames } from "@intentius/pinhole";
5567
5777
  async function composeEstate(projectDirs, opts = {}) {
5568
5778
  const names = shortStackNames(projectDirs);
@@ -5622,7 +5832,7 @@ function pathAlignment(declared, dir, isDir = realIsDir) {
5622
5832
  const have = root.split(sep2).filter(Boolean);
5623
5833
  for (let k = Math.min(want.length, have.length); k >= 1; k--) {
5624
5834
  if (!have.slice(-k).every((s, i) => s === want[i])) continue;
5625
- if (!isDir(join12(root, ...want.slice(k)))) continue;
5835
+ if (!isDir(join13(root, ...want.slice(k)))) continue;
5626
5836
  return k;
5627
5837
  }
5628
5838
  return 0;
@@ -5652,10 +5862,10 @@ function joinNamespaceBindings(members, isDir = realIsDir) {
5652
5862
  if (!winner || tied) continue;
5653
5863
  const prev = best.get(winner.dir);
5654
5864
  if (prev === "ambiguous") continue;
5655
- const join18 = { dir: winner.dir, namespace: b.namespace, path: b.path, declaredBy: b.declaredBy };
5656
- if (!prev) best.set(winner.dir, { score: winner.score, join: join18 });
5865
+ const join19 = { dir: winner.dir, namespace: b.namespace, path: b.path, declaredBy: b.declaredBy };
5866
+ if (!prev) best.set(winner.dir, { score: winner.score, join: join19 });
5657
5867
  else if (prev.join.namespace !== b.namespace) best.set(winner.dir, "ambiguous");
5658
- else if (winner.score > prev.score) best.set(winner.dir, { score: winner.score, join: join18 });
5868
+ else if (winner.score > prev.score) best.set(winner.dir, { score: winner.score, join: join19 });
5659
5869
  }
5660
5870
  return [...best.values()].filter((v) => v !== "ambiguous").map((v) => v.join);
5661
5871
  }
@@ -5717,8 +5927,8 @@ async function composeEstateOverlay(projectDirs, opts, classify) {
5717
5927
  }
5718
5928
 
5719
5929
  // src/events.ts
5720
- import { watch, existsSync as existsSync10 } from "node:fs";
5721
- import { join as join13 } from "node:path";
5930
+ import { watch, existsSync as existsSync11 } from "node:fs";
5931
+ import { join as join14 } from "node:path";
5722
5932
  var Broadcaster = class {
5723
5933
  listeners = /* @__PURE__ */ new Set();
5724
5934
  subscribe(fn) {
@@ -5736,7 +5946,7 @@ var Broadcaster = class {
5736
5946
  };
5737
5947
  var IGNORE = /(^|[\\/])(node_modules|dist|\.git)([\\/]|$)/;
5738
5948
  function watchSource(projectDir, onChange, debounceMs = 200) {
5739
- const dir = existsSync10(join13(projectDir, "src")) ? join13(projectDir, "src") : projectDir;
5949
+ const dir = existsSync11(join14(projectDir, "src")) ? join14(projectDir, "src") : projectDir;
5740
5950
  let timer;
5741
5951
  const watcher = watch(dir, { recursive: true }, (_event, file) => {
5742
5952
  const name = typeof file === "string" ? file : "";
@@ -5840,7 +6050,7 @@ var FrameBuffer = class {
5840
6050
  };
5841
6051
 
5842
6052
  // src/lanes.ts
5843
- import { renderMorphHtml, layoutIr as layoutIr2 } from "@intentius/pinhole";
6053
+ import { renderMorphHtml as renderMorphHtml2, layoutIr as layoutIr2 } from "@intentius/pinhole";
5844
6054
  function safeJson(value) {
5845
6055
  return JSON.stringify(value).replace(/</g, "\\u003c");
5846
6056
  }
@@ -5967,7 +6177,7 @@ function renderLanes(frames, summaries) {
5967
6177
  ir: f.ir,
5968
6178
  layout: layoutIr2(f.ir)
5969
6179
  }));
5970
- const doc = renderMorphHtml(views, { title: "Deployment lanes" });
6180
+ const doc = renderMorphHtml2(views, { title: "Deployment lanes" });
5971
6181
  const laneFrames = frames.map((f, i) => ({
5972
6182
  t: summaries[i].t,
5973
6183
  name: new Date(summaries[i].t).toISOString().slice(11, 19),
@@ -6014,13 +6224,13 @@ async function emulatorDown(projectDir) {
6014
6224
  }
6015
6225
 
6016
6226
  // src/demos.ts
6017
- import { readFileSync as readFileSync11, existsSync as existsSync11, cpSync } from "node:fs";
6018
- import { join as join14, relative as relative2, resolve as resolve4, sep as sep3 } from "node:path";
6019
- import { spawn as spawn4, spawnSync } from "node:child_process";
6227
+ import { readFileSync as readFileSync12, existsSync as existsSync12, cpSync } from "node:fs";
6228
+ import { join as join15, relative as relative2, resolve as resolve4, sep as sep3 } from "node:path";
6229
+ import { spawn as spawn5, spawnSync } from "node:child_process";
6020
6230
  function loadDemoRegistry(pkgRoot2) {
6021
6231
  let raw;
6022
6232
  try {
6023
- raw = JSON.parse(readFileSync11(join14(pkgRoot2, "demos.json"), "utf8"));
6233
+ raw = JSON.parse(readFileSync12(join15(pkgRoot2, "demos.json"), "utf8"));
6024
6234
  } catch {
6025
6235
  return [];
6026
6236
  }
@@ -6061,16 +6271,16 @@ function fetchesFromNetwork(entry) {
6061
6271
  }
6062
6272
  function demoTargetDir(entry, cwd = process.cwd()) {
6063
6273
  const legacy = resolve4(cwd, "behold-demo");
6064
- if (entry.name === "writes" && existsSync11(legacy)) return legacy;
6274
+ if (entry.name === "writes" && existsSync12(legacy)) return legacy;
6065
6275
  return resolve4(cwd, "behold-demos", entry.name);
6066
6276
  }
6067
6277
  async function loadDemo(entry, opts) {
6068
6278
  const { pkgRoot: pkgRoot2, target } = opts;
6069
6279
  const say = (line) => opts.log?.(`behold demo ${entry.name} \u2192 ${line}`);
6070
- if (!existsSync11(target)) {
6280
+ if (!existsSync12(target)) {
6071
6281
  if (entry.source === "bundled") {
6072
- const bundled = join14(pkgRoot2, entry.dir);
6073
- if (!existsSync11(bundled)) return { ok: false, error: `this install has no bundled ${entry.dir}` };
6282
+ const bundled = join15(pkgRoot2, entry.dir);
6283
+ if (!existsSync12(bundled)) return { ok: false, error: `this install has no bundled ${entry.dir}` };
6074
6284
  say(`copying to ${target} (it's yours \u2014 edit it)`);
6075
6285
  try {
6076
6286
  cpSync(bundled, target, {
@@ -6089,7 +6299,7 @@ async function loadDemo(entry, opts) {
6089
6299
  } else {
6090
6300
  say(`reusing ${target}`);
6091
6301
  }
6092
- if (existsSync11(join14(target, "package.json")) && !existsSync11(join14(target, "node_modules"))) {
6302
+ if (existsSync12(join15(target, "package.json")) && !existsSync12(join15(target, "node_modules"))) {
6093
6303
  say("npm install\u2026");
6094
6304
  if (await runStep("npm", ["install"], { cwd: target, shell: process.platform === "win32" }) !== 0) {
6095
6305
  return { ok: false, error: `npm install failed in ${target}` };
@@ -6101,19 +6311,19 @@ async function loadDemo(entry, opts) {
6101
6311
  return { ok: false, error: `setup failed (${entry.setup})` };
6102
6312
  }
6103
6313
  }
6104
- return { ok: true, serveDirs: entry.serve.dirs?.length ? entry.serve.dirs.map((d) => join14(target, d)) : [target] };
6314
+ return { ok: true, serveDirs: entry.serve.dirs?.length ? entry.serve.dirs.map((d) => join15(target, d)) : [target] };
6105
6315
  }
6106
6316
  function runStep(cmd, args, opts = {}) {
6107
6317
  return new Promise((res) => {
6108
- const child = spawn4(cmd, args, { stdio: "inherit", cwd: opts.cwd, shell: opts.shell ?? false });
6318
+ const child = spawn5(cmd, args, { stdio: "inherit", cwd: opts.cwd, shell: opts.shell ?? false });
6109
6319
  child.on("error", () => res(-1));
6110
6320
  child.on("close", (code) => res(code ?? 1));
6111
6321
  });
6112
6322
  }
6113
6323
 
6114
6324
  // src/server.ts
6115
- var pkgRoot = join15(dirname4(fileURLToPath3(import.meta.url)), "..");
6116
- var webRoot = join15(pkgRoot, "web");
6325
+ var pkgRoot = join16(dirname4(fileURLToPath3(import.meta.url)), "..");
6326
+ var webRoot = join16(pkgRoot, "web");
6117
6327
  var execFileP = async (cmd, args) => (await promisify3(execFile3)(cmd, args, { encoding: "utf8", timeout: 1e4 })).stdout;
6118
6328
  function optsFromQuery(url, tierEnvVar, projectDir) {
6119
6329
  const q = url.searchParams;
@@ -6163,7 +6373,7 @@ function tierFailure(tier, message) {
6163
6373
  function beholdVersion() {
6164
6374
  try {
6165
6375
  const here = dirname4(fileURLToPath3(import.meta.url));
6166
- return JSON.parse(readFileSync12(join15(here, "..", "package.json"), "utf8")).version ?? "unknown";
6376
+ return JSON.parse(readFileSync13(join16(here, "..", "package.json"), "utf8")).version ?? "unknown";
6167
6377
  } catch {
6168
6378
  return "unknown";
6169
6379
  }
@@ -6208,7 +6418,7 @@ async function captureFrame(projectDir, env, frames, broadcaster) {
6208
6418
  }
6209
6419
  }
6210
6420
  function carveRoutes(app, reportPath, demo) {
6211
- const load = () => readCarveReport(reportPath, (p) => readFileSync12(p, "utf8"));
6421
+ const load = () => readCarveReport(reportPath, (p) => readFileSync13(p, "utf8"));
6212
6422
  const demoBlock = () => carveWriteBlock(demo);
6213
6423
  const demoInfo = () => {
6214
6424
  if (!demo) return null;
@@ -6224,6 +6434,9 @@ function carveRoutes(app, reportPath, demo) {
6224
6434
  fromLabel: relative3(demo.root, demo.from).split(sep4).join("/"),
6225
6435
  runnable: !block,
6226
6436
  ...block ? { reason: block } : {},
6437
+ // The live tier, when this boot armed it: the scratch endpoint the
6438
+ // stepper names, and the fact the plan button exists at all.
6439
+ ...demo.live ? { live: { endpoint: demo.live.endpoint, container: demo.live.container, applied: demo.live.applied } } : {},
6227
6440
  ...demo.degraded ? { degraded: demo.degraded } : {},
6228
6441
  buildCaveat: BUILD_CAVEAT
6229
6442
  };
@@ -6232,11 +6445,21 @@ function carveRoutes(app, reportPath, demo) {
6232
6445
  const parsed = load();
6233
6446
  return parsed.ok ? c.json(parsed.report) : c.json(parsed.refusal, 422);
6234
6447
  });
6235
- app.get("/api/graph", (c) => {
6448
+ let appGraph;
6449
+ const appGraphOnce = () => {
6450
+ if (!demo) return Promise.resolve(null);
6451
+ appGraph ??= graphIr(demo.project).then((ir) => ir.nodes.length ? { ir, label: relative3(demo.root, demo.project).split(sep4).join("/") } : null).catch(() => null);
6452
+ return appGraph;
6453
+ };
6454
+ app.get("/api/graph", async (c) => {
6236
6455
  const parsed = load();
6237
6456
  if (!parsed.ok) return c.json(parsed.refusal, 422);
6238
- const ir = carveReportToIr(parsed.report);
6239
- const { svg } = renderBanded(ir);
6457
+ const tfIr = carveReportToIr(parsed.report);
6458
+ const appSide = await appGraphOnce();
6459
+ const { svg, ir } = appSide ? renderCarveEstate(tfIr, appSide.ir, {
6460
+ tfTitle: `${relative3(demo.root, demo.from).split(sep4).join("/")} \u2014 terraform`,
6461
+ appTitle: `${appSide.label} \u2014 chant`
6462
+ }) : { ...renderBanded(tfIr), ir: tfIr };
6240
6463
  return c.json({
6241
6464
  ir,
6242
6465
  svg,
@@ -6248,10 +6471,44 @@ function carveRoutes(app, reportPath, demo) {
6248
6471
  carve: true,
6249
6472
  // A demo whose own advisor run failed says so on the statusbar, not
6250
6473
  // only in the terminal the viewer isn't looking at.
6251
- note: carveNote(parsed.report, ir) + (demo?.degraded ? ` Degraded: ${demo.degraded}` : "")
6474
+ note: carveNote(parsed.report, tfIr) + (demo?.degraded ? ` Degraded: ${demo.degraded}` : "")
6252
6475
  }
6253
6476
  });
6254
6477
  });
6478
+ app.get("/carve/morph", async (c) => {
6479
+ const parsed = load();
6480
+ if (!parsed.ok) return c.json(parsed.refusal, 422);
6481
+ const select = c.req.query("select") ?? "";
6482
+ const ranked = (parsed.report.resources ?? []).some((r) => r.address === select);
6483
+ if (!ranked) {
6484
+ return c.json(
6485
+ {
6486
+ error: `"${select}" is not a ranked address in this report`,
6487
+ code: "carve_bad_select",
6488
+ remedy: "pass ?select=<address> naming a resource the report ranks \u2014 GET /api/carve lists them"
6489
+ },
6490
+ 400
6491
+ );
6492
+ }
6493
+ const appSide = await appGraphOnce();
6494
+ if (!appSide) {
6495
+ return c.json(
6496
+ {
6497
+ error: "the morph needs the demo estate's chant project, and this server isn't serving a demo copy",
6498
+ code: "carve_no_demo",
6499
+ remedy: "run `behold demo carve` \u2014 the bundled estate has the chant box the card moves into"
6500
+ },
6501
+ 404
6502
+ );
6503
+ }
6504
+ const tfIr = carveReportToIr(parsed.report);
6505
+ const html = renderCarveMorph(tfIr, appSide.ir, [select], {
6506
+ tfTitle: `${relative3(demo.root, demo.from).split(sep4).join("/")} \u2014 terraform`,
6507
+ appTitle: `${appSide.label} \u2014 chant`,
6508
+ title: `carve \u2014 ${select}`
6509
+ });
6510
+ return c.html(html);
6511
+ });
6255
6512
  app.get("/api/project", (c) => {
6256
6513
  const parsed = load();
6257
6514
  return c.json({
@@ -6314,6 +6571,17 @@ function carveRoutes(app, reportPath, demo) {
6314
6571
  };
6315
6572
  app.post("/api/carve/emit", (c) => runStep2(c, runCarveEmit));
6316
6573
  app.post("/api/carve/bridge", (c) => runStep2(c, runCarveBridge));
6574
+ app.post("/api/carve/plan", async (c) => {
6575
+ const block = demoBlock();
6576
+ if (block) {
6577
+ return c.json({ error: block, code: "read-only", remedy: "The live plan runs inside a demo copy \u2014 `behold demo carve --live`." }, 403);
6578
+ }
6579
+ if (!(c.req.header("content-type") ?? "").includes("application/json")) {
6580
+ return c.json({ error: "send application/json", code: "carve-action", remedy: "POST {} with content-type application/json" }, 415);
6581
+ }
6582
+ const result = await runCarvePlan(demo);
6583
+ return result.ok ? c.json(result) : c.json(result.refusal, 422);
6584
+ });
6317
6585
  app.get("/api/substrates", (c) => c.json({ substrates: [] }));
6318
6586
  app.get("/api/history", (c) => c.json({ commits: [] }));
6319
6587
  app.get("/api/resources", (c) => c.json({ byComponent: {} }));
@@ -6410,9 +6678,9 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
6410
6678
  return c.json({ started: true, name, ran: label });
6411
6679
  });
6412
6680
  app.post("/api/local/reset", (c) => {
6413
- const down = join15(cfg.projectDir, "scripts/local/local-down.sh");
6414
- const up = join15(cfg.projectDir, "scripts/local/local-up.sh");
6415
- if (!existsSync12(down) || !existsSync12(up)) {
6681
+ const down = join16(cfg.projectDir, "scripts/local/local-down.sh");
6682
+ const up = join16(cfg.projectDir, "scripts/local/local-up.sh");
6683
+ if (!existsSync13(down) || !existsSync13(up)) {
6416
6684
  return c.json({ error: "no local-down.sh / local-up.sh in scripts/local \u2014 reset is only for local emulator projects" }, 400);
6417
6685
  }
6418
6686
  if (!runner.bringUp("reset local emulator", "bash", ["-c", "bash scripts/local/local-down.sh && bash scripts/local/local-up.sh"], cfg.projectDir)) {
@@ -6490,8 +6758,8 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
6490
6758
  if (cfg.previewMode) return c.json({ error: "switching projects is locked in preview mode" }, 403);
6491
6759
  const body = await c.req.json().catch(() => ({}));
6492
6760
  const dir = typeof body.dir === "string" && body.dir.trim() ? resolve5(body.dir.trim()) : "";
6493
- if (!dir || !existsSync12(dir)) return c.json({ error: `no such directory: ${dir || "(no dir given)"}` }, 400);
6494
- if (!existsSync12(join15(dir, "chant.config.ts"))) {
6761
+ if (!dir || !existsSync13(dir)) return c.json({ error: `no such directory: ${dir || "(no dir given)"}` }, 400);
6762
+ if (!existsSync13(join16(dir, "chant.config.ts"))) {
6495
6763
  return c.json({ error: `${dir} doesn't look like a chant project \u2014 no chant.config.ts` }, 400);
6496
6764
  }
6497
6765
  switchServedProject([dir]);
@@ -6535,7 +6803,7 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
6535
6803
  fetches: fetchesFromNetwork(e),
6536
6804
  ...e.repo ? { repo: e.repo } : {},
6537
6805
  target,
6538
- loaded: existsSync12(target),
6806
+ loaded: existsSync13(target),
6539
6807
  satisfiable: missing.length === 0,
6540
6808
  // #254: the carve walkthrough runs fine here — it just isn't a project
6541
6809
  // to switch INTO. Carve mode claims `/api/graph` and `/api/project` at
@@ -6579,7 +6847,7 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
6579
6847
  const loaded = await loadDemo(entry, { pkgRoot, target, log: (line) => process.stdout.write(line + "\n") });
6580
6848
  if (!loaded.ok) return c.json({ error: loaded.error }, 500);
6581
6849
  const dirs = loaded.serveDirs;
6582
- if (!existsSync12(join15(dirs[0], "chant.config.ts"))) {
6850
+ if (!existsSync13(join16(dirs[0], "chant.config.ts"))) {
6583
6851
  return c.json({ error: `${entry.name} loaded to ${target} but ${dirs[0]} has no chant.config.ts` }, 500);
6584
6852
  }
6585
6853
  if (entry.serve.local) {
@@ -6785,7 +7053,7 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
6785
7053
  srcCompositeEdgesAttached = 0;
6786
7054
  }
6787
7055
  }
6788
- if (!multi && !components && !logical && !opts.lens && ir.nodes.length === 0 && !existsSync12(join15(cfg.projectDir, "chant.config.ts"))) {
7056
+ if (!multi && !components && !logical && !opts.lens && ir.nodes.length === 0 && !existsSync13(join16(cfg.projectDir, "chant.config.ts"))) {
6789
7057
  return c.json(noProjectError(cfg.projectDir), 404);
6790
7058
  }
6791
7059
  const radial = new URL(c.req.url).searchParams.get("radial") === "1";
@@ -7125,7 +7393,7 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
7125
7393
  });
7126
7394
  const rel = relative3(process.cwd(), webRoot) || ".";
7127
7395
  app.use("/*", serveStatic({ root: rel }));
7128
- app.get("/", serveStatic({ path: join15(rel, "index.html") }));
7396
+ app.get("/", serveStatic({ path: join16(rel, "index.html") }));
7129
7397
  return app;
7130
7398
  }
7131
7399
  async function bootLocalEmulators(dir, who) {
@@ -7154,7 +7422,7 @@ async function bootLocalEmulators(dir, who) {
7154
7422
  async function startServer(cfg) {
7155
7423
  if (cfg.local) cfg.emulators = await bootLocalEmulators(cfg.projectDir, "behold serve --local");
7156
7424
  for (const dir of cfg.projectDirs ?? [cfg.projectDir]) {
7157
- if (existsSync12(join15(dir, "chant.config.ts"))) addRecent(dir);
7425
+ if (existsSync13(join16(dir, "chant.config.ts"))) addRecent(dir);
7158
7426
  }
7159
7427
  const broadcaster = new Broadcaster();
7160
7428
  const frames = new FrameBuffer();
@@ -7165,10 +7433,10 @@ async function startServer(cfg) {
7165
7433
  });
7166
7434
  const app = createApp(cfg, broadcaster, frames, runner);
7167
7435
  const autoSync = cfg.autoSync ?? "off";
7168
- const capture = () => captureFrame(cfg.projectDir, cfg.env, frames, broadcaster);
7436
+ const capture2 = () => captureFrame(cfg.projectDir, cfg.env, frames, broadcaster);
7169
7437
  const onEstateChange = () => {
7170
7438
  broadcaster.emit("changed");
7171
- void capture();
7439
+ void capture2();
7172
7440
  };
7173
7441
  const onPollDrift = (movedLexicons) => {
7174
7442
  onEstateChange();
@@ -7215,18 +7483,24 @@ async function startServer(cfg) {
7215
7483
  };
7216
7484
  process.stdout.write(` switched \u2192 ${dir}
7217
7485
  `);
7218
- void capture();
7486
+ void capture2();
7219
7487
  };
7220
- if (!carve) void capture();
7488
+ if (!carve) void capture2();
7221
7489
  let shuttingDown = false;
7222
7490
  const shutdown = () => {
7223
7491
  if (shuttingDown) return;
7224
7492
  shuttingDown = true;
7225
7493
  stopWatch();
7226
7494
  stopPoll();
7227
- 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)}
7228
- `)) : Promise.resolve();
7229
- void done.finally(() => process.exit(0));
7495
+ const downs = [];
7496
+ if (cfg.local && cfg.emulators && cfg.emulators.length) {
7497
+ downs.push(
7498
+ emulatorDown(cfg.projectDir).catch((err) => process.stderr.write(`emulator down: ${err instanceof Error ? err.message : String(err)}
7499
+ `))
7500
+ );
7501
+ }
7502
+ if (cfg.carveDemo?.live) downs.push(teardownScratchFloci().catch(() => void 0));
7503
+ void Promise.all(downs).finally(() => process.exit(0));
7230
7504
  };
7231
7505
  process.on("SIGINT", shutdown);
7232
7506
  process.on("SIGTERM", shutdown);
@@ -7238,7 +7512,8 @@ async function startServer(cfg) {
7238
7512
  green = carve now, amber = boundary work, grey = leave in Terraform.
7239
7513
  ` + (cfg.carveDemo ? ` walkthrough: the panel's Carve tab \u2014 advise \u2192 pick \u2192 emit \u2192 bridge \u2192 handoff \u2192 done.
7240
7514
  Emit and bridge write only into ${cfg.carveDemo.out}; your Terraform is never edited.
7241
- ` + (cfg.carveDemo.degraded ? ` degraded: ${cfg.carveDemo.degraded}
7515
+ ` + (cfg.carveDemo.live ? ` live: scratch Floci at ${cfg.carveDemo.live.endpoint} \u2014 deleted on Ctrl-C.
7516
+ ` : "") + (cfg.carveDemo.degraded ? ` degraded: ${cfg.carveDemo.degraded}
7242
7517
  ` : "") : ` Read-only advisory: behold emits nothing and touches no Terraform. Ctrl-C to stop.
7243
7518
  `)
7244
7519
  );
@@ -7275,8 +7550,8 @@ async function startServer(cfg) {
7275
7550
  }
7276
7551
 
7277
7552
  // src/export.ts
7278
- import { mkdirSync as mkdirSync4, writeFileSync as writeFileSync3, copyFileSync, readFileSync as readFileSync13, readdirSync as readdirSync5 } from "node:fs";
7279
- import { join as join16, dirname as dirname5, basename } from "node:path";
7553
+ import { mkdirSync as mkdirSync4, writeFileSync as writeFileSync4, copyFileSync, readFileSync as readFileSync14, readdirSync as readdirSync5 } from "node:fs";
7554
+ import { join as join17, dirname as dirname5, basename } from "node:path";
7280
7555
  import { fileURLToPath as fileURLToPath4 } from "node:url";
7281
7556
  var LENS_PARAMS = ["components", "detail", "env", "logical", "radial", "tier"];
7282
7557
  function canonicalKey(path, params) {
@@ -7324,7 +7599,7 @@ function captureKeys(axes) {
7324
7599
  return [...keys];
7325
7600
  }
7326
7601
  function webDir() {
7327
- return join16(dirname5(fileURLToPath4(import.meta.url)), "..", "web");
7602
+ return join17(dirname5(fileURLToPath4(import.meta.url)), "..", "web");
7328
7603
  }
7329
7604
  function workerName(project, override) {
7330
7605
  const raw = override ?? `behold-${basename(project)}`;
@@ -7335,7 +7610,7 @@ async function runExport(cfg, outDir, opts = {}) {
7335
7610
  const app = createApp({ ...cfg, layoutWrites: false });
7336
7611
  const proj = await (await app.request("/api/project")).json();
7337
7612
  const axes = { environments: proj.environments ?? [], tiers: proj.tiers ?? [] };
7338
- const snapDir = join16(outDir, "snapshots");
7613
+ const snapDir = join17(outDir, "snapshots");
7339
7614
  mkdirSync4(snapDir, { recursive: true });
7340
7615
  const keyToFile = {};
7341
7616
  let ok = 0;
@@ -7344,7 +7619,7 @@ async function runExport(cfg, outDir, opts = {}) {
7344
7619
  const res = await app.request(`${key}${key.includes("?") ? "&" : "?"}layout=1`);
7345
7620
  const body = await res.text();
7346
7621
  const file = slug2(key);
7347
- writeFileSync3(join16(snapDir, file), body);
7622
+ writeFileSync4(join17(snapDir, file), body);
7348
7623
  keyToFile[key] = `snapshots/${file}`;
7349
7624
  if (res.ok) ok++;
7350
7625
  else failed++;
@@ -7356,21 +7631,21 @@ async function runExport(cfg, outDir, opts = {}) {
7356
7631
  axes,
7357
7632
  keyToFile
7358
7633
  };
7359
- writeFileSync3(join16(outDir, "manifest.json"), JSON.stringify(manifest, null, 2));
7360
- const html = readFileSync13(join16(webDir(), "index.html"), "utf8").replace(
7634
+ writeFileSync4(join17(outDir, "manifest.json"), JSON.stringify(manifest, null, 2));
7635
+ const html = readFileSync14(join17(webDir(), "index.html"), "utf8").replace(
7361
7636
  /<\/head>/i,
7362
7637
  ` <script>window.__BEHOLD_STATIC__ = true;</script>
7363
7638
  </head>`
7364
7639
  );
7365
- writeFileSync3(join16(outDir, "index.html"), html);
7640
+ writeFileSync4(join17(outDir, "index.html"), html);
7366
7641
  for (const f of readdirSync5(webDir())) {
7367
7642
  if (f === "index.html") continue;
7368
- copyFileSync(join16(webDir(), f), join16(outDir, f));
7643
+ copyFileSync(join17(webDir(), f), join17(outDir, f));
7369
7644
  }
7370
- writeFileSync3(join16(outDir, "README.md"), BUNDLE_README);
7645
+ writeFileSync4(join17(outDir, "README.md"), BUNDLE_README);
7371
7646
  const name = workerName(cfg.projectDir, opts.name);
7372
- writeFileSync3(
7373
- join16(outDir, "wrangler.jsonc"),
7647
+ writeFileSync4(
7648
+ join17(outDir, "wrangler.jsonc"),
7374
7649
  JSON.stringify(
7375
7650
  { $schema: "node_modules/wrangler/config-schema.json", name, compatibility_date: "2025-06-01", assets: { directory: "." } },
7376
7651
  null,
@@ -7637,6 +7912,7 @@ var USAGE = `behold \u2014 a live control plane on chant (read-only core)
7637
7912
 
7638
7913
  Usage:
7639
7914
  behold demo [name] [target-dir] [--port <n>] [--list]
7915
+ behold demo carve [--live] [--port <n>]
7640
7916
  behold doctor [project-dir] [--json]
7641
7917
  behold preview [project-dir] [--port <n>] [--emulator]
7642
7918
  behold export [project-dir] [--out <dir>] [--env <name>] [--name <worker>] [--emulator]
@@ -7669,7 +7945,10 @@ Usage:
7669
7945
  up on a throwaway k3d cluster instead. \`behold demo carve\` is the
7670
7946
  odd one out: no cluster, no Docker, no cloud \u2014 a half-migrated
7671
7947
  Terraform/chant estate plus the six-step carve walkthrough on the
7672
- panel's Carve tab. Needs Docker (and per-demo tools --list names).
7948
+ panel's Carve tab. Its \`--live\` tier flips that: docker + terraform
7949
+ on PATH, a scratch Floci booted and deleted on exit, the starred
7950
+ resources REALLY applied, and a real \`terraform plan\` button at
7951
+ Handoff. Needs Docker (and per-demo tools --list names).
7673
7952
  Loaded demos land in the panel's recents, so switching between them
7674
7953
  is the Scope tab \u2014 which lists this whole catalog too (#268), one
7675
7954
  click from any served project.
@@ -7836,7 +8115,7 @@ function warnIfNotChantProject(dir) {
7836
8115
  if (shape.kind === "estate") {
7837
8116
  process.stderr.write(
7838
8117
  `behold: warning \u2014 ${dir} is an estate root, not a chant project itself.
7839
- Serve its members composed: behold serve ${shape.members.map((m) => join17(dir, m)).join(" ")}
8118
+ Serve its members composed: behold serve ${shape.members.map((m) => join18(dir, m)).join(" ")}
7840
8119
  `
7841
8120
  );
7842
8121
  return;
@@ -7872,7 +8151,7 @@ async function runCarve(rest) {
7872
8151
  process.exit(2);
7873
8152
  }
7874
8153
  const reportPath = resolve7(fileArg);
7875
- const parsed = readCarveReport(reportPath, (p) => readFileSync14(p, "utf8"));
8154
+ const parsed = readCarveReport(reportPath, (p) => readFileSync15(p, "utf8"));
7876
8155
  if (!parsed.ok) {
7877
8156
  process.stderr.write(`behold carve: ${parsed.refusal.error}
7878
8157
  ${parsed.refusal.remedy}
@@ -7899,7 +8178,7 @@ async function runDoctor(rest) {
7899
8178
  }
7900
8179
  }
7901
8180
  const dir = dirArg ?? ".";
7902
- if (!existsSync13(resolve7(dir))) {
8181
+ if (!existsSync14(resolve7(dir))) {
7903
8182
  process.stderr.write(`behold doctor: no such directory: ${resolve7(dir)}
7904
8183
  `);
7905
8184
  process.exit(2);
@@ -7909,14 +8188,16 @@ async function runDoctor(rest) {
7909
8188
  if (!report.ok) process.exitCode = 1;
7910
8189
  }
7911
8190
  async function runDemo(rest) {
7912
- const pkgRoot2 = join17(dirname6(fileURLToPath5(import.meta.url)), "..");
8191
+ const pkgRoot2 = join18(dirname6(fileURLToPath5(import.meta.url)), "..");
7913
8192
  const registry = loadDemoRegistry(pkgRoot2);
7914
8193
  let port = 4600;
8194
+ let live = false;
7915
8195
  let name;
7916
8196
  let dirArg;
7917
8197
  for (let i = 0; i < rest.length; i++) {
7918
8198
  const a = rest[i];
7919
8199
  if (a === "--port") port = Number(rest[++i]);
8200
+ else if (a === "--live") live = true;
7920
8201
  else if (a === "--list") {
7921
8202
  if (!registry.length) {
7922
8203
  process.stdout.write("behold demo: no catalog in this install (demos.json missing)\n");
@@ -7969,9 +8250,14 @@ async function runDemo(rest) {
7969
8250
  process.exit(1);
7970
8251
  }
7971
8252
  if (entry.serve.carve) {
7972
- await serveCarveDemo(target, entry.serve.carve, port);
8253
+ await serveCarveDemo(target, entry.serve.carve, port, live);
7973
8254
  return;
7974
8255
  }
8256
+ if (live) {
8257
+ process.stderr.write(`behold demo ${entry.name}: --live is the carve demo's tier \u2014 only \`behold demo carve --live\` takes it.
8258
+ `);
8259
+ process.exit(2);
8260
+ }
7975
8261
  process.stdout.write(`behold demo ${entry.name} \u2192 serving. Blue = declared; Deploy turns it green.
7976
8262
  `);
7977
8263
  const serveArgs = ["serve", ...loaded.serveDirs, "--port", String(port)];
@@ -7981,18 +8267,53 @@ async function runDemo(rest) {
7981
8267
  }
7982
8268
  function spawnStep(cmd, args, cwd) {
7983
8269
  return new Promise((res) => {
7984
- const child = spawn5(cmd, args, { stdio: "inherit", cwd, shell: process.platform === "win32" });
8270
+ const child = spawn6(cmd, args, { stdio: "inherit", cwd, shell: process.platform === "win32" });
7985
8271
  child.on("error", () => res(-1));
7986
8272
  child.on("close", (code) => res(code ?? 1));
7987
8273
  });
7988
8274
  }
7989
- async function serveCarveDemo(target, carve, port) {
8275
+ async function serveCarveDemo(target, carve, port, live = false) {
7990
8276
  const at = (rel) => resolve7(target, rel);
7991
8277
  const project = at(carve.project);
7992
8278
  const from = at(carve.from);
7993
8279
  const state = carve.state ? at(carve.state) : void 0;
7994
8280
  const committed = at(carve.report);
7995
- if (existsSync13(join17(project, "package.json")) && !existsSync13(join17(project, "node_modules"))) {
8281
+ let liveInfo;
8282
+ if (live) {
8283
+ for (const bin of ["docker", "terraform"]) {
8284
+ const probe2 = spawnSync2(process.platform === "win32" ? "where" : "which", [bin], { stdio: "ignore" });
8285
+ if (probe2.status !== 0) {
8286
+ process.stderr.write(`behold demo carve --live: needs ${bin} on PATH (the offline tier doesn't \u2014 drop --live).
8287
+ `);
8288
+ process.exit(2);
8289
+ }
8290
+ }
8291
+ process.stdout.write(`behold demo carve --live \u2192 scratch Floci (${LIVE_CONTAINER}, :${LIVE_PORT}, deleted on exit)\u2026
8292
+ `);
8293
+ const bootErr = await bootScratchFloci();
8294
+ if (bootErr) {
8295
+ process.stderr.write(`behold demo carve --live: ${bootErr}
8296
+ `);
8297
+ process.exit(1);
8298
+ }
8299
+ process.stdout.write("behold demo carve --live \u2192 terraform init + apply (the starred resources, into the scratch Floci)\u2026\n");
8300
+ if (state) rmSync2(state, { force: true });
8301
+ const applyErr = await applyIntoFloci(from, spawnStep);
8302
+ if (applyErr) {
8303
+ await teardownScratchFloci();
8304
+ process.stderr.write(`behold demo carve --live: ${applyErr}
8305
+ `);
8306
+ process.exit(1);
8307
+ }
8308
+ liveInfo = {
8309
+ container: LIVE_CONTAINER,
8310
+ port: LIVE_PORT,
8311
+ endpoint: `http://localhost:${LIVE_PORT}`,
8312
+ applied: LIVE_TARGETS
8313
+ };
8314
+ process.stdout.write("behold demo carve --live \u2192 the tfstate is real now; the advisor reads what terraform wrote.\n");
8315
+ }
8316
+ if (existsSync14(join18(project, "package.json")) && !existsSync14(join18(project, "node_modules"))) {
7996
8317
  process.stdout.write(`behold demo carve \u2192 npm install in ${carve.project}/ (the chant this walkthrough shells)\u2026
7997
8318
  `);
7998
8319
  const code = await spawnStep("npm", ["install"], project);
@@ -8003,7 +8324,7 @@ async function serveCarveDemo(target, carve, port) {
8003
8324
  }
8004
8325
  }
8005
8326
  let degraded;
8006
- if (!existsSync13(join17(target, "node_modules", "@cdktf", "hcl2json"))) {
8327
+ if (!existsSync14(join18(target, "node_modules", "@cdktf", "hcl2json"))) {
8007
8328
  process.stdout.write("behold demo carve \u2192 npm install @cdktf/hcl2json (chant's HCL parser, ~2MB, once)\u2026\n");
8008
8329
  const code = await spawnStep("npm", ["install", "--no-save", "--no-package-lock", "@cdktf/hcl2json"], target);
8009
8330
  if (code !== 0) degraded = "couldn't install @cdktf/hcl2json (chant's HCL parser) \u2014 no network?";
@@ -8019,7 +8340,7 @@ async function serveCarveDemo(target, carve, port) {
8019
8340
  );
8020
8341
  if (code !== 0) degraded = `chant carve advise exited ${code}`;
8021
8342
  }
8022
- const serving = !degraded && existsSync13(report) ? report : committed;
8343
+ const serving = !degraded && existsSync14(report) ? report : committed;
8023
8344
  if (degraded) {
8024
8345
  process.stderr.write(
8025
8346
  `behold demo carve: ${degraded}
@@ -8027,7 +8348,7 @@ async function serveCarveDemo(target, carve, port) {
8027
8348
  `
8028
8349
  );
8029
8350
  }
8030
- if (!existsSync13(serving)) {
8351
+ if (!existsSync14(serving)) {
8031
8352
  process.stderr.write(`behold demo carve: no carve report at ${serving}
8032
8353
  `);
8033
8354
  process.exit(2);
@@ -8044,6 +8365,7 @@ async function serveCarveDemo(target, carve, port) {
8044
8365
  ...state ? { state } : {},
8045
8366
  project,
8046
8367
  out: at(carve.out),
8368
+ ...liveInfo ? { live: liveInfo } : {},
8047
8369
  ...degraded ? { degraded: `${degraded} \u2014 showing the committed report shipped with the demo.` } : {}
8048
8370
  },
8049
8371
  port
@@ -8072,7 +8394,7 @@ async function runPreview(rest) {
8072
8394
  process.exit(2);
8073
8395
  }
8074
8396
  const projectDir = resolve7(dirArg ?? process.cwd());
8075
- if (!existsSync13(projectDir)) {
8397
+ if (!existsSync14(projectDir)) {
8076
8398
  process.stderr.write(`behold preview: project not found at ${projectDir}
8077
8399
  `);
8078
8400
  process.exit(2);
@@ -8111,7 +8433,7 @@ async function runExportCmd(rest) {
8111
8433
  injectEmulatorEnv(env);
8112
8434
  env ??= "local";
8113
8435
  }
8114
- if (!existsSync13(projectDir)) {
8436
+ if (!existsSync14(projectDir)) {
8115
8437
  process.stderr.write(`behold export: project not found at ${projectDir}
8116
8438
  `);
8117
8439
  process.exit(2);