@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 +437 -115
- package/example-argo-estate/app-a/package.json +2 -2
- package/example-argo-estate/app-b/package.json +2 -2
- package/example-argo-estate/control-plane/package.json +2 -2
- package/example-argo-estate/package-lock.json +17 -17
- package/example-carve/README.md +15 -8
- package/example-flux-estate/app-a/package.json +2 -2
- package/example-flux-estate/app-b/package.json +2 -2
- package/example-flux-estate/control-plane/package.json +2 -2
- package/example-flux-estate/package-lock.json +17 -17
- package/package.json +2 -2
- package/web/app.js +26 -0
- package/web/carve-steps.js +53 -6
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
|
|
1042
|
-
import { realpathSync, existsSync as
|
|
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
|
|
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
|
|
1124
|
-
import { dirname as dirname4, join as
|
|
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 {
|
|
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
|
|
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
|
-
|
|
4231
|
-
|
|
4232
|
-
|
|
4233
|
-
|
|
4234
|
-
|
|
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
|
|
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
|
|
4410
|
-
import { isAbsolute, join as
|
|
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 (!
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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(
|
|
4912
|
+
boundary = JSON.parse(readFileSync9(reportFile, "utf8"));
|
|
4709
4913
|
} catch {
|
|
4710
4914
|
}
|
|
4711
|
-
const lintPath = relative(demo.project,
|
|
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
|
|
4775
|
-
import { join as
|
|
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 =
|
|
4798
|
-
if (!
|
|
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 =
|
|
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
|
|
5327
|
-
import { existsSync as
|
|
5328
|
-
import { join as
|
|
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 =
|
|
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
|
|
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 =
|
|
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 (!
|
|
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 (
|
|
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(
|
|
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
|
|
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(
|
|
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
|
|
5656
|
-
if (!prev) best.set(winner.dir, { score: winner.score, join:
|
|
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:
|
|
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
|
|
5721
|
-
import { join as
|
|
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 =
|
|
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 =
|
|
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
|
|
6018
|
-
import { join as
|
|
6019
|
-
import { spawn as
|
|
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(
|
|
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" &&
|
|
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 (!
|
|
6280
|
+
if (!existsSync12(target)) {
|
|
6071
6281
|
if (entry.source === "bundled") {
|
|
6072
|
-
const bundled =
|
|
6073
|
-
if (!
|
|
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 (
|
|
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) =>
|
|
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 =
|
|
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 =
|
|
6116
|
-
var webRoot =
|
|
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(
|
|
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) =>
|
|
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
|
-
|
|
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
|
|
6239
|
-
const
|
|
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,
|
|
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 =
|
|
6414
|
-
const up =
|
|
6415
|
-
if (!
|
|
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 || !
|
|
6494
|
-
if (!
|
|
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:
|
|
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 (!
|
|
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 && !
|
|
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:
|
|
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 (
|
|
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
|
|
7436
|
+
const capture2 = () => captureFrame(cfg.projectDir, cfg.env, frames, broadcaster);
|
|
7169
7437
|
const onEstateChange = () => {
|
|
7170
7438
|
broadcaster.emit("changed");
|
|
7171
|
-
void
|
|
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
|
|
7486
|
+
void capture2();
|
|
7219
7487
|
};
|
|
7220
|
-
if (!carve) void
|
|
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
|
|
7228
|
-
|
|
7229
|
-
|
|
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.
|
|
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
|
|
7279
|
-
import { join as
|
|
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
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
7360
|
-
const html =
|
|
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
|
-
|
|
7640
|
+
writeFileSync4(join17(outDir, "index.html"), html);
|
|
7366
7641
|
for (const f of readdirSync5(webDir())) {
|
|
7367
7642
|
if (f === "index.html") continue;
|
|
7368
|
-
copyFileSync(
|
|
7643
|
+
copyFileSync(join17(webDir(), f), join17(outDir, f));
|
|
7369
7644
|
}
|
|
7370
|
-
|
|
7645
|
+
writeFileSync4(join17(outDir, "README.md"), BUNDLE_README);
|
|
7371
7646
|
const name = workerName(cfg.projectDir, opts.name);
|
|
7372
|
-
|
|
7373
|
-
|
|
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.
|
|
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) =>
|
|
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) =>
|
|
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 (!
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
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 (!
|
|
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 &&
|
|
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 (!
|
|
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 (!
|
|
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 (!
|
|
8436
|
+
if (!existsSync14(projectDir)) {
|
|
8115
8437
|
process.stderr.write(`behold export: project not found at ${projectDir}
|
|
8116
8438
|
`);
|
|
8117
8439
|
process.exit(2);
|