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