@intentius/behold 0.8.0 → 0.9.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.
Files changed (56) hide show
  1. package/AGENTS.md +85 -0
  2. package/README.md +123 -2
  3. package/demos.json +17 -1
  4. package/dist/cli.js +2522 -294
  5. package/example-argo-estate/README.md +43 -18
  6. package/example-argo-estate/app-a/chant.config.ts +10 -2
  7. package/example-argo-estate/app-a/package.json +2 -2
  8. package/example-argo-estate/app-b/chant.config.ts +11 -2
  9. package/example-argo-estate/app-b/package.json +2 -2
  10. package/example-argo-estate/control-plane/chant.config.ts +20 -5
  11. package/example-argo-estate/control-plane/package.json +2 -2
  12. package/example-argo-estate/package-lock.json +17 -17
  13. package/example-carve/README.md +194 -0
  14. package/example-carve/app/chant.config.ts +6 -0
  15. package/example-carve/app/package-lock.json +1075 -0
  16. package/example-carve/app/package.json +13 -0
  17. package/example-carve/app/src/carved.ts +30 -0
  18. package/example-carve/app/tsconfig.json +1 -0
  19. package/example-carve/carve-report.json +872 -0
  20. package/example-carve/legacy-tf/cdn.tf +23 -0
  21. package/example-carve/legacy-tf/compute.tf +63 -0
  22. package/example-carve/legacy-tf/floci-override.tf.disabled +61 -0
  23. package/example-carve/legacy-tf/modules/cdn/main.tf +72 -0
  24. package/example-carve/legacy-tf/naming.tf +10 -0
  25. package/example-carve/legacy-tf/network.tf +119 -0
  26. package/example-carve/legacy-tf/observability.tf +18 -0
  27. package/example-carve/legacy-tf/outputs.tf +16 -0
  28. package/example-carve/legacy-tf/storage.tf +33 -0
  29. package/example-carve/legacy-tf/terraform.tfstate +602 -0
  30. package/example-carve/legacy-tf/versions.tf +40 -0
  31. package/example-flux-estate/README.md +9 -4
  32. package/example-flux-estate/app-a/package.json +2 -2
  33. package/example-flux-estate/app-a/src/app.ts +2 -1
  34. package/example-flux-estate/app-b/chant.config.ts +4 -3
  35. package/example-flux-estate/app-b/package.json +2 -2
  36. package/example-flux-estate/app-b/src/app.ts +5 -3
  37. package/example-flux-estate/control-plane/package.json +2 -2
  38. package/example-flux-estate/control-plane/src/flux.ts +4 -2
  39. package/example-flux-estate/package-lock.json +17 -17
  40. package/example-k8s/package-lock.json +18 -18
  41. package/example-k8s/package.json +3 -3
  42. package/example-writes/package-lock.json +14 -14
  43. package/example-writes/package.json +3 -3
  44. package/package.json +8 -6
  45. package/web/app.js +714 -57
  46. package/web/carve-steps.js +610 -0
  47. package/web/carve-steps.test.js +233 -0
  48. package/web/demos.js +71 -0
  49. package/web/demos.test.js +83 -0
  50. package/web/index.html +93 -1
  51. package/web/json-view.js +334 -0
  52. package/web/json-view.test.js +218 -0
  53. package/web/layout-store.js +164 -4
  54. package/web/layout-store.test.js +226 -1
  55. package/web/panel.js +28 -0
  56. package/web/theme.js +57 -1
package/dist/cli.js CHANGED
@@ -1,8 +1,1047 @@
1
+ var __defProp = Object.defineProperty;
2
+ var __getOwnPropNames = Object.getOwnPropertyNames;
3
+ var __esm = (fn, res, err) => function __init() {
4
+ if (err) throw err[0];
5
+ try {
6
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
7
+ } catch (e) {
8
+ throw err = [e], e;
9
+ }
10
+ };
11
+ var __export = (target, all) => {
12
+ for (var name in all)
13
+ __defProp(target, name, { get: all[name], enumerable: true });
14
+ };
15
+
16
+ // node_modules/@intentius/chant-k8s-client/src/errors.ts
17
+ function firstLine(text, max = 300) {
18
+ const line = text.split("\n").find((l) => l.trim().length > 0)?.trim();
19
+ if (!line) return void 0;
20
+ return line.length > max ? `${line.slice(0, max - 3)}...` : line;
21
+ }
22
+ var K8sApiError, K8sTransportError, K8sClientUnavailableError, ExecCredentialNotAllowedError, FieldManagerError, KubeConfigError, UnknownResourceError;
23
+ var init_errors = __esm({
24
+ "node_modules/@intentius/chant-k8s-client/src/errors.ts"() {
25
+ K8sApiError = class _K8sApiError extends Error {
26
+ constructor(statusCode, reason, apiMessage, target, status) {
27
+ super(
28
+ `${target ? `${target}: ` : ""}${apiMessage || "request failed"} (HTTP ${statusCode}${reason ? `, ${reason}` : ""})`
29
+ );
30
+ this.statusCode = statusCode;
31
+ this.reason = reason;
32
+ this.apiMessage = apiMessage;
33
+ this.target = target;
34
+ this.status = status;
35
+ this.name = "K8sApiError";
36
+ }
37
+ statusCode;
38
+ reason;
39
+ apiMessage;
40
+ target;
41
+ status;
42
+ /**
43
+ * Which cluster the failing read actually talked to, e.g.
44
+ * `context "k3d-fountain-local" (bound by k8s.profiles.local.context)`.
45
+ * Stamped by the client that issued the request (chant #1488) — a
46
+ * `read-failed` that does not name the cluster it read cost an afternoon on
47
+ * a laptop with two k3d clusters, so the failure carries it from birth.
48
+ */
49
+ contextNote;
50
+ /** The object is not there. The only failure that establishes absence. */
51
+ get notFound() {
52
+ return this.statusCode === 404 || this.reason === "NotFound";
53
+ }
54
+ /** RBAC denied the read. Proves nothing about whether the object exists. */
55
+ get forbidden() {
56
+ return this.statusCode === 403 || this.reason === "Forbidden";
57
+ }
58
+ /** No usable credentials for this cluster. */
59
+ get unauthorized() {
60
+ return this.statusCode === 401 || this.reason === "Unauthorized";
61
+ }
62
+ /**
63
+ * Server-side-apply field-ownership conflict. `./conflict.ts`'s
64
+ * {@link import("./conflict").FieldManagerConflictError} is the presented
65
+ * form (chant #1075); this predicate still answers for both.
66
+ */
67
+ get conflict() {
68
+ return this.statusCode === 409 || this.reason === "Conflict";
69
+ }
70
+ /**
71
+ * Build from a raw response body, which is a `Status` on every well-behaved
72
+ * Kubernetes error and occasionally plain text from a proxy in front of one.
73
+ */
74
+ static fromResponse(statusCode, body, target) {
75
+ let status;
76
+ try {
77
+ const parsed = JSON.parse(body);
78
+ if (parsed && typeof parsed === "object" && parsed.kind === "Status") {
79
+ status = parsed;
80
+ }
81
+ } catch {
82
+ }
83
+ const message = status?.message ?? firstLine(body) ?? "";
84
+ return new _K8sApiError(statusCode, status?.reason, message, target, status);
85
+ }
86
+ };
87
+ K8sTransportError = class extends Error {
88
+ constructor(message, target, options) {
89
+ super(target ? `${target}: ${message}` : message);
90
+ this.target = target;
91
+ this.name = "K8sTransportError";
92
+ if (options && "cause" in options) this.cause = options.cause;
93
+ }
94
+ target;
95
+ /** Which cluster context the failed request was aimed at — see {@link K8sApiError.contextNote}. */
96
+ contextNote;
97
+ };
98
+ K8sClientUnavailableError = class extends Error {
99
+ constructor(cause) {
100
+ super(
101
+ "the Kubernetes API client is unavailable \u2014 @kubernetes/client-node could not be loaded. Install it with `npm i @intentius/chant-k8s-client` (it is an optional dependency of @intentius/chant-lexicon-k8s, so `--omit=optional` installs skip it)."
102
+ );
103
+ this.name = "K8sClientUnavailableError";
104
+ if (cause !== void 0) this.cause = cause;
105
+ }
106
+ };
107
+ ExecCredentialNotAllowedError = class extends Error {
108
+ constructor(command, allowed) {
109
+ super(
110
+ `k8s: the kubeconfig for this context authenticates with the exec credential plugin "${command}", which is not on chant's allowlist (${allowed.join(", ")}). An exec plugin is an arbitrary binary named in a file chant did not write. If "${command}" is expected, add it to k8s.execCredentialPlugins in chant.config.ts.`
111
+ );
112
+ this.command = command;
113
+ this.allowed = allowed;
114
+ this.name = "ExecCredentialNotAllowedError";
115
+ }
116
+ command;
117
+ allowed;
118
+ };
119
+ FieldManagerError = class extends Error {
120
+ constructor(message) {
121
+ super(`k8s: ${message}`);
122
+ this.name = "FieldManagerError";
123
+ }
124
+ };
125
+ KubeConfigError = class extends Error {
126
+ constructor(message) {
127
+ super(`k8s: ${message}`);
128
+ this.name = "KubeConfigError";
129
+ }
130
+ };
131
+ UnknownResourceError = class extends Error {
132
+ constructor(selectorText2, message) {
133
+ super(message ?? `k8s: the cluster's API discovery reports no resource matching "${selectorText2}"`);
134
+ this.selectorText = selectorText2;
135
+ this.name = "UnknownResourceError";
136
+ }
137
+ selectorText;
138
+ };
139
+ }
140
+ });
141
+
142
+ // node_modules/@intentius/chant-k8s-client/src/credentials.ts
143
+ function execConfigOf(user) {
144
+ if (!user) return void 0;
145
+ if (user.exec) return user.exec;
146
+ const providerExec = user.authProvider?.config?.exec;
147
+ if (providerExec) return providerExec;
148
+ return void 0;
149
+ }
150
+ function execCommandName(command) {
151
+ const base = command.split(/[\\/]/).pop() ?? command;
152
+ return base.replace(/\.(exe|cmd|bat)$/i, "");
153
+ }
154
+ function assertExecCredentialAllowed(user, allowlist = DEFAULT_EXEC_ALLOWLIST) {
155
+ const exec = execConfigOf(user);
156
+ if (!exec?.command) return;
157
+ const name = execCommandName(exec.command);
158
+ if (allowlist.some((entry) => execCommandName(entry) === name)) return;
159
+ throw new ExecCredentialNotAllowedError(exec.command, allowlist);
160
+ }
161
+ function credentialPathOf(user) {
162
+ const exec = execConfigOf(user);
163
+ if (exec?.command) return { credential: "exec-plugin", execCommand: exec.command };
164
+ if (user?.authProvider?.name) return { credential: "auth-provider" };
165
+ if (user?.token) return { credential: "token" };
166
+ if (user?.certData || user?.certFile) return { credential: "client-certificate" };
167
+ if (user?.username) return { credential: "basic-auth" };
168
+ return { credential: "none" };
169
+ }
170
+ var DEFAULT_EXEC_ALLOWLIST;
171
+ var init_credentials = __esm({
172
+ "node_modules/@intentius/chant-k8s-client/src/credentials.ts"() {
173
+ init_errors();
174
+ DEFAULT_EXEC_ALLOWLIST = [
175
+ "aws",
176
+ // EKS — `aws eks get-token`
177
+ "aws-iam-authenticator",
178
+ // EKS, pre-`aws eks get-token`
179
+ "gke-gcloud-auth-plugin",
180
+ // GKE
181
+ "kubelogin",
182
+ // AKS
183
+ "kubectl"
184
+ // OIDC via `kubectl oidc-login`, and k3d/kind setups
185
+ ];
186
+ }
187
+ });
188
+
189
+ // node_modules/@intentius/chant-k8s-client/src/conflict.ts
190
+ function renderConflictReport(report) {
191
+ const { conflicts, fieldManager, target } = report;
192
+ const subject = target ? `${target}` : "this object";
193
+ if (conflicts.length === 0) {
194
+ return `k8s: server-side apply of ${subject} was refused with a field-ownership conflict, but the API server reported no field causes${report.apiMessage ? ` \u2014 it said: ${report.apiMessage}` : ""}. chant applied as field manager "${fieldManager}".`;
195
+ }
196
+ const grouped = /* @__PURE__ */ new Map();
197
+ for (const conflict of conflicts) {
198
+ const list3 = grouped.get(conflict.manager) ?? [];
199
+ if (!list3.includes(conflict.field)) list3.push(conflict.field);
200
+ grouped.set(conflict.manager, list3);
201
+ }
202
+ const count = new Set(conflicts.map((c) => c.field)).size;
203
+ const lines = [
204
+ `k8s: server-side apply of ${subject} was refused \u2014 ${count} ${count === 1 ? "field is" : "fields are"} owned by another field manager.`,
205
+ ""
206
+ ];
207
+ for (const [manager, fields] of [...grouped].sort(([a], [b]) => a.localeCompare(b))) {
208
+ lines.push(` "${manager}" owns:`);
209
+ for (const field of [...fields].sort()) lines.push(` ${field}`);
210
+ }
211
+ lines.push(
212
+ "",
213
+ `chant applied as field manager "${fieldManager}". Taking these fields means the managers above`,
214
+ `stop owning them, and will contest them again on their next apply.`,
215
+ "",
216
+ "chant does not force this for you. Either:",
217
+ " - remove the contested fields from your chant source, leaving them to their current owner; or",
218
+ " - re-run this apply with force-conflicts on, deliberately (the `force: true` activity argument,",
219
+ " or `forceConflicts: true` on ApplyOp), which transfers ownership to chant."
220
+ );
221
+ return lines.join("\n");
222
+ }
223
+ function parseFieldConflicts(status, message) {
224
+ const fromCauses = causesOf(status);
225
+ if (fromCauses.length > 0) return fromCauses;
226
+ return parseConflictMessage(message ?? status?.message ?? "");
227
+ }
228
+ function causesOf(status) {
229
+ const details = status?.details;
230
+ if (!details || typeof details !== "object") return [];
231
+ const causes = details.causes;
232
+ if (!Array.isArray(causes)) return [];
233
+ const out = [];
234
+ for (const raw of causes) {
235
+ if (!raw || typeof raw !== "object") continue;
236
+ if (raw.type !== void 0 && raw.type !== "FieldManagerConflict") continue;
237
+ const field = typeof raw.field === "string" ? raw.field : void 0;
238
+ if (!field) continue;
239
+ const { manager, apiVersion } = parseCauseMessage(raw.message ?? "");
240
+ out.push({
241
+ manager: manager ?? "an unnamed manager",
242
+ field,
243
+ ...apiVersion ? { apiVersion } : {}
244
+ });
245
+ }
246
+ return out;
247
+ }
248
+ function parseCauseMessage(message) {
249
+ const manager = /conflicts? with "([^"]+)"/.exec(message)?.[1];
250
+ const apiVersion = /\busing ([^\s:]+)\b/.exec(message)?.[1];
251
+ return { ...manager ? { manager } : {}, ...apiVersion ? { apiVersion } : {} };
252
+ }
253
+ function parseConflictMessage(message) {
254
+ if (!message) return [];
255
+ const out = [];
256
+ let manager;
257
+ let apiVersion;
258
+ for (const rawLine of message.split("\n")) {
259
+ const line = rawLine.trim();
260
+ if (line.length === 0) continue;
261
+ const header = /conflicts? with "([^"]+)"(?: using ([^\s:]+))?/.exec(line);
262
+ if (header) {
263
+ manager = header[1];
264
+ apiVersion = header[2];
265
+ const inline = /:\s*(\.[^\s]+)$/.exec(line);
266
+ if (inline) out.push({ manager, field: inline[1], ...apiVersion ? { apiVersion } : {} });
267
+ continue;
268
+ }
269
+ const bullet = /^-\s*(\S.*)$/.exec(line);
270
+ if (bullet && manager) {
271
+ out.push({ manager, field: bullet[1].trim(), ...apiVersion ? { apiVersion } : {} });
272
+ }
273
+ }
274
+ return out;
275
+ }
276
+ function asFieldManagerConflict(error, fieldManager) {
277
+ if (!(error instanceof K8sApiError) || !error.conflict) return error;
278
+ if (error instanceof FieldManagerConflictError) return error;
279
+ return new FieldManagerConflictError(
280
+ error.statusCode,
281
+ error.apiMessage,
282
+ parseFieldConflicts(error.status, error.apiMessage),
283
+ fieldManager,
284
+ error.target,
285
+ error.status
286
+ );
287
+ }
288
+ var FieldManagerConflictError;
289
+ var init_conflict = __esm({
290
+ "node_modules/@intentius/chant-k8s-client/src/conflict.ts"() {
291
+ init_errors();
292
+ FieldManagerConflictError = class extends K8sApiError {
293
+ /** Every contested field, in the order the server reported them. */
294
+ conflicts;
295
+ /** The field manager chant applied as, and which was refused. */
296
+ fieldManager;
297
+ constructor(statusCode, apiMessage, conflicts, fieldManager, target, status) {
298
+ super(statusCode, status?.reason ?? "Conflict", apiMessage, target, status);
299
+ this.name = "FieldManagerConflictError";
300
+ this.conflicts = conflicts;
301
+ this.fieldManager = fieldManager;
302
+ this.message = renderConflictReport({ conflicts, fieldManager, target, apiMessage });
303
+ }
304
+ /** Contested paths grouped by the manager that owns them, managers sorted. */
305
+ get byManager() {
306
+ const grouped = {};
307
+ for (const conflict of this.conflicts) {
308
+ (grouped[conflict.manager] ??= []).push(conflict.field);
309
+ }
310
+ return Object.fromEntries(
311
+ Object.entries(grouped).sort(([a], [b]) => a.localeCompare(b)).map(([manager, fields]) => [manager, [...fields].sort()])
312
+ );
313
+ }
314
+ /** The competing managers, sorted. */
315
+ get managers() {
316
+ return Object.keys(this.byManager);
317
+ }
318
+ /** The contested paths, sorted and deduplicated. */
319
+ get fields() {
320
+ return [...new Set(this.conflicts.map((c) => c.field))].sort();
321
+ }
322
+ };
323
+ }
324
+ });
325
+
326
+ // node_modules/@intentius/chant-k8s-client/src/field-manager.ts
327
+ function fieldManagerFor(identity) {
328
+ const stack = identity?.stack?.trim();
329
+ if (!stack) return CHANT_FIELD_MANAGER;
330
+ const manager = `${CHANT_FIELD_MANAGER}${FIELD_MANAGER_SEPARATOR}${stack}`;
331
+ assertValidFieldManager(manager, stack);
332
+ return manager;
333
+ }
334
+ function assertValidFieldManager(manager, stack) {
335
+ const source = stack === void 0 ? `field manager "${manager}"` : `ownership.stack "${stack}"`;
336
+ if (manager.length === 0) {
337
+ throw new FieldManagerError(`${source} produces an empty field manager, which the API server rejects`);
338
+ }
339
+ if (manager.length > FIELD_MANAGER_MAX_LENGTH) {
340
+ throw new FieldManagerError(
341
+ `${source} produces the field manager "${manager}" (${manager.length} characters), over the API server's ${FIELD_MANAGER_MAX_LENGTH}-character limit`
342
+ );
343
+ }
344
+ const badIndex = [...manager].findIndex((ch) => {
345
+ const code = ch.codePointAt(0) ?? 0;
346
+ return code <= 32 || code === 127;
347
+ });
348
+ if (badIndex !== -1) {
349
+ throw new FieldManagerError(
350
+ `${source} produces the field manager "${manager}", which contains whitespace or a control character at position ${badIndex}. A field manager is an identity recorded on every object chant applies; keep it to printable, space-free text.`
351
+ );
352
+ }
353
+ }
354
+ function isChantFieldManager(manager) {
355
+ if (!manager) return false;
356
+ return manager === CHANT_FIELD_MANAGER || manager.startsWith(`${CHANT_FIELD_MANAGER}${FIELD_MANAGER_SEPARATOR}`);
357
+ }
358
+ function chantStackOf(manager) {
359
+ if (!manager || !isChantFieldManager(manager)) return void 0;
360
+ const stack = manager.slice(CHANT_FIELD_MANAGER.length + FIELD_MANAGER_SEPARATOR.length);
361
+ return stack.length > 0 ? stack : void 0;
362
+ }
363
+ var CHANT_FIELD_MANAGER, FIELD_MANAGER_SEPARATOR, FIELD_MANAGER_MAX_LENGTH;
364
+ var init_field_manager = __esm({
365
+ "node_modules/@intentius/chant-k8s-client/src/field-manager.ts"() {
366
+ init_errors();
367
+ CHANT_FIELD_MANAGER = "chant";
368
+ FIELD_MANAGER_SEPARATOR = ":";
369
+ FIELD_MANAGER_MAX_LENGTH = 128;
370
+ }
371
+ });
372
+
373
+ // node_modules/@intentius/chant-k8s-client/src/concurrency.ts
374
+ async function mapConcurrent(items, fn, limit = DEFAULT_CONCURRENCY) {
375
+ const width = Math.max(1, Math.min(limit, items.length));
376
+ const results = new Array(items.length);
377
+ let next = 0;
378
+ async function worker() {
379
+ while (true) {
380
+ const index = next++;
381
+ if (index >= items.length) return;
382
+ results[index] = await fn(items[index], index);
383
+ }
384
+ }
385
+ await Promise.all(Array.from({ length: width }, () => worker()));
386
+ return results;
387
+ }
388
+ var DEFAULT_CONCURRENCY;
389
+ var init_concurrency = __esm({
390
+ "node_modules/@intentius/chant-k8s-client/src/concurrency.ts"() {
391
+ DEFAULT_CONCURRENCY = 8;
392
+ }
393
+ });
394
+
395
+ // node_modules/@intentius/chant-k8s-client/src/kubeconfig.ts
396
+ import { delimiter } from "node:path";
397
+ function kubeconfigPathList() {
398
+ const raw = process.env.KUBECONFIG;
399
+ if (!raw) return [];
400
+ return raw.split(delimiter).filter((f) => f.length > 0);
401
+ }
402
+ function mergeKubeconfigFiles(mod, files) {
403
+ const clusters = [];
404
+ const users = [];
405
+ const contexts = [];
406
+ const seenClusters = /* @__PURE__ */ new Set();
407
+ const seenUsers = /* @__PURE__ */ new Set();
408
+ const seenContexts = /* @__PURE__ */ new Set();
409
+ let currentContext;
410
+ for (const file of files) {
411
+ const part = new mod.KubeConfig();
412
+ try {
413
+ part.loadFromFile(file);
414
+ } catch {
415
+ continue;
416
+ }
417
+ for (const cluster of part.getClusters()) {
418
+ if (seenClusters.has(cluster.name)) continue;
419
+ seenClusters.add(cluster.name);
420
+ clusters.push(cluster);
421
+ }
422
+ for (const user of part.getUsers()) {
423
+ if (seenUsers.has(user.name)) continue;
424
+ seenUsers.add(user.name);
425
+ users.push(user);
426
+ }
427
+ for (const context of part.getContexts()) {
428
+ if (seenContexts.has(context.name)) continue;
429
+ seenContexts.add(context.name);
430
+ contexts.push(context);
431
+ }
432
+ if (currentContext === void 0) {
433
+ const current = part.getCurrentContext();
434
+ if (current) currentContext = current;
435
+ }
436
+ }
437
+ const kc = new mod.KubeConfig();
438
+ kc.loadFromOptions({ clusters, users, contexts, currentContext });
439
+ return kc;
440
+ }
441
+ function loadKubeConfig(mod, options = {}) {
442
+ if (options.kubeconfig !== void 0) {
443
+ const kc2 = new mod.KubeConfig();
444
+ kc2.loadFromString(options.kubeconfig);
445
+ return { kc: kc2, source: "explicit-string" };
446
+ }
447
+ if (options.kubeconfigPath !== void 0) {
448
+ const kc2 = new mod.KubeConfig();
449
+ kc2.loadFromFile(options.kubeconfigPath);
450
+ return { kc: kc2, source: "explicit-path" };
451
+ }
452
+ const files = kubeconfigPathList();
453
+ if (files.length > 1) return { kc: mergeKubeconfigFiles(mod, files), source: "default" };
454
+ const kc = new mod.KubeConfig();
455
+ kc.loadFromDefault(void 0, true);
456
+ return { kc, source: kc.getCurrentContext() === "inCluster" ? "in-cluster" : "default" };
457
+ }
458
+ var init_kubeconfig = __esm({
459
+ "node_modules/@intentius/chant-k8s-client/src/kubeconfig.ts"() {
460
+ }
461
+ });
462
+
463
+ // node_modules/@intentius/chant-k8s-client/src/client.ts
464
+ async function loadClientNode() {
465
+ if (!clientNodeModule) {
466
+ clientNodeModule = import("@kubernetes/client-node").catch((err) => {
467
+ clientNodeModule = void 0;
468
+ throw new K8sClientUnavailableError(err);
469
+ });
470
+ }
471
+ return clientNodeModule;
472
+ }
473
+ async function isK8sClientAvailable() {
474
+ try {
475
+ await loadClientNode();
476
+ return true;
477
+ } catch {
478
+ return false;
479
+ }
480
+ }
481
+ async function readAmbientContext(options = {}) {
482
+ try {
483
+ const { kc } = loadKubeConfig(await loadClientNode(), options);
484
+ return kc.getCurrentContext() || void 0;
485
+ } catch {
486
+ return void 0;
487
+ }
488
+ }
489
+ async function readKubeconfigView(options = {}) {
490
+ let kc;
491
+ try {
492
+ ({ kc } = loadKubeConfig(await loadClientNode(), options));
493
+ } catch {
494
+ return { contexts: [] };
495
+ }
496
+ try {
497
+ const servers = /* @__PURE__ */ new Map();
498
+ for (const cluster of kc.getClusters()) servers.set(cluster.name, cluster.server || void 0);
499
+ const contexts = kc.getContexts().map((context) => {
500
+ const server = servers.get(context.cluster);
501
+ return {
502
+ name: context.name,
503
+ cluster: context.cluster ?? "",
504
+ ...context.user ? { user: context.user } : {},
505
+ ...context.namespace ? { namespace: context.namespace } : {},
506
+ ...server ? { server } : {}
507
+ };
508
+ });
509
+ return { contexts, ...kc.getCurrentContext() ? { currentContext: kc.getCurrentContext() } : {} };
510
+ } catch {
511
+ return { contexts: [] };
512
+ }
513
+ }
514
+ async function createK8sClient(options = {}) {
515
+ const mod = await loadClientNode();
516
+ const { kc, source: kubeconfigSource } = loadKubeConfig(mod, options);
517
+ if (options.context !== void 0) {
518
+ if (!kc.getContextObject(options.context)) {
519
+ const known = kc.getContexts().map((c) => c.name);
520
+ throw new KubeConfigError(
521
+ `the kubeconfig has no context named "${options.context}" (it has ${known.length > 0 ? known.map((n) => `"${n}"`).join(", ") : "no contexts at all"}). This is the context the environment is bound to via k8s.profiles.<env>.context.`
522
+ );
523
+ }
524
+ kc.setCurrentContext(options.context);
525
+ }
526
+ const cluster = kc.getCurrentCluster();
527
+ if (!cluster) {
528
+ throw new KubeConfigError(
529
+ `the kubeconfig resolves to no cluster for context "${kc.getCurrentContext() || "(unset)"}"`
530
+ );
531
+ }
532
+ const user = kc.getCurrentUser();
533
+ assertExecCredentialAllowed(user, options.execAllowlist ?? DEFAULT_EXEC_ALLOWLIST);
534
+ const provenance = {
535
+ server: cluster.server,
536
+ context: kc.getCurrentContext() || void 0,
537
+ contextSource: options.contextSource ?? "ambient",
538
+ kubeconfigSource,
539
+ ...credentialPathOf(user)
540
+ };
541
+ const configuration = mod.createConfiguration({
542
+ baseServer: new mod.ServerConfiguration(cluster.server, {}),
543
+ authMethods: { default: kc },
544
+ ...options.requestLayer ? {
545
+ httpApi: mod.wrapHttpLibrary({
546
+ send: (request) => Promise.resolve(
547
+ options.requestLayer.send(request)
548
+ )
549
+ })
550
+ } : {}
551
+ });
552
+ const defaultNamespace = kc.getContextObject(kc.getCurrentContext())?.namespace || "default";
553
+ const concurrency = options.concurrency ?? DEFAULT_CONCURRENCY;
554
+ const contextNote = `context "${provenance.context ?? "(unset)"}" (${options.contextLabel ?? provenance.contextSource})`;
555
+ function noted(err) {
556
+ err.contextNote = contextNote;
557
+ return err;
558
+ }
559
+ const discoveryCache = /* @__PURE__ */ new Map();
560
+ let rootDiscoveryCache;
561
+ async function send(path, method, opts = {}) {
562
+ const ctx = configuration.baseServer.makeRequestContext(path, method);
563
+ ctx.setHeaderParam("Accept", "application/json");
564
+ for (const [key, value] of Object.entries(opts.query ?? {})) ctx.setQueryParam(key, value);
565
+ if (opts.body !== void 0) {
566
+ ctx.setHeaderParam("Content-Type", opts.contentType ?? "application/json");
567
+ ctx.setBody(opts.body);
568
+ }
569
+ if (opts.signal) ctx.setSignal(opts.signal);
570
+ await kc.applySecurityAuthentication(ctx);
571
+ let response;
572
+ try {
573
+ response = await configuration.httpApi.send(ctx).toPromise();
574
+ } catch (err) {
575
+ throw noted(
576
+ new K8sTransportError(err instanceof Error ? err.message : String(err), opts.target ?? `${method} ${path}`, {
577
+ cause: err
578
+ })
579
+ );
580
+ }
581
+ let text;
582
+ try {
583
+ text = await response.body.text();
584
+ } catch (err) {
585
+ throw noted(
586
+ new K8sTransportError(
587
+ `response body could not be read: ${err instanceof Error ? err.message : String(err)}`,
588
+ opts.target ?? `${method} ${path}`,
589
+ { cause: err }
590
+ )
591
+ );
592
+ }
593
+ return { status: response.httpStatusCode, body: text };
594
+ }
595
+ async function sendJson(path, method, opts = {}) {
596
+ const { status, body } = await send(path, method, opts);
597
+ if (status < 200 || status > 299) {
598
+ throw noted(K8sApiError.fromResponse(status, body, opts.target));
599
+ }
600
+ try {
601
+ return JSON.parse(body);
602
+ } catch (err) {
603
+ throw noted(
604
+ new K8sTransportError(
605
+ `the API server returned HTTP ${status} with a body that is not JSON`,
606
+ opts.target ?? `${method} ${path}`,
607
+ { cause: err }
608
+ )
609
+ );
610
+ }
611
+ }
612
+ async function apiResourceList(apiVersion, signal) {
613
+ const cached = discoveryCache.get(apiVersion);
614
+ if (cached) return cached;
615
+ const pending = (async () => {
616
+ try {
617
+ return await sendJson(apiVersionPath(apiVersion), "GET", {
618
+ signal,
619
+ target: `discovery ${apiVersion}`
620
+ });
621
+ } catch (err) {
622
+ if (err instanceof K8sApiError && err.notFound) return null;
623
+ discoveryCache.delete(apiVersion);
624
+ throw err;
625
+ }
626
+ })();
627
+ discoveryCache.set(apiVersion, pending);
628
+ return pending;
629
+ }
630
+ async function rootDiscovery(signal) {
631
+ if (rootDiscoveryCache) return rootDiscoveryCache;
632
+ rootDiscoveryCache = (async () => {
633
+ const core = await sendJson("/api", "GET", {
634
+ signal,
635
+ target: "discovery /api"
636
+ });
637
+ const groups = await sendJson("/apis", "GET", { signal, target: "discovery /apis" });
638
+ return {
639
+ coreVersions: core.versions ?? ["v1"],
640
+ groups: (groups.groups ?? []).map((group) => ({
641
+ preferred: group.preferredVersion?.groupVersion,
642
+ versions: (group.versions ?? []).map((v) => v.groupVersion).filter((gv) => typeof gv === "string" && gv.length > 0)
643
+ }))
644
+ };
645
+ })().catch((err) => {
646
+ rootDiscoveryCache = void 0;
647
+ throw err;
648
+ });
649
+ return rootDiscoveryCache;
650
+ }
651
+ async function servedGroupVersions(signal) {
652
+ const root = await rootDiscovery(signal);
653
+ const out = [...root.coreVersions];
654
+ for (const group of root.groups) {
655
+ if (group.preferred) out.push(group.preferred);
656
+ for (const gv of group.versions) {
657
+ if (gv !== group.preferred) out.push(gv);
658
+ }
659
+ }
660
+ return [...new Set(out)];
661
+ }
662
+ async function preferredGroupVersions(signal) {
663
+ const root = await rootDiscovery(signal);
664
+ const core = root.coreVersions.includes("v1") ? "v1" : root.coreVersions[0];
665
+ const out = core ? [core] : [];
666
+ for (const group of root.groups) {
667
+ const gv = group.preferred ?? group.versions[0];
668
+ if (gv) out.push(gv);
669
+ }
670
+ return [...new Set(out)];
671
+ }
672
+ function toInfo(apiVersion, entry) {
673
+ const [group, version] = splitApiVersion(apiVersion);
674
+ return {
675
+ name: entry.name ?? "",
676
+ singularName: entry.singularName || void 0,
677
+ kind: entry.kind ?? "",
678
+ namespaced: entry.namespaced === true,
679
+ verbs: entry.verbs ?? [],
680
+ shortNames: entry.shortNames,
681
+ group,
682
+ version,
683
+ apiVersion
684
+ };
685
+ }
686
+ async function resolveByGvk(apiVersion, kind, signal) {
687
+ const list4 = await apiResourceList(apiVersion, signal);
688
+ if (!list4) return void 0;
689
+ const entry = (list4.resources ?? []).find((r) => r.kind === kind && !(r.name ?? "").includes("/"));
690
+ return entry ? toInfo(apiVersion, entry) : void 0;
691
+ }
692
+ async function resolveByResourceString(resource, group, signal) {
693
+ const dot = resource.indexOf(".");
694
+ const bare = dot === -1 ? resource : resource.slice(0, dot);
695
+ const fromString = dot === -1 ? void 0 : resource.slice(dot + 1);
696
+ const wantedGroup = fromString ?? group;
697
+ const needle = bare.toLowerCase();
698
+ const all = await servedGroupVersions(signal);
699
+ const candidates = wantedGroup === void 0 ? all : all.filter((gv) => splitApiVersion(gv)[0] === (wantedGroup === "" ? "" : wantedGroup));
700
+ const lists = await mapConcurrent(
701
+ candidates,
702
+ async (gv) => ({ gv, list: await apiResourceList(gv, signal).catch(() => null) }),
703
+ concurrency
704
+ );
705
+ const matchers = [
706
+ (r) => (r.name ?? "").toLowerCase() === needle,
707
+ (r) => (r.singularName ?? "").toLowerCase() === needle,
708
+ (r) => (r.kind ?? "").toLowerCase() === needle,
709
+ (r) => (r.shortNames ?? []).some((s) => s.toLowerCase() === needle)
710
+ ];
711
+ for (const matches of matchers) {
712
+ for (const { gv, list: list4 } of lists) {
713
+ const entry = (list4?.resources ?? []).find((r) => !(r.name ?? "").includes("/") && matches(r));
714
+ if (entry) return toInfo(gv, entry);
715
+ }
716
+ }
717
+ return void 0;
718
+ }
719
+ async function resolve8(selector, signal) {
720
+ return "apiVersion" in selector ? resolveByGvk(selector.apiVersion, selector.kind, signal) : resolveByResourceString(selector.resource, selector.group, signal);
721
+ }
722
+ async function resolveOrThrow(selector, signal) {
723
+ const info = await resolve8(selector, signal);
724
+ if (!info) throw new UnknownResourceError(selectorText(selector));
725
+ return info;
726
+ }
727
+ function objectPath(info, name, namespace) {
728
+ const parts = [apiVersionPath(info.apiVersion)];
729
+ if (info.namespaced) parts.push("namespaces", encodeURIComponent(namespace || defaultNamespace));
730
+ parts.push(info.name);
731
+ if (name) parts.push(encodeURIComponent(name));
732
+ return parts.join("/");
733
+ }
734
+ async function pathFor(ref, signal) {
735
+ const info = await resolve8({ apiVersion: ref.apiVersion, kind: ref.kind }, signal);
736
+ if (!info) return void 0;
737
+ return objectPath(info, ref.name, ref.namespace);
738
+ }
739
+ async function read(ref, opts = {}) {
740
+ const info = await resolveOrThrow({ apiVersion: ref.apiVersion, kind: ref.kind }, opts.signal);
741
+ return sendJson(objectPath(info, ref.name, ref.namespace), "GET", {
742
+ signal: opts.signal,
743
+ target: refText(ref)
744
+ });
745
+ }
746
+ async function readIfPresent(ref, opts = {}) {
747
+ try {
748
+ return await read(ref, opts);
749
+ } catch (err) {
750
+ if (err instanceof K8sApiError && err.notFound) return void 0;
751
+ throw err;
752
+ }
753
+ }
754
+ async function list3(selector, opts = {}) {
755
+ const info = await resolveOrThrow(selector, opts.signal);
756
+ const items = [];
757
+ let cont;
758
+ do {
759
+ const query = {};
760
+ if (cont) query.continue = cont;
761
+ if (opts.labelSelector) query.labelSelector = opts.labelSelector;
762
+ const page = await sendJson(
763
+ // Omitting the namespace segment lists across all namespaces, which is
764
+ // what `kubectl get <kind> -A` does and what the import path wants.
765
+ opts.namespace ? objectPath(info, void 0, opts.namespace) : `${apiVersionPath(info.apiVersion)}/${info.name}`,
766
+ "GET",
767
+ {
768
+ signal: opts.signal,
769
+ query: Object.keys(query).length > 0 ? query : void 0,
770
+ target: `list ${selectorText(selector)}`
771
+ }
772
+ );
773
+ items.push(...page.items ?? []);
774
+ cont = page.metadata?.continue || void 0;
775
+ } while (cont);
776
+ return items;
777
+ }
778
+ async function apply(object, opts = {}) {
779
+ const apiVersion = object.apiVersion;
780
+ const kind = object.kind;
781
+ if (!apiVersion || !kind) {
782
+ throw new KubeConfigError(
783
+ `cannot apply an object without both apiVersion and kind (got apiVersion=${String(apiVersion)}, kind=${String(kind)})`
784
+ );
785
+ }
786
+ const name = object.metadata?.name;
787
+ if (!name) {
788
+ throw new KubeConfigError(`cannot apply a ${apiVersion} ${kind} without metadata.name`);
789
+ }
790
+ const fieldManager = opts.fieldManager ?? CHANT_FIELD_MANAGER;
791
+ assertValidFieldManager(fieldManager);
792
+ const info = await resolveOrThrow({ apiVersion, kind }, opts.signal);
793
+ const query = {
794
+ fieldManager,
795
+ force: String(opts.force ?? false)
796
+ };
797
+ if (opts.dryRun) query.dryRun = "All";
798
+ try {
799
+ return await sendJson(objectPath(info, name, object.metadata?.namespace), "PATCH", {
800
+ // Server-side apply. JSON is valid YAML, so the JSON body is accepted
801
+ // under the apply-patch content type without a YAML round trip.
802
+ contentType: "application/apply-patch+yaml",
803
+ body: JSON.stringify(object),
804
+ query,
805
+ signal: opts.signal,
806
+ target: refText({ apiVersion, kind, name, namespace: object.metadata?.namespace })
807
+ });
808
+ } catch (err) {
809
+ throw asFieldManagerConflict(err, fieldManager);
810
+ }
811
+ }
812
+ async function readLog(ref, opts = {}) {
813
+ const info = await resolveOrThrow({ apiVersion: ref.apiVersion, kind: ref.kind }, opts.signal);
814
+ const query = {};
815
+ if (opts.container) query.container = opts.container;
816
+ if (opts.previous) query.previous = "true";
817
+ if (opts.tailLines !== void 0) query.tailLines = String(opts.tailLines);
818
+ if (opts.sinceSeconds !== void 0) query.sinceSeconds = String(opts.sinceSeconds);
819
+ if (opts.timestamps) query.timestamps = "true";
820
+ const target = `${refText(ref)} logs`;
821
+ const { status, body } = await send(`${objectPath(info, ref.name, ref.namespace)}/log`, "GET", {
822
+ query: Object.keys(query).length > 0 ? query : void 0,
823
+ signal: opts.signal,
824
+ target
825
+ });
826
+ if (status < 200 || status > 299) {
827
+ throw noted(K8sApiError.fromResponse(status, body, target));
828
+ }
829
+ return body;
830
+ }
831
+ async function remove(ref, opts = {}) {
832
+ const info = await resolveOrThrow({ apiVersion: ref.apiVersion, kind: ref.kind }, opts.signal);
833
+ const query = {};
834
+ if (opts.propagationPolicy) query.propagationPolicy = opts.propagationPolicy;
835
+ if (opts.dryRun) query.dryRun = "All";
836
+ await sendJson(objectPath(info, ref.name, ref.namespace), "DELETE", {
837
+ signal: opts.signal,
838
+ query: Object.keys(query).length > 0 ? query : void 0,
839
+ target: refText(ref)
840
+ });
841
+ }
842
+ async function resourcesOf(apiVersion, signal) {
843
+ const list4 = await apiResourceList(apiVersion, signal);
844
+ if (!list4) return [];
845
+ return (list4.resources ?? []).filter((r) => !(r.name ?? "").includes("/")).map((entry) => toInfo(apiVersion, entry));
846
+ }
847
+ return {
848
+ provenance,
849
+ defaultNamespace,
850
+ resolve: resolve8,
851
+ read,
852
+ pathFor,
853
+ readIfPresent,
854
+ list: list3,
855
+ readLog,
856
+ apply,
857
+ delete: remove,
858
+ concurrently: (items, fn) => mapConcurrent(items, fn, concurrency),
859
+ resources: resourcesOf,
860
+ preferredGroupVersions,
861
+ discoveryCacheKeys: () => [...discoveryCache.keys()].sort()
862
+ };
863
+ }
864
+ function apiVersionPath(apiVersion) {
865
+ return apiVersion.includes("/") ? `/apis/${apiVersion}` : `/api/${apiVersion}`;
866
+ }
867
+ function splitApiVersion(apiVersion) {
868
+ const slash = apiVersion.indexOf("/");
869
+ return slash === -1 ? ["", apiVersion] : [apiVersion.slice(0, slash), apiVersion.slice(slash + 1)];
870
+ }
871
+ function selectorText(selector) {
872
+ return "apiVersion" in selector ? `${selector.apiVersion} ${selector.kind}` : selector.group ? `${selector.resource}.${selector.group}` : selector.resource;
873
+ }
874
+ function refText(ref) {
875
+ return `${ref.apiVersion} ${ref.kind} ${ref.namespace ? `${ref.namespace}/` : ""}${ref.name}`;
876
+ }
877
+ var clientNodeModule;
878
+ var init_client = __esm({
879
+ "node_modules/@intentius/chant-k8s-client/src/client.ts"() {
880
+ init_errors();
881
+ init_credentials();
882
+ init_conflict();
883
+ init_field_manager();
884
+ init_concurrency();
885
+ init_kubeconfig();
886
+ }
887
+ });
888
+
889
+ // node_modules/@intentius/chant-k8s-client/src/managed-fields.ts
890
+ function managedFieldsOf(object) {
891
+ const entries = object?.metadata?.managedFields;
892
+ if (!Array.isArray(entries)) return [];
893
+ return entries.filter((e) => e !== null && typeof e === "object").map((e) => e);
894
+ }
895
+ function fieldSetsOf(object) {
896
+ return managedFieldsOf(object).filter((entry) => typeof entry.manager === "string" && entry.manager.length > 0).map((entry) => ({
897
+ manager: entry.manager,
898
+ operation: entry.operation ?? "Update",
899
+ ...entry.apiVersion !== void 0 ? { apiVersion: entry.apiVersion } : {},
900
+ ...entry.subresource !== void 0 ? { subresource: entry.subresource } : {},
901
+ ...entry.time !== void 0 ? { time: entry.time } : {},
902
+ fields: fieldPathsOf(entry.fieldsV1)
903
+ }));
904
+ }
905
+ function managersOf(object) {
906
+ const seen = /* @__PURE__ */ new Set();
907
+ for (const entry of managedFieldsOf(object)) {
908
+ if (typeof entry.manager === "string" && entry.manager.length > 0) seen.add(entry.manager);
909
+ }
910
+ return [...seen];
911
+ }
912
+ function fieldsOwnedBy(object, manager, options = {}) {
913
+ const matches = typeof manager === "function" ? manager : (m) => m === manager;
914
+ const paths = /* @__PURE__ */ new Set();
915
+ for (const set of fieldSetsOf(object)) {
916
+ if (!matches(set.manager)) continue;
917
+ if (set.subresource !== void 0 && options.includeSubresources !== true) continue;
918
+ for (const path of set.fields) paths.add(path);
919
+ }
920
+ return [...paths].sort();
921
+ }
922
+ function chantOwnedFields(object, options = {}) {
923
+ return fieldsOwnedBy(object, isChantFieldManager, options);
924
+ }
925
+ function fieldOwners(object, options = {}) {
926
+ const owners = /* @__PURE__ */ new Map();
927
+ for (const set of fieldSetsOf(object)) {
928
+ if (set.subresource !== void 0 && options.includeSubresources !== true) continue;
929
+ for (const path of set.fields) {
930
+ const list3 = owners.get(path) ?? [];
931
+ if (!list3.includes(set.manager)) list3.push(set.manager);
932
+ owners.set(path, list3);
933
+ }
934
+ }
935
+ return owners;
936
+ }
937
+ function fieldPathsOf(fieldsV1, prefix = "") {
938
+ return [...collect(fieldsV1, prefix, /* @__PURE__ */ new Set())].sort();
939
+ }
940
+ function collect(node, prefix, into) {
941
+ if (node === null || typeof node !== "object" || Array.isArray(node)) return into;
942
+ for (const [key, child] of Object.entries(node)) {
943
+ if (key === ".") {
944
+ if (prefix !== "") into.add(prefix);
945
+ continue;
946
+ }
947
+ const segment = renderSegment(key);
948
+ if (segment === void 0) continue;
949
+ const path = `${prefix}${segment}`;
950
+ into.add(path);
951
+ collect(child, path, into);
952
+ }
953
+ return into;
954
+ }
955
+ function renderSegment(key) {
956
+ if (key.startsWith("f:")) return `.${key.slice(2)}`;
957
+ if (key.startsWith("i:")) return `[${key.slice(2)}]`;
958
+ if (key.startsWith("v:")) return `[=${key.slice(2)}]`;
959
+ if (key.startsWith("k:")) return renderKeySegment(key.slice(2));
960
+ return void 0;
961
+ }
962
+ function renderKeySegment(json) {
963
+ let parsed;
964
+ try {
965
+ parsed = JSON.parse(json);
966
+ } catch {
967
+ return `[${json}]`;
968
+ }
969
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) return `[${json}]`;
970
+ const parts = Object.entries(parsed).map(
971
+ ([name, value]) => `${name}=${JSON.stringify(value)}`
972
+ );
973
+ return `[${parts.join(",")}]`;
974
+ }
975
+ var init_managed_fields = __esm({
976
+ "node_modules/@intentius/chant-k8s-client/src/managed-fields.ts"() {
977
+ init_field_manager();
978
+ }
979
+ });
980
+
981
+ // node_modules/@intentius/chant-k8s-client/src/index.ts
982
+ var src_exports = {};
983
+ __export(src_exports, {
984
+ CHANT_FIELD_MANAGER: () => CHANT_FIELD_MANAGER,
985
+ DEFAULT_CONCURRENCY: () => DEFAULT_CONCURRENCY,
986
+ DEFAULT_EXEC_ALLOWLIST: () => DEFAULT_EXEC_ALLOWLIST,
987
+ ExecCredentialNotAllowedError: () => ExecCredentialNotAllowedError,
988
+ FIELD_MANAGER_MAX_LENGTH: () => FIELD_MANAGER_MAX_LENGTH,
989
+ FIELD_MANAGER_SEPARATOR: () => FIELD_MANAGER_SEPARATOR,
990
+ FieldManagerConflictError: () => FieldManagerConflictError,
991
+ FieldManagerError: () => FieldManagerError,
992
+ K8sApiError: () => K8sApiError,
993
+ K8sClientUnavailableError: () => K8sClientUnavailableError,
994
+ K8sTransportError: () => K8sTransportError,
995
+ KubeConfigError: () => KubeConfigError,
996
+ UnknownResourceError: () => UnknownResourceError,
997
+ apiVersionPath: () => apiVersionPath,
998
+ asFieldManagerConflict: () => asFieldManagerConflict,
999
+ assertExecCredentialAllowed: () => assertExecCredentialAllowed,
1000
+ assertValidFieldManager: () => assertValidFieldManager,
1001
+ chantOwnedFields: () => chantOwnedFields,
1002
+ chantStackOf: () => chantStackOf,
1003
+ createK8sClient: () => createK8sClient,
1004
+ credentialPathOf: () => credentialPathOf,
1005
+ execCommandName: () => execCommandName,
1006
+ execConfigOf: () => execConfigOf,
1007
+ fieldManagerFor: () => fieldManagerFor,
1008
+ fieldOwners: () => fieldOwners,
1009
+ fieldPathsOf: () => fieldPathsOf,
1010
+ fieldSetsOf: () => fieldSetsOf,
1011
+ fieldsOwnedBy: () => fieldsOwnedBy,
1012
+ isChantFieldManager: () => isChantFieldManager,
1013
+ isK8sClientAvailable: () => isK8sClientAvailable,
1014
+ loadClientNode: () => loadClientNode,
1015
+ managedFieldsOf: () => managedFieldsOf,
1016
+ managersOf: () => managersOf,
1017
+ mapConcurrent: () => mapConcurrent,
1018
+ parseConflictMessage: () => parseConflictMessage,
1019
+ parseFieldConflicts: () => parseFieldConflicts,
1020
+ readAmbientContext: () => readAmbientContext,
1021
+ readKubeconfigView: () => readKubeconfigView,
1022
+ refText: () => refText,
1023
+ renderConflictReport: () => renderConflictReport,
1024
+ renderSegment: () => renderSegment,
1025
+ selectorText: () => selectorText,
1026
+ splitApiVersion: () => splitApiVersion
1027
+ });
1028
+ var init_src = __esm({
1029
+ "node_modules/@intentius/chant-k8s-client/src/index.ts"() {
1030
+ init_client();
1031
+ init_errors();
1032
+ init_field_manager();
1033
+ init_conflict();
1034
+ init_managed_fields();
1035
+ init_credentials();
1036
+ init_concurrency();
1037
+ }
1038
+ });
1039
+
1
1040
  // src/cli.ts
2
- import { resolve as resolve4, dirname as dirname6, join as join14, relative as relative3, sep } from "node:path";
3
- import { realpathSync, existsSync as existsSync10, cpSync } from "node:fs";
4
- import { spawnSync as spawnSync2 } from "node:child_process";
1041
+ import { resolve as resolve7, dirname as dirname6, join as join17 } from "node:path";
1042
+ import { realpathSync, existsSync as existsSync13, readFileSync as readFileSync14 } from "node:fs";
5
1043
  import { fileURLToPath as fileURLToPath5 } from "node:url";
1044
+ import { spawn as spawn5 } from "node:child_process";
6
1045
 
7
1046
  // src/server.ts
8
1047
  import { Hono } from "hono";
@@ -33,41 +1072,32 @@ function targetEnvOverrides(targets, chosen) {
33
1072
  }
34
1073
 
35
1074
  // src/k8s-target.ts
36
- import { execFile } from "node:child_process";
37
- import { promisify } from "node:util";
38
- var run = promisify(execFile);
39
1075
  var EMPTY = { contexts: /* @__PURE__ */ new Map(), servers: /* @__PURE__ */ new Map() };
40
- function str(v) {
41
- return typeof v === "string" && v.length > 0 ? v : void 0;
42
- }
43
- function readKubeconfigJson(json) {
1076
+ function kubeconfigFromView(view) {
44
1077
  const contexts = /* @__PURE__ */ new Map();
45
- for (const entry of json.contexts ?? []) {
46
- const name = str(entry?.name);
47
- const cluster = str(entry?.context?.cluster);
48
- if (name && cluster) contexts.set(name, cluster);
49
- }
50
1078
  const servers = /* @__PURE__ */ new Map();
51
- for (const entry of json.clusters ?? []) {
52
- const name = str(entry?.name);
53
- const server = str(entry?.cluster?.server);
54
- if (name && server) servers.set(name, server);
1079
+ for (const entry of view.contexts) {
1080
+ if (entry.name && entry.cluster) contexts.set(entry.name, entry.cluster);
1081
+ if (entry.cluster && entry.server) servers.set(entry.cluster, entry.server);
55
1082
  }
56
- const current = str(json["current-context"]);
57
- return { contexts, servers, ...current ? { currentContext: current } : {} };
1083
+ return { contexts, servers, ...view.currentContext ? { currentContext: view.currentContext } : {} };
1084
+ }
1085
+ async function loadKubeconfig(options = {}) {
1086
+ const client = await k8sClient();
1087
+ if (!client) return EMPTY;
1088
+ return kubeconfigFromView(await client.readKubeconfigView(options));
1089
+ }
1090
+ async function ambientContext(options = {}) {
1091
+ const client = await k8sClient();
1092
+ return client ? await client.readAmbientContext(options) : void 0;
58
1093
  }
59
- async function loadKubeconfig(exec = defaultExec) {
1094
+ async function k8sClient() {
60
1095
  try {
61
- const stdout = await exec("kubectl", ["config", "view", "--raw", "-o", "json"]);
62
- return readKubeconfigJson(JSON.parse(stdout));
1096
+ return await Promise.resolve().then(() => (init_src(), src_exports));
63
1097
  } catch {
64
- return EMPTY;
1098
+ return void 0;
65
1099
  }
66
1100
  }
67
- async function defaultExec(cmd, args) {
68
- const { stdout } = await run(cmd, args, { encoding: "utf8", timeout: 1e4 });
69
- return stdout;
70
- }
71
1101
  function resolveK8sTarget(profiles, env, kubeconfig) {
72
1102
  const declared = env ? profiles?.[env]?.context : void 0;
73
1103
  const context = declared ?? kubeconfig.currentContext;
@@ -88,10 +1118,10 @@ import { streamSSE } from "hono/streaming";
88
1118
  import { serveStatic } from "@hono/node-server/serve-static";
89
1119
  import { serve } from "@hono/node-server";
90
1120
  import { fileURLToPath as fileURLToPath3 } from "node:url";
91
- import { execFile as execFile4 } from "node:child_process";
92
- import { promisify as promisify4 } from "node:util";
93
- import { existsSync as existsSync9, readFileSync as readFileSync9 } from "node:fs";
94
- import { dirname as dirname4, join as join11, relative, resolve as resolve2 } from "node:path";
1121
+ import { execFile as execFile3 } from "node:child_process";
1122
+ import { promisify as promisify3 } from "node:util";
1123
+ import { existsSync as existsSync12, readFileSync as readFileSync12 } from "node:fs";
1124
+ import { dirname as dirname4, join as join15, relative as relative3, resolve as resolve5, sep as sep4 } from "node:path";
95
1125
 
96
1126
  // src/recents.ts
97
1127
  import { readFileSync, writeFileSync, mkdirSync, existsSync } from "node:fs";
@@ -563,6 +1593,7 @@ function graphFlags(opts) {
563
1593
  if (opts.env) flags.push("--env", opts.env);
564
1594
  if (opts.live) flags.push("--live");
565
1595
  if (opts.overlay) flags.push("--overlay");
1596
+ if (opts.namespace) flags.push("--namespace", opts.namespace);
566
1597
  return flags;
567
1598
  }
568
1599
  function packageManifestFrom(req, name) {
@@ -803,10 +1834,10 @@ function applyArgs(target, env) {
803
1834
  }
804
1835
 
805
1836
  // src/cluster-root.ts
806
- import { execFile as execFile2 } from "node:child_process";
807
- import { promisify as promisify2 } from "node:util";
808
- var run2 = promisify2(execFile2);
809
- async function runningK3dClusters(exec = defaultExec2) {
1837
+ import { execFile } from "node:child_process";
1838
+ import { promisify } from "node:util";
1839
+ var run = promisify(execFile);
1840
+ async function runningK3dClusters(exec = defaultExec) {
810
1841
  try {
811
1842
  const out = await exec("k3d", ["cluster", "list", "--no-headers"]);
812
1843
  const clusters = /* @__PURE__ */ new Map();
@@ -822,8 +1853,8 @@ async function runningK3dClusters(exec = defaultExec2) {
822
1853
  return void 0;
823
1854
  }
824
1855
  }
825
- async function defaultExec2(cmd, args) {
826
- const { stdout } = await run2(cmd, args, { encoding: "utf8", timeout: 1e4 });
1856
+ async function defaultExec(cmd, args) {
1857
+ const { stdout } = await run(cmd, args, { encoding: "utf8", timeout: 1e4 });
827
1858
  return stdout;
828
1859
  }
829
1860
  function k3dClusterName(node) {
@@ -1004,19 +2035,19 @@ function finishPipelineProgress(state, exitCode) {
1004
2035
  }
1005
2036
 
1006
2037
  // src/gh-run.ts
1007
- var defaultGhExec = (args) => new Promise((resolve5) => {
2038
+ var defaultGhExec = (args) => new Promise((resolve8) => {
1008
2039
  let out = "";
1009
2040
  let proc;
1010
2041
  try {
1011
2042
  proc = spawn2("gh", args, { stdio: ["ignore", "pipe", "pipe"] });
1012
2043
  } catch {
1013
- resolve5({ code: 127, out: "" });
2044
+ resolve8({ code: 127, out: "" });
1014
2045
  return;
1015
2046
  }
1016
2047
  proc.stdout.on("data", (d) => out += d);
1017
2048
  proc.stderr.on("data", (d) => out += d);
1018
- proc.on("error", () => resolve5({ code: 127, out }));
1019
- proc.on("close", (code) => resolve5({ code: code ?? 1, out }));
2049
+ proc.on("error", () => resolve8({ code: 127, out }));
2050
+ proc.on("close", (code) => resolve8({ code: code ?? 1, out }));
1020
2051
  });
1021
2052
  async function ghReady(exec = defaultGhExec) {
1022
2053
  const { code, out } = await exec(["auth", "status"]);
@@ -1160,11 +2191,11 @@ function joinCiProgress(ir, state) {
1160
2191
  }
1161
2192
  for (const n of ir.nodes) {
1162
2193
  if (n.kind !== "Component") continue;
1163
- const run4 = byComponent.get(n.id);
1164
- if (!run4) continue;
2194
+ const run3 = byComponent.get(n.id);
2195
+ if (!run3) continue;
1165
2196
  const attrs = n.attrs ??= {};
1166
- attrs.ci = run4.status;
1167
- attrs._ciJob = run4.job;
2197
+ attrs.ci = run3.status;
2198
+ attrs._ciJob = run3.job;
1168
2199
  }
1169
2200
  return ir;
1170
2201
  }
@@ -1329,14 +2360,14 @@ var WORKLOAD_KINDS = /* @__PURE__ */ new Set([
1329
2360
  function rec(v) {
1330
2361
  return v && typeof v === "object" && !Array.isArray(v) ? v : void 0;
1331
2362
  }
1332
- function str2(v) {
2363
+ function str(v) {
1333
2364
  return typeof v === "string" && v.length > 0 ? v : void 0;
1334
2365
  }
1335
2366
  function namespaceOf(n) {
1336
- return str2(rec(n.attrs?.metadata)?.namespace) ?? "default";
2367
+ return str(rec(n.attrs?.metadata)?.namespace) ?? "default";
1337
2368
  }
1338
2369
  function nameOf(n) {
1339
- return str2(rec(n.attrs?.metadata)?.name);
2370
+ return str(rec(n.attrs?.metadata)?.name);
1340
2371
  }
1341
2372
  function labelRecord(v) {
1342
2373
  const r = rec(v);
@@ -1368,16 +2399,16 @@ var FLUX_SOURCE_KINDS = /* @__PURE__ */ new Set([
1368
2399
  ]);
1369
2400
  function sourceRefOf(refObj, referrer, defaultKind) {
1370
2401
  const ref = rec(refObj);
1371
- const kind = str2(ref?.kind) ?? defaultKind;
1372
- const name = str2(ref?.name);
2402
+ const kind = str(ref?.kind) ?? defaultKind;
2403
+ const name = str(ref?.name);
1373
2404
  if (!kind || !name) return void 0;
1374
- return { kind, name, namespace: str2(ref?.namespace) ?? namespaceOf(referrer) };
2405
+ return { kind, name, namespace: str(ref?.namespace) ?? namespaceOf(referrer) };
1375
2406
  }
1376
2407
  function namedRefOf(refObj, referrer) {
1377
2408
  const ref = rec(refObj);
1378
- const name = str2(ref?.name);
2409
+ const name = str(ref?.name);
1379
2410
  if (!name) return void 0;
1380
- return { name, namespace: str2(ref?.namespace) ?? namespaceOf(referrer) };
2411
+ return { name, namespace: str(ref?.namespace) ?? namespaceOf(referrer) };
1381
2412
  }
1382
2413
  function list(v) {
1383
2414
  return Array.isArray(v) ? v : [];
@@ -1388,7 +2419,7 @@ function helmReleaseSourceRef(n) {
1388
2419
  function ingressBackendServices(n) {
1389
2420
  const spec = rec(n.attrs?.spec);
1390
2421
  const out = [];
1391
- const backendName = (b) => str2(rec(rec(b)?.service)?.name);
2422
+ const backendName = (b) => str(rec(rec(b)?.service)?.name);
1392
2423
  const fromDefault = backendName(spec?.defaultBackend);
1393
2424
  if (fromDefault) out.push(fromDefault);
1394
2425
  const rules = Array.isArray(spec?.rules) ? spec.rules : [];
@@ -1462,8 +2493,8 @@ function deriveK8sEdges(nodes) {
1462
2493
  }
1463
2494
  if (n.kind === "K8s::Autoscaling::HorizontalPodAutoscaler") {
1464
2495
  const ref = rec(rec(n.attrs?.spec)?.scaleTargetRef);
1465
- const refKind = str2(ref?.kind);
1466
- const refName = str2(ref?.name);
2496
+ const refKind = str(ref?.kind);
2497
+ const refName = str(ref?.name);
1467
2498
  if (!refKind || !refName) continue;
1468
2499
  for (const w of workloads) {
1469
2500
  if (namespaceOf(w) !== namespaceOf(n)) continue;
@@ -1498,11 +2529,11 @@ function deriveK8sEdges(nodes) {
1498
2529
  continue;
1499
2530
  }
1500
2531
  if (n.kind === "K8s::Argo::Application") {
1501
- addProject(n, str2(rec(n.attrs?.spec)?.project), "project");
2532
+ addProject(n, str(rec(n.attrs?.spec)?.project), "project");
1502
2533
  continue;
1503
2534
  }
1504
2535
  if (n.kind === "K8s::Argo::ApplicationSet") {
1505
- addProject(n, str2(rec(rec(rec(n.attrs?.spec)?.template)?.spec)?.project), "template project");
2536
+ addProject(n, str(rec(rec(rec(n.attrs?.spec)?.template)?.spec)?.project), "template project");
1506
2537
  }
1507
2538
  }
1508
2539
  return out;
@@ -2706,9 +3737,12 @@ function logicalKept(before, after) {
2706
3737
  if (after > 0 && after * 3 >= before) return void 0;
2707
3738
  return after === 0 ? `logical projected nothing from ${before} resources \u2014 it is a cloud-topology lens, and this estate declares none of the kinds it nests (behold#74)` : `logical kept ${after} of ${before} resources \u2014 it is a cloud-topology lens, and the rest are kinds it does not nest (behold#74)`;
2708
3739
  }
2709
- function edgelessNote(zoom, ir) {
3740
+ function edgelessNote(zoom, ir, detail) {
2710
3741
  if (zoom === "components" || zoom === "logical") return void 0;
2711
3742
  if (ir.nodes.length === 0 || ir.edges.length > 0) return void 0;
3743
+ if (detail !== void 0 && detail < 3) {
3744
+ return "no edges at this detail \u2014 sourceRef/dependsOn and other attrs-derived references only appear at detail 3 (\u2318K \u2192 attributes, or add &detail=3)";
3745
+ }
2712
3746
  return "no edges \u2014 nothing in this estate references anything else";
2713
3747
  }
2714
3748
  var CLUSTER_SCOPED = /* @__PURE__ */ new Set([
@@ -2727,9 +3761,14 @@ function namespaceMismatchNote(nodes) {
2727
3761
  if (!k8s.some((n) => n.attrs?._status === "good")) return void 0;
2728
3762
  return `${pending.length} pending k8s objects declare no metadata.namespace \u2014 if a controller stamps it at apply time (e.g. Flux's targetNamespace), the live read looked in "default", not where they run`;
2729
3763
  }
2730
- function notesFor(zoom, ir, compositeEdgesAttached, logicalBefore) {
3764
+ function namespaceJoinNote(joined) {
3765
+ if (joined.length === 0) return void 0;
3766
+ const each = joined.map((j) => `${j.name} in "${j.namespace}"`).join(", ");
3767
+ return `read ${each} \u2014 the namespace the estate's own Kustomization targetNamespace binds, not the app project's own declaration`;
3768
+ }
3769
+ function notesFor(zoom, ir, compositeEdgesAttached, logicalBefore, detail) {
2731
3770
  const primary = zoom === "logical" && logicalBefore !== void 0 ? logicalKept(logicalBefore, ir.nodes.length) : zoomNote(zoom, ir, compositeEdgesAttached);
2732
- const notes = [primary, edgelessNote(zoom, ir)].filter((n) => n !== void 0);
3771
+ const notes = [primary, edgelessNote(zoom, ir, detail)].filter((n) => n !== void 0);
2733
3772
  return notes.length ? notes.join(" \xB7 ") : void 0;
2734
3773
  }
2735
3774
  function tierMismatchNote(ir, tiers, currentTier) {
@@ -2844,6 +3883,20 @@ var cache = /* @__PURE__ */ new Map();
2844
3883
  function unprefix(body) {
2845
3884
  return body.replace(/\s(?:inkscape|sodipodi):[\w-]+\s*=\s*(["'])[\s\S]*?\1/g, "").replace(/\sxlink:href\s*=/g, " href=");
2846
3885
  }
3886
+ var PLATE_FILL = "#ffffff";
3887
+ var PLATE_INSET = 0.1;
3888
+ var PLATED = /* @__PURE__ */ new Set(["cncf/helm"]);
3889
+ function plate(glyph) {
3890
+ const [minX, minY, w, h] = glyph.viewBox.trim().split(/[\s,]+/).map(Number);
3891
+ const side = Math.max(w, h);
3892
+ const cx = minX + w / 2;
3893
+ const cy = minY + h / 2;
3894
+ const k = 1 - 2 * PLATE_INSET;
3895
+ const round = (n) => Math.round(n * 1e4) / 1e4;
3896
+ const ground = `<rect x="${round(cx - side / 2)}" y="${round(cy - side / 2)}" width="${round(side)}" height="${round(side)}" rx="${round(side * 0.18)}" fill="${PLATE_FILL}"/>`;
3897
+ const inset = `translate(${round(cx * (1 - k))} ${round(cy * (1 - k))}) scale(${k})`;
3898
+ return { ...glyph, body: `${ground}<g transform="${inset}">${glyph.body}</g>` };
3899
+ }
2847
3900
  function loadIcon(rel) {
2848
3901
  const hit = cache.get(rel);
2849
3902
  if (hit) return hit;
@@ -2854,7 +3907,8 @@ function loadIcon(rel) {
2854
3907
  if (open < 0 || openEnd < 0 || close < 0) throw new Error(`malformed vendored icon: ${rel}.svg`);
2855
3908
  const viewBox = /\bviewBox\s*=\s*(["'])(.*?)\1/.exec(raw.slice(open, openEnd))?.[2];
2856
3909
  if (!viewBox) throw new Error(`vendored icon has no viewBox: ${rel}.svg`);
2857
- const glyph = { body: unprefix(raw.slice(openEnd + 1, close).trim()), colored: true, viewBox };
3910
+ const bare = { body: unprefix(raw.slice(openEnd + 1, close).trim()), colored: true, viewBox };
3911
+ const glyph = PLATED.has(rel) ? plate(bare) : bare;
2858
3912
  cache.set(rel, glyph);
2859
3913
  return glyph;
2860
3914
  }
@@ -2907,9 +3961,200 @@ function helmIconFor(kind) {
2907
3961
  return HELM_MARK_KINDS.has(kind) ? loadIcon("cncf/helm") : void 0;
2908
3962
  }
2909
3963
 
3964
+ // src/carve-lens.ts
3965
+ var SUPPORTED_MAJOR = 1;
3966
+ var refuse = (error, remedy) => ({ ok: false, refusal: { error, code: "carve-report", remedy } });
3967
+ var HOW_TO_GET_ONE = "Generate one with `chant carve advise --from <terraform-dir> --report report.json` (chant's read-only Terraform peelability advisor), then `behold carve report.json`.";
3968
+ var isRecord = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
3969
+ function parseCarveReport(value) {
3970
+ if (!isRecord(value)) {
3971
+ return refuse("That file isn't a carve report \u2014 its top level isn't a JSON object.", HOW_TO_GET_ONE);
3972
+ }
3973
+ if (!Array.isArray(value.resources)) {
3974
+ return refuse(
3975
+ "That JSON doesn't look like a carve report \u2014 it has no `resources` array.",
3976
+ HOW_TO_GET_ONE
3977
+ );
3978
+ }
3979
+ const bad = value.resources.findIndex(
3980
+ (r) => !isRecord(r) || typeof r.address !== "string" || typeof r.score !== "number" || typeof r.band !== "string"
3981
+ );
3982
+ if (bad >= 0) {
3983
+ return refuse(
3984
+ `That JSON has a \`resources\` array, but entry ${bad} isn't a scored resource (needs a string \`address\`, a numeric \`score\` and a string \`band\`).`,
3985
+ HOW_TO_GET_ONE
3986
+ );
3987
+ }
3988
+ const version = value.version ?? value.schemaVersion;
3989
+ if (version !== void 0) {
3990
+ const major = Number(String(version).split(".")[0]);
3991
+ if (!Number.isFinite(major) || major !== SUPPORTED_MAJOR) {
3992
+ return refuse(
3993
+ `This carve report declares schema version ${String(version)}; behold reads version ${SUPPORTED_MAJOR}.`,
3994
+ "Upgrade behold, or render the report with a behold that speaks its version."
3995
+ );
3996
+ }
3997
+ }
3998
+ return { ok: true, report: value };
3999
+ }
4000
+ function readCarveReport(path, readFile) {
4001
+ let text;
4002
+ try {
4003
+ text = readFile(path);
4004
+ } catch (err) {
4005
+ return refuse(
4006
+ `Couldn't read ${path}: ${err instanceof Error ? err.message : String(err)}`,
4007
+ HOW_TO_GET_ONE
4008
+ );
4009
+ }
4010
+ let json;
4011
+ try {
4012
+ json = JSON.parse(text);
4013
+ } catch (err) {
4014
+ return refuse(
4015
+ `${path} isn't valid JSON: ${err instanceof Error ? err.message : String(err)}`,
4016
+ HOW_TO_GET_ONE
4017
+ );
4018
+ }
4019
+ return parseCarveReport(json);
4020
+ }
4021
+ function statusForBand(band, score) {
4022
+ switch (band) {
4023
+ case "clean leaf":
4024
+ return "good";
4025
+ case "carvable w/ edits":
4026
+ return "warn";
4027
+ case "leave in Terraform":
4028
+ return "neutral";
4029
+ default:
4030
+ return score >= 80 ? "good" : score >= 50 ? "warn" : "neutral";
4031
+ }
4032
+ }
4033
+ function verdictForStatus(status) {
4034
+ return status === "good" ? "carve now" : status === "warn" ? "boundary work" : "leave in Terraform";
4035
+ }
4036
+ function tfTypeOf(address, kind) {
4037
+ if (kind === "module" || address.startsWith("module.")) return "module";
4038
+ const dot = address.indexOf(".");
4039
+ return dot > 0 ? address.slice(0, dot) : address;
4040
+ }
4041
+ var directionOf = (e) => e.direction ?? "inbound";
4042
+ function boundaryEdgesOf(r) {
4043
+ const b = r.boundary;
4044
+ if (!b) return [];
4045
+ const list3 = Array.isArray(b) ? b : [...b.inbound ?? [], ...b.outbound ?? []];
4046
+ return list3.filter((e) => isRecord(e) && typeof e.survivor === "string" && typeof e.carved === "string");
4047
+ }
4048
+ function scoreArithmetic(r) {
4049
+ const b = r.breakdown;
4050
+ if (!b || b.tier === null) return `${r.score} (no known native mapping \u2014 nothing to carve into)`;
4051
+ const p = b.penalties ?? {};
4052
+ const terms = [];
4053
+ if (p.inbound) terms.push(`- 12x${b.inbound ?? 0} inbound`);
4054
+ if (p.outbound) terms.push(`- 4x${b.outbound ?? 0} outbound`);
4055
+ if (p.tier) terms.push(`- 15x${(b.tier ?? 1) - 1} tier${b.tier}`);
4056
+ if (p.dynamic) terms.push(`- 10 dynamic`);
4057
+ if (p.instances) terms.push(`- 3x${(b.instances ?? 1) - 1} instances`);
4058
+ if (!terms.length) return `100 (no penalties) = ${r.score}`;
4059
+ const raw = 100 + Object.values(p).reduce((s, n) => s + (n ?? 0), 0);
4060
+ const clamped = raw !== r.score ? ` (clamped from ${raw})` : "";
4061
+ return `100 ${terms.join(" ")} = ${r.score}${clamped}`;
4062
+ }
4063
+ function boundaryWorkOf(r) {
4064
+ const b = r.breakdown;
4065
+ if (!b || b.tier === null) return "no known native mapping (unsupported provider/type)";
4066
+ const parts = [];
4067
+ if (b.inbound) parts.push(`${b.inbound} inbound (a data-source patch each)`);
4068
+ if (b.outbound) parts.push(`${b.outbound} outbound (a deferred input each)`);
4069
+ if ((b.tier ?? 1) > 1) parts.push(`tier ${b.tier} map`);
4070
+ if (b.hasDynamic) parts.push("count/for_each/data present");
4071
+ if ((b.instances ?? 1) > 1) parts.push(`${b.instances} instances`);
4072
+ return parts.length ? parts.join(", ") : "clean 1:1 native map, no boundary edges";
4073
+ }
4074
+ function carveCardFields(node) {
4075
+ const score = node.attrs.score;
4076
+ const carve = node.attrs.carve;
4077
+ if (typeof score !== "number") return void 0;
4078
+ return [
4079
+ { label: "score", value: String(score) },
4080
+ ...typeof carve === "string" ? [{ label: "carve", value: carve }] : []
4081
+ ];
4082
+ }
4083
+ function carveReportToIr(report) {
4084
+ const resources = report.resources ?? [];
4085
+ const known = new Set(resources.map((r) => r.address));
4086
+ const nodes = resources.map((r) => {
4087
+ const status = statusForBand(r.band, r.score);
4088
+ const b = r.breakdown ?? {};
4089
+ const edges2 = boundaryEdgesOf(r);
4090
+ const patches = edges2.filter((e) => e.carved === r.address && directionOf(e) === "inbound").map((e) => e.survivor);
4091
+ const inputs = edges2.filter((e) => e.carved === r.address && directionOf(e) === "outbound").map((e) => e.survivor);
4092
+ return {
4093
+ id: r.address,
4094
+ kind: tfTypeOf(r.address, r.kind),
4095
+ lexicon: "terraform",
4096
+ attrs: {
4097
+ // The drift palette's channel — `_`-prefixed, so it paints the card and
4098
+ // stays out of both the card's fields and the inspect pane's list.
4099
+ _status: status,
4100
+ score: r.score,
4101
+ carve: verdictForStatus(status),
4102
+ band: r.band,
4103
+ arithmetic: scoreArithmetic(r),
4104
+ boundaryWork: boundaryWorkOf(r),
4105
+ ...r.mapsTo ? { mapsTo: r.mapsTo } : {},
4106
+ tier: b.tier === null || b.tier === void 0 ? "none" : b.tier,
4107
+ inbound: b.inbound ?? 0,
4108
+ outbound: b.outbound ?? 0,
4109
+ instances: b.instances ?? 1,
4110
+ dynamic: b.hasDynamic ?? false,
4111
+ // The predicted diff, when the report carries the edge lists: who needs
4112
+ // a `data` source the moment this is carved, and what becomes a
4113
+ // deploy-time input. Absent (not empty) when the report has no lists —
4114
+ // "none" and "not reported" are different claims.
4115
+ ...patches.length ? { patchOnCarve: patches.join(", ") } : {},
4116
+ ...inputs.length ? { deferredInputs: inputs.join(", ") } : {}
4117
+ }
4118
+ };
4119
+ });
4120
+ const seen = /* @__PURE__ */ new Set();
4121
+ const edges = [];
4122
+ for (const r of resources) {
4123
+ for (const e of boundaryEdgesOf(r)) {
4124
+ const direction = directionOf(e);
4125
+ const from = direction === "inbound" ? e.survivor : e.carved;
4126
+ const to = direction === "inbound" ? e.carved : e.survivor;
4127
+ if (!known.has(from) || !known.has(to)) continue;
4128
+ const key = `${from}|${to}`;
4129
+ if (seen.has(key)) continue;
4130
+ seen.add(key);
4131
+ edges.push({
4132
+ from,
4133
+ to,
4134
+ kind: "ref",
4135
+ viaAttr: direction,
4136
+ ...e.attrs?.length ? { toAttr: e.attrs.join(", ") } : {}
4137
+ });
4138
+ }
4139
+ }
4140
+ const byStack = {};
4141
+ for (const order of ["good", "warn", "neutral"]) {
4142
+ const members = resources.filter((r) => statusForBand(r.band, r.score) === order).map((r) => r.address);
4143
+ if (members.length) byStack[verdictForStatus(order)] = members;
4144
+ }
4145
+ return { nodes, edges, groups: { byStack } };
4146
+ }
4147
+ function carveNote(report, ir) {
4148
+ const counts = Object.entries(report.bands ?? {}).filter(([, n]) => n > 0).map(([band, n]) => `${n} ${band}`).join(" \xB7 ");
4149
+ const head = `chant carve advisory${report.from ? ` for ${report.from}` : ""}${counts ? ` \u2014 ${counts}` : ""}`;
4150
+ if (ir.edges.length) return `${head}. Read-only: nothing is emitted, patched, or applied.`;
4151
+ return `${head}. This report carries per-resource boundary COUNTS but no edge lists, so no boundary edges are drawn (chant#1636). Read-only: nothing is emitted, patched, or applied.`;
4152
+ }
4153
+
2910
4154
  // src/render.ts
2911
4155
  registerPack({ lexicon: "k8s", iconFor: k8sIconFor });
2912
4156
  registerPack({ lexicon: "helm", iconFor: helmIconFor });
4157
+ registerPack({ lexicon: "terraform", iconFor: () => void 0, fields: carveCardFields });
2913
4158
  function renderArchitecture(ir, byContainer, opts = {}) {
2914
4159
  const spread = Math.min(1.5, ir.edges.length / Math.max(ir.nodes.length, 1));
2915
4160
  const layout = layoutArchitecture(ir, byContainer, {
@@ -2940,6 +4185,59 @@ function renderGraph(ir, opts = {}) {
2940
4185
  });
2941
4186
  return { svg };
2942
4187
  }
4188
+ function renderBanded(ir, opts = {}) {
4189
+ const bands = ir.groups.byStack ?? {};
4190
+ const size = footprints(ir);
4191
+ const dims = (id) => size.get(id) ?? { w: NODE_W, h: NODE_H };
4192
+ const ids = ir.nodes.map((n) => n.id);
4193
+ const cellW = Math.max(NODE_W, ...ids.map((id) => dims(id).w));
4194
+ 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
+ const cols = Math.max(1, Math.round(Math.sqrt(ids.length * (cellH + GAP) * 2 / (cellW + GAP))));
4200
+ const contentW = cols * cellW + (cols - 1) * GAP;
4201
+ const panelW = contentW + PAD * 2;
4202
+ const claimed = new Set(Object.values(bands).flat());
4203
+ const orphans = ids.filter((id) => !claimed.has(id));
4204
+ const panels = [
4205
+ ...Object.entries(bands).map(([title, members]) => [title, members.filter((id) => size.has(id))]),
4206
+ ...orphans.length ? [["unbanded", orphans]] : []
4207
+ ].filter(([, members]) => members.length > 0);
4208
+ const statusOf = new Map(ir.nodes.map((n) => [n.id, n.attrs?._status]));
4209
+ const placed = [];
4210
+ const boxes = [];
4211
+ let top = 0;
4212
+ for (const [title, members] of panels) {
4213
+ const rows = Math.ceil(members.length / cols);
4214
+ const panelH = TITLE + PAD + rows * cellH + (rows - 1) * GAP + PAD;
4215
+ members.forEach((id, i) => {
4216
+ placed.push({
4217
+ id,
4218
+ x: PAD + i % cols * (cellW + GAP) + cellW / 2,
4219
+ y: top + TITLE + PAD + Math.floor(i / cols) * (cellH + GAP) + cellH / 2
4220
+ });
4221
+ });
4222
+ const statuses = new Set(members.map((id) => statusOf.get(id)));
4223
+ const status = statuses.size === 1 ? [...statuses][0] : void 0;
4224
+ boxes.push({ title, x: panelW / 2, y: top + panelH / 2, w: panelW, h: panelH, ...status ? { status } : {} });
4225
+ top += panelH + BAND_GAP;
4226
+ }
4227
+ const height = Math.max(1, top - BAND_GAP);
4228
+ const layout = {
4229
+ width: panelW,
4230
+ height,
4231
+ nodes: placed.map((p) => ({ id: p.id, x: p.x, y: height - p.y }))
4232
+ };
4233
+ const svg = renderSvg(ir, layout, {
4234
+ fit: true,
4235
+ hideTitle: true,
4236
+ groups: boxes.map((b) => ({ ...b, y: height - b.y })),
4237
+ ...opts.theme ? { theme: opts.theme } : {}
4238
+ });
4239
+ return { svg };
4240
+ }
2943
4241
  var NODE_W = 175;
2944
4242
  var NODE_H = 104;
2945
4243
  function footprints(ir) {
@@ -3104,9 +4402,374 @@ function radializeLayout(layout, groupOf, size = /* @__PURE__ */ new Map()) {
3104
4402
  layout.height = Math.max(...nodes.map((n) => n.y + hOf(n) / 2)) + pad;
3105
4403
  }
3106
4404
 
3107
- // src/ops.ts
3108
- import { readdirSync as readdirSync3, readFileSync as readFileSync7, existsSync as existsSync6 } from "node:fs";
4405
+ // src/carve-actions.ts
4406
+ import { existsSync as existsSync7, mkdirSync as mkdirSync3, readFileSync as readFileSync8, readdirSync as readdirSync3, statSync as statSync2 } from "node:fs";
4407
+ import { isAbsolute, join as join9, relative, resolve as resolve2, sep } from "node:path";
4408
+
4409
+ // src/layout.ts
4410
+ import { existsSync as existsSync6, mkdirSync as mkdirSync2, readFileSync as readFileSync7, renameSync, rmSync, writeFileSync as writeFileSync2, accessSync, constants } from "node:fs";
3109
4411
  import { join as join8 } from "node:path";
4412
+ var LAYOUT_DIR = ".behold";
4413
+ var LAYOUT_FILE = "layout.json";
4414
+ var MAX_BODY_BYTES = 64 * 1024;
4415
+ var MAX_FILE_BYTES = 256 * 1024;
4416
+ var MAX_IDS_PER_LENS = 2e3;
4417
+ var MAX_LENSES = 64;
4418
+ var MAX_ID_LENGTH = 512;
4419
+ var MAX_LENS_LENGTH = 128;
4420
+ var NUM = ["dx", "dy", "dw", "dh"];
4421
+ function slug(s) {
4422
+ return String(s ?? "").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "unknown";
4423
+ }
4424
+ function normalizeLens(raw) {
4425
+ if (typeof raw !== "string" || !raw.trim() || raw.length > MAX_LENS_LENGTH) return null;
4426
+ const parts = raw.split("+").map((p) => slug(p)).filter((p) => p && p !== "unknown");
4427
+ if (!parts.length) return null;
4428
+ const key = parts.join("+");
4429
+ return key === "__proto__" || key === "constructor" || key === "prototype" ? null : key;
4430
+ }
4431
+ function normalizeDeltas(raw) {
4432
+ const out = {};
4433
+ if (!raw || typeof raw !== "object") return out;
4434
+ for (const [id, d] of Object.entries(raw)) {
4435
+ if (!id || id.length > MAX_ID_LENGTH || id === "__proto__" || !d || typeof d !== "object") continue;
4436
+ const clean = {};
4437
+ for (const k of NUM) {
4438
+ const v = Number(d[k]);
4439
+ if (Number.isFinite(v) && v !== 0) clean[k] = v;
4440
+ }
4441
+ if (Object.keys(clean).length) out[id] = clean;
4442
+ if (Object.keys(out).length >= MAX_IDS_PER_LENS) break;
4443
+ }
4444
+ return out;
4445
+ }
4446
+ function lensFromQuery(params) {
4447
+ const zoom = params.get("components") === "1" ? "components" : params.get("logical") === "1" ? "logical" : params.get("runtime") === "1" ? "runtime" : { 1: "composites", 2: "resources", 3: "attributes" }[Number(params.get("detail") ?? 2)] ?? "resources";
4448
+ const stack = params.get("stack");
4449
+ return [slug(zoom), params.get("radial") === "1" ? "radial" : null, stack ? slug(`stack-${stack}`) : null].filter(Boolean).join("+");
4450
+ }
4451
+ function layoutPath(projectDir) {
4452
+ return join8(projectDir, LAYOUT_DIR, LAYOUT_FILE);
4453
+ }
4454
+ function unwritableReason(projectDir) {
4455
+ if (!projectDir || !existsSync6(projectDir)) return `no such project directory: ${projectDir || "(none)"}`;
4456
+ try {
4457
+ accessSync(projectDir, constants.W_OK);
4458
+ } catch {
4459
+ return "the served project directory is read-only";
4460
+ }
4461
+ return null;
4462
+ }
4463
+ function readLayoutFile(projectDir) {
4464
+ const empty = { version: 1, lenses: {} };
4465
+ const file = layoutPath(projectDir);
4466
+ try {
4467
+ if (!existsSync6(file)) return empty;
4468
+ const raw = readFileSync7(file, "utf8");
4469
+ if (raw.length > MAX_FILE_BYTES) return empty;
4470
+ const parsed = JSON.parse(raw);
4471
+ if (!parsed || typeof parsed !== "object" || !parsed.lenses || typeof parsed.lenses !== "object") return empty;
4472
+ const lenses = {};
4473
+ for (const [lens, deltas] of Object.entries(parsed.lenses)) {
4474
+ const key = normalizeLens(lens);
4475
+ if (!key) continue;
4476
+ const clean = normalizeDeltas(deltas);
4477
+ if (Object.keys(clean).length) lenses[key] = clean;
4478
+ if (Object.keys(lenses).length >= MAX_LENSES) break;
4479
+ }
4480
+ return { version: 1, lenses };
4481
+ } catch {
4482
+ return empty;
4483
+ }
4484
+ }
4485
+ function readLens(projectDir, lens) {
4486
+ const key = normalizeLens(lens);
4487
+ if (!key) return {};
4488
+ return readLayoutFile(projectDir).lenses[key] ?? {};
4489
+ }
4490
+ var LayoutTooLarge = class extends Error {
4491
+ };
4492
+ function writeLens(projectDir, lens, deltas) {
4493
+ const key = normalizeLens(lens);
4494
+ if (!key) throw new Error(`not a lens key: ${String(lens)}`);
4495
+ const clean = normalizeDeltas(deltas);
4496
+ const current = readLayoutFile(projectDir);
4497
+ const lenses = { ...current.lenses };
4498
+ if (Object.keys(clean).length) lenses[key] = clean;
4499
+ else delete lenses[key];
4500
+ if (Object.keys(lenses).length > MAX_LENSES) throw new LayoutTooLarge(`a layout sidecar holds at most ${MAX_LENSES} lenses`);
4501
+ const file = layoutPath(projectDir);
4502
+ if (!Object.keys(lenses).length) {
4503
+ rmSync(file, { force: true });
4504
+ return { lens: key, deltas: clean, bytes: 0 };
4505
+ }
4506
+ const body = JSON.stringify({ version: 1, lenses }, null, 2) + "\n";
4507
+ if (body.length > MAX_FILE_BYTES) throw new LayoutTooLarge(`a layout sidecar is capped at ${MAX_FILE_BYTES} bytes`);
4508
+ mkdirSync2(join8(projectDir, LAYOUT_DIR), { recursive: true });
4509
+ const tmp = `${file}.tmp`;
4510
+ writeFileSync2(tmp, body, "utf8");
4511
+ renameSync(tmp, file);
4512
+ return { lens: key, deltas: clean, bytes: body.length };
4513
+ }
4514
+ function nodeTransform(base, d) {
4515
+ const dx = d.dx || 0;
4516
+ const dy = d.dy || 0;
4517
+ if (!dx && !dy) return base;
4518
+ return `translate(${dx}, ${dy}) ${base}`.trim();
4519
+ }
4520
+ function pathAnchors(d) {
4521
+ const n = String(d || "").match(/-?\d*\.?\d+(?:e[-+]?\d+)?/gi);
4522
+ if (!n || n.length < 4) return null;
4523
+ return { sx: +n[0], sy: +n[1], ex: +n[n.length - 2], ey: +n[n.length - 1] };
4524
+ }
4525
+ function straightEdge(anchors, from, to) {
4526
+ const { sx, sy, ex, ey } = anchors;
4527
+ return `M ${sx + (from && from.dx || 0)} ${sy + (from && from.dy || 0)} L ${ex + (to && to.dx || 0)} ${ey + (to && to.dy || 0)}`;
4528
+ }
4529
+ function unescapeAttr(v) {
4530
+ return v.replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&quot;/g, '"').replace(/&#39;/g, "'").replace(/&apos;/g, "'").replace(/&amp;/g, "&");
4531
+ }
4532
+ function attrOf(tag, name) {
4533
+ const m = new RegExp(`\\b${name}="([^"]*)"`).exec(tag);
4534
+ return m ? unescapeAttr(m[1]) : null;
4535
+ }
4536
+ function groupEnd(svg, from) {
4537
+ const tok = /<g\b|<\/g\s*>/g;
4538
+ tok.lastIndex = from;
4539
+ let depth = 1;
4540
+ let m;
4541
+ while (m = tok.exec(svg)) {
4542
+ if (m[0][1] === "/") {
4543
+ depth--;
4544
+ if (!depth) return m.index;
4545
+ } else depth++;
4546
+ }
4547
+ return svg.length;
4548
+ }
4549
+ function applyLayoutToSvg(svg, deltas) {
4550
+ const moved = {};
4551
+ for (const [id, d] of Object.entries(normalizeDeltas(deltas))) if (d.dx || d.dy) moved[id] = d;
4552
+ if (!Object.keys(moved).length || typeof svg !== "string" || !svg) return { svg, applied: 0 };
4553
+ const placed = /* @__PURE__ */ new Set();
4554
+ let out = svg.replace(/<g\b([^>]*?)(\/?)>/g, (tag, attrs, selfClose) => {
4555
+ const id = attrOf(attrs, "data-node-id");
4556
+ const d = id ? moved[id] : void 0;
4557
+ if (!id || !d) return tag;
4558
+ placed.add(id);
4559
+ const base = /\btransform="([^"]*)"/.exec(attrs);
4560
+ const next = nodeTransform(base ? base[1] : "", d);
4561
+ if (base) return `<g${attrs.replace(base[0], () => `transform="${next}"`)}${selfClose}>`;
4562
+ return `<g${attrs}${/\s$/.test(attrs) ? "" : " "}transform="${next}"${selfClose}>`;
4563
+ });
4564
+ out = reanchorEdges(out, moved);
4565
+ return { svg: out, applied: placed.size };
4566
+ }
4567
+ function reanchorEdges(svg, moved) {
4568
+ const open = /<g\b([^>]*\bdata-edge-from="[^"]*"[^>]*?)(\/?)>/g;
4569
+ let out = "";
4570
+ let cursor = 0;
4571
+ let m;
4572
+ while (m = open.exec(svg)) {
4573
+ const bodyStart = m.index + m[0].length;
4574
+ if (m[2] === "/") continue;
4575
+ const bodyEnd = groupEnd(svg, bodyStart);
4576
+ open.lastIndex = bodyEnd;
4577
+ const from = moved[attrOf(m[1], "data-edge-from") ?? ""];
4578
+ const to = moved[attrOf(m[1], "data-edge-to") ?? ""];
4579
+ if (!from && !to) continue;
4580
+ const body = svg.slice(bodyStart, bodyEnd);
4581
+ const first = /<path\b[^>]*\bd="([^"]*)"/.exec(body);
4582
+ const anchors = first ? pathAnchors(first[1]) : null;
4583
+ if (!anchors) continue;
4584
+ const d = straightEdge(anchors, from, to);
4585
+ out += svg.slice(cursor, bodyStart) + body.replace(/(<path\b[^>]*\bd=")([^"]*)(")/g, (_t, a, _d, z) => a + d + z);
4586
+ cursor = bodyEnd;
4587
+ }
4588
+ return out + svg.slice(cursor);
4589
+ }
4590
+
4591
+ // src/carve-actions.ts
4592
+ var MAX_ARTIFACT_BYTES = 64 * 1024;
4593
+ var MAX_ARTIFACTS = 24;
4594
+ var MAX_OUTPUT_BYTES = 32 * 1024;
4595
+ var refuse2 = (code, error, remedy) => ({
4596
+ ok: false,
4597
+ refusal: { error, code, remedy }
4598
+ });
4599
+ var BUILD_CAVEAT = 'The gate shown is `chant lint`, not `chant build`. `carve emit` folds the bucket\'s versioning and public-access-block sub-resources into the carve set but does not yet carry them as native props, so `chant build` fails two AWS policy rules on source the advisor scored 88 (chant#1637). example-carve/README.md, "Known rough edge in emit", has the full statement.';
4600
+ function insideDemo(root, p) {
4601
+ const rel = relative(resolve2(root), resolve2(p));
4602
+ return rel !== "" && !rel.startsWith("..") && !isAbsolute(rel);
4603
+ }
4604
+ function selectFromReport(report, raw) {
4605
+ if (typeof raw !== "string" || !raw || raw.length > 512) return null;
4606
+ return (report?.resources ?? []).some((r) => r.address === raw) ? raw : null;
4607
+ }
4608
+ function carveWriteBlock(demo) {
4609
+ if (!demo) {
4610
+ return "this server isn't running a carve demo \u2014 the carve actions only exist inside a `behold demo carve` copy";
4611
+ }
4612
+ if (!existsSync7(demo.from)) return `the demo copy has no Terraform estate at ${demo.from}`;
4613
+ if (!insideDemo(demo.root, demo.out)) return "the carve output directory is outside the demo copy";
4614
+ return unwritableReason(demo.root);
4615
+ }
4616
+ function readArtifacts(demo, dir, filter) {
4617
+ const out = [];
4618
+ const walk = (d, depth) => {
4619
+ if (out.length >= MAX_ARTIFACTS || depth > 2) return;
4620
+ let entries;
4621
+ try {
4622
+ entries = readdirSync3(d).sort();
4623
+ } catch {
4624
+ return;
4625
+ }
4626
+ for (const name of entries) {
4627
+ if (out.length >= MAX_ARTIFACTS) return;
4628
+ if (name === "node_modules" || name.startsWith(".")) continue;
4629
+ const full = join9(d, name);
4630
+ let st;
4631
+ try {
4632
+ st = statSync2(full);
4633
+ } catch {
4634
+ continue;
4635
+ }
4636
+ if (st.isDirectory()) {
4637
+ walk(full, depth + 1);
4638
+ continue;
4639
+ }
4640
+ const rel = relative(demo.root, full).split(sep).join("/");
4641
+ if (!filter(rel)) continue;
4642
+ let text = "";
4643
+ try {
4644
+ text = readFileSync8(full, "utf8");
4645
+ } catch {
4646
+ continue;
4647
+ }
4648
+ const truncated = text.length > MAX_ARTIFACT_BYTES;
4649
+ out.push({
4650
+ path: rel,
4651
+ kind: (name.split(".").pop() ?? "").toLowerCase(),
4652
+ bytes: st.size,
4653
+ text: truncated ? text.slice(0, MAX_ARTIFACT_BYTES) : text,
4654
+ truncated
4655
+ });
4656
+ }
4657
+ };
4658
+ walk(dir, 0);
4659
+ return out;
4660
+ }
4661
+ function carveSlug(select) {
4662
+ return select.replace(/[^A-Za-z0-9_]+/g, "-");
4663
+ }
4664
+ var clip = (s) => s.length > MAX_OUTPUT_BYTES ? s.slice(0, MAX_OUTPUT_BYTES) + "\n\u2026 (truncated)" : s;
4665
+ function shortenIn(arg, root) {
4666
+ return arg.startsWith(root + sep) ? arg.slice(root.length + 1) : arg === root ? "." : arg;
4667
+ }
4668
+ function merge(r, root) {
4669
+ const joined = stripAnsi([r.stdout, r.stderr].filter((s) => s.trim()).join("\n").trim());
4670
+ return clip(joined.split(root + sep).join("").split(root).join("."));
4671
+ }
4672
+ async function runCarveEmit(demo, select) {
4673
+ const block = carveWriteBlock(demo);
4674
+ if (block) return refuse2("read-only", block, "Start the walkthrough with `behold demo carve`.");
4675
+ mkdirSync3(demo.out, { recursive: true });
4676
+ const reportFile = join9(demo.out, `${carveSlug(select)}-boundary.json`);
4677
+ const args = [
4678
+ "carve",
4679
+ "emit",
4680
+ "--from",
4681
+ demo.from,
4682
+ ...demo.state ? ["--state", demo.state] : [],
4683
+ "--select",
4684
+ select,
4685
+ "--output",
4686
+ demo.out,
4687
+ "--report",
4688
+ reportFile
4689
+ ];
4690
+ const run3 = await runChantRaw(args, demo.project).catch((err) => ({
4691
+ code: 127,
4692
+ stdout: "",
4693
+ stderr: err instanceof Error ? err.message : String(err)
4694
+ }));
4695
+ const output = merge(run3, demo.root);
4696
+ if (run3.code !== 0) {
4697
+ return refuse2(
4698
+ "carve-action",
4699
+ `chant carve emit exited ${run3.code}: ${output || "(no output)"}`,
4700
+ "The offline emit needs `@cdktf/hcl2json` in the demo copy and the demo's own chant install \u2014 `npm install` in the copy's `app/` and re-run `behold demo carve`."
4701
+ );
4702
+ }
4703
+ let boundary = null;
4704
+ try {
4705
+ boundary = JSON.parse(readFileSync8(reportFile, "utf8"));
4706
+ } catch {
4707
+ }
4708
+ const lintPath = relative(demo.project, join9(demo.out, "src")).split(sep).join("/");
4709
+ const lintArgs = ["lint", lintPath];
4710
+ const lint = await runChantRaw(lintArgs, demo.project).catch((err) => ({
4711
+ code: 127,
4712
+ stdout: "",
4713
+ stderr: err instanceof Error ? err.message : String(err)
4714
+ }));
4715
+ return {
4716
+ ok: true,
4717
+ select,
4718
+ command: `chant ${args.map((a) => shortenIn(a, demo.root)).join(" ")}`,
4719
+ output,
4720
+ // Emitted source first, scaffolding after: `src/assets.ts` is the thing the
4721
+ // step is about, and a package.json sorting above it buries the answer.
4722
+ artifacts: readArtifacts(demo, demo.out, (rel) => /\.(ts|json)$/.test(rel) && !rel.endsWith("tsconfig.json")).sort(
4723
+ (a, b) => Number(!/\/src\//.test(a.path)) - Number(!/\/src\//.test(b.path))
4724
+ ),
4725
+ boundary,
4726
+ lint: { ok: lint.code === 0, code: lint.code, command: `chant ${lintArgs.join(" ")}`, output: merge(lint, demo.root) },
4727
+ buildCaveat: BUILD_CAVEAT
4728
+ };
4729
+ }
4730
+ async function runCarveBridge(demo, select) {
4731
+ const block = carveWriteBlock(demo);
4732
+ if (block) return refuse2("read-only", block, "Start the walkthrough with `behold demo carve`.");
4733
+ mkdirSync3(demo.out, { recursive: true });
4734
+ const args = [
4735
+ "carve",
4736
+ "bridge",
4737
+ "--from",
4738
+ demo.from,
4739
+ ...demo.state ? ["--state", demo.state] : [],
4740
+ "--select",
4741
+ select,
4742
+ "--output",
4743
+ demo.out
4744
+ ];
4745
+ const run3 = await runChantRaw(args, demo.project).catch((err) => ({
4746
+ code: 127,
4747
+ stdout: "",
4748
+ stderr: err instanceof Error ? err.message : String(err)
4749
+ }));
4750
+ const output = merge(run3, demo.root);
4751
+ if (run3.code !== 0) {
4752
+ return refuse2(
4753
+ "carve-action",
4754
+ `chant carve bridge exited ${run3.code}: ${output || "(no output)"}`,
4755
+ "Run the Emit step first \u2014 bridge reads the carve manifest emit leaves in the output directory."
4756
+ );
4757
+ }
4758
+ const slug3 = carveSlug(select);
4759
+ const all = readArtifacts(demo, demo.out, (rel) => /\.(tf|md|patch)$/.test(rel));
4760
+ return {
4761
+ ok: true,
4762
+ select,
4763
+ command: `chant ${args.map((a) => shortenIn(a, demo.root)).join(" ")}`,
4764
+ output,
4765
+ runbook: all.find((a) => a.path.endsWith(`${slug3}-runbook.md`)) ?? null,
4766
+ proposals: all.filter((a) => !a.path.endsWith(`${slug3}-runbook.md`))
4767
+ };
4768
+ }
4769
+
4770
+ // src/ops.ts
4771
+ import { readdirSync as readdirSync4, readFileSync as readFileSync9, existsSync as existsSync8 } from "node:fs";
4772
+ import { join as join10 } from "node:path";
3110
4773
  var APPLY_TARGET_LEXICON = {
3111
4774
  cloudformation: "aws",
3112
4775
  kubectl: "k8s",
@@ -3128,11 +4791,11 @@ function discoverOps(projectDir) {
3128
4791
  const seen = /* @__PURE__ */ new Set();
3129
4792
  const out = [];
3130
4793
  for (const sub of ["ops", "src", "."]) {
3131
- const dir = join8(projectDir, sub);
3132
- if (!existsSync6(dir)) continue;
3133
- for (const f of readdirSync3(dir)) {
4794
+ const dir = join10(projectDir, sub);
4795
+ if (!existsSync8(dir)) continue;
4796
+ for (const f of readdirSync4(dir)) {
3134
4797
  if (!f.endsWith(".op.ts")) continue;
3135
- const content = readFileSync7(join8(dir, f), "utf8");
4798
+ const content = readFileSync9(join10(dir, f), "utf8");
3136
4799
  const name = content.match(/name:\s*["'`]([^"'`]+)["'`]/)?.[1];
3137
4800
  if (!name || seen.has(name)) continue;
3138
4801
  seen.add(name);
@@ -3248,7 +4911,7 @@ function classifyHealth(status) {
3248
4911
  function rec2(v) {
3249
4912
  return typeof v === "object" && v !== null && !Array.isArray(v) ? v : void 0;
3250
4913
  }
3251
- function str3(v) {
4914
+ function str2(v) {
3252
4915
  return typeof v === "string" && v.length > 0 ? v : void 0;
3253
4916
  }
3254
4917
  function statusTree(o) {
@@ -3269,24 +4932,40 @@ var ARGO_SYNC = {
3269
4932
  OutOfSync: "progressing",
3270
4933
  Unknown: "unknown"
3271
4934
  };
4935
+ var ARGO_ERROR_CONDITION = /Error$/;
4936
+ function argoErrorMessage(tree) {
4937
+ const conditions = tree?.conditions;
4938
+ if (!Array.isArray(conditions)) return void 0;
4939
+ for (const c of conditions) {
4940
+ const type = str2(rec2(c)?.type);
4941
+ if (!type || !ARGO_ERROR_CONDITION.test(type)) continue;
4942
+ const message = str2(rec2(c)?.message);
4943
+ if (message) return message;
4944
+ }
4945
+ return void 0;
4946
+ }
3272
4947
  function argoVerdict(o) {
3273
4948
  if (o.type !== "K8s::Argo::Application") return void 0;
3274
4949
  const tree = statusTree(o);
3275
- let health = str3(rec2(tree?.health)?.status);
3276
- let sync = str3(rec2(tree?.sync)?.status);
4950
+ let health = str2(rec2(tree?.health)?.status);
4951
+ let sync = str2(rec2(tree?.sync)?.status);
3277
4952
  if (!health && !sync) {
3278
- const word = str3(o.status);
4953
+ const word = str2(o.status);
3279
4954
  if (word === "READY") {
3280
4955
  health = "Healthy";
3281
4956
  sync = "Synced";
4957
+ } else if (word && word in ARGO_SYNC) {
4958
+ sync = word;
4959
+ health = "Healthy";
3282
4960
  } else if (word && word in ARGO_HEALTH) health = word;
3283
- else if (word && word in ARGO_SYNC) sync = word;
3284
4961
  }
3285
4962
  const verdicts = [health ? ARGO_HEALTH[health] : void 0, sync ? ARGO_SYNC[sync] : void 0].filter(
3286
4963
  (v) => v !== void 0
3287
4964
  );
3288
4965
  if (verdicts.length === 0) return void 0;
3289
- const detail = [health ? `health=${health}` : void 0, sync ? `sync=${sync}` : void 0].filter(Boolean).join(", ");
4966
+ const pair = [health ? `health=${health}` : void 0, sync ? `sync=${sync}` : void 0].filter(Boolean).join(", ");
4967
+ const message = argoErrorMessage(tree);
4968
+ const detail = message ? `${pair}: ${message}` : pair;
3290
4969
  for (const rung of ["degraded", "progressing", "unknown"]) {
3291
4970
  if (verdicts.includes(rung)) return { health: rung, detail };
3292
4971
  }
@@ -3299,9 +4978,9 @@ function readyFromConditions(tree) {
3299
4978
  for (const c of conditions) {
3300
4979
  const cond = rec2(c);
3301
4980
  if (!cond || cond.type !== "Ready") continue;
3302
- const status = str3(cond.status);
4981
+ const status = str2(cond.status);
3303
4982
  if (status !== "True" && status !== "False" && status !== "Unknown") continue;
3304
- return { status, reason: str3(cond.reason) };
4983
+ return { status, reason: str2(cond.reason) };
3305
4984
  }
3306
4985
  return void 0;
3307
4986
  }
@@ -3318,7 +4997,7 @@ function readyFromChantConditions(o) {
3318
4997
  return void 0;
3319
4998
  }
3320
4999
  function readyFromStatusWord(o) {
3321
- const word = str3(o.status);
5000
+ const word = str2(o.status);
3322
5001
  if (!word || word === "PRESENT") return void 0;
3323
5002
  if (word === "READY") return { status: "True" };
3324
5003
  if (word === "NOT-READY") return { status: "False" };
@@ -3328,8 +5007,8 @@ function describeReady(ready) {
3328
5007
  return `Ready=${ready.status}${ready.reason ? ` (${ready.reason})` : ""}`;
3329
5008
  }
3330
5009
  function revisionVerdict(tree) {
3331
- const applied = str3(tree?.lastAppliedRevision);
3332
- const attempted = str3(tree?.lastAttemptedRevision);
5010
+ const applied = str2(tree?.lastAppliedRevision);
5011
+ const attempted = str2(tree?.lastAttemptedRevision);
3333
5012
  if (!applied || !attempted || applied === attempted) return void 0;
3334
5013
  return { health: "progressing", detail: `applied ${applied}, attempted ${attempted}` };
3335
5014
  }
@@ -3347,7 +5026,7 @@ function fluxVerdict(o) {
3347
5026
  return cond?.type === "Reconciling" && cond.status === "True";
3348
5027
  });
3349
5028
  if (reconciling) return { health: "progressing", detail: "Reconciling=True" };
3350
- if (!str3(o.status) || o.status === "PRESENT") {
5029
+ if (!str2(o.status) || o.status === "PRESENT") {
3351
5030
  return { health: "progressing", detail: "no Ready condition yet" };
3352
5031
  }
3353
5032
  return void 0;
@@ -3642,23 +5321,23 @@ var OpRunner = class {
3642
5321
 
3643
5322
  // src/substrates.ts
3644
5323
  import { spawn as spawn3 } from "node:child_process";
3645
- import { existsSync as existsSync7, readFileSync as readFileSync8 } from "node:fs";
3646
- import { join as join9 } from "node:path";
5324
+ import { existsSync as existsSync9, readFileSync as readFileSync10 } from "node:fs";
5325
+ import { join as join11 } from "node:path";
3647
5326
  import { platform } from "node:os";
3648
5327
  function probe(cmd, args) {
3649
- return new Promise((resolve5) => {
5328
+ return new Promise((resolve8) => {
3650
5329
  let out = "";
3651
5330
  let proc;
3652
5331
  try {
3653
5332
  proc = spawn3(cmd, args, { stdio: ["ignore", "pipe", "pipe"] });
3654
5333
  } catch {
3655
- resolve5({ code: 127, out: "" });
5334
+ resolve8({ code: 127, out: "" });
3656
5335
  return;
3657
5336
  }
3658
5337
  proc.stdout.on("data", (d) => out += d);
3659
5338
  proc.stderr.on("data", (d) => out += d);
3660
- proc.on("error", () => resolve5({ code: 127, out }));
3661
- proc.on("close", (code) => resolve5({ code: code ?? 1, out }));
5339
+ proc.on("error", () => resolve8({ code: 127, out }));
5340
+ proc.on("close", (code) => resolve8({ code: code ?? 1, out }));
3662
5341
  });
3663
5342
  }
3664
5343
  async function dockerAvailable() {
@@ -3671,11 +5350,11 @@ async function dockerRunning(nameFilter) {
3671
5350
  return out.split(/\r?\n/).map((s) => s.trim()).filter(Boolean);
3672
5351
  }
3673
5352
  function scriptBringUp(projectDir, relPath, label) {
3674
- return existsSync7(join9(projectDir, relPath)) ? { label, cmd: "bash", args: [relPath] } : void 0;
5353
+ return existsSync9(join11(projectDir, relPath)) ? { label, cmd: "bash", args: [relPath] } : void 0;
3675
5354
  }
3676
5355
  function projectLexicons(projectDir) {
3677
5356
  try {
3678
- const src = readFileSync8(join9(projectDir, "chant.config.ts"), "utf-8");
5357
+ const src = readFileSync10(join11(projectDir, "chant.config.ts"), "utf-8");
3679
5358
  const m = src.match(/lexicons\s*:\s*\[([^\]]*)\]/);
3680
5359
  if (!m) return [];
3681
5360
  return [...m[1].matchAll(/["']([^"']+)["']/g)].map((x) => x[1]);
@@ -3725,7 +5404,7 @@ async function detectSubstrates(projectDir, preview = false, boundContext) {
3725
5404
  ["forgejo", "Forgejo", ".forgejo", "test/forgejo-runtime-e2e.sh"]
3726
5405
  ];
3727
5406
  for (const [name, label, marker, script] of forges) {
3728
- if (!existsSync7(join9(projectDir, marker))) continue;
5407
+ if (!existsSync9(join11(projectDir, marker))) continue;
3729
5408
  const c = docker ? await dockerRunning(name) : [];
3730
5409
  const d = dep(c.length > 0, "container up", "on-demand (pipeline run)");
3731
5410
  subs.push({
@@ -3755,7 +5434,7 @@ async function detectSubstrates(projectDir, preview = false, boundContext) {
3755
5434
  detail: endpoint ? `targeting ${endpoint}` : "real Fly (FLY_FLAPS_BASE_URL unset)"
3756
5435
  });
3757
5436
  }
3758
- if (existsSync7(join9(projectDir, ".github", "workflows"))) {
5437
+ if (existsSync9(join11(projectDir, ".github", "workflows"))) {
3759
5438
  const gh = await probe("gh", ["auth", "status"]);
3760
5439
  const ready = gh.code === 0;
3761
5440
  subs.push({
@@ -3768,7 +5447,7 @@ async function detectSubstrates(projectDir, preview = false, boundContext) {
3768
5447
  if (lexicons.includes("temporal")) {
3769
5448
  let hasProfiles = false;
3770
5449
  try {
3771
- hasProfiles = /temporal\s*:\s*\{[\s\S]{0,400}?profiles\s*:/.test(readFileSync8(join9(projectDir, "chant.config.ts"), "utf-8"));
5450
+ hasProfiles = /temporal\s*:\s*\{[\s\S]{0,400}?profiles\s*:/.test(readFileSync10(join11(projectDir, "chant.config.ts"), "utf-8"));
3772
5451
  } catch {
3773
5452
  }
3774
5453
  subs.push({
@@ -3780,8 +5459,8 @@ async function detectSubstrates(projectDir, preview = false, boundContext) {
3780
5459
  }
3781
5460
  if (lexicons.includes("helm")) {
3782
5461
  const helm = await probe("helm", ["version", "--short"]);
3783
- const ambient = helm.code === 0 && !boundContext ? await probe("kubectl", ["config", "current-context"]) : { code: 127, out: "" };
3784
- const ctx = boundContext ?? (ambient.code === 0 ? ambient.out.trim() : "");
5462
+ const ambient = helm.code === 0 && !boundContext ? await ambientContext() : void 0;
5463
+ const ctx = boundContext ?? ambient ?? "";
3785
5464
  subs.push({
3786
5465
  name: "helm",
3787
5466
  label: "Helm",
@@ -3831,9 +5510,9 @@ function pickAutoSyncOps(mode, ops, running, movedLexicons, suspended = /* @__PU
3831
5510
  }
3832
5511
 
3833
5512
  // src/history.ts
3834
- import { execFile as execFile3 } from "node:child_process";
3835
- import { promisify as promisify3 } from "node:util";
3836
- var execFileAsync = promisify3(execFile3);
5513
+ import { execFile as execFile2 } from "node:child_process";
5514
+ import { promisify as promisify2 } from "node:util";
5515
+ var execFileAsync = promisify2(execFile2);
3837
5516
  var SEP = "";
3838
5517
  function parseGitLog(stdout) {
3839
5518
  return stdout.split("\n").map((l) => l.trim()).filter(Boolean).map((line) => {
@@ -3872,6 +5551,8 @@ async function openRollbackBranches(projectDir, env) {
3872
5551
  }
3873
5552
 
3874
5553
  // src/estate.ts
5554
+ import { statSync as statSync3 } from "node:fs";
5555
+ import { join as join12, resolve as resolve3, sep as sep2 } from "node:path";
3875
5556
  import { composeStacks, shortStackNames } from "@intentius/pinhole";
3876
5557
  async function composeEstate(projectDirs, opts = {}) {
3877
5558
  const names = shortStackNames(projectDirs);
@@ -3880,7 +5561,7 @@ async function composeEstate(projectDirs, opts = {}) {
3880
5561
  );
3881
5562
  return composeStacks(stacks);
3882
5563
  }
3883
- var firstLine = (e) => {
5564
+ var firstLine2 = (e) => {
3884
5565
  const msg = e instanceof Error ? e.message : String(e);
3885
5566
  const m = msg.match(/exited \d+:\s*([\s\S]*)/);
3886
5567
  return (m ? m[1] : msg).split("\n")[0].trim().slice(0, 160);
@@ -3890,20 +5571,116 @@ function namespaceRuntimeOwners(name, ir) {
3890
5571
  for (const n of ir.nodes) {
3891
5572
  if (n.runtimeOwner && own.has(n.runtimeOwner)) n.runtimeOwner = `${name}/${n.runtimeOwner}`;
3892
5573
  }
3893
- return ir;
5574
+ return ir;
5575
+ }
5576
+ var NAMESPACE_JOIN_FLOOR = "0.44.5";
5577
+ var rec3 = (v) => v && typeof v === "object" && !Array.isArray(v) ? v : void 0;
5578
+ var str3 = (v) => typeof v === "string" && v.length > 0 ? v : void 0;
5579
+ var realIsDir = (p) => {
5580
+ try {
5581
+ return statSync3(p).isDirectory();
5582
+ } catch {
5583
+ return false;
5584
+ }
5585
+ };
5586
+ function declaredNamespaceBindings(ir) {
5587
+ const out = [];
5588
+ for (const n of ir.nodes) {
5589
+ if (n.kind !== "K8s::Flux::Kustomization") continue;
5590
+ const spec = rec3(n.attrs?.spec);
5591
+ const namespace = str3(spec?.targetNamespace);
5592
+ const path = str3(spec?.path);
5593
+ if (namespace && path) out.push({ path, namespace });
5594
+ }
5595
+ return out;
5596
+ }
5597
+ function awaitsNamespaceBinding(ir) {
5598
+ const namespaced = ir.nodes.filter((n) => n.lexicon === "k8s" && !CLUSTER_SCOPED.has(n.kind ?? ""));
5599
+ if (namespaced.length === 0) return false;
5600
+ return namespaced.every((n) => !str3(rec3(n.attrs?.metadata)?.namespace));
5601
+ }
5602
+ function declaredSegments(path) {
5603
+ if (path.startsWith("/")) return void 0;
5604
+ const segs = path.split("/").filter((s) => s !== "" && s !== ".");
5605
+ if (segs.length === 0 || segs.includes("..")) return void 0;
5606
+ return segs;
5607
+ }
5608
+ function pathAlignment(declared, dir, isDir = realIsDir) {
5609
+ const want = declaredSegments(declared);
5610
+ if (!want) return 0;
5611
+ const root = resolve3(dir);
5612
+ const have = root.split(sep2).filter(Boolean);
5613
+ for (let k = Math.min(want.length, have.length); k >= 1; k--) {
5614
+ if (!have.slice(-k).every((s, i) => s === want[i])) continue;
5615
+ if (!isDir(join12(root, ...want.slice(k)))) continue;
5616
+ return k;
5617
+ }
5618
+ return 0;
5619
+ }
5620
+ function joinNamespaceBindings(members, isDir = realIsDir) {
5621
+ const bindings = members.flatMap(
5622
+ (m) => m.ir ? declaredNamespaceBindings(m.ir).map((b) => ({ ...b, declaredBy: m.dir })) : []
5623
+ );
5624
+ if (bindings.length === 0) return [];
5625
+ const awaiting = members.filter((m) => m.ir && awaitsNamespaceBinding(m.ir));
5626
+ if (awaiting.length === 0) return [];
5627
+ const best = /* @__PURE__ */ new Map();
5628
+ for (const b of bindings) {
5629
+ let winner;
5630
+ let tied = false;
5631
+ for (const m of awaiting) {
5632
+ if (m.dir === b.declaredBy) continue;
5633
+ const score = pathAlignment(b.path, m.dir, isDir);
5634
+ if (score === 0) continue;
5635
+ if (!winner || score > winner.score) {
5636
+ winner = { dir: m.dir, score };
5637
+ tied = false;
5638
+ } else if (score === winner.score) {
5639
+ tied = true;
5640
+ }
5641
+ }
5642
+ if (!winner || tied) continue;
5643
+ const prev = best.get(winner.dir);
5644
+ if (prev === "ambiguous") continue;
5645
+ const join18 = { dir: winner.dir, namespace: b.namespace, path: b.path, declaredBy: b.declaredBy };
5646
+ if (!prev) best.set(winner.dir, { score: winner.score, join: join18 });
5647
+ else if (prev.join.namespace !== b.namespace) best.set(winner.dir, "ambiguous");
5648
+ else if (winner.score > prev.score) best.set(winner.dir, { score: winner.score, join: join18 });
5649
+ }
5650
+ return [...best.values()].filter((v) => v !== "ambiguous").map((v) => v.join);
5651
+ }
5652
+ async function estateNamespaceScopes(projectDirs, opts, isDir = realIsDir) {
5653
+ if (projectDirs.length < 2) return /* @__PURE__ */ new Map();
5654
+ const { live: _live, overlay: _overlay, namespace: _namespace, ...src } = opts;
5655
+ const irs = await Promise.all(projectDirs.map((dir) => graphIr(dir, { ...src, detail: 3 }).catch(() => void 0)));
5656
+ const scopes = /* @__PURE__ */ new Map();
5657
+ for (const j of joinNamespaceBindings(projectDirs.map((dir, i) => ({ dir, ir: irs[i] })), isDir)) {
5658
+ if (!meetsFloor(resolveChant(j.dir).version, NAMESPACE_JOIN_FLOOR)) continue;
5659
+ scopes.set(j.dir, j.namespace);
5660
+ }
5661
+ return scopes;
5662
+ }
5663
+ function withoutJoinedMembers(nodes, joined) {
5664
+ if (joined.length === 0) return [...nodes];
5665
+ return nodes.filter((n) => !joined.some((j) => n.id.startsWith(`${j.name}/`)));
3894
5666
  }
3895
5667
  async function composeEstateOverlay(projectDirs, opts, classify) {
3896
5668
  const names = shortStackNames(projectDirs);
3897
5669
  const unobserved = [];
3898
5670
  const dropped = [];
5671
+ const joined = [];
3899
5672
  const stacks = new Array(projectDirs.length);
5673
+ const scopes = await estateNamespaceScopes(projectDirs, opts);
3900
5674
  await Promise.all(
3901
5675
  projectDirs.map(async (dir, i) => {
3902
5676
  const name = names[i];
5677
+ const namespace = scopes.get(dir);
3903
5678
  try {
3904
- stacks[i] = { name, ir: namespaceRuntimeOwners(name, classify(await graphIr(dir, { ...opts, live: true, overlay: true }))) };
5679
+ const live = { ...opts, live: true, overlay: true, ...namespace ? { namespace } : {} };
5680
+ stacks[i] = { name, ir: namespaceRuntimeOwners(name, classify(await graphIr(dir, live))) };
5681
+ if (namespace) joined.push({ name, namespace });
3905
5682
  } catch (err) {
3906
- const reason = firstLine(err);
5683
+ const reason = firstLine2(err);
3907
5684
  try {
3908
5685
  const { env: _env, live: _live, overlay: _overlay, ...srcOpts } = opts;
3909
5686
  const src = await graphIr(dir, srcOpts);
@@ -3911,7 +5688,7 @@ async function composeEstateOverlay(projectDirs, opts, classify) {
3911
5688
  stacks[i] = { name, ir: src };
3912
5689
  unobserved.push({ name, reason });
3913
5690
  } catch (err2) {
3914
- dropped.push({ name, reason: firstLine(err2) });
5691
+ dropped.push({ name, reason: firstLine2(err2) });
3915
5692
  }
3916
5693
  }
3917
5694
  })
@@ -3922,13 +5699,16 @@ async function composeEstateOverlay(projectDirs, opts, classify) {
3922
5699
  observed: present.length - unobserved.length,
3923
5700
  total: projectDirs.length,
3924
5701
  unobserved,
3925
- dropped
5702
+ dropped,
5703
+ // Concurrent reads finish in whatever order the clusters answer; the note
5704
+ // this feeds should read the same twice.
5705
+ joined: joined.sort((a, b) => a.name.localeCompare(b.name))
3926
5706
  };
3927
5707
  }
3928
5708
 
3929
5709
  // src/events.ts
3930
- import { watch, existsSync as existsSync8 } from "node:fs";
3931
- import { join as join10 } from "node:path";
5710
+ import { watch, existsSync as existsSync10 } from "node:fs";
5711
+ import { join as join13 } from "node:path";
3932
5712
  var Broadcaster = class {
3933
5713
  listeners = /* @__PURE__ */ new Set();
3934
5714
  subscribe(fn) {
@@ -3946,7 +5726,7 @@ var Broadcaster = class {
3946
5726
  };
3947
5727
  var IGNORE = /(^|[\\/])(node_modules|dist|\.git)([\\/]|$)/;
3948
5728
  function watchSource(projectDir, onChange, debounceMs = 200) {
3949
- const dir = existsSync8(join10(projectDir, "src")) ? join10(projectDir, "src") : projectDir;
5729
+ const dir = existsSync10(join13(projectDir, "src")) ? join13(projectDir, "src") : projectDir;
3950
5730
  let timer;
3951
5731
  const watcher = watch(dir, { recursive: true }, (_event, file) => {
3952
5732
  const name = typeof file === "string" ? file : "";
@@ -4223,9 +6003,108 @@ async function emulatorDown(projectDir) {
4223
6003
  await runChantRaw(["emulator", "down"], projectDir);
4224
6004
  }
4225
6005
 
6006
+ // src/demos.ts
6007
+ import { readFileSync as readFileSync11, existsSync as existsSync11, cpSync } from "node:fs";
6008
+ import { join as join14, relative as relative2, resolve as resolve4, sep as sep3 } from "node:path";
6009
+ import { spawn as spawn4, spawnSync } from "node:child_process";
6010
+ function loadDemoRegistry(pkgRoot2) {
6011
+ let raw;
6012
+ try {
6013
+ raw = JSON.parse(readFileSync11(join14(pkgRoot2, "demos.json"), "utf8"));
6014
+ } catch {
6015
+ return [];
6016
+ }
6017
+ const list3 = raw?.demos;
6018
+ if (!Array.isArray(list3)) return [];
6019
+ return list3.filter((e) => {
6020
+ const d = e;
6021
+ if (!d || typeof d.name !== "string" || !d.name || typeof d.description !== "string") return false;
6022
+ if (d.source === "bundled") {
6023
+ if (typeof d.dir !== "string" || !d.dir) return false;
6024
+ } else if (d.source === "git") {
6025
+ if (typeof d.repo !== "string" || !d.repo) return false;
6026
+ } else {
6027
+ return false;
6028
+ }
6029
+ if (!Array.isArray(d.requires) || d.requires.some((r) => typeof r !== "string")) return false;
6030
+ if (!d.serve || typeof d.serve !== "object") return false;
6031
+ if (d.serve.dirs !== void 0 && (!Array.isArray(d.serve.dirs) || d.serve.dirs.some((x) => typeof x !== "string") || !d.serve.dirs.length))
6032
+ return false;
6033
+ if (d.serve.carve !== void 0) {
6034
+ const c = d.serve.carve;
6035
+ const rel = (v) => typeof v === "string" && !!v && !v.startsWith("/") && !v.split("/").includes("..");
6036
+ if (!c || typeof c !== "object") return false;
6037
+ if (!rel(c.report) || !rel(c.from) || !rel(c.project) || !rel(c.out)) return false;
6038
+ if (c.state !== void 0 && !rel(c.state)) return false;
6039
+ if (!`${c.out}/`.startsWith(`${c.project}/`)) return false;
6040
+ }
6041
+ return true;
6042
+ });
6043
+ }
6044
+ function missingRequirements(entry) {
6045
+ const bins = entry.source === "git" && !entry.requires.includes("git") ? [...entry.requires, "git"] : entry.requires;
6046
+ const finder = process.platform === "win32" ? "where" : "which";
6047
+ return bins.filter((bin) => spawnSync(finder, [bin], { stdio: "ignore" }).status !== 0);
6048
+ }
6049
+ function fetchesFromNetwork(entry) {
6050
+ return entry.source === "git";
6051
+ }
6052
+ function demoTargetDir(entry, cwd = process.cwd()) {
6053
+ const legacy = resolve4(cwd, "behold-demo");
6054
+ if (entry.name === "writes" && existsSync11(legacy)) return legacy;
6055
+ return resolve4(cwd, "behold-demos", entry.name);
6056
+ }
6057
+ async function loadDemo(entry, opts) {
6058
+ const { pkgRoot: pkgRoot2, target } = opts;
6059
+ const say = (line) => opts.log?.(`behold demo ${entry.name} \u2192 ${line}`);
6060
+ if (!existsSync11(target)) {
6061
+ if (entry.source === "bundled") {
6062
+ const bundled = join14(pkgRoot2, entry.dir);
6063
+ if (!existsSync11(bundled)) return { ok: false, error: `this install has no bundled ${entry.dir}` };
6064
+ say(`copying to ${target} (it's yours \u2014 edit it)`);
6065
+ try {
6066
+ cpSync(bundled, target, {
6067
+ recursive: true,
6068
+ filter: (src) => !relative2(bundled, src).split(sep3).includes("node_modules")
6069
+ });
6070
+ } catch (err) {
6071
+ return { ok: false, error: `copy failed: ${err instanceof Error ? err.message : String(err)}` };
6072
+ }
6073
+ } else {
6074
+ say(`cloning ${entry.repo} to ${target}`);
6075
+ if (await runStep("git", ["clone", "--depth", "1", entry.repo, target]) !== 0) {
6076
+ return { ok: false, error: `clone of ${entry.repo} failed` };
6077
+ }
6078
+ }
6079
+ } else {
6080
+ say(`reusing ${target}`);
6081
+ }
6082
+ if (existsSync11(join14(target, "package.json")) && !existsSync11(join14(target, "node_modules"))) {
6083
+ say("npm install\u2026");
6084
+ if (await runStep("npm", ["install"], { cwd: target, shell: process.platform === "win32" }) !== 0) {
6085
+ return { ok: false, error: `npm install failed in ${target}` };
6086
+ }
6087
+ }
6088
+ if (entry.setup) {
6089
+ say(entry.setup);
6090
+ if (await runStep(entry.setup, [], { cwd: target, shell: true }) !== 0) {
6091
+ return { ok: false, error: `setup failed (${entry.setup})` };
6092
+ }
6093
+ }
6094
+ return { ok: true, serveDirs: entry.serve.dirs?.length ? entry.serve.dirs.map((d) => join14(target, d)) : [target] };
6095
+ }
6096
+ function runStep(cmd, args, opts = {}) {
6097
+ return new Promise((res) => {
6098
+ const child = spawn4(cmd, args, { stdio: "inherit", cwd: opts.cwd, shell: opts.shell ?? false });
6099
+ child.on("error", () => res(-1));
6100
+ child.on("close", (code) => res(code ?? 1));
6101
+ });
6102
+ }
6103
+
4226
6104
  // src/server.ts
4227
- var webRoot = join11(dirname4(fileURLToPath3(import.meta.url)), "..", "web");
4228
- var execFileP = async (cmd, args) => (await promisify4(execFile4)(cmd, args, { encoding: "utf8", timeout: 1e4 })).stdout;
6105
+ var pkgRoot = join15(dirname4(fileURLToPath3(import.meta.url)), "..");
6106
+ var webRoot = join15(pkgRoot, "web");
6107
+ var execFileP = async (cmd, args) => (await promisify3(execFile3)(cmd, args, { encoding: "utf8", timeout: 1e4 })).stdout;
4229
6108
  function optsFromQuery(url, tierEnvVar, projectDir) {
4230
6109
  const q = url.searchParams;
4231
6110
  const opts = {};
@@ -4274,7 +6153,7 @@ function tierFailure(tier, message) {
4274
6153
  function beholdVersion() {
4275
6154
  try {
4276
6155
  const here = dirname4(fileURLToPath3(import.meta.url));
4277
- return JSON.parse(readFileSync9(join11(here, "..", "package.json"), "utf8")).version ?? "unknown";
6156
+ return JSON.parse(readFileSync12(join15(here, "..", "package.json"), "utf8")).version ?? "unknown";
4278
6157
  } catch {
4279
6158
  return "unknown";
4280
6159
  }
@@ -4318,12 +6197,125 @@ async function captureFrame(projectDir, env, frames, broadcaster) {
4318
6197
  return null;
4319
6198
  }
4320
6199
  }
6200
+ function carveRoutes(app, reportPath, demo) {
6201
+ const load = () => readCarveReport(reportPath, (p) => readFileSync12(p, "utf8"));
6202
+ const demoBlock = () => carveWriteBlock(demo);
6203
+ const demoInfo = () => {
6204
+ if (!demo) return null;
6205
+ const block = demoBlock();
6206
+ return {
6207
+ root: demo.root,
6208
+ from: demo.from,
6209
+ state: demo.state ?? null,
6210
+ project: demo.project,
6211
+ out: demo.out,
6212
+ // Paths the UI can print without leaking the operator's whole home dir.
6213
+ outLabel: relative3(demo.root, demo.out).split(sep4).join("/"),
6214
+ fromLabel: relative3(demo.root, demo.from).split(sep4).join("/"),
6215
+ runnable: !block,
6216
+ ...block ? { reason: block } : {},
6217
+ ...demo.degraded ? { degraded: demo.degraded } : {},
6218
+ buildCaveat: BUILD_CAVEAT
6219
+ };
6220
+ };
6221
+ app.get("/api/carve", (c) => {
6222
+ const parsed = load();
6223
+ return parsed.ok ? c.json(parsed.report) : c.json(parsed.refusal, 422);
6224
+ });
6225
+ app.get("/api/graph", (c) => {
6226
+ const parsed = load();
6227
+ if (!parsed.ok) return c.json(parsed.refusal, 422);
6228
+ const ir = carveReportToIr(parsed.report);
6229
+ const { svg } = renderBanded(ir);
6230
+ return c.json({
6231
+ ir,
6232
+ svg,
6233
+ meta: {
6234
+ projectDir: reportPath,
6235
+ env: null,
6236
+ tier: null,
6237
+ target: null,
6238
+ carve: true,
6239
+ // A demo whose own advisor run failed says so on the statusbar, not
6240
+ // only in the terminal the viewer isn't looking at.
6241
+ note: carveNote(parsed.report, ir) + (demo?.degraded ? ` Degraded: ${demo.degraded}` : "")
6242
+ }
6243
+ });
6244
+ });
6245
+ app.get("/api/project", (c) => {
6246
+ const parsed = load();
6247
+ return c.json({
6248
+ projectDir: reportPath,
6249
+ recents: [],
6250
+ // No envs, tiers, stacks or targets: a peelability report is a static
6251
+ // analysis of foreign Terraform, so every picker that would imply a live
6252
+ // axis stays empty and the SPA renders none of them.
6253
+ environments: [],
6254
+ lexicons: ["terraform"],
6255
+ currentEnv: null,
6256
+ targets: [],
6257
+ carve: parsed.ok ? {
6258
+ report: reportPath,
6259
+ from: parsed.report.from ?? null,
6260
+ count: parsed.report.count ?? parsed.report.resources.length,
6261
+ bands: parsed.report.bands ?? {},
6262
+ advisory: parsed.report.advisory ?? null,
6263
+ demo: demoInfo()
6264
+ } : { report: reportPath, demo: demoInfo() }
6265
+ });
6266
+ });
6267
+ const runStep2 = async (c, run3) => {
6268
+ const block = demoBlock();
6269
+ if (block) {
6270
+ return c.json(
6271
+ {
6272
+ error: block,
6273
+ code: "read-only",
6274
+ remedy: "The carve steps run inside a demo copy \u2014 start the walkthrough with `behold demo carve`."
6275
+ },
6276
+ 403
6277
+ );
6278
+ }
6279
+ if (!(c.req.header("content-type") ?? "").includes("application/json")) {
6280
+ return c.json({ error: "send application/json", code: "carve-select", remedy: 'POST {"select": "<terraform address>"}' }, 415);
6281
+ }
6282
+ const text = await c.req.text().catch(() => "");
6283
+ if (text.length > 4096) return c.json({ error: "a carve step body is capped at 4096 bytes", code: "carve-select", remedy: 'POST {"select": "\u2026"}' }, 413);
6284
+ let body;
6285
+ try {
6286
+ body = JSON.parse(text || "null");
6287
+ } catch {
6288
+ return c.json({ error: "body must be JSON", code: "carve-select", remedy: 'POST {"select": "<terraform address>"}' }, 400);
6289
+ }
6290
+ const parsed = load();
6291
+ const select = selectFromReport(parsed.ok ? parsed.report : void 0, body?.select);
6292
+ if (!select) {
6293
+ return c.json(
6294
+ {
6295
+ error: `\`select\` must name a resource this report ranks \u2014 ${JSON.stringify(body?.select ?? null)} isn't one of them.`,
6296
+ code: "carve-select",
6297
+ remedy: "Pick a card in the graph, or read the addresses off GET /api/carve."
6298
+ },
6299
+ 400
6300
+ );
6301
+ }
6302
+ const result = await run3(demo, select);
6303
+ return result.ok ? c.json(result) : c.json(result.refusal, 422);
6304
+ };
6305
+ app.post("/api/carve/emit", (c) => runStep2(c, runCarveEmit));
6306
+ app.post("/api/carve/bridge", (c) => runStep2(c, runCarveBridge));
6307
+ app.get("/api/substrates", (c) => c.json({ substrates: [] }));
6308
+ app.get("/api/history", (c) => c.json({ commits: [] }));
6309
+ app.get("/api/resources", (c) => c.json({ byComponent: {} }));
6310
+ app.get("/api/ci", (c) => c.json({ stages: [], jobs: [], forge: null }));
6311
+ }
4321
6312
  function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffer(), runner = new OpRunner({
4322
6313
  projectDir: cfg.projectDir,
4323
6314
  broadcaster,
4324
6315
  onDone: (opEnv) => captureFrame(cfg.projectDir, opEnv ?? cfg.env, frames, broadcaster)
4325
6316
  })) {
4326
6317
  const app = new Hono();
6318
+ if (cfg.carveReport) carveRoutes(app, cfg.carveReport, cfg.carveDemo);
4327
6319
  let beholdConfig = loadBeholdConfig(cfg.projectDir);
4328
6320
  let tierEnvVar = beholdConfig.tiers?.envVar;
4329
6321
  const boundK8sContext = async (env) => {
@@ -4408,9 +6400,9 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
4408
6400
  return c.json({ started: true, name, ran: label });
4409
6401
  });
4410
6402
  app.post("/api/local/reset", (c) => {
4411
- const down = join11(cfg.projectDir, "scripts/local/local-down.sh");
4412
- const up = join11(cfg.projectDir, "scripts/local/local-up.sh");
4413
- if (!existsSync9(down) || !existsSync9(up)) {
6403
+ const down = join15(cfg.projectDir, "scripts/local/local-down.sh");
6404
+ const up = join15(cfg.projectDir, "scripts/local/local-up.sh");
6405
+ if (!existsSync12(down) || !existsSync12(up)) {
4414
6406
  return c.json({ error: "no local-down.sh / local-up.sh in scripts/local \u2014 reset is only for local emulator projects" }, 400);
4415
6407
  }
4416
6408
  if (!runner.bringUp("reset local emulator", "bash", ["-c", "bash scripts/local/local-down.sh && bash scripts/local/local-up.sh"], cfg.projectDir)) {
@@ -4487,26 +6479,29 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
4487
6479
  app.post("/api/project/open", async (c) => {
4488
6480
  if (cfg.previewMode) return c.json({ error: "switching projects is locked in preview mode" }, 403);
4489
6481
  const body = await c.req.json().catch(() => ({}));
4490
- const dir = typeof body.dir === "string" && body.dir.trim() ? resolve2(body.dir.trim()) : "";
4491
- if (!dir || !existsSync9(dir)) return c.json({ error: `no such directory: ${dir || "(no dir given)"}` }, 400);
4492
- if (!existsSync9(join11(dir, "chant.config.ts"))) {
6482
+ const dir = typeof body.dir === "string" && body.dir.trim() ? resolve5(body.dir.trim()) : "";
6483
+ if (!dir || !existsSync12(dir)) return c.json({ error: `no such directory: ${dir || "(no dir given)"}` }, 400);
6484
+ if (!existsSync12(join15(dir, "chant.config.ts"))) {
4493
6485
  return c.json({ error: `${dir} doesn't look like a chant project \u2014 no chant.config.ts` }, 400);
4494
6486
  }
6487
+ switchServedProject([dir]);
6488
+ return c.json({ ok: true, projectDir: dir });
6489
+ });
6490
+ const switchServedProject = (dirs, env) => {
4495
6491
  addRecent(cfg.projectDir);
4496
- cfg.projectDir = dir;
4497
- cfg.projectDirs = void 0;
4498
- cfg.env = void 0;
4499
- beholdConfig = loadBeholdConfig(dir);
6492
+ cfg.projectDir = dirs[0];
6493
+ cfg.projectDirs = dirs.length > 1 ? dirs : void 0;
6494
+ cfg.env = env;
6495
+ beholdConfig = loadBeholdConfig(dirs[0]);
4500
6496
  tierEnvVar = beholdConfig.tiers?.envVar;
4501
- runner.retarget(dir);
4502
- addRecent(dir);
4503
- cfg.onProjectSwitch?.(dir);
6497
+ runner.retarget(dirs[0]);
6498
+ for (const d of dirs) addRecent(d);
6499
+ cfg.onProjectSwitch?.(dirs[0]);
4504
6500
  broadcaster.emit("changed");
4505
- return c.json({ ok: true, projectDir: dir });
4506
- });
6501
+ };
4507
6502
  app.post("/api/project/reveal", async (c) => {
4508
6503
  const body = await c.req.json().catch(() => ({}));
4509
- const dir = typeof body.dir === "string" && body.dir.trim() ? resolve2(body.dir.trim()) : cfg.projectDir;
6504
+ const dir = typeof body.dir === "string" && body.dir.trim() ? resolve5(body.dir.trim()) : cfg.projectDir;
4510
6505
  const known = /* @__PURE__ */ new Set([cfg.projectDir, ...cfg.projectDirs ?? [], ...listRecents().map((r) => r.dir)]);
4511
6506
  if (!known.has(dir)) return c.json({ error: "not a served or recent project directory" }, 400);
4512
6507
  const opener = process.platform === "darwin" ? "open" : process.platform === "win32" ? "explorer" : "xdg-open";
@@ -4517,6 +6512,144 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
4517
6512
  return c.json({ error: err instanceof Error ? err.message : String(err) }, 500);
4518
6513
  }
4519
6514
  });
6515
+ const demoRow = (e) => {
6516
+ const missing = missingRequirements(e);
6517
+ const target = demoTargetDir(e);
6518
+ return {
6519
+ name: e.name,
6520
+ description: e.description,
6521
+ requires: e.requires,
6522
+ source: e.source,
6523
+ // #268's consent half: a git entry is cloned from a public repo, so the
6524
+ // button says so (and names the repo) BEFORE anything is fetched.
6525
+ fetches: fetchesFromNetwork(e),
6526
+ ...e.repo ? { repo: e.repo } : {},
6527
+ target,
6528
+ loaded: existsSync12(target),
6529
+ satisfiable: missing.length === 0,
6530
+ // #254: the carve walkthrough runs fine here — it just isn't a project
6531
+ // to switch INTO. Carve mode claims `/api/graph` and `/api/project` at
6532
+ // app creation (see carveRoutes), so a running server cannot become one;
6533
+ // the row stays in the catalog, disabled, saying what to run instead.
6534
+ ...e.serve.carve ? { switchable: false } : {},
6535
+ ...missing.length ? { reason: `needs ${missing.join(", ")} on PATH` } : e.serve.carve ? { reason: `serves a carve report, not a project \u2014 run \`behold demo ${e.name}\`` } : {}
6536
+ };
6537
+ };
6538
+ app.get(
6539
+ "/api/demos",
6540
+ (c) => c.json({
6541
+ demos: loadDemoRegistry(pkgRoot).map(demoRow),
6542
+ ...cfg.previewMode ? { locked: "loading a demo is locked in preview mode" } : {}
6543
+ })
6544
+ );
6545
+ let loadingDemo = null;
6546
+ app.post("/api/demos/open", async (c) => {
6547
+ if (cfg.previewMode) return c.json({ error: "loading a demo is locked in preview mode" }, 403);
6548
+ const body = await c.req.json().catch(() => ({}));
6549
+ const name = typeof body.name === "string" ? body.name.trim() : "";
6550
+ const entry = loadDemoRegistry(pkgRoot).find((e) => e.name === name);
6551
+ if (!entry) {
6552
+ return c.json({ error: `no "${name || "(no name given)"}" in this install's demo catalog` }, 400);
6553
+ }
6554
+ const missing = missingRequirements(entry);
6555
+ if (missing.length) return c.json({ error: `${entry.name} needs ${missing.join(", ")} on PATH` }, 400);
6556
+ if (entry.serve.carve) {
6557
+ return c.json(
6558
+ {
6559
+ error: `${entry.name} serves a carve report, not a chant project \u2014 a running server can't switch into carve mode.`,
6560
+ remedy: `Run \`behold demo ${entry.name}\` in a terminal.`
6561
+ },
6562
+ 400
6563
+ );
6564
+ }
6565
+ if (loadingDemo) return c.json({ error: `${loadingDemo} is still loading` }, 409);
6566
+ loadingDemo = entry.name;
6567
+ try {
6568
+ const target = demoTargetDir(entry);
6569
+ const loaded = await loadDemo(entry, { pkgRoot, target, log: (line) => process.stdout.write(line + "\n") });
6570
+ if (!loaded.ok) return c.json({ error: loaded.error }, 500);
6571
+ const dirs = loaded.serveDirs;
6572
+ if (!existsSync12(join15(dirs[0], "chant.config.ts"))) {
6573
+ return c.json({ error: `${entry.name} loaded to ${target} but ${dirs[0]} has no chant.config.ts` }, 500);
6574
+ }
6575
+ if (entry.serve.local) {
6576
+ if (cfg.emulators?.length) await emulatorDown(cfg.projectDir).catch(() => {
6577
+ });
6578
+ cfg.local = true;
6579
+ cfg.emulators = await bootLocalEmulators(dirs[0], `behold demo ${entry.name} --local`);
6580
+ }
6581
+ switchServedProject(dirs, entry.serve.env);
6582
+ return c.json({ ok: true, demo: entry.name, projectDir: dirs[0], projectDirs: dirs, env: entry.serve.env ?? null });
6583
+ } finally {
6584
+ loadingDemo = null;
6585
+ }
6586
+ });
6587
+ const layoutWriteBlock = () => {
6588
+ if (cfg.previewMode) return "the layout sidecar is read-only in preview mode";
6589
+ if (cfg.carveReport) return "a carve report isn't a project \u2014 there's nowhere to keep a hand layout";
6590
+ if (cfg.layoutWrites === false) return "a static export captures a snapshot \u2014 it doesn't write to the project";
6591
+ return unwritableReason(cfg.projectDir);
6592
+ };
6593
+ app.get("/api/layout", (c) => {
6594
+ const block = layoutWriteBlock();
6595
+ const shared = { path: layoutPath(cfg.projectDir), writable: !block, ...block ? { reason: block } : {} };
6596
+ const raw = new URL(c.req.url).searchParams.get("lens");
6597
+ if (raw === null) return c.json({ ...shared, lenses: readLayoutFile(cfg.projectDir).lenses });
6598
+ const lens = normalizeLens(raw);
6599
+ if (!lens) return c.json({ error: `not a lens key: ${raw}`, code: "bad-layout" }, 400);
6600
+ return c.json({ ...shared, lens, deltas: readLens(cfg.projectDir, lens) });
6601
+ });
6602
+ app.post("/api/layout", async (c) => {
6603
+ const block = layoutWriteBlock();
6604
+ if (block) return c.json({ error: block, code: "read-only" }, 403);
6605
+ if (!(c.req.header("content-type") ?? "").includes("application/json")) {
6606
+ return c.json({ error: "send application/json", code: "bad-layout" }, 415);
6607
+ }
6608
+ const declared = Number(c.req.header("content-length") ?? 0);
6609
+ if (declared > MAX_BODY_BYTES) return c.json({ error: `a layout body is capped at ${MAX_BODY_BYTES} bytes`, code: "too-large" }, 413);
6610
+ const text = await c.req.text().catch(() => "");
6611
+ if (text.length > MAX_BODY_BYTES) return c.json({ error: `a layout body is capped at ${MAX_BODY_BYTES} bytes`, code: "too-large" }, 413);
6612
+ let body;
6613
+ try {
6614
+ body = JSON.parse(text || "null");
6615
+ } catch {
6616
+ return c.json({ error: "body must be JSON", code: "bad-layout" }, 400);
6617
+ }
6618
+ const lens = normalizeLens(body?.lens);
6619
+ if (!lens) return c.json({ error: "body needs a `lens` key (the zoom stop, plus +radial / +stack-<name>)", code: "bad-layout" }, 400);
6620
+ if (!body?.deltas || typeof body.deltas !== "object" || Array.isArray(body.deltas)) {
6621
+ return c.json({ error: "body needs `deltas`: {<node id>: {dx,dy,dw,dh}}", code: "bad-layout" }, 400);
6622
+ }
6623
+ try {
6624
+ const stored = writeLens(cfg.projectDir, lens, body.deltas);
6625
+ return c.json({ ok: true, lens: stored.lens, deltas: stored.deltas, count: Object.keys(stored.deltas).length, path: layoutPath(cfg.projectDir) });
6626
+ } catch (err) {
6627
+ if (err instanceof LayoutTooLarge) return c.json({ error: err.message, code: "too-large" }, 413);
6628
+ return c.json({ error: err instanceof Error ? err.message : String(err) }, 500);
6629
+ }
6630
+ });
6631
+ const bakeHandLayout = async (c, next) => {
6632
+ await next();
6633
+ const url = new URL(c.req.url);
6634
+ if (url.searchParams.get("layout") !== "1") return;
6635
+ const res = c.res;
6636
+ if (!res || res.status !== 200 || !(res.headers.get("content-type") ?? "").includes("json")) return;
6637
+ const lens = lensFromQuery(url.searchParams);
6638
+ const deltas = readLens(cfg.projectDir, lens);
6639
+ if (!Object.keys(deltas).length) return;
6640
+ let body;
6641
+ try {
6642
+ body = await res.clone().json();
6643
+ } catch {
6644
+ return;
6645
+ }
6646
+ if (typeof body.svg !== "string") return;
6647
+ const { svg, applied } = applyLayoutToSvg(body.svg, deltas);
6648
+ if (!applied) return;
6649
+ c.res = c.json({ ...body, svg, meta: { ...body.meta ?? {}, layout: { lens, applied } } });
6650
+ };
6651
+ app.use("/api/graph", bakeHandLayout);
6652
+ app.use("/api/overlay", bakeHandLayout);
4520
6653
  app.get(
4521
6654
  "/api",
4522
6655
  (c) => c.json({
@@ -4528,8 +6661,29 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
4528
6661
  { method: "GET", path: "/api/project", desc: "project info: dir, recents, environments, tiers, targets, stacks, preview lock" },
4529
6662
  { method: "POST", path: "/api/project/open", desc: "switch the served project: JSON body {dir} (validated; preview-locked)" },
4530
6663
  { method: "POST", path: "/api/project/reveal", desc: "open the OS file manager at a served/recent project dir: JSON body {dir?}" },
4531
- { method: "GET", path: "/api/graph", desc: "the graph {ir, svg, meta} \u2014 params: detail=0..3, components=1, logical=1, env, stack, tier, target, lens, up=1, down=1, radial=1" },
6664
+ { method: "GET", path: "/api/demos", desc: "the bundled demo catalog: [{name, description, requires, satisfiable, reason?, fetches, repo?, target, loaded}]" },
6665
+ {
6666
+ method: "POST",
6667
+ path: "/api/demos/open",
6668
+ desc: "load a demo and serve it: JSON body {name} (a catalog name, never a path) \u2014 copies/clones, installs, runs its setup, then switches (preview-locked)"
6669
+ },
6670
+ { method: "GET", path: "/api/graph", desc: "the graph {ir, svg, meta} \u2014 params: detail=0..3, components=1, logical=1, env, stack, tier, target, lens, up=1, down=1, radial=1, layout=1" },
6671
+ ...cfg.carveReport ? [
6672
+ { method: "GET", path: "/api/carve", desc: "carve mode: the raw `chant carve advise --json` peelability report this server is rendering" },
6673
+ {
6674
+ method: "POST",
6675
+ path: "/api/carve/emit",
6676
+ desc: "carve demo only (#254): run `chant carve emit --state --select <addr>` into the demo copy \u2014 JSON body {select}; answers {artifacts, boundary, lint}"
6677
+ },
6678
+ {
6679
+ method: "POST",
6680
+ path: "/api/carve/bridge",
6681
+ desc: "carve demo only (#254): run `chant carve bridge` (never --apply-rewrites) \u2014 JSON body {select}; answers {runbook, proposals}"
6682
+ }
6683
+ ] : [],
4532
6684
  { method: "GET", path: "/api/overlay", desc: "live drift overlay for ?env= \u2014 same shape/params as /api/graph, plus runtime=1" },
6685
+ { method: "GET", path: "/api/layout", desc: "hand-layout sidecar (.behold/layout.json): ?lens=<key> \u2192 {lens, deltas, writable}; no lens \u2192 every lens" },
6686
+ { method: "POST", path: "/api/layout", desc: "store one lens's deltas: JSON body {lens, deltas: {<node id>: {dx,dy,dw,dh}}} (the only file behold writes in your project)" },
4533
6687
  { method: "GET", path: "/api/diff", desc: "per-node live diff for ?env= \u2014 {env, nodes: {<id>: {observed, diff, health, fieldDrift}}}" },
4534
6688
  { method: "GET", path: "/api/reconcile", desc: "pending-change summary for ?env=" },
4535
6689
  { method: "GET", path: "/api/resources", desc: "component \u2192 declared resources" },
@@ -4621,7 +6775,7 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
4621
6775
  srcCompositeEdgesAttached = 0;
4622
6776
  }
4623
6777
  }
4624
- if (!multi && !components && !logical && !opts.lens && ir.nodes.length === 0 && !existsSync9(join11(cfg.projectDir, "chant.config.ts"))) {
6778
+ if (!multi && !components && !logical && !opts.lens && ir.nodes.length === 0 && !existsSync12(join15(cfg.projectDir, "chant.config.ts"))) {
4625
6779
  return c.json(noProjectError(cfg.projectDir), 404);
4626
6780
  }
4627
6781
  const radial = new URL(c.req.url).searchParams.get("radial") === "1";
@@ -4726,10 +6880,10 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
4726
6880
  return c.json({ error: "overlay needs an environment \u2014 pick one, or start behold with --env <name>" }, 400);
4727
6881
  }
4728
6882
  const logical = new URL(c.req.url).searchParams.get("logical") === "1";
6883
+ const runtime = new URL(c.req.url).searchParams.get("runtime") === "1";
4729
6884
  try {
4730
6885
  if (cfg.projectDirs && cfg.projectDirs.length > 1) {
4731
- const runtime = new URL(c.req.url).searchParams.get("runtime") === "1";
4732
- const detail = logical ? 3 : query.detail;
6886
+ const detail = logical || runtime ? 3 : query.detail;
4733
6887
  const est = await composeEstateOverlay(cfg.projectDirs, { ...tierTargetOpts(query), detail, env }, reclassifyOverlay);
4734
6888
  if (est.dropped.length === est.total) {
4735
6889
  return c.json({ error: `no project in the estate could be graphed \u2014 ${est.dropped.map((d) => `${d.name}: ${d.reason}`).join("; ")}` }, 500);
@@ -4749,7 +6903,12 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
4749
6903
  const logicalBefore = ir2.nodes.length;
4750
6904
  const { ir: projected, byContainer } = projectTopology(ir2, env, boundContext2, await estateSourceRoots(query));
4751
6905
  const { svg: svg3 } = renderArchitecture(projected, byContainer);
4752
- const note3 = [notesFor("logical", projected, void 0, logicalBefore), coverNote, namespaceMismatchNote(projected.nodes)].filter(Boolean).join(" \xB7 ");
6906
+ const note3 = [
6907
+ notesFor("logical", projected, void 0, logicalBefore),
6908
+ coverNote,
6909
+ namespaceJoinNote(est.joined),
6910
+ namespaceMismatchNote(withoutJoinedMembers(projected.nodes, est.joined))
6911
+ ].filter(Boolean).join(" \xB7 ");
4753
6912
  return c.json({
4754
6913
  ir: projected,
4755
6914
  svg: svg3,
@@ -4758,14 +6917,19 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
4758
6917
  });
4759
6918
  }
4760
6919
  const { svg: svg2 } = renderGraph(ir2, { boxes: runtime ? "byContainer" : "byStack" });
4761
- const note2 = [coverNote, namespaceMismatchNote(ir2.nodes), runtime ? notesFor("runtime", ir2) : void 0].filter(Boolean).join(" \xB7 ");
6920
+ const note2 = [
6921
+ coverNote,
6922
+ namespaceJoinNote(est.joined),
6923
+ namespaceMismatchNote(withoutJoinedMembers(ir2.nodes, est.joined)),
6924
+ runtime ? notesFor("runtime", ir2, void 0, void 0, detail) : void 0
6925
+ ].filter(Boolean).join(" \xB7 ");
4762
6926
  return c.json({
4763
6927
  ir: ir2,
4764
6928
  svg: svg2,
4765
6929
  meta: { projectDir: cfg.projectDir, env, mode: "overlay", estate: est.total, ...note2 ? { note: note2 } : {} }
4766
6930
  });
4767
6931
  }
4768
- const opts = { ...query, live: true, overlay: true, env, ...logical ? { detail: 3 } : {} };
6932
+ const opts = { ...query, live: true, overlay: true, env, ...logical || runtime ? { detail: 3 } : {} };
4769
6933
  let ir = reclassifyOverlay(await graphIr(cfg.projectDir, opts));
4770
6934
  const boundContext = await boundK8sContext(env);
4771
6935
  ir = mergeClusterRoot(ir, await clusterRootGraphIr(cfg.projectDir, query), await runningK3dClusters());
@@ -4775,7 +6939,7 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
4775
6939
  applyHelmArtifacts(ir, observed);
4776
6940
  synthesizeHelmReleases(ir, observed, discoverReleaseUnits(cfg.projectDir));
4777
6941
  }
4778
- ir = new URL(c.req.url).searchParams.get("runtime") === "1" ? attachRuntimeContainment(ir) : pruneRuntimeChildren(ir);
6942
+ ir = runtime ? attachRuntimeContainment(ir) : pruneRuntimeChildren(ir);
4779
6943
  if (logical) {
4780
6944
  const logicalBefore = ir.nodes.length;
4781
6945
  const { ir: projected, byContainer } = projectTopology(addK8sDeclaredEdges(addValueMatchEdges(ir)), env, boundContext, [await graphPath(cfg.projectDir, opts), cfg.projectDir]);
@@ -4800,10 +6964,10 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
4800
6964
  }
4801
6965
  }
4802
6966
  const { svg } = renderGraph(ir, { boxes: "byContainer", radial: new URL(c.req.url).searchParams.get("radial") === "1" });
4803
- const zoom = new URL(c.req.url).searchParams.get("runtime") === "1" ? "runtime" : query.detail === 1 ? "composites" : query.detail === 3 ? "attributes" : "resources";
6967
+ const zoom = runtime ? "runtime" : query.detail === 1 ? "composites" : query.detail === 3 ? "attributes" : "resources";
4804
6968
  const tierNote = tierMismatchNote(ir, beholdConfig.tiers, query.tier);
4805
6969
  const nsNote = namespaceMismatchNote(ir.nodes);
4806
- const zoomNotes = notesFor(zoom, ir, compositeEdgesAttached);
6970
+ const zoomNotes = notesFor(zoom, ir, compositeEdgesAttached, void 0, opts.detail ?? 2);
4807
6971
  const note = [tierNote, nsNote, zoomNotes].filter(Boolean).join(" \xB7 ");
4808
6972
  return c.json({ ir, svg, meta: { projectDir: cfg.projectDir, env, mode: "overlay", ...note ? { note } : {} } });
4809
6973
  } catch (err) {
@@ -4949,38 +7113,38 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
4949
7113
  }
4950
7114
  return c.json({ started: true, component, env });
4951
7115
  });
4952
- const rel = relative(process.cwd(), webRoot) || ".";
7116
+ const rel = relative3(process.cwd(), webRoot) || ".";
4953
7117
  app.use("/*", serveStatic({ root: rel }));
4954
- app.get("/", serveStatic({ path: join11(rel, "index.html") }));
7118
+ app.get("/", serveStatic({ path: join15(rel, "index.html") }));
4955
7119
  return app;
4956
7120
  }
4957
- async function startServer(cfg) {
4958
- if (cfg.local) {
4959
- try {
4960
- const emulators = await emulatorUp(cfg.projectDir);
4961
- cfg.emulators = emulators;
4962
- if (emulators.length === 0) {
4963
- process.stderr.write(
4964
- "behold serve --local: no configured lexicon has a local emulator \u2014 serving without one.\n"
4965
- );
4966
- } else {
4967
- Object.assign(process.env, mergedEnv(emulators));
4968
- for (const e of emulators) {
4969
- process.stdout.write(` local: ${e.lexicon} ${e.name} up on ${e.endpoint}
7121
+ async function bootLocalEmulators(dir, who) {
7122
+ try {
7123
+ const emulators = await emulatorUp(dir);
7124
+ if (emulators.length === 0) {
7125
+ process.stderr.write(`${who}: no configured lexicon has a local emulator \u2014 serving without one.
4970
7126
  `);
4971
- }
4972
- }
4973
- } catch (err) {
4974
- cfg.emulators = [];
4975
- process.stderr.write(
4976
- `behold serve --local: ${err instanceof Error ? err.message : String(err)}
7127
+ return [];
7128
+ }
7129
+ Object.assign(process.env, mergedEnv(emulators));
7130
+ for (const e of emulators) {
7131
+ process.stdout.write(` local: ${e.lexicon} ${e.name} up on ${e.endpoint}
7132
+ `);
7133
+ }
7134
+ return emulators;
7135
+ } catch (err) {
7136
+ process.stderr.write(
7137
+ `${who}: ${err instanceof Error ? err.message : String(err)}
4977
7138
  Serving the source graph without the emulator \u2014 start Docker and restart to enable local deploys.
4978
7139
  `
4979
- );
4980
- }
7140
+ );
7141
+ return [];
4981
7142
  }
7143
+ }
7144
+ async function startServer(cfg) {
7145
+ if (cfg.local) cfg.emulators = await bootLocalEmulators(cfg.projectDir, "behold serve --local");
4982
7146
  for (const dir of cfg.projectDirs ?? [cfg.projectDir]) {
4983
- if (existsSync9(join11(dir, "chant.config.ts"))) addRecent(dir);
7147
+ if (existsSync12(join15(dir, "chant.config.ts"))) addRecent(dir);
4984
7148
  }
4985
7149
  const broadcaster = new Broadcaster();
4986
7150
  const frames = new FrameBuffer();
@@ -5022,8 +7186,10 @@ async function startServer(cfg) {
5022
7186
  }
5023
7187
  }
5024
7188
  };
5025
- let stopWatch = watchSource(cfg.projectDir, onEstateChange);
5026
- let stopPoll = cfg.env && cfg.pollSecs ? startDriftPoll({
7189
+ const carve = !!cfg.carveReport;
7190
+ let stopWatch = carve ? () => {
7191
+ } : watchSource(cfg.projectDir, onEstateChange);
7192
+ let stopPoll = !carve && cfg.env && cfg.pollSecs ? startDriftPoll({
5027
7193
  intervalMs: cfg.pollSecs * 1e3,
5028
7194
  query: () => graphIr(cfg.projectDir, { live: true, overlay: true, env: cfg.env }),
5029
7195
  onChange: onPollDrift,
@@ -5041,7 +7207,7 @@ async function startServer(cfg) {
5041
7207
  `);
5042
7208
  void capture();
5043
7209
  };
5044
- void capture();
7210
+ if (!carve) void capture();
5045
7211
  let shuttingDown = false;
5046
7212
  const shutdown = () => {
5047
7213
  if (shuttingDown) return;
@@ -5055,6 +7221,19 @@ async function startServer(cfg) {
5055
7221
  process.on("SIGINT", shutdown);
5056
7222
  process.on("SIGTERM", shutdown);
5057
7223
  const server = serve({ fetch: app.fetch, port: cfg.port }, (info) => {
7224
+ if (carve) {
7225
+ process.stdout.write(
7226
+ `behold \u2192 http://localhost:${info.port}
7227
+ carve report: ${cfg.carveReport}
7228
+ green = carve now, amber = boundary work, grey = leave in Terraform.
7229
+ ` + (cfg.carveDemo ? ` walkthrough: the panel's Carve tab \u2014 advise \u2192 pick \u2192 emit \u2192 bridge \u2192 handoff \u2192 done.
7230
+ Emit and bridge write only into ${cfg.carveDemo.out}; your Terraform is never edited.
7231
+ ` + (cfg.carveDemo.degraded ? ` degraded: ${cfg.carveDemo.degraded}
7232
+ ` : "") : ` Read-only advisory: behold emits nothing and touches no Terraform. Ctrl-C to stop.
7233
+ `)
7234
+ );
7235
+ return;
7236
+ }
5058
7237
  const poll = cfg.env && cfg.pollSecs ? `, polling drift every ${cfg.pollSecs}s` : "";
5059
7238
  const auto = autoSync !== "off" ? ` auto-sync: ${autoSync}` : "";
5060
7239
  const localTag = cfg.emulators && cfg.emulators.length ? ` local: ${cfg.emulators.map((e) => e.name).join(", ")} up (creds-free \u2014 deploys hit the emulator)` : "";
@@ -5085,45 +7264,9 @@ async function startServer(cfg) {
5085
7264
  });
5086
7265
  }
5087
7266
 
5088
- // src/demos.ts
5089
- import { readFileSync as readFileSync10 } from "node:fs";
5090
- import { join as join12 } from "node:path";
5091
- import { spawnSync } from "node:child_process";
5092
- function loadDemoRegistry(pkgRoot) {
5093
- let raw;
5094
- try {
5095
- raw = JSON.parse(readFileSync10(join12(pkgRoot, "demos.json"), "utf8"));
5096
- } catch {
5097
- return [];
5098
- }
5099
- const list3 = raw?.demos;
5100
- if (!Array.isArray(list3)) return [];
5101
- return list3.filter((e) => {
5102
- const d = e;
5103
- if (!d || typeof d.name !== "string" || !d.name || typeof d.description !== "string") return false;
5104
- if (d.source === "bundled") {
5105
- if (typeof d.dir !== "string" || !d.dir) return false;
5106
- } else if (d.source === "git") {
5107
- if (typeof d.repo !== "string" || !d.repo) return false;
5108
- } else {
5109
- return false;
5110
- }
5111
- if (!Array.isArray(d.requires) || d.requires.some((r) => typeof r !== "string")) return false;
5112
- if (!d.serve || typeof d.serve !== "object") return false;
5113
- if (d.serve.dirs !== void 0 && (!Array.isArray(d.serve.dirs) || d.serve.dirs.some((x) => typeof x !== "string") || !d.serve.dirs.length))
5114
- return false;
5115
- return true;
5116
- });
5117
- }
5118
- function missingRequirements(entry) {
5119
- const bins = entry.source === "git" && !entry.requires.includes("git") ? [...entry.requires, "git"] : entry.requires;
5120
- const finder = process.platform === "win32" ? "where" : "which";
5121
- return bins.filter((bin) => spawnSync(finder, [bin], { stdio: "ignore" }).status !== 0);
5122
- }
5123
-
5124
7267
  // src/export.ts
5125
- import { mkdirSync as mkdirSync2, writeFileSync as writeFileSync2, copyFileSync, readFileSync as readFileSync11, readdirSync as readdirSync4 } from "node:fs";
5126
- import { join as join13, dirname as dirname5, basename } from "node:path";
7268
+ import { mkdirSync as mkdirSync4, writeFileSync as writeFileSync3, copyFileSync, readFileSync as readFileSync13, readdirSync as readdirSync5 } from "node:fs";
7269
+ import { join as join16, dirname as dirname5, basename } from "node:path";
5127
7270
  import { fileURLToPath as fileURLToPath4 } from "node:url";
5128
7271
  var LENS_PARAMS = ["components", "detail", "env", "logical", "radial", "tier"];
5129
7272
  function canonicalKey(path, params) {
@@ -5131,7 +7274,7 @@ function canonicalKey(path, params) {
5131
7274
  const q = LENS_PARAMS.filter((k) => params.has(k) && !(flat && (k === "detail" || k === "radial"))).map((k) => `${k}=${params.get(k)}`).join("&");
5132
7275
  return q ? `${path}?${q}` : path;
5133
7276
  }
5134
- function slug(key) {
7277
+ function slug2(key) {
5135
7278
  const base = key.replace(/^\//, "").replace(/[^a-zA-Z0-9=_.-]+/g, "_").slice(0, 120);
5136
7279
  return `${base}.json`;
5137
7280
  }
@@ -5171,7 +7314,7 @@ function captureKeys(axes) {
5171
7314
  return [...keys];
5172
7315
  }
5173
7316
  function webDir() {
5174
- return join13(dirname5(fileURLToPath4(import.meta.url)), "..", "web");
7317
+ return join16(dirname5(fileURLToPath4(import.meta.url)), "..", "web");
5175
7318
  }
5176
7319
  function workerName(project, override) {
5177
7320
  const raw = override ?? `behold-${basename(project)}`;
@@ -5179,19 +7322,19 @@ function workerName(project, override) {
5179
7322
  return name || "behold-export";
5180
7323
  }
5181
7324
  async function runExport(cfg, outDir, opts = {}) {
5182
- const app = createApp(cfg);
7325
+ const app = createApp({ ...cfg, layoutWrites: false });
5183
7326
  const proj = await (await app.request("/api/project")).json();
5184
7327
  const axes = { environments: proj.environments ?? [], tiers: proj.tiers ?? [] };
5185
- const snapDir = join13(outDir, "snapshots");
5186
- mkdirSync2(snapDir, { recursive: true });
7328
+ const snapDir = join16(outDir, "snapshots");
7329
+ mkdirSync4(snapDir, { recursive: true });
5187
7330
  const keyToFile = {};
5188
7331
  let ok = 0;
5189
7332
  let failed = 0;
5190
7333
  for (const key of captureKeys(axes)) {
5191
- const res = await app.request(key);
7334
+ const res = await app.request(`${key}${key.includes("?") ? "&" : "?"}layout=1`);
5192
7335
  const body = await res.text();
5193
- const file = slug(key);
5194
- writeFileSync2(join13(snapDir, file), body);
7336
+ const file = slug2(key);
7337
+ writeFileSync3(join16(snapDir, file), body);
5195
7338
  keyToFile[key] = `snapshots/${file}`;
5196
7339
  if (res.ok) ok++;
5197
7340
  else failed++;
@@ -5203,21 +7346,21 @@ async function runExport(cfg, outDir, opts = {}) {
5203
7346
  axes,
5204
7347
  keyToFile
5205
7348
  };
5206
- writeFileSync2(join13(outDir, "manifest.json"), JSON.stringify(manifest, null, 2));
5207
- const html = readFileSync11(join13(webDir(), "index.html"), "utf8").replace(
7349
+ writeFileSync3(join16(outDir, "manifest.json"), JSON.stringify(manifest, null, 2));
7350
+ const html = readFileSync13(join16(webDir(), "index.html"), "utf8").replace(
5208
7351
  /<\/head>/i,
5209
7352
  ` <script>window.__BEHOLD_STATIC__ = true;</script>
5210
7353
  </head>`
5211
7354
  );
5212
- writeFileSync2(join13(outDir, "index.html"), html);
5213
- for (const f of readdirSync4(webDir())) {
7355
+ writeFileSync3(join16(outDir, "index.html"), html);
7356
+ for (const f of readdirSync5(webDir())) {
5214
7357
  if (f === "index.html") continue;
5215
- copyFileSync(join13(webDir(), f), join13(outDir, f));
7358
+ copyFileSync(join16(webDir(), f), join16(outDir, f));
5216
7359
  }
5217
- writeFileSync2(join13(outDir, "README.md"), BUNDLE_README);
7360
+ writeFileSync3(join16(outDir, "README.md"), BUNDLE_README);
5218
7361
  const name = workerName(cfg.projectDir, opts.name);
5219
- writeFileSync2(
5220
- join13(outDir, "wrangler.jsonc"),
7362
+ writeFileSync3(
7363
+ join16(outDir, "wrangler.jsonc"),
5221
7364
  JSON.stringify(
5222
7365
  { $schema: "node_modules/wrangler/config-schema.json", name, compatibility_date: "2025-06-01", assets: { directory: "." } },
5223
7366
  null,
@@ -5265,7 +7408,7 @@ It's just files \u2014 GitHub Pages, S3, nginx, or Cloudflare Pages
5265
7408
  `;
5266
7409
 
5267
7410
  // src/doctor.ts
5268
- import { relative as relative2, resolve as resolve3 } from "node:path";
7411
+ import { relative as relative4, resolve as resolve6 } from "node:path";
5269
7412
  var list2 = (xs) => xs.join(", ");
5270
7413
  var EMPTY_KUBECONFIG = { contexts: /* @__PURE__ */ new Map(), servers: /* @__PURE__ */ new Map() };
5271
7414
  function summarize(xs, max = 4) {
@@ -5273,7 +7416,7 @@ function summarize(xs, max = 4) {
5273
7416
  }
5274
7417
  function labelFor(root, target, estate) {
5275
7418
  if (!estate) return "";
5276
- const rel = relative2(root, target);
7419
+ const rel = relative4(root, target);
5277
7420
  return `${rel || "."}: `;
5278
7421
  }
5279
7422
  function chantCheck(root, targets, estate) {
@@ -5286,7 +7429,7 @@ function chantCheck(root, targets, estate) {
5286
7429
  name: "chant",
5287
7430
  status: "fail",
5288
7431
  detail: `${list2(resolutions.map(describe))} \u2014 behold shells the project's own chant`,
5289
- fix: `Run \`npm install\` in ${list2(missing.map(({ target }) => relative2(root, target) || target))}`
7432
+ fix: `Run \`npm install\` in ${list2(missing.map(({ target }) => relative4(root, target) || target))}`
5290
7433
  };
5291
7434
  }
5292
7435
  const stale = resolutions.filter(({ res }) => !meetsFloor(res.version, floor));
@@ -5343,8 +7486,8 @@ function kubeCheck(lexicons, profiles, envs, kubeconfig) {
5343
7486
  return {
5344
7487
  name: "kube",
5345
7488
  status: "warn",
5346
- detail: "no kubeconfig readable (kubectl absent, or it has no contexts) \u2014 the declared graph still serves",
5347
- fix: "Install kubectl and point KUBECONFIG at your cluster, or run `behold demo k8s` for a throwaway k3d one."
7489
+ detail: "no kubeconfig readable (none on this machine, or it has no contexts) \u2014 the declared graph still serves",
7490
+ fix: "Point KUBECONFIG at your cluster's kubeconfig (or put one at ~/.kube/config), or run `behold demo k8s` for a throwaway k3d one."
5348
7491
  };
5349
7492
  }
5350
7493
  const bound = envs.filter((e) => profiles[e]?.context);
@@ -5414,7 +7557,7 @@ function opsCheck(root, ops, chantSource, estate) {
5414
7557
  return { name: "ops", status: "pass", detail: `${ops.length} committed: ${list2(names)}; ${mcp}` };
5415
7558
  }
5416
7559
  async function diagnose(dir, probes = {}) {
5417
- const root = resolve3(dir);
7560
+ const root = resolve6(dir);
5418
7561
  const shape = detectProjectShape(root);
5419
7562
  const behold = beholdVersion();
5420
7563
  if (shape.kind === "none") {
@@ -5434,16 +7577,16 @@ async function diagnose(dir, probes = {}) {
5434
7577
  };
5435
7578
  }
5436
7579
  const estate = shape.kind === "estate";
5437
- const members = (shape.members ?? []).map((m) => resolve3(root, m));
7580
+ const members = (shape.members ?? []).map((m) => resolve6(root, m));
5438
7581
  const targets = estate ? members : [root];
5439
7582
  const primary = targets[0];
5440
7583
  const projectCheck = estate ? {
5441
7584
  name: "project",
5442
7585
  status: "pass",
5443
7586
  detail: `estate of ${members.length} projects (${shape.membersFrom === "behold-config" ? ".behold.json members" : "npm workspaces"}): ${list2(
5444
- members.map((m) => relative2(root, m))
7587
+ members.map((m) => relative4(root, m))
5445
7588
  )}`
5446
- } : { name: "project", status: "pass", detail: `chant project (${relative2(root, shape.configFile)})` };
7589
+ } : { name: "project", status: "pass", detail: `chant project (${relative4(root, shape.configFile)})` };
5447
7590
  const infos = await Promise.all(targets.map(async (t) => ({ target: t, info: await detectProject(t) })));
5448
7591
  const declared = new Map(infos.map(({ target, info }) => [target, info.lexicons]));
5449
7592
  const lexicons = [...new Set(infos.flatMap(({ info }) => info.lexicons))];
@@ -5488,6 +7631,16 @@ Usage:
5488
7631
  behold preview [project-dir] [--port <n>] [--emulator]
5489
7632
  behold export [project-dir] [--out <dir>] [--env <name>] [--name <worker>] [--emulator]
5490
7633
  behold serve <project-dir\u2026> [--port <n>] [--env <name>] [--poll <secs>] [--local]
7634
+ behold carve <report.json> [--port <n>]
7635
+
7636
+ carve Render a chant Terraform peelability report \u2014 the JSON from
7637
+ \`chant carve advise --from <tf-dir> --json\` \u2014 as a graph. One card
7638
+ per ranked resource, coloured by band: green = carve now, amber =
7639
+ has boundary work, grey = leave in Terraform. Click a card for the
7640
+ score arithmetic behind its rank. Read-only twice over: chant's
7641
+ advisor emits nothing, and behold only draws what it says.
7642
+ \`GET /api/carve\` serves the raw report to agents. behold parses no
7643
+ Terraform and needs no Terraform tooling \u2014 the report is the contract.
5491
7644
 
5492
7645
  doctor Why won't this project serve well? A read-only diagnosis of
5493
7646
  everything behold needs \u2014 the project's kind, its own chant install
@@ -5503,9 +7656,13 @@ Usage:
5503
7656
  clone a public estate. Bare \`behold demo\` is the AWS example \u2014 an
5504
7657
  S3 bucket + policy on a local emulator: blue = declared, click
5505
7658
  Deploy, watch it turn green. \`behold demo k8s\` stands a workload
5506
- up on a throwaway k3d cluster instead. Needs Docker (and per-demo
5507
- tools --list names). Loaded demos land in the panel's recents, so
5508
- switching between them is the Scope tab.
7659
+ up on a throwaway k3d cluster instead. \`behold demo carve\` is the
7660
+ odd one out: no cluster, no Docker, no cloud \u2014 a half-migrated
7661
+ Terraform/chant estate plus the six-step carve walkthrough on the
7662
+ panel's Carve tab. Needs Docker (and per-demo tools --list names).
7663
+ Loaded demos land in the panel's recents, so switching between them
7664
+ is the Scope tab \u2014 which lists this whole catalog too (#268), one
7665
+ click from any served project.
5509
7666
 
5510
7667
  export Capture the live estate into a self-contained, interactive STATIC
5511
7668
  bundle (default ./behold-export) \u2014 every env/tier \xD7 zoom \xD7 radial,
@@ -5526,7 +7683,7 @@ Usage:
5526
7683
  overlay, and rollback act on it).
5527
7684
 
5528
7685
  Options:
5529
- --port <n> Port (default 4600). preview/serve.
7686
+ --port <n> Port (default 4600). preview/serve/carve.
5530
7687
  --env <name> Environment name \u2014 turns on the live drift overlay.
5531
7688
  export/serve.
5532
7689
  --poll <secs> Re-query live drift every <secs> and push updates (needs --env).
@@ -5565,7 +7722,7 @@ Options:
5565
7722
  -h, --help This text.
5566
7723
  -v, --version Print the behold version.
5567
7724
  `;
5568
- async function run3(argv) {
7725
+ async function run2(argv) {
5569
7726
  const [cmd, ...rest] = argv;
5570
7727
  if (!cmd || cmd === "-h" || cmd === "--help") {
5571
7728
  process.stdout.write(USAGE);
@@ -5591,6 +7748,10 @@ async function run3(argv) {
5591
7748
  await runExportCmd(rest);
5592
7749
  return;
5593
7750
  }
7751
+ if (cmd === "carve") {
7752
+ await runCarve(rest);
7753
+ return;
7754
+ }
5594
7755
  if (cmd !== "serve") {
5595
7756
  process.stderr.write(`behold: unknown command '${cmd}'
5596
7757
 
@@ -5646,7 +7807,7 @@ ${USAGE}`);
5646
7807
  process.stderr.write("behold serve: --auto-sync needs --env and --poll (it acts on polled drift)\n");
5647
7808
  process.exit(2);
5648
7809
  }
5649
- const dirs = projectDirs.map((d) => resolve4(d));
7810
+ const dirs = projectDirs.map((d) => resolve7(d));
5650
7811
  for (const d of dirs) warnIfNotChantProject(d);
5651
7812
  await startServer({
5652
7813
  projectDir: dirs[0],
@@ -5665,7 +7826,7 @@ function warnIfNotChantProject(dir) {
5665
7826
  if (shape.kind === "estate") {
5666
7827
  process.stderr.write(
5667
7828
  `behold: warning \u2014 ${dir} is an estate root, not a chant project itself.
5668
- Serve its members composed: behold serve ${shape.members.map((m) => join14(dir, m)).join(" ")}
7829
+ Serve its members composed: behold serve ${shape.members.map((m) => join17(dir, m)).join(" ")}
5669
7830
  `
5670
7831
  );
5671
7832
  return;
@@ -5676,6 +7837,44 @@ function warnIfNotChantProject(dir) {
5676
7837
  `
5677
7838
  );
5678
7839
  }
7840
+ async function runCarve(rest) {
7841
+ let port = 4600;
7842
+ let fileArg;
7843
+ for (let i = 0; i < rest.length; i++) {
7844
+ const a = rest[i];
7845
+ if (a === "--port") port = Number(rest[++i]);
7846
+ else if (a === "-h" || a === "--help") return void process.stdout.write(USAGE);
7847
+ else if (!a.startsWith("-")) fileArg = a;
7848
+ else {
7849
+ process.stderr.write(`behold carve: unexpected argument '${a}'
7850
+ `);
7851
+ process.exit(2);
7852
+ }
7853
+ }
7854
+ if (!Number.isFinite(port)) {
7855
+ process.stderr.write("behold carve: --port must be a number\n");
7856
+ process.exit(2);
7857
+ }
7858
+ if (!fileArg) {
7859
+ process.stderr.write(
7860
+ "behold carve: missing <report.json>\n Generate one: chant carve advise --from <terraform-dir> --report report.json\n"
7861
+ );
7862
+ process.exit(2);
7863
+ }
7864
+ const reportPath = resolve7(fileArg);
7865
+ const parsed = readCarveReport(reportPath, (p) => readFileSync14(p, "utf8"));
7866
+ if (!parsed.ok) {
7867
+ process.stderr.write(`behold carve: ${parsed.refusal.error}
7868
+ ${parsed.refusal.remedy}
7869
+ `);
7870
+ process.exit(2);
7871
+ }
7872
+ process.stdout.write(
7873
+ `behold carve \u2014 ${parsed.report.resources.length} resource(s)/module(s) ranked${parsed.report.from ? ` from ${parsed.report.from}` : ""}
7874
+ `
7875
+ );
7876
+ await startServer({ projectDir: dirname6(reportPath), carveReport: reportPath, port });
7877
+ }
5679
7878
  async function runDoctor(rest) {
5680
7879
  let json = false;
5681
7880
  let dirArg;
@@ -5690,8 +7889,8 @@ async function runDoctor(rest) {
5690
7889
  }
5691
7890
  }
5692
7891
  const dir = dirArg ?? ".";
5693
- if (!existsSync10(resolve4(dir))) {
5694
- process.stderr.write(`behold doctor: no such directory: ${resolve4(dir)}
7892
+ if (!existsSync13(resolve7(dir))) {
7893
+ process.stderr.write(`behold doctor: no such directory: ${resolve7(dir)}
5695
7894
  `);
5696
7895
  process.exit(2);
5697
7896
  }
@@ -5700,8 +7899,8 @@ async function runDoctor(rest) {
5700
7899
  if (!report.ok) process.exitCode = 1;
5701
7900
  }
5702
7901
  async function runDemo(rest) {
5703
- const pkgRoot = join14(dirname6(fileURLToPath5(import.meta.url)), "..");
5704
- const registry = loadDemoRegistry(pkgRoot);
7902
+ const pkgRoot2 = join17(dirname6(fileURLToPath5(import.meta.url)), "..");
7903
+ const registry = loadDemoRegistry(pkgRoot2);
5705
7904
  let port = 4600;
5706
7905
  let name;
5707
7906
  let dirArg;
@@ -5752,64 +7951,93 @@ async function runDemo(rest) {
5752
7951
  `);
5753
7952
  process.exit(2);
5754
7953
  }
5755
- const target = resolve4(
5756
- dirArg ?? (entry.name === "writes" && existsSync10("behold-demo") ? "behold-demo" : join14("behold-demos", entry.name))
5757
- );
5758
- if (!existsSync10(target)) {
5759
- if (entry.source === "bundled") {
5760
- const bundled = join14(pkgRoot, entry.dir);
5761
- if (!existsSync10(bundled)) {
5762
- process.stderr.write(`behold demo ${entry.name}: this install has no bundled ${entry.dir}
7954
+ const target = dirArg ? resolve7(dirArg) : demoTargetDir(entry);
7955
+ const loaded = await loadDemo(entry, { pkgRoot: pkgRoot2, target, log: (line) => process.stdout.write(line + "\n") });
7956
+ if (!loaded.ok) {
7957
+ process.stderr.write(`behold demo ${entry.name}: ${loaded.error}
5763
7958
  `);
5764
- process.exit(2);
5765
- }
5766
- process.stdout.write(`behold demo ${entry.name} \u2192 copying to ${target} (it's yours \u2014 edit it)
7959
+ process.exit(1);
7960
+ }
7961
+ if (entry.serve.carve) {
7962
+ await serveCarveDemo(target, entry.serve.carve, port);
7963
+ return;
7964
+ }
7965
+ process.stdout.write(`behold demo ${entry.name} \u2192 serving. Blue = declared; Deploy turns it green.
5767
7966
  `);
5768
- cpSync(bundled, target, {
5769
- recursive: true,
5770
- filter: (src) => !relative3(bundled, src).split(sep).includes("node_modules")
5771
- });
5772
- } else {
5773
- process.stdout.write(`behold demo ${entry.name} \u2192 cloning ${entry.repo} to ${target}
7967
+ const serveArgs = ["serve", ...loaded.serveDirs, "--port", String(port)];
7968
+ if (entry.serve.local) serveArgs.push("--local");
7969
+ if (entry.serve.env) serveArgs.push("--env", entry.serve.env);
7970
+ await run2(serveArgs);
7971
+ }
7972
+ function spawnStep(cmd, args, cwd) {
7973
+ return new Promise((res) => {
7974
+ const child = spawn5(cmd, args, { stdio: "inherit", cwd, shell: process.platform === "win32" });
7975
+ child.on("error", () => res(-1));
7976
+ child.on("close", (code) => res(code ?? 1));
7977
+ });
7978
+ }
7979
+ async function serveCarveDemo(target, carve, port) {
7980
+ const at = (rel) => resolve7(target, rel);
7981
+ const project = at(carve.project);
7982
+ const from = at(carve.from);
7983
+ const state = carve.state ? at(carve.state) : void 0;
7984
+ const committed = at(carve.report);
7985
+ if (existsSync13(join17(project, "package.json")) && !existsSync13(join17(project, "node_modules"))) {
7986
+ process.stdout.write(`behold demo carve \u2192 npm install in ${carve.project}/ (the chant this walkthrough shells)\u2026
5774
7987
  `);
5775
- const r = spawnSync2("git", ["clone", "--depth", "1", entry.repo, target], { stdio: "inherit" });
5776
- if (r.status !== 0) {
5777
- process.stderr.write(`behold demo ${entry.name}: clone failed
7988
+ const code = await spawnStep("npm", ["install"], project);
7989
+ if (code !== 0) {
7990
+ process.stderr.write(`behold demo carve: npm install failed in ${project}
5778
7991
  `);
5779
- process.exit(r.status ?? 1);
5780
- }
7992
+ process.exit(code || 1);
5781
7993
  }
5782
- } else {
5783
- process.stdout.write(`behold demo ${entry.name} \u2192 reusing ${target}
5784
- `);
5785
7994
  }
5786
- if (existsSync10(join14(target, "package.json")) && !existsSync10(join14(target, "node_modules"))) {
5787
- process.stdout.write(`behold demo ${entry.name} \u2192 npm install\u2026
5788
- `);
5789
- const r = spawnSync2("npm", ["install"], { cwd: target, stdio: "inherit", shell: process.platform === "win32" });
5790
- if (r.status !== 0) {
5791
- process.stderr.write(`behold demo: npm install failed in ${target}${r.error ? ` (${r.error.message})` : ""}
5792
- `);
5793
- process.exit(r.status ?? 1);
5794
- }
7995
+ let degraded;
7996
+ if (!existsSync13(join17(target, "node_modules", "@cdktf", "hcl2json"))) {
7997
+ process.stdout.write("behold demo carve \u2192 npm install @cdktf/hcl2json (chant's HCL parser, ~2MB, once)\u2026\n");
7998
+ const code = await spawnStep("npm", ["install", "--no-save", "--no-package-lock", "@cdktf/hcl2json"], target);
7999
+ if (code !== 0) degraded = "couldn't install @cdktf/hcl2json (chant's HCL parser) \u2014 no network?";
8000
+ }
8001
+ const report = at("carve-report.json");
8002
+ if (!degraded) {
8003
+ const bin = resolveChant(project).bin;
8004
+ process.stdout.write("behold demo carve \u2192 chant carve advise (read-only; emits nothing)\u2026\n");
8005
+ const code = await spawnStep(
8006
+ bin,
8007
+ ["carve", "advise", "--from", carve.from, ...carve.state ? ["--state", carve.state] : [], "--report", report],
8008
+ target
8009
+ );
8010
+ if (code !== 0) degraded = `chant carve advise exited ${code}`;
5795
8011
  }
5796
- if (entry.setup) {
5797
- process.stdout.write(`behold demo ${entry.name} \u2192 ${entry.setup}
5798
- `);
5799
- const r = spawnSync2(entry.setup, { cwd: target, stdio: "inherit", shell: true });
5800
- if (r.status !== 0) {
5801
- process.stderr.write(`behold demo ${entry.name}: setup failed (${entry.setup})
5802
- `);
5803
- process.exit(r.status ?? 1);
5804
- }
8012
+ const serving = !degraded && existsSync13(report) ? report : committed;
8013
+ if (degraded) {
8014
+ process.stderr.write(
8015
+ `behold demo carve: ${degraded}
8016
+ Serving the committed report (${carve.report}) instead \u2014 the bands are real, just not regenerated here.
8017
+ `
8018
+ );
5805
8019
  }
5806
- process.stdout.write(`behold demo ${entry.name} \u2192 serving. Blue = declared; Deploy turns it green.
8020
+ if (!existsSync13(serving)) {
8021
+ process.stderr.write(`behold demo carve: no carve report at ${serving}
5807
8022
  `);
5808
- const serveDirs = entry.serve.dirs?.length ? entry.serve.dirs.map((d) => join14(target, d)) : [target];
5809
- const serveArgs = ["serve", ...serveDirs, "--port", String(port)];
5810
- if (entry.serve.local) serveArgs.push("--local");
5811
- if (entry.serve.env) serveArgs.push("--env", entry.serve.env);
5812
- await run3(serveArgs);
8023
+ process.exit(2);
8024
+ }
8025
+ process.stdout.write(
8026
+ "behold demo carve \u2192 serving the walkthrough. Green = carve now; the Carve tab walks the six steps.\n"
8027
+ );
8028
+ await startServer({
8029
+ projectDir: target,
8030
+ carveReport: serving,
8031
+ carveDemo: {
8032
+ root: target,
8033
+ from,
8034
+ ...state ? { state } : {},
8035
+ project,
8036
+ out: at(carve.out),
8037
+ ...degraded ? { degraded: `${degraded} \u2014 showing the committed report shipped with the demo.` } : {}
8038
+ },
8039
+ port
8040
+ });
5813
8041
  }
5814
8042
  function injectEmulatorEnv(env) {
5815
8043
  process.env.LOOM_ENV ??= env ?? "local";
@@ -5833,8 +8061,8 @@ async function runPreview(rest) {
5833
8061
  process.stderr.write("behold preview: --port must be a number\n");
5834
8062
  process.exit(2);
5835
8063
  }
5836
- const projectDir = resolve4(dirArg ?? process.cwd());
5837
- if (!existsSync10(projectDir)) {
8064
+ const projectDir = resolve7(dirArg ?? process.cwd());
8065
+ if (!existsSync13(projectDir)) {
5838
8066
  process.stderr.write(`behold preview: project not found at ${projectDir}
5839
8067
  `);
5840
8068
  process.exit(2);
@@ -5854,26 +8082,26 @@ async function runPreview(rest) {
5854
8082
  await startServer({ projectDir, port, env: "local", previewMode: true });
5855
8083
  }
5856
8084
  async function runExportCmd(rest) {
5857
- let outDir = resolve4("behold-export");
8085
+ let outDir = resolve7("behold-export");
5858
8086
  let env;
5859
8087
  let name;
5860
8088
  let dirArg;
5861
8089
  let emulator = false;
5862
8090
  for (let i = 0; i < rest.length; i++) {
5863
8091
  const a = rest[i];
5864
- if (a === "--out") outDir = resolve4(rest[++i]);
8092
+ if (a === "--out") outDir = resolve7(rest[++i]);
5865
8093
  else if (a === "--env") env = rest[++i];
5866
8094
  else if (a === "--name") name = rest[++i];
5867
8095
  else if (a === "--emulator") emulator = true;
5868
8096
  else if (a === "-h" || a === "--help") return void process.stdout.write(USAGE);
5869
8097
  else if (!a.startsWith("-")) dirArg = a;
5870
8098
  }
5871
- const projectDir = resolve4(dirArg ?? process.cwd());
8099
+ const projectDir = resolve7(dirArg ?? process.cwd());
5872
8100
  if (emulator) {
5873
8101
  injectEmulatorEnv(env);
5874
8102
  env ??= "local";
5875
8103
  }
5876
- if (!existsSync10(projectDir)) {
8104
+ if (!existsSync13(projectDir)) {
5877
8105
  process.stderr.write(`behold export: project not found at ${projectDir}
5878
8106
  `);
5879
8107
  process.exit(2);
@@ -5890,12 +8118,12 @@ function isMainModule() {
5890
8118
  }
5891
8119
  }
5892
8120
  if (isMainModule()) {
5893
- run3(process.argv.slice(2)).catch((err) => {
8121
+ run2(process.argv.slice(2)).catch((err) => {
5894
8122
  process.stderr.write(`behold: fatal: ${err?.message ?? err}
5895
8123
  `);
5896
8124
  process.exit(3);
5897
8125
  });
5898
8126
  }
5899
8127
  export {
5900
- run3 as run
8128
+ run2 as run
5901
8129
  };