@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.
- package/AGENTS.md +85 -0
- package/README.md +123 -2
- package/demos.json +17 -1
- package/dist/cli.js +2522 -294
- package/example-argo-estate/README.md +43 -18
- package/example-argo-estate/app-a/chant.config.ts +10 -2
- package/example-argo-estate/app-a/package.json +2 -2
- package/example-argo-estate/app-b/chant.config.ts +11 -2
- package/example-argo-estate/app-b/package.json +2 -2
- package/example-argo-estate/control-plane/chant.config.ts +20 -5
- package/example-argo-estate/control-plane/package.json +2 -2
- package/example-argo-estate/package-lock.json +17 -17
- package/example-carve/README.md +194 -0
- package/example-carve/app/chant.config.ts +6 -0
- package/example-carve/app/package-lock.json +1075 -0
- package/example-carve/app/package.json +13 -0
- package/example-carve/app/src/carved.ts +30 -0
- package/example-carve/app/tsconfig.json +1 -0
- package/example-carve/carve-report.json +872 -0
- package/example-carve/legacy-tf/cdn.tf +23 -0
- package/example-carve/legacy-tf/compute.tf +63 -0
- package/example-carve/legacy-tf/floci-override.tf.disabled +61 -0
- package/example-carve/legacy-tf/modules/cdn/main.tf +72 -0
- package/example-carve/legacy-tf/naming.tf +10 -0
- package/example-carve/legacy-tf/network.tf +119 -0
- package/example-carve/legacy-tf/observability.tf +18 -0
- package/example-carve/legacy-tf/outputs.tf +16 -0
- package/example-carve/legacy-tf/storage.tf +33 -0
- package/example-carve/legacy-tf/terraform.tfstate +602 -0
- package/example-carve/legacy-tf/versions.tf +40 -0
- package/example-flux-estate/README.md +9 -4
- package/example-flux-estate/app-a/package.json +2 -2
- package/example-flux-estate/app-a/src/app.ts +2 -1
- package/example-flux-estate/app-b/chant.config.ts +4 -3
- package/example-flux-estate/app-b/package.json +2 -2
- package/example-flux-estate/app-b/src/app.ts +5 -3
- package/example-flux-estate/control-plane/package.json +2 -2
- package/example-flux-estate/control-plane/src/flux.ts +4 -2
- package/example-flux-estate/package-lock.json +17 -17
- package/example-k8s/package-lock.json +18 -18
- package/example-k8s/package.json +3 -3
- package/example-writes/package-lock.json +14 -14
- package/example-writes/package.json +3 -3
- package/package.json +8 -6
- package/web/app.js +714 -57
- package/web/carve-steps.js +610 -0
- package/web/carve-steps.test.js +233 -0
- package/web/demos.js +71 -0
- package/web/demos.test.js +83 -0
- package/web/index.html +93 -1
- package/web/json-view.js +334 -0
- package/web/json-view.test.js +218 -0
- package/web/layout-store.js +164 -4
- package/web/layout-store.test.js +226 -1
- package/web/panel.js +28 -0
- 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
|
|
3
|
-
import { realpathSync, existsSync as
|
|
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
|
|
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
|
|
52
|
-
|
|
53
|
-
|
|
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
|
-
|
|
57
|
-
|
|
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
|
|
1094
|
+
async function k8sClient() {
|
|
60
1095
|
try {
|
|
61
|
-
|
|
62
|
-
return readKubeconfigJson(JSON.parse(stdout));
|
|
1096
|
+
return await Promise.resolve().then(() => (init_src(), src_exports));
|
|
63
1097
|
} catch {
|
|
64
|
-
return
|
|
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
|
|
92
|
-
import { promisify as
|
|
93
|
-
import { existsSync as
|
|
94
|
-
import { dirname as dirname4, join as
|
|
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
|
|
807
|
-
import { promisify
|
|
808
|
-
var
|
|
809
|
-
async function runningK3dClusters(exec =
|
|
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
|
|
826
|
-
const { stdout } = await
|
|
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((
|
|
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
|
-
|
|
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", () =>
|
|
1019
|
-
proc.on("close", (code) =>
|
|
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
|
|
1164
|
-
if (!
|
|
2194
|
+
const run3 = byComponent.get(n.id);
|
|
2195
|
+
if (!run3) continue;
|
|
1165
2196
|
const attrs = n.attrs ??= {};
|
|
1166
|
-
attrs.ci =
|
|
1167
|
-
attrs._ciJob =
|
|
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
|
|
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
|
|
2367
|
+
return str(rec(n.attrs?.metadata)?.namespace) ?? "default";
|
|
1337
2368
|
}
|
|
1338
2369
|
function nameOf(n) {
|
|
1339
|
-
return
|
|
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 =
|
|
1372
|
-
const 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:
|
|
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 =
|
|
2409
|
+
const name = str(ref?.name);
|
|
1379
2410
|
if (!name) return void 0;
|
|
1380
|
-
return { name, namespace:
|
|
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) =>
|
|
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 =
|
|
1466
|
-
const refName =
|
|
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,
|
|
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,
|
|
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
|
|
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
|
|
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/
|
|
3108
|
-
import {
|
|
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(/</g, "<").replace(/>/g, ">").replace(/"/g, '"').replace(/'/g, "'").replace(/'/g, "'").replace(/&/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 =
|
|
3132
|
-
if (!
|
|
3133
|
-
for (const f of
|
|
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 =
|
|
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
|
|
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 =
|
|
3276
|
-
let sync =
|
|
4950
|
+
let health = str2(rec2(tree?.health)?.status);
|
|
4951
|
+
let sync = str2(rec2(tree?.sync)?.status);
|
|
3277
4952
|
if (!health && !sync) {
|
|
3278
|
-
const word =
|
|
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
|
|
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 =
|
|
4981
|
+
const status = str2(cond.status);
|
|
3303
4982
|
if (status !== "True" && status !== "False" && status !== "Unknown") continue;
|
|
3304
|
-
return { status, 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 =
|
|
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 =
|
|
3332
|
-
const attempted =
|
|
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 (!
|
|
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
|
|
3646
|
-
import { join as
|
|
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((
|
|
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
|
-
|
|
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", () =>
|
|
3661
|
-
proc.on("close", (code) =>
|
|
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
|
|
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 =
|
|
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 (!
|
|
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 (
|
|
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(
|
|
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
|
|
3784
|
-
const ctx = boundContext ??
|
|
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
|
|
3835
|
-
import { promisify as
|
|
3836
|
-
var execFileAsync =
|
|
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
|
|
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
|
-
|
|
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 =
|
|
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:
|
|
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
|
|
3931
|
-
import { join as
|
|
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 =
|
|
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
|
|
4228
|
-
var
|
|
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(
|
|
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 =
|
|
4412
|
-
const up =
|
|
4413
|
-
if (!
|
|
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() ?
|
|
4491
|
-
if (!dir || !
|
|
4492
|
-
if (!
|
|
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 =
|
|
4497
|
-
cfg.projectDirs = void 0;
|
|
4498
|
-
cfg.env =
|
|
4499
|
-
beholdConfig = loadBeholdConfig(
|
|
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(
|
|
4502
|
-
addRecent(
|
|
4503
|
-
cfg.onProjectSwitch?.(
|
|
6497
|
+
runner.retarget(dirs[0]);
|
|
6498
|
+
for (const d of dirs) addRecent(d);
|
|
6499
|
+
cfg.onProjectSwitch?.(dirs[0]);
|
|
4504
6500
|
broadcaster.emit("changed");
|
|
4505
|
-
|
|
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() ?
|
|
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/
|
|
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 && !
|
|
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
|
|
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 = [
|
|
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 = [
|
|
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 =
|
|
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 =
|
|
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 =
|
|
7116
|
+
const rel = relative3(process.cwd(), webRoot) || ".";
|
|
4953
7117
|
app.use("/*", serveStatic({ root: rel }));
|
|
4954
|
-
app.get("/", serveStatic({ path:
|
|
7118
|
+
app.get("/", serveStatic({ path: join15(rel, "index.html") }));
|
|
4955
7119
|
return app;
|
|
4956
7120
|
}
|
|
4957
|
-
async function
|
|
4958
|
-
|
|
4959
|
-
|
|
4960
|
-
|
|
4961
|
-
|
|
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
|
-
|
|
4974
|
-
|
|
4975
|
-
process.
|
|
4976
|
-
|
|
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 (
|
|
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
|
-
|
|
5026
|
-
let
|
|
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
|
|
5126
|
-
import { join as
|
|
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
|
|
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
|
|
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 =
|
|
5186
|
-
|
|
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 =
|
|
5194
|
-
|
|
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
|
-
|
|
5207
|
-
const html =
|
|
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
|
-
|
|
5213
|
-
for (const f of
|
|
7355
|
+
writeFileSync3(join16(outDir, "index.html"), html);
|
|
7356
|
+
for (const f of readdirSync5(webDir())) {
|
|
5214
7357
|
if (f === "index.html") continue;
|
|
5215
|
-
copyFileSync(
|
|
7358
|
+
copyFileSync(join16(webDir(), f), join16(outDir, f));
|
|
5216
7359
|
}
|
|
5217
|
-
|
|
7360
|
+
writeFileSync3(join16(outDir, "README.md"), BUNDLE_README);
|
|
5218
7361
|
const name = workerName(cfg.projectDir, opts.name);
|
|
5219
|
-
|
|
5220
|
-
|
|
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
|
|
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 =
|
|
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 }) =>
|
|
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 (
|
|
5347
|
-
fix: "
|
|
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 =
|
|
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) =>
|
|
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) =>
|
|
7587
|
+
members.map((m) => relative4(root, m))
|
|
5445
7588
|
)}`
|
|
5446
|
-
} : { name: "project", status: "pass", detail: `chant project (${
|
|
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.
|
|
5507
|
-
|
|
5508
|
-
|
|
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
|
|
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) =>
|
|
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) =>
|
|
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 (!
|
|
5694
|
-
process.stderr.write(`behold doctor: no such directory: ${
|
|
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
|
|
5704
|
-
const registry = loadDemoRegistry(
|
|
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 =
|
|
5756
|
-
|
|
5757
|
-
)
|
|
5758
|
-
|
|
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
|
-
|
|
5765
|
-
|
|
5766
|
-
|
|
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
|
-
|
|
5769
|
-
|
|
5770
|
-
|
|
5771
|
-
|
|
5772
|
-
|
|
5773
|
-
|
|
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
|
-
|
|
5776
|
-
|
|
5777
|
-
|
|
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
|
-
|
|
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
|
-
|
|
5787
|
-
|
|
5788
|
-
|
|
5789
|
-
const
|
|
5790
|
-
if (
|
|
5791
|
-
|
|
5792
|
-
|
|
5793
|
-
|
|
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
|
-
|
|
5797
|
-
|
|
5798
|
-
|
|
5799
|
-
|
|
5800
|
-
|
|
5801
|
-
|
|
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
|
-
|
|
8020
|
+
if (!existsSync13(serving)) {
|
|
8021
|
+
process.stderr.write(`behold demo carve: no carve report at ${serving}
|
|
5807
8022
|
`);
|
|
5808
|
-
|
|
5809
|
-
|
|
5810
|
-
|
|
5811
|
-
|
|
5812
|
-
|
|
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 =
|
|
5837
|
-
if (!
|
|
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 =
|
|
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 =
|
|
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 =
|
|
8099
|
+
const projectDir = resolve7(dirArg ?? process.cwd());
|
|
5872
8100
|
if (emulator) {
|
|
5873
8101
|
injectEmulatorEnv(env);
|
|
5874
8102
|
env ??= "local";
|
|
5875
8103
|
}
|
|
5876
|
-
if (!
|
|
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
|
-
|
|
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
|
-
|
|
8128
|
+
run2 as run
|
|
5901
8129
|
};
|