@intentius/behold 0.7.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 +94 -2
- package/README.md +133 -2
- package/demos.json +25 -1
- package/dist/cli.js +3176 -330
- package/example-argo-estate/README.md +71 -0
- package/example-argo-estate/app-a/chant.config.ts +18 -0
- package/example-argo-estate/app-a/manifests/app.yaml +54 -0
- package/example-argo-estate/app-a/package.json +13 -0
- package/example-argo-estate/app-a/src/app.ts +35 -0
- package/example-argo-estate/app-a/tsconfig.json +1 -0
- package/example-argo-estate/app-b/chant.config.ts +18 -0
- package/example-argo-estate/app-b/manifests/app.yaml +54 -0
- package/example-argo-estate/app-b/package.json +13 -0
- package/example-argo-estate/app-b/src/app.ts +34 -0
- package/example-argo-estate/app-b/tsconfig.json +1 -0
- package/example-argo-estate/control-plane/chant.config.ts +29 -0
- package/example-argo-estate/control-plane/package.json +13 -0
- package/example-argo-estate/control-plane/src/argo.ts +69 -0
- package/example-argo-estate/control-plane/tsconfig.json +1 -0
- package/example-argo-estate/package-lock.json +1949 -0
- package/example-argo-estate/package.json +10 -0
- 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 +22 -12
- 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 +10 -2
- package/example-flux-estate/package-lock.json +17 -17
- package/example-k8s/README.md +16 -2
- package/example-k8s/base/deployment.yaml +21 -0
- package/example-k8s/base/kustomization.yaml +3 -0
- package/example-k8s/base/service.yaml +12 -0
- package/example-k8s/chant.config.ts +10 -0
- package/example-k8s/overlays/dev/kustomization.yaml +6 -0
- 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 +9 -6
- package/web/app.js +1027 -42
- 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/icons/cncf/argo.svg +1 -0
- package/web/icons/cncf/flux.svg +1 -0
- package/web/icons/cncf/helm.svg +1 -0
- package/web/icons/k8s/c-role.svg +1 -0
- package/web/icons/k8s/cm.svg +1 -0
- package/web/icons/k8s/crb.svg +1 -0
- package/web/icons/k8s/crd.svg +1 -0
- package/web/icons/k8s/cronjob.svg +1 -0
- package/web/icons/k8s/deploy.svg +1 -0
- package/web/icons/k8s/ds.svg +1 -0
- package/web/icons/k8s/ep.svg +1 -0
- package/web/icons/k8s/group.svg +1 -0
- package/web/icons/k8s/hpa.svg +1 -0
- package/web/icons/k8s/ing.svg +1 -0
- package/web/icons/k8s/job.svg +1 -0
- package/web/icons/k8s/limits.svg +1 -0
- package/web/icons/k8s/netpol.svg +1 -0
- package/web/icons/k8s/ns.svg +1 -0
- package/web/icons/k8s/pod.svg +1 -0
- package/web/icons/k8s/psp.svg +1 -0
- package/web/icons/k8s/pv.svg +1 -0
- package/web/icons/k8s/pvc.svg +1 -0
- package/web/icons/k8s/quota.svg +1 -0
- package/web/icons/k8s/rb.svg +1 -0
- package/web/icons/k8s/role.svg +1 -0
- package/web/icons/k8s/rs.svg +1 -0
- package/web/icons/k8s/sa.svg +1 -0
- package/web/icons/k8s/sc.svg +1 -0
- package/web/icons/k8s/secret.svg +1 -0
- package/web/icons/k8s/sts.svg +1 -0
- package/web/icons/k8s/svc.svg +1 -0
- package/web/icons/k8s/user.svg +1 -0
- package/web/icons/k8s/vol.svg +1 -0
- package/web/index.html +285 -85
- package/web/json-view.js +334 -0
- package/web/json-view.test.js +218 -0
- package/web/layout-store.js +278 -0
- package/web/layout-store.test.js +388 -0
- package/web/panel.js +28 -0
- package/web/theme.js +114 -6
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 {
|
|
5
|
-
import {
|
|
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";
|
|
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;
|
|
@@ -87,11 +1117,11 @@ function contextBindsCluster(context, clusterName) {
|
|
|
87
1117
|
import { streamSSE } from "hono/streaming";
|
|
88
1118
|
import { serveStatic } from "@hono/node-server/serve-static";
|
|
89
1119
|
import { serve } from "@hono/node-server";
|
|
90
|
-
import { fileURLToPath } from "node:url";
|
|
91
|
-
import { execFile as
|
|
92
|
-
import { promisify as
|
|
93
|
-
import { existsSync as
|
|
94
|
-
import { dirname as
|
|
1120
|
+
import { fileURLToPath as fileURLToPath3 } from "node:url";
|
|
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";
|
|
@@ -125,8 +1155,9 @@ function addRecent(dir, now = () => (/* @__PURE__ */ new Date()).toISOString())
|
|
|
125
1155
|
|
|
126
1156
|
// src/chant.ts
|
|
127
1157
|
import { spawn } from "node:child_process";
|
|
128
|
-
import { existsSync as existsSync3 } from "node:fs";
|
|
1158
|
+
import { existsSync as existsSync3, readFileSync as readFileSync3 } from "node:fs";
|
|
129
1159
|
import { createRequire } from "node:module";
|
|
1160
|
+
import { fileURLToPath } from "node:url";
|
|
130
1161
|
import { dirname as dirname2, join as join3, resolve } from "node:path";
|
|
131
1162
|
|
|
132
1163
|
// src/project.ts
|
|
@@ -135,7 +1166,7 @@ import { join as join2 } from "node:path";
|
|
|
135
1166
|
import { pathToFileURL } from "node:url";
|
|
136
1167
|
var CONFIG_NAMES = ["chant.config.ts", "chant.config.mts", "chant.config.js", "chant.config.mjs"];
|
|
137
1168
|
var BEHOLD_CONFIG_NAME = ".behold.json";
|
|
138
|
-
function
|
|
1169
|
+
function chantConfigPath(projectDir) {
|
|
139
1170
|
for (const name of CONFIG_NAMES) {
|
|
140
1171
|
const p = join2(projectDir, name);
|
|
141
1172
|
if (existsSync2(p)) return p;
|
|
@@ -222,7 +1253,7 @@ function parseStringLiteral(content, key) {
|
|
|
222
1253
|
return m?.[1];
|
|
223
1254
|
}
|
|
224
1255
|
async function detectProject(projectDir) {
|
|
225
|
-
const path =
|
|
1256
|
+
const path = chantConfigPath(projectDir);
|
|
226
1257
|
if (!path) return { environments: [], lexicons: [] };
|
|
227
1258
|
try {
|
|
228
1259
|
const mod = await import(pathToFileURL(path).href);
|
|
@@ -253,11 +1284,30 @@ function loadBeholdConfig(projectDir) {
|
|
|
253
1284
|
try {
|
|
254
1285
|
const raw = JSON.parse(readFileSync2(path, "utf8"));
|
|
255
1286
|
const tiers = readTiers(raw);
|
|
256
|
-
|
|
1287
|
+
const members = Array.isArray(raw.members) ? raw.members.filter((m) => typeof m === "string" && !!m) : [];
|
|
1288
|
+
return { ...tiers ? { tiers } : {}, ...members.length ? { members } : {} };
|
|
257
1289
|
} catch {
|
|
258
1290
|
return {};
|
|
259
1291
|
}
|
|
260
1292
|
}
|
|
1293
|
+
function readWorkspaces(projectDir) {
|
|
1294
|
+
try {
|
|
1295
|
+
const raw = JSON.parse(readFileSync2(join2(projectDir, "package.json"), "utf8"));
|
|
1296
|
+
const ws = Array.isArray(raw.workspaces) ? raw.workspaces : raw.workspaces?.packages;
|
|
1297
|
+
return Array.isArray(ws) ? ws.filter((w) => typeof w === "string") : [];
|
|
1298
|
+
} catch {
|
|
1299
|
+
return [];
|
|
1300
|
+
}
|
|
1301
|
+
}
|
|
1302
|
+
function detectProjectShape(projectDir) {
|
|
1303
|
+
const configFile = chantConfigPath(projectDir);
|
|
1304
|
+
if (configFile) return { kind: "project", configFile };
|
|
1305
|
+
const declared = loadBeholdConfig(projectDir).members;
|
|
1306
|
+
const from = declared ? "behold-config" : "workspaces";
|
|
1307
|
+
const members = (declared ?? readWorkspaces(projectDir)).filter((m) => !!chantConfigPath(join2(projectDir, m)));
|
|
1308
|
+
if (members.length) return { kind: "estate", members, membersFrom: from };
|
|
1309
|
+
return { kind: "none" };
|
|
1310
|
+
}
|
|
261
1311
|
|
|
262
1312
|
// node_modules/@intentius/chant/src/yaml.ts
|
|
263
1313
|
function parseYAML(content) {
|
|
@@ -543,21 +1593,21 @@ function graphFlags(opts) {
|
|
|
543
1593
|
if (opts.env) flags.push("--env", opts.env);
|
|
544
1594
|
if (opts.live) flags.push("--live");
|
|
545
1595
|
if (opts.overlay) flags.push("--overlay");
|
|
1596
|
+
if (opts.namespace) flags.push("--namespace", opts.namespace);
|
|
546
1597
|
return flags;
|
|
547
1598
|
}
|
|
548
|
-
function
|
|
1599
|
+
function packageManifestFrom(req, name) {
|
|
549
1600
|
let entry;
|
|
550
1601
|
try {
|
|
551
|
-
entry = req.resolve(
|
|
1602
|
+
entry = req.resolve(name);
|
|
552
1603
|
} catch {
|
|
553
1604
|
return void 0;
|
|
554
1605
|
}
|
|
555
1606
|
let dir = dirname2(entry);
|
|
556
1607
|
for (; ; ) {
|
|
557
|
-
const manifest = join3(dir, "package.json");
|
|
558
1608
|
try {
|
|
559
|
-
const pkg = createRequire(import.meta.url)(
|
|
560
|
-
if (pkg.name ===
|
|
1609
|
+
const pkg = createRequire(import.meta.url)(join3(dir, "package.json"));
|
|
1610
|
+
if (pkg.name === name) return { root: dir, manifest: pkg };
|
|
561
1611
|
} catch {
|
|
562
1612
|
}
|
|
563
1613
|
const parent = dirname2(dir);
|
|
@@ -566,14 +1616,52 @@ function chantBinFrom(req) {
|
|
|
566
1616
|
}
|
|
567
1617
|
return void 0;
|
|
568
1618
|
}
|
|
569
|
-
function
|
|
1619
|
+
function chantPackageFrom(req) {
|
|
1620
|
+
const found = packageManifestFrom(req, "@intentius/chant");
|
|
1621
|
+
if (!found) return void 0;
|
|
1622
|
+
const { root, manifest } = found;
|
|
1623
|
+
return { root, bin: join3(root, manifest.bin?.chant ?? "bin/chant"), ...manifest.version ? { version: manifest.version } : {} };
|
|
1624
|
+
}
|
|
1625
|
+
function resolveLexicons(projectDir, lexicons) {
|
|
1626
|
+
const req = createRequire(join3(resolve(projectDir), "noop.js"));
|
|
1627
|
+
return lexicons.map((lexicon) => {
|
|
1628
|
+
const pkg = `@intentius/chant-lexicon-${lexicon}`;
|
|
1629
|
+
const found = packageManifestFrom(req, pkg);
|
|
1630
|
+
return { lexicon, pkg, installed: !!found, ...found?.manifest.version ? { version: found.manifest.version } : {} };
|
|
1631
|
+
});
|
|
1632
|
+
}
|
|
1633
|
+
function resolveChant(projectDir) {
|
|
1634
|
+
const own = chantPackageFrom(createRequire(import.meta.url));
|
|
570
1635
|
if (projectDir) {
|
|
571
|
-
const fromProject =
|
|
572
|
-
if (fromProject) return fromProject;
|
|
1636
|
+
const fromProject = chantPackageFrom(createRequire(join3(resolve(projectDir), "noop.js")));
|
|
1637
|
+
if (fromProject) return { source: fromProject.root === own?.root ? "behold" : "project", ...fromProject };
|
|
573
1638
|
}
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
1639
|
+
if (own) return { source: "behold", ...own };
|
|
1640
|
+
return { source: "path", bin: "chant" };
|
|
1641
|
+
}
|
|
1642
|
+
function chantFloor() {
|
|
1643
|
+
try {
|
|
1644
|
+
const here = dirname2(fileURLToPath(import.meta.url));
|
|
1645
|
+
const pkg = JSON.parse(readFileSync3(join3(here, "..", "package.json"), "utf8"));
|
|
1646
|
+
return pkg.dependencies?.["@intentius/chant"]?.replace(/^[^0-9]*/, "") || void 0;
|
|
1647
|
+
} catch {
|
|
1648
|
+
return void 0;
|
|
1649
|
+
}
|
|
1650
|
+
}
|
|
1651
|
+
function meetsFloor(version, floor) {
|
|
1652
|
+
if (!version || !floor) return true;
|
|
1653
|
+
const parts = (v) => v.split("-")[0].split(".").map((n) => Number.parseInt(n, 10));
|
|
1654
|
+
const a = parts(version);
|
|
1655
|
+
const b = parts(floor);
|
|
1656
|
+
if (a.some(Number.isNaN) || b.some(Number.isNaN)) return true;
|
|
1657
|
+
for (let i = 0; i < Math.max(a.length, b.length); i++) {
|
|
1658
|
+
const d = (a[i] ?? 0) - (b[i] ?? 0);
|
|
1659
|
+
if (d !== 0) return d > 0;
|
|
1660
|
+
}
|
|
1661
|
+
return true;
|
|
1662
|
+
}
|
|
1663
|
+
function chantBin(projectDir) {
|
|
1664
|
+
return resolveChant(projectDir).bin;
|
|
577
1665
|
}
|
|
578
1666
|
function runChantRaw(args, projectDir, envOverride) {
|
|
579
1667
|
return new Promise((resolvePromise, reject) => {
|
|
@@ -746,10 +1834,10 @@ function applyArgs(target, env) {
|
|
|
746
1834
|
}
|
|
747
1835
|
|
|
748
1836
|
// src/cluster-root.ts
|
|
749
|
-
import { execFile
|
|
750
|
-
import { promisify
|
|
751
|
-
var
|
|
752
|
-
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) {
|
|
753
1841
|
try {
|
|
754
1842
|
const out = await exec("k3d", ["cluster", "list", "--no-headers"]);
|
|
755
1843
|
const clusters = /* @__PURE__ */ new Map();
|
|
@@ -765,8 +1853,8 @@ async function runningK3dClusters(exec = defaultExec2) {
|
|
|
765
1853
|
return void 0;
|
|
766
1854
|
}
|
|
767
1855
|
}
|
|
768
|
-
async function
|
|
769
|
-
const { stdout } = await
|
|
1856
|
+
async function defaultExec(cmd, args) {
|
|
1857
|
+
const { stdout } = await run(cmd, args, { encoding: "utf8", timeout: 1e4 });
|
|
770
1858
|
return stdout;
|
|
771
1859
|
}
|
|
772
1860
|
function k3dClusterName(node) {
|
|
@@ -798,7 +1886,7 @@ function mergeClusterRoot(ir, clusterIr, running) {
|
|
|
798
1886
|
}
|
|
799
1887
|
|
|
800
1888
|
// src/helm-releases.ts
|
|
801
|
-
import { existsSync as existsSync4, readdirSync, readFileSync as
|
|
1889
|
+
import { existsSync as existsSync4, readdirSync, readFileSync as readFileSync4, statSync } from "node:fs";
|
|
802
1890
|
import { join as join4 } from "node:path";
|
|
803
1891
|
var RELEASE_KEY = /^release\/([^/]+)\/(.+)$/;
|
|
804
1892
|
function declaredChartNames(ir) {
|
|
@@ -839,7 +1927,7 @@ function discoverReleaseUnits(projectDir) {
|
|
|
839
1927
|
for (const file of files) {
|
|
840
1928
|
let content;
|
|
841
1929
|
try {
|
|
842
|
-
content =
|
|
1930
|
+
content = readFileSync4(file, "utf8");
|
|
843
1931
|
} catch {
|
|
844
1932
|
continue;
|
|
845
1933
|
}
|
|
@@ -892,7 +1980,7 @@ function synthesizeHelmReleases(ir, observed, owners) {
|
|
|
892
1980
|
|
|
893
1981
|
// src/gh-run.ts
|
|
894
1982
|
import { spawn as spawn2 } from "node:child_process";
|
|
895
|
-
import { readdirSync as readdirSync2, readFileSync as
|
|
1983
|
+
import { readdirSync as readdirSync2, readFileSync as readFileSync5 } from "node:fs";
|
|
896
1984
|
import { join as join5 } from "node:path";
|
|
897
1985
|
|
|
898
1986
|
// src/ci-run.ts
|
|
@@ -947,19 +2035,19 @@ function finishPipelineProgress(state, exitCode) {
|
|
|
947
2035
|
}
|
|
948
2036
|
|
|
949
2037
|
// src/gh-run.ts
|
|
950
|
-
var defaultGhExec = (args) => new Promise((
|
|
2038
|
+
var defaultGhExec = (args) => new Promise((resolve8) => {
|
|
951
2039
|
let out = "";
|
|
952
2040
|
let proc;
|
|
953
2041
|
try {
|
|
954
2042
|
proc = spawn2("gh", args, { stdio: ["ignore", "pipe", "pipe"] });
|
|
955
2043
|
} catch {
|
|
956
|
-
|
|
2044
|
+
resolve8({ code: 127, out: "" });
|
|
957
2045
|
return;
|
|
958
2046
|
}
|
|
959
2047
|
proc.stdout.on("data", (d) => out += d);
|
|
960
2048
|
proc.stderr.on("data", (d) => out += d);
|
|
961
|
-
proc.on("error", () =>
|
|
962
|
-
proc.on("close", (code) =>
|
|
2049
|
+
proc.on("error", () => resolve8({ code: 127, out }));
|
|
2050
|
+
proc.on("close", (code) => resolve8({ code: code ?? 1, out }));
|
|
963
2051
|
});
|
|
964
2052
|
async function ghReady(exec = defaultGhExec) {
|
|
965
2053
|
const { code, out } = await exec(["auth", "status"]);
|
|
@@ -993,7 +2081,7 @@ function pickWorkflow(projectDir, pipeline) {
|
|
|
993
2081
|
for (const f of files) {
|
|
994
2082
|
let text;
|
|
995
2083
|
try {
|
|
996
|
-
text =
|
|
2084
|
+
text = readFileSync5(join5(dir, f), "utf8");
|
|
997
2085
|
} catch {
|
|
998
2086
|
continue;
|
|
999
2087
|
}
|
|
@@ -1103,11 +2191,11 @@ function joinCiProgress(ir, state) {
|
|
|
1103
2191
|
}
|
|
1104
2192
|
for (const n of ir.nodes) {
|
|
1105
2193
|
if (n.kind !== "Component") continue;
|
|
1106
|
-
const
|
|
1107
|
-
if (!
|
|
2194
|
+
const run3 = byComponent.get(n.id);
|
|
2195
|
+
if (!run3) continue;
|
|
1108
2196
|
const attrs = n.attrs ??= {};
|
|
1109
|
-
attrs.ci =
|
|
1110
|
-
attrs._ciJob =
|
|
2197
|
+
attrs.ci = run3.status;
|
|
2198
|
+
attrs._ciJob = run3.job;
|
|
1111
2199
|
}
|
|
1112
2200
|
return ir;
|
|
1113
2201
|
}
|
|
@@ -1272,14 +2360,14 @@ var WORKLOAD_KINDS = /* @__PURE__ */ new Set([
|
|
|
1272
2360
|
function rec(v) {
|
|
1273
2361
|
return v && typeof v === "object" && !Array.isArray(v) ? v : void 0;
|
|
1274
2362
|
}
|
|
1275
|
-
function
|
|
2363
|
+
function str(v) {
|
|
1276
2364
|
return typeof v === "string" && v.length > 0 ? v : void 0;
|
|
1277
2365
|
}
|
|
1278
2366
|
function namespaceOf(n) {
|
|
1279
|
-
return
|
|
2367
|
+
return str(rec(n.attrs?.metadata)?.namespace) ?? "default";
|
|
1280
2368
|
}
|
|
1281
2369
|
function nameOf(n) {
|
|
1282
|
-
return
|
|
2370
|
+
return str(rec(n.attrs?.metadata)?.name);
|
|
1283
2371
|
}
|
|
1284
2372
|
function labelRecord(v) {
|
|
1285
2373
|
const r = rec(v);
|
|
@@ -1309,12 +2397,21 @@ var FLUX_SOURCE_KINDS = /* @__PURE__ */ new Set([
|
|
|
1309
2397
|
"K8s::Flux::HelmChart",
|
|
1310
2398
|
"K8s::Flux::Bucket"
|
|
1311
2399
|
]);
|
|
1312
|
-
function sourceRefOf(refObj, referrer) {
|
|
2400
|
+
function sourceRefOf(refObj, referrer, defaultKind) {
|
|
1313
2401
|
const ref = rec(refObj);
|
|
1314
|
-
const kind =
|
|
1315
|
-
const name =
|
|
2402
|
+
const kind = str(ref?.kind) ?? defaultKind;
|
|
2403
|
+
const name = str(ref?.name);
|
|
1316
2404
|
if (!kind || !name) return void 0;
|
|
1317
|
-
return { kind, name, namespace:
|
|
2405
|
+
return { kind, name, namespace: str(ref?.namespace) ?? namespaceOf(referrer) };
|
|
2406
|
+
}
|
|
2407
|
+
function namedRefOf(refObj, referrer) {
|
|
2408
|
+
const ref = rec(refObj);
|
|
2409
|
+
const name = str(ref?.name);
|
|
2410
|
+
if (!name) return void 0;
|
|
2411
|
+
return { name, namespace: str(ref?.namespace) ?? namespaceOf(referrer) };
|
|
2412
|
+
}
|
|
2413
|
+
function list(v) {
|
|
2414
|
+
return Array.isArray(v) ? v : [];
|
|
1318
2415
|
}
|
|
1319
2416
|
function helmReleaseSourceRef(n) {
|
|
1320
2417
|
return rec(rec(rec(n.attrs?.spec)?.chart)?.spec)?.sourceRef;
|
|
@@ -1322,7 +2419,7 @@ function helmReleaseSourceRef(n) {
|
|
|
1322
2419
|
function ingressBackendServices(n) {
|
|
1323
2420
|
const spec = rec(n.attrs?.spec);
|
|
1324
2421
|
const out = [];
|
|
1325
|
-
const backendName = (b) =>
|
|
2422
|
+
const backendName = (b) => str(rec(rec(b)?.service)?.name);
|
|
1326
2423
|
const fromDefault = backendName(spec?.defaultBackend);
|
|
1327
2424
|
if (fromDefault) out.push(fromDefault);
|
|
1328
2425
|
const rules = Array.isArray(spec?.rules) ? spec.rules : [];
|
|
@@ -1343,19 +2440,38 @@ function deriveK8sEdges(nodes) {
|
|
|
1343
2440
|
if (from !== to) out.push({ from, to, kind: "ref", viaAttr, inferred: true });
|
|
1344
2441
|
};
|
|
1345
2442
|
const workloads = k8s.filter((n) => WORKLOAD_KINDS.has(n.kind));
|
|
1346
|
-
const
|
|
1347
|
-
const
|
|
2443
|
+
const byKindNsName = /* @__PURE__ */ new Map();
|
|
2444
|
+
const appProjectsByName = /* @__PURE__ */ new Map();
|
|
1348
2445
|
for (const n of k8s) {
|
|
1349
2446
|
const name = nameOf(n);
|
|
1350
2447
|
if (!name) continue;
|
|
1351
|
-
|
|
1352
|
-
if (
|
|
2448
|
+
byKindNsName.set(`${n.kind}\0${namespaceOf(n)}\0${name}`, n);
|
|
2449
|
+
if (n.kind === "K8s::Argo::AppProject") {
|
|
2450
|
+
const same = appProjectsByName.get(name) ?? [];
|
|
2451
|
+
same.push(n);
|
|
2452
|
+
appProjectsByName.set(name, same);
|
|
2453
|
+
}
|
|
1353
2454
|
}
|
|
1354
|
-
const
|
|
1355
|
-
|
|
2455
|
+
const lookup = (kind, namespace, name) => byKindNsName.get(`${kind}\0${namespace}\0${name}`);
|
|
2456
|
+
const addFluxRef = (n, refObj, viaAttr, allowed, defaultKind) => {
|
|
2457
|
+
const ref = sourceRefOf(refObj, n, defaultKind);
|
|
2458
|
+
if (!ref || ref.name === "*") return;
|
|
2459
|
+
const kind = `K8s::Flux::${ref.kind}`;
|
|
2460
|
+
if (allowed && !allowed.has(kind)) return;
|
|
2461
|
+
const target = lookup(kind, ref.namespace, ref.name);
|
|
2462
|
+
if (target) add(n.id, target.id, viaAttr);
|
|
2463
|
+
};
|
|
2464
|
+
const addSourceRef = (n, refObj, viaAttr, defaultKind) => addFluxRef(n, refObj, viaAttr, FLUX_SOURCE_KINDS, defaultKind);
|
|
2465
|
+
const addNamedRef = (n, refObj, kind, viaAttr) => {
|
|
2466
|
+
const ref = namedRefOf(refObj, n);
|
|
1356
2467
|
if (!ref) return;
|
|
1357
|
-
const
|
|
1358
|
-
if (
|
|
2468
|
+
const target = lookup(kind, ref.namespace, ref.name);
|
|
2469
|
+
if (target) add(n.id, target.id, viaAttr);
|
|
2470
|
+
};
|
|
2471
|
+
const addProject = (n, project, viaAttr) => {
|
|
2472
|
+
const candidates = project ? appProjectsByName.get(project) ?? [] : [];
|
|
2473
|
+
const target = candidates.length === 1 ? candidates[0] : candidates.find((p) => namespaceOf(p) === namespaceOf(n));
|
|
2474
|
+
if (target) add(n.id, target.id, viaAttr);
|
|
1359
2475
|
};
|
|
1360
2476
|
for (const n of k8s) {
|
|
1361
2477
|
if (n.kind === "K8s::Core::Service") {
|
|
@@ -1370,15 +2486,15 @@ function deriveK8sEdges(nodes) {
|
|
|
1370
2486
|
}
|
|
1371
2487
|
if (n.kind === "K8s::Networking::Ingress") {
|
|
1372
2488
|
for (const svcName of ingressBackendServices(n)) {
|
|
1373
|
-
const svc =
|
|
2489
|
+
const svc = lookup("K8s::Core::Service", namespaceOf(n), svcName);
|
|
1374
2490
|
if (svc) add(n.id, svc.id, "ingress backend");
|
|
1375
2491
|
}
|
|
1376
2492
|
continue;
|
|
1377
2493
|
}
|
|
1378
2494
|
if (n.kind === "K8s::Autoscaling::HorizontalPodAutoscaler") {
|
|
1379
2495
|
const ref = rec(rec(n.attrs?.spec)?.scaleTargetRef);
|
|
1380
|
-
const refKind =
|
|
1381
|
-
const refName =
|
|
2496
|
+
const refKind = str(ref?.kind);
|
|
2497
|
+
const refName = str(ref?.name);
|
|
1382
2498
|
if (!refKind || !refName) continue;
|
|
1383
2499
|
for (const w of workloads) {
|
|
1384
2500
|
if (namespaceOf(w) !== namespaceOf(n)) continue;
|
|
@@ -1388,10 +2504,36 @@ function deriveK8sEdges(nodes) {
|
|
|
1388
2504
|
}
|
|
1389
2505
|
if (n.kind === "K8s::Flux::Kustomization") {
|
|
1390
2506
|
addSourceRef(n, rec(n.attrs?.spec)?.sourceRef, "sourceRef");
|
|
2507
|
+
for (const dep of list(rec(n.attrs?.spec)?.dependsOn)) {
|
|
2508
|
+
addNamedRef(n, dep, "K8s::Flux::Kustomization", "dependsOn");
|
|
2509
|
+
}
|
|
1391
2510
|
continue;
|
|
1392
2511
|
}
|
|
1393
2512
|
if (n.kind === "K8s::Flux::HelmRelease") {
|
|
1394
2513
|
addSourceRef(n, helmReleaseSourceRef(n), "chart sourceRef");
|
|
2514
|
+
continue;
|
|
2515
|
+
}
|
|
2516
|
+
if (n.kind === "K8s::Flux::ImagePolicy") {
|
|
2517
|
+
addNamedRef(n, rec(n.attrs?.spec)?.imageRepositoryRef, "K8s::Flux::ImageRepository", "imageRepositoryRef");
|
|
2518
|
+
continue;
|
|
2519
|
+
}
|
|
2520
|
+
if (n.kind === "K8s::Flux::ImageUpdateAutomation") {
|
|
2521
|
+
addSourceRef(n, rec(n.attrs?.spec)?.sourceRef, "sourceRef", "GitRepository");
|
|
2522
|
+
continue;
|
|
2523
|
+
}
|
|
2524
|
+
if (n.kind === "K8s::Flux::Alert") {
|
|
2525
|
+
addNamedRef(n, rec(n.attrs?.spec)?.providerRef, "K8s::Flux::Provider", "providerRef");
|
|
2526
|
+
for (const src of list(rec(n.attrs?.spec)?.eventSources)) {
|
|
2527
|
+
addFluxRef(n, src, "eventSource");
|
|
2528
|
+
}
|
|
2529
|
+
continue;
|
|
2530
|
+
}
|
|
2531
|
+
if (n.kind === "K8s::Argo::Application") {
|
|
2532
|
+
addProject(n, str(rec(n.attrs?.spec)?.project), "project");
|
|
2533
|
+
continue;
|
|
2534
|
+
}
|
|
2535
|
+
if (n.kind === "K8s::Argo::ApplicationSet") {
|
|
2536
|
+
addProject(n, str(rec(rec(rec(n.attrs?.spec)?.template)?.spec)?.project), "template project");
|
|
1395
2537
|
}
|
|
1396
2538
|
}
|
|
1397
2539
|
return out;
|
|
@@ -1883,6 +3025,13 @@ function metaStr(node, key) {
|
|
|
1883
3025
|
const v = meta?.[key];
|
|
1884
3026
|
return typeof v === "string" ? v : void 0;
|
|
1885
3027
|
}
|
|
3028
|
+
function destinationNamespace(n) {
|
|
3029
|
+
if (n.kind !== "K8s::Argo::Application") return void 0;
|
|
3030
|
+
const spec = n.attrs?.spec;
|
|
3031
|
+
const dest = spec?.destination;
|
|
3032
|
+
const ns = dest?.namespace;
|
|
3033
|
+
return typeof ns === "string" && ns.length > 0 ? ns : void 0;
|
|
3034
|
+
}
|
|
1886
3035
|
function declaredNamespaces(nodes) {
|
|
1887
3036
|
const out = /* @__PURE__ */ new Set();
|
|
1888
3037
|
for (const n of nodes) {
|
|
@@ -1893,6 +3042,8 @@ function declaredNamespaces(nodes) {
|
|
|
1893
3042
|
}
|
|
1894
3043
|
const ns = metaStr(n, "namespace");
|
|
1895
3044
|
if (ns) out.add(ns);
|
|
3045
|
+
const dest = destinationNamespace(n);
|
|
3046
|
+
if (dest) out.add(dest);
|
|
1896
3047
|
}
|
|
1897
3048
|
return out;
|
|
1898
3049
|
}
|
|
@@ -2223,9 +3374,9 @@ function enrichedRefs(ir, aws, byId) {
|
|
|
2223
3374
|
const producersByName = /* @__PURE__ */ new Map();
|
|
2224
3375
|
for (const ex of ir.exports ?? []) {
|
|
2225
3376
|
if (!ex.node || !byId.has(ex.node)) continue;
|
|
2226
|
-
const
|
|
2227
|
-
if (isExample(byId.get(ex.node)))
|
|
2228
|
-
else
|
|
3377
|
+
const list3 = producersByName.get(ex.name) ?? producersByName.set(ex.name, []).get(ex.name);
|
|
3378
|
+
if (isExample(byId.get(ex.node))) list3.push(ex.node);
|
|
3379
|
+
else list3.unshift(ex.node);
|
|
2229
3380
|
}
|
|
2230
3381
|
const exportNames = [...producersByName.keys()].sort((a, b) => b.length - a.length);
|
|
2231
3382
|
const bridge = (paramId, producers) => {
|
|
@@ -2586,9 +3737,12 @@ function logicalKept(before, after) {
|
|
|
2586
3737
|
if (after > 0 && after * 3 >= before) return void 0;
|
|
2587
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)`;
|
|
2588
3739
|
}
|
|
2589
|
-
function edgelessNote(zoom, ir) {
|
|
3740
|
+
function edgelessNote(zoom, ir, detail) {
|
|
2590
3741
|
if (zoom === "components" || zoom === "logical") return void 0;
|
|
2591
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
|
+
}
|
|
2592
3746
|
return "no edges \u2014 nothing in this estate references anything else";
|
|
2593
3747
|
}
|
|
2594
3748
|
var CLUSTER_SCOPED = /* @__PURE__ */ new Set([
|
|
@@ -2607,9 +3761,14 @@ function namespaceMismatchNote(nodes) {
|
|
|
2607
3761
|
if (!k8s.some((n) => n.attrs?._status === "good")) return void 0;
|
|
2608
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`;
|
|
2609
3763
|
}
|
|
2610
|
-
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) {
|
|
2611
3770
|
const primary = zoom === "logical" && logicalBefore !== void 0 ? logicalKept(logicalBefore, ir.nodes.length) : zoomNote(zoom, ir, compositeEdgesAttached);
|
|
2612
|
-
const notes = [primary, edgelessNote(zoom, ir)].filter((n) => n !== void 0);
|
|
3771
|
+
const notes = [primary, edgelessNote(zoom, ir, detail)].filter((n) => n !== void 0);
|
|
2613
3772
|
return notes.length ? notes.join(" \xB7 ") : void 0;
|
|
2614
3773
|
}
|
|
2615
3774
|
function tierMismatchNote(ir, tiers, currentTier) {
|
|
@@ -2713,7 +3872,289 @@ function summarizePlan(plan, byComponent, nonResource) {
|
|
|
2713
3872
|
}
|
|
2714
3873
|
|
|
2715
3874
|
// src/render.ts
|
|
2716
|
-
import { layoutIr, layoutArchitecture, renderSvg, cardSizes } from "@intentius/pinhole";
|
|
3875
|
+
import { layoutIr, layoutArchitecture, renderSvg, cardSizes, registerPack } from "@intentius/pinhole";
|
|
3876
|
+
|
|
3877
|
+
// src/icon-packs.ts
|
|
3878
|
+
import { readFileSync as readFileSync6 } from "node:fs";
|
|
3879
|
+
import { dirname as dirname3, join as join7 } from "node:path";
|
|
3880
|
+
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
3881
|
+
var ICON_ROOT = join7(dirname3(fileURLToPath2(import.meta.url)), "..", "web", "icons");
|
|
3882
|
+
var cache = /* @__PURE__ */ new Map();
|
|
3883
|
+
function unprefix(body) {
|
|
3884
|
+
return body.replace(/\s(?:inkscape|sodipodi):[\w-]+\s*=\s*(["'])[\s\S]*?\1/g, "").replace(/\sxlink:href\s*=/g, " href=");
|
|
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
|
+
}
|
|
3900
|
+
function loadIcon(rel) {
|
|
3901
|
+
const hit = cache.get(rel);
|
|
3902
|
+
if (hit) return hit;
|
|
3903
|
+
const raw = readFileSync6(join7(ICON_ROOT, `${rel}.svg`), "utf8");
|
|
3904
|
+
const open = raw.indexOf("<svg");
|
|
3905
|
+
const openEnd = raw.indexOf(">", open);
|
|
3906
|
+
const close = raw.lastIndexOf("</svg");
|
|
3907
|
+
if (open < 0 || openEnd < 0 || close < 0) throw new Error(`malformed vendored icon: ${rel}.svg`);
|
|
3908
|
+
const viewBox = /\bviewBox\s*=\s*(["'])(.*?)\1/.exec(raw.slice(open, openEnd))?.[2];
|
|
3909
|
+
if (!viewBox) throw new Error(`vendored icon has no viewBox: ${rel}.svg`);
|
|
3910
|
+
const bare = { body: unprefix(raw.slice(openEnd + 1, close).trim()), colored: true, viewBox };
|
|
3911
|
+
const glyph = PLATED.has(rel) ? plate(bare) : bare;
|
|
3912
|
+
cache.set(rel, glyph);
|
|
3913
|
+
return glyph;
|
|
3914
|
+
}
|
|
3915
|
+
var K8S_ICON_BY_KIND = {
|
|
3916
|
+
"K8s::Apiextensions::CustomResourceDefinition": "crd",
|
|
3917
|
+
"K8s::Apps::DaemonSet": "ds",
|
|
3918
|
+
"K8s::Apps::Deployment": "deploy",
|
|
3919
|
+
"K8s::Apps::ReplicaSet": "rs",
|
|
3920
|
+
"K8s::Apps::StatefulSet": "sts",
|
|
3921
|
+
"K8s::Autoscaling::HorizontalPodAutoscaler": "hpa",
|
|
3922
|
+
"K8s::Batch::CronJob": "cronjob",
|
|
3923
|
+
"K8s::Batch::Job": "job",
|
|
3924
|
+
"K8s::Core::ConfigMap": "cm",
|
|
3925
|
+
"K8s::Core::Endpoints": "ep",
|
|
3926
|
+
"K8s::Core::LimitRange": "limits",
|
|
3927
|
+
"K8s::Core::Namespace": "ns",
|
|
3928
|
+
"K8s::Core::PersistentVolume": "pv",
|
|
3929
|
+
"K8s::Core::PersistentVolumeClaim": "pvc",
|
|
3930
|
+
"K8s::Core::Pod": "pod",
|
|
3931
|
+
"K8s::Core::PodTemplate": "pod",
|
|
3932
|
+
"K8s::Core::ReplicationController": "rs",
|
|
3933
|
+
// the pre-ReplicaSet spelling of the same thing
|
|
3934
|
+
"K8s::Core::ResourceQuota": "quota",
|
|
3935
|
+
"K8s::Core::Secret": "secret",
|
|
3936
|
+
"K8s::Core::Service": "svc",
|
|
3937
|
+
"K8s::Core::ServiceAccount": "sa",
|
|
3938
|
+
"K8s::Core::Volume": "vol",
|
|
3939
|
+
"K8s::Networking::Ingress": "ing",
|
|
3940
|
+
"K8s::Networking::IngressClass": "ing",
|
|
3941
|
+
"K8s::Networking::NetworkPolicy": "netpol",
|
|
3942
|
+
"K8s::Rbac::ClusterRole": "c-role",
|
|
3943
|
+
"K8s::Rbac::ClusterRoleBinding": "crb",
|
|
3944
|
+
"K8s::Rbac::Role": "role",
|
|
3945
|
+
"K8s::Rbac::RoleBinding": "rb",
|
|
3946
|
+
"K8s::Storage::StorageClass": "sc"
|
|
3947
|
+
};
|
|
3948
|
+
var K8S_MARK_BY_PREFIX = [
|
|
3949
|
+
["K8s::Flux::", "cncf/flux"],
|
|
3950
|
+
["K8s::Argo::", "cncf/argo"]
|
|
3951
|
+
];
|
|
3952
|
+
var HELM_MARK_KINDS = /* @__PURE__ */ new Set(["Helm::Chart", "Helm::Release"]);
|
|
3953
|
+
function k8sIconFor(kind) {
|
|
3954
|
+
for (const [prefix, rel] of K8S_MARK_BY_PREFIX) {
|
|
3955
|
+
if (kind.startsWith(prefix)) return loadIcon(rel);
|
|
3956
|
+
}
|
|
3957
|
+
const stem = K8S_ICON_BY_KIND[kind];
|
|
3958
|
+
return stem ? loadIcon(`k8s/${stem}`) : void 0;
|
|
3959
|
+
}
|
|
3960
|
+
function helmIconFor(kind) {
|
|
3961
|
+
return HELM_MARK_KINDS.has(kind) ? loadIcon("cncf/helm") : void 0;
|
|
3962
|
+
}
|
|
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
|
+
|
|
4154
|
+
// src/render.ts
|
|
4155
|
+
registerPack({ lexicon: "k8s", iconFor: k8sIconFor });
|
|
4156
|
+
registerPack({ lexicon: "helm", iconFor: helmIconFor });
|
|
4157
|
+
registerPack({ lexicon: "terraform", iconFor: () => void 0, fields: carveCardFields });
|
|
2717
4158
|
function renderArchitecture(ir, byContainer, opts = {}) {
|
|
2718
4159
|
const spread = Math.min(1.5, ir.edges.length / Math.max(ir.nodes.length, 1));
|
|
2719
4160
|
const layout = layoutArchitecture(ir, byContainer, {
|
|
@@ -2744,6 +4185,59 @@ function renderGraph(ir, opts = {}) {
|
|
|
2744
4185
|
});
|
|
2745
4186
|
return { svg };
|
|
2746
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
|
+
}
|
|
2747
4241
|
var NODE_W = 175;
|
|
2748
4242
|
var NODE_H = 104;
|
|
2749
4243
|
function footprints(ir) {
|
|
@@ -2908,9 +4402,374 @@ function radializeLayout(layout, groupOf, size = /* @__PURE__ */ new Map()) {
|
|
|
2908
4402
|
layout.height = Math.max(...nodes.map((n) => n.y + hOf(n) / 2)) + pad;
|
|
2909
4403
|
}
|
|
2910
4404
|
|
|
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";
|
|
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
|
+
|
|
2911
4770
|
// src/ops.ts
|
|
2912
|
-
import { readdirSync as
|
|
2913
|
-
import { join as
|
|
4771
|
+
import { readdirSync as readdirSync4, readFileSync as readFileSync9, existsSync as existsSync8 } from "node:fs";
|
|
4772
|
+
import { join as join10 } from "node:path";
|
|
2914
4773
|
var APPLY_TARGET_LEXICON = {
|
|
2915
4774
|
cloudformation: "aws",
|
|
2916
4775
|
kubectl: "k8s",
|
|
@@ -2932,11 +4791,11 @@ function discoverOps(projectDir) {
|
|
|
2932
4791
|
const seen = /* @__PURE__ */ new Set();
|
|
2933
4792
|
const out = [];
|
|
2934
4793
|
for (const sub of ["ops", "src", "."]) {
|
|
2935
|
-
const dir =
|
|
2936
|
-
if (!
|
|
2937
|
-
for (const f of
|
|
4794
|
+
const dir = join10(projectDir, sub);
|
|
4795
|
+
if (!existsSync8(dir)) continue;
|
|
4796
|
+
for (const f of readdirSync4(dir)) {
|
|
2938
4797
|
if (!f.endsWith(".op.ts")) continue;
|
|
2939
|
-
const content =
|
|
4798
|
+
const content = readFileSync9(join10(dir, f), "utf8");
|
|
2940
4799
|
const name = content.match(/name:\s*["'`]([^"'`]+)["'`]/)?.[1];
|
|
2941
4800
|
if (!name || seen.has(name)) continue;
|
|
2942
4801
|
seen.add(name);
|
|
@@ -3049,6 +4908,133 @@ function classifyHealth(status) {
|
|
|
3049
4908
|
if (HEALTHY.test(text)) return "healthy";
|
|
3050
4909
|
return "unknown";
|
|
3051
4910
|
}
|
|
4911
|
+
function rec2(v) {
|
|
4912
|
+
return typeof v === "object" && v !== null && !Array.isArray(v) ? v : void 0;
|
|
4913
|
+
}
|
|
4914
|
+
function str2(v) {
|
|
4915
|
+
return typeof v === "string" && v.length > 0 ? v : void 0;
|
|
4916
|
+
}
|
|
4917
|
+
function statusTree(o) {
|
|
4918
|
+
const attrs = rec2(o.attributes);
|
|
4919
|
+
if (!attrs) return void 0;
|
|
4920
|
+
return rec2(attrs.status) ?? attrs;
|
|
4921
|
+
}
|
|
4922
|
+
var ARGO_HEALTH = {
|
|
4923
|
+
Healthy: "healthy",
|
|
4924
|
+
Degraded: "degraded",
|
|
4925
|
+
Progressing: "progressing",
|
|
4926
|
+
Suspended: "progressing",
|
|
4927
|
+
Missing: "progressing",
|
|
4928
|
+
Unknown: "unknown"
|
|
4929
|
+
};
|
|
4930
|
+
var ARGO_SYNC = {
|
|
4931
|
+
Synced: "healthy",
|
|
4932
|
+
OutOfSync: "progressing",
|
|
4933
|
+
Unknown: "unknown"
|
|
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
|
+
}
|
|
4947
|
+
function argoVerdict(o) {
|
|
4948
|
+
if (o.type !== "K8s::Argo::Application") return void 0;
|
|
4949
|
+
const tree = statusTree(o);
|
|
4950
|
+
let health = str2(rec2(tree?.health)?.status);
|
|
4951
|
+
let sync = str2(rec2(tree?.sync)?.status);
|
|
4952
|
+
if (!health && !sync) {
|
|
4953
|
+
const word = str2(o.status);
|
|
4954
|
+
if (word === "READY") {
|
|
4955
|
+
health = "Healthy";
|
|
4956
|
+
sync = "Synced";
|
|
4957
|
+
} else if (word && word in ARGO_SYNC) {
|
|
4958
|
+
sync = word;
|
|
4959
|
+
health = "Healthy";
|
|
4960
|
+
} else if (word && word in ARGO_HEALTH) health = word;
|
|
4961
|
+
}
|
|
4962
|
+
const verdicts = [health ? ARGO_HEALTH[health] : void 0, sync ? ARGO_SYNC[sync] : void 0].filter(
|
|
4963
|
+
(v) => v !== void 0
|
|
4964
|
+
);
|
|
4965
|
+
if (verdicts.length === 0) return void 0;
|
|
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;
|
|
4969
|
+
for (const rung of ["degraded", "progressing", "unknown"]) {
|
|
4970
|
+
if (verdicts.includes(rung)) return { health: rung, detail };
|
|
4971
|
+
}
|
|
4972
|
+
return { health: "healthy", detail };
|
|
4973
|
+
}
|
|
4974
|
+
var FLUX_PENDING_REASON = /^(progressing|reconciling)/i;
|
|
4975
|
+
function readyFromConditions(tree) {
|
|
4976
|
+
const conditions = tree?.conditions;
|
|
4977
|
+
if (!Array.isArray(conditions)) return void 0;
|
|
4978
|
+
for (const c of conditions) {
|
|
4979
|
+
const cond = rec2(c);
|
|
4980
|
+
if (!cond || cond.type !== "Ready") continue;
|
|
4981
|
+
const status = str2(cond.status);
|
|
4982
|
+
if (status !== "True" && status !== "False" && status !== "Unknown") continue;
|
|
4983
|
+
return { status, reason: str2(cond.reason) };
|
|
4984
|
+
}
|
|
4985
|
+
return void 0;
|
|
4986
|
+
}
|
|
4987
|
+
function readyFromChantConditions(o) {
|
|
4988
|
+
const conditions = rec2(o.attributes)?.conditions;
|
|
4989
|
+
if (!Array.isArray(conditions)) return void 0;
|
|
4990
|
+
for (const c of conditions) {
|
|
4991
|
+
if (typeof c !== "string") continue;
|
|
4992
|
+
const m = /^Ready(?:=([^:]*))?(?::|$)/.exec(c.trim());
|
|
4993
|
+
if (!m) continue;
|
|
4994
|
+
const reason = m[1]?.trim() || void 0;
|
|
4995
|
+
return { status: reason && FLUX_PENDING_REASON.test(reason) ? "Unknown" : "False", reason };
|
|
4996
|
+
}
|
|
4997
|
+
return void 0;
|
|
4998
|
+
}
|
|
4999
|
+
function readyFromStatusWord(o) {
|
|
5000
|
+
const word = str2(o.status);
|
|
5001
|
+
if (!word || word === "PRESENT") return void 0;
|
|
5002
|
+
if (word === "READY") return { status: "True" };
|
|
5003
|
+
if (word === "NOT-READY") return { status: "False" };
|
|
5004
|
+
return { status: FLUX_PENDING_REASON.test(word) ? "Unknown" : "False", reason: word };
|
|
5005
|
+
}
|
|
5006
|
+
function describeReady(ready) {
|
|
5007
|
+
return `Ready=${ready.status}${ready.reason ? ` (${ready.reason})` : ""}`;
|
|
5008
|
+
}
|
|
5009
|
+
function revisionVerdict(tree) {
|
|
5010
|
+
const applied = str2(tree?.lastAppliedRevision);
|
|
5011
|
+
const attempted = str2(tree?.lastAttemptedRevision);
|
|
5012
|
+
if (!applied || !attempted || applied === attempted) return void 0;
|
|
5013
|
+
return { health: "progressing", detail: `applied ${applied}, attempted ${attempted}` };
|
|
5014
|
+
}
|
|
5015
|
+
var FLUX_TYPE_PREFIX = "K8s::Flux::";
|
|
5016
|
+
function fluxVerdict(o) {
|
|
5017
|
+
if (!o.type?.startsWith(FLUX_TYPE_PREFIX)) return void 0;
|
|
5018
|
+
const tree = statusTree(o);
|
|
5019
|
+
const ready = readyFromConditions(tree) ?? readyFromChantConditions(o) ?? readyFromStatusWord(o);
|
|
5020
|
+
if (ready?.status === "False") return { health: "degraded", detail: describeReady(ready) };
|
|
5021
|
+
if (ready?.status === "Unknown") return { health: "progressing", detail: describeReady(ready) };
|
|
5022
|
+
if (ready?.status === "True") return revisionVerdict(tree) ?? { health: "healthy", detail: describeReady(ready) };
|
|
5023
|
+
const conditions = tree?.conditions;
|
|
5024
|
+
const reconciling = Array.isArray(conditions) && conditions.some((c) => {
|
|
5025
|
+
const cond = rec2(c);
|
|
5026
|
+
return cond?.type === "Reconciling" && cond.status === "True";
|
|
5027
|
+
});
|
|
5028
|
+
if (reconciling) return { health: "progressing", detail: "Reconciling=True" };
|
|
5029
|
+
if (!str2(o.status) || o.status === "PRESENT") {
|
|
5030
|
+
return { health: "progressing", detail: "no Ready condition yet" };
|
|
5031
|
+
}
|
|
5032
|
+
return void 0;
|
|
5033
|
+
}
|
|
5034
|
+
function classifyObservedHealth(observed) {
|
|
5035
|
+
if (!observed) return { health: "unknown" };
|
|
5036
|
+
return argoVerdict(observed) ?? fluxVerdict(observed) ?? { health: classifyHealth(observed.status) };
|
|
5037
|
+
}
|
|
3052
5038
|
|
|
3053
5039
|
// src/apply.ts
|
|
3054
5040
|
var PROGRESS_EVENT_TYPES = /* @__PURE__ */ new Set([
|
|
@@ -3335,23 +5321,23 @@ var OpRunner = class {
|
|
|
3335
5321
|
|
|
3336
5322
|
// src/substrates.ts
|
|
3337
5323
|
import { spawn as spawn3 } from "node:child_process";
|
|
3338
|
-
import { existsSync as
|
|
3339
|
-
import { join as
|
|
5324
|
+
import { existsSync as existsSync9, readFileSync as readFileSync10 } from "node:fs";
|
|
5325
|
+
import { join as join11 } from "node:path";
|
|
3340
5326
|
import { platform } from "node:os";
|
|
3341
5327
|
function probe(cmd, args) {
|
|
3342
|
-
return new Promise((
|
|
5328
|
+
return new Promise((resolve8) => {
|
|
3343
5329
|
let out = "";
|
|
3344
5330
|
let proc;
|
|
3345
5331
|
try {
|
|
3346
5332
|
proc = spawn3(cmd, args, { stdio: ["ignore", "pipe", "pipe"] });
|
|
3347
5333
|
} catch {
|
|
3348
|
-
|
|
5334
|
+
resolve8({ code: 127, out: "" });
|
|
3349
5335
|
return;
|
|
3350
5336
|
}
|
|
3351
5337
|
proc.stdout.on("data", (d) => out += d);
|
|
3352
5338
|
proc.stderr.on("data", (d) => out += d);
|
|
3353
|
-
proc.on("error", () =>
|
|
3354
|
-
proc.on("close", (code) =>
|
|
5339
|
+
proc.on("error", () => resolve8({ code: 127, out }));
|
|
5340
|
+
proc.on("close", (code) => resolve8({ code: code ?? 1, out }));
|
|
3355
5341
|
});
|
|
3356
5342
|
}
|
|
3357
5343
|
async function dockerAvailable() {
|
|
@@ -3364,11 +5350,11 @@ async function dockerRunning(nameFilter) {
|
|
|
3364
5350
|
return out.split(/\r?\n/).map((s) => s.trim()).filter(Boolean);
|
|
3365
5351
|
}
|
|
3366
5352
|
function scriptBringUp(projectDir, relPath, label) {
|
|
3367
|
-
return
|
|
5353
|
+
return existsSync9(join11(projectDir, relPath)) ? { label, cmd: "bash", args: [relPath] } : void 0;
|
|
3368
5354
|
}
|
|
3369
5355
|
function projectLexicons(projectDir) {
|
|
3370
5356
|
try {
|
|
3371
|
-
const src =
|
|
5357
|
+
const src = readFileSync10(join11(projectDir, "chant.config.ts"), "utf-8");
|
|
3372
5358
|
const m = src.match(/lexicons\s*:\s*\[([^\]]*)\]/);
|
|
3373
5359
|
if (!m) return [];
|
|
3374
5360
|
return [...m[1].matchAll(/["']([^"']+)["']/g)].map((x) => x[1]);
|
|
@@ -3418,7 +5404,7 @@ async function detectSubstrates(projectDir, preview = false, boundContext) {
|
|
|
3418
5404
|
["forgejo", "Forgejo", ".forgejo", "test/forgejo-runtime-e2e.sh"]
|
|
3419
5405
|
];
|
|
3420
5406
|
for (const [name, label, marker, script] of forges) {
|
|
3421
|
-
if (!
|
|
5407
|
+
if (!existsSync9(join11(projectDir, marker))) continue;
|
|
3422
5408
|
const c = docker ? await dockerRunning(name) : [];
|
|
3423
5409
|
const d = dep(c.length > 0, "container up", "on-demand (pipeline run)");
|
|
3424
5410
|
subs.push({
|
|
@@ -3448,7 +5434,7 @@ async function detectSubstrates(projectDir, preview = false, boundContext) {
|
|
|
3448
5434
|
detail: endpoint ? `targeting ${endpoint}` : "real Fly (FLY_FLAPS_BASE_URL unset)"
|
|
3449
5435
|
});
|
|
3450
5436
|
}
|
|
3451
|
-
if (
|
|
5437
|
+
if (existsSync9(join11(projectDir, ".github", "workflows"))) {
|
|
3452
5438
|
const gh = await probe("gh", ["auth", "status"]);
|
|
3453
5439
|
const ready = gh.code === 0;
|
|
3454
5440
|
subs.push({
|
|
@@ -3461,7 +5447,7 @@ async function detectSubstrates(projectDir, preview = false, boundContext) {
|
|
|
3461
5447
|
if (lexicons.includes("temporal")) {
|
|
3462
5448
|
let hasProfiles = false;
|
|
3463
5449
|
try {
|
|
3464
|
-
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"));
|
|
3465
5451
|
} catch {
|
|
3466
5452
|
}
|
|
3467
5453
|
subs.push({
|
|
@@ -3473,8 +5459,8 @@ async function detectSubstrates(projectDir, preview = false, boundContext) {
|
|
|
3473
5459
|
}
|
|
3474
5460
|
if (lexicons.includes("helm")) {
|
|
3475
5461
|
const helm = await probe("helm", ["version", "--short"]);
|
|
3476
|
-
const ambient = helm.code === 0 && !boundContext ? await
|
|
3477
|
-
const ctx = boundContext ??
|
|
5462
|
+
const ambient = helm.code === 0 && !boundContext ? await ambientContext() : void 0;
|
|
5463
|
+
const ctx = boundContext ?? ambient ?? "";
|
|
3478
5464
|
subs.push({
|
|
3479
5465
|
name: "helm",
|
|
3480
5466
|
label: "Helm",
|
|
@@ -3524,9 +5510,9 @@ function pickAutoSyncOps(mode, ops, running, movedLexicons, suspended = /* @__PU
|
|
|
3524
5510
|
}
|
|
3525
5511
|
|
|
3526
5512
|
// src/history.ts
|
|
3527
|
-
import { execFile as
|
|
3528
|
-
import { promisify as
|
|
3529
|
-
var execFileAsync =
|
|
5513
|
+
import { execFile as execFile2 } from "node:child_process";
|
|
5514
|
+
import { promisify as promisify2 } from "node:util";
|
|
5515
|
+
var execFileAsync = promisify2(execFile2);
|
|
3530
5516
|
var SEP = "";
|
|
3531
5517
|
function parseGitLog(stdout) {
|
|
3532
5518
|
return stdout.split("\n").map((l) => l.trim()).filter(Boolean).map((line) => {
|
|
@@ -3565,6 +5551,8 @@ async function openRollbackBranches(projectDir, env) {
|
|
|
3565
5551
|
}
|
|
3566
5552
|
|
|
3567
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";
|
|
3568
5556
|
import { composeStacks, shortStackNames } from "@intentius/pinhole";
|
|
3569
5557
|
async function composeEstate(projectDirs, opts = {}) {
|
|
3570
5558
|
const names = shortStackNames(projectDirs);
|
|
@@ -3573,23 +5561,126 @@ async function composeEstate(projectDirs, opts = {}) {
|
|
|
3573
5561
|
);
|
|
3574
5562
|
return composeStacks(stacks);
|
|
3575
5563
|
}
|
|
3576
|
-
var
|
|
5564
|
+
var firstLine2 = (e) => {
|
|
3577
5565
|
const msg = e instanceof Error ? e.message : String(e);
|
|
3578
5566
|
const m = msg.match(/exited \d+:\s*([\s\S]*)/);
|
|
3579
5567
|
return (m ? m[1] : msg).split("\n")[0].trim().slice(0, 160);
|
|
3580
5568
|
};
|
|
5569
|
+
function namespaceRuntimeOwners(name, ir) {
|
|
5570
|
+
const own = new Set(ir.nodes.map((n) => n.id));
|
|
5571
|
+
for (const n of ir.nodes) {
|
|
5572
|
+
if (n.runtimeOwner && own.has(n.runtimeOwner)) n.runtimeOwner = `${name}/${n.runtimeOwner}`;
|
|
5573
|
+
}
|
|
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}/`)));
|
|
5666
|
+
}
|
|
3581
5667
|
async function composeEstateOverlay(projectDirs, opts, classify) {
|
|
3582
5668
|
const names = shortStackNames(projectDirs);
|
|
3583
5669
|
const unobserved = [];
|
|
3584
5670
|
const dropped = [];
|
|
5671
|
+
const joined = [];
|
|
3585
5672
|
const stacks = new Array(projectDirs.length);
|
|
5673
|
+
const scopes = await estateNamespaceScopes(projectDirs, opts);
|
|
3586
5674
|
await Promise.all(
|
|
3587
5675
|
projectDirs.map(async (dir, i) => {
|
|
3588
5676
|
const name = names[i];
|
|
5677
|
+
const namespace = scopes.get(dir);
|
|
3589
5678
|
try {
|
|
3590
|
-
|
|
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 });
|
|
3591
5682
|
} catch (err) {
|
|
3592
|
-
const reason =
|
|
5683
|
+
const reason = firstLine2(err);
|
|
3593
5684
|
try {
|
|
3594
5685
|
const { env: _env, live: _live, overlay: _overlay, ...srcOpts } = opts;
|
|
3595
5686
|
const src = await graphIr(dir, srcOpts);
|
|
@@ -3597,7 +5688,7 @@ async function composeEstateOverlay(projectDirs, opts, classify) {
|
|
|
3597
5688
|
stacks[i] = { name, ir: src };
|
|
3598
5689
|
unobserved.push({ name, reason });
|
|
3599
5690
|
} catch (err2) {
|
|
3600
|
-
dropped.push({ name, reason:
|
|
5691
|
+
dropped.push({ name, reason: firstLine2(err2) });
|
|
3601
5692
|
}
|
|
3602
5693
|
}
|
|
3603
5694
|
})
|
|
@@ -3608,13 +5699,16 @@ async function composeEstateOverlay(projectDirs, opts, classify) {
|
|
|
3608
5699
|
observed: present.length - unobserved.length,
|
|
3609
5700
|
total: projectDirs.length,
|
|
3610
5701
|
unobserved,
|
|
3611
|
-
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))
|
|
3612
5706
|
};
|
|
3613
5707
|
}
|
|
3614
5708
|
|
|
3615
5709
|
// src/events.ts
|
|
3616
|
-
import { watch, existsSync as
|
|
3617
|
-
import { join as
|
|
5710
|
+
import { watch, existsSync as existsSync10 } from "node:fs";
|
|
5711
|
+
import { join as join13 } from "node:path";
|
|
3618
5712
|
var Broadcaster = class {
|
|
3619
5713
|
listeners = /* @__PURE__ */ new Set();
|
|
3620
5714
|
subscribe(fn) {
|
|
@@ -3632,7 +5726,7 @@ var Broadcaster = class {
|
|
|
3632
5726
|
};
|
|
3633
5727
|
var IGNORE = /(^|[\\/])(node_modules|dist|\.git)([\\/]|$)/;
|
|
3634
5728
|
function watchSource(projectDir, onChange, debounceMs = 200) {
|
|
3635
|
-
const dir =
|
|
5729
|
+
const dir = existsSync10(join13(projectDir, "src")) ? join13(projectDir, "src") : projectDir;
|
|
3636
5730
|
let timer;
|
|
3637
5731
|
const watcher = watch(dir, { recursive: true }, (_event, file) => {
|
|
3638
5732
|
const name = typeof file === "string" ? file : "";
|
|
@@ -3740,19 +5834,21 @@ import { renderMorphHtml, layoutIr as layoutIr2 } from "@intentius/pinhole";
|
|
|
3740
5834
|
function safeJson(value) {
|
|
3741
5835
|
return JSON.stringify(value).replace(/</g, "\\u003c");
|
|
3742
5836
|
}
|
|
5837
|
+
var LANES_MONO = `ui-monospace, "SF Mono", SFMono-Regular, "Cascadia Code", "JetBrains Mono", "IBM Plex Mono", Menlo, Consolas, monospace`;
|
|
3743
5838
|
var LANES_CSS = `
|
|
3744
|
-
body { background: var(--bg, #
|
|
3745
|
-
#behold-lanes { position: fixed; left: 0; right: 0; bottom: 0; background: var(--bg, #
|
|
3746
|
-
border-top: 1px solid var(--line, #
|
|
5839
|
+
body { background: var(--bg, #1e1e2e); color: var(--fg, #cdd6f4); padding-bottom: 150px; }
|
|
5840
|
+
#behold-lanes { position: fixed; left: 0; right: 0; bottom: 0; background: var(--bg, #1e1e2e);
|
|
5841
|
+
border-top: 1px solid var(--line, #3b3c4a); padding: 8px 12px 10px; font: 11px/1.5 ${LANES_MONO}; color: var(--muted, #777a8c); }
|
|
3747
5842
|
#behold-lanes .hd { display: flex; gap: 14px; align-items: baseline; margin-bottom: 4px; }
|
|
3748
|
-
#behold-lanes .hd .rt { color: var(--foreign, #
|
|
3749
|
-
#behold-lanes .hd a { color: var(--pending, #
|
|
5843
|
+
#behold-lanes .hd .rt { color: var(--foreign, #f9e2af); }
|
|
5844
|
+
#behold-lanes .hd a { color: var(--pending, #89b4fa); text-decoration: none; margin-left: auto; }
|
|
3750
5845
|
#behold-lanes canvas { display: block; width: 100%; cursor: pointer; }
|
|
3751
5846
|
#behold-diff { position: fixed; right: 12px; bottom: 156px; width: 260px; max-height: 40vh; overflow: auto;
|
|
3752
|
-
background: var(--panel, #
|
|
3753
|
-
color: var(--fg, #
|
|
3754
|
-
#behold-diff h4 { margin: 0 0 6px; font
|
|
3755
|
-
|
|
5847
|
+
background: var(--panel, #2e2f40); border: 1px solid var(--rule, #4e4f64); border-radius: 3px; padding: 10px 12px; font: 11px/1.5 ${LANES_MONO};
|
|
5848
|
+
color: var(--fg, #cdd6f4); display: none; }
|
|
5849
|
+
#behold-diff h4 { margin: 0 0 6px; font: 600 10.5px/1.4 ui-sans-serif, system-ui, sans-serif;
|
|
5850
|
+
text-transform: uppercase; letter-spacing: .07em; color: var(--muted, #777a8c); }
|
|
5851
|
+
#behold-diff .a { color: var(--managed, #a6e3a1); } #behold-diff .r { color: var(--degraded, #f38ba8); } #behold-diff .c { color: var(--foreign, #f9e2af); }`;
|
|
3756
5852
|
var THEME_BOOT = `<script type="module">import { initTheme, onThemeChange, mountThemePicker } from "/theme.js"; initTheme(); mountThemePicker(document.getElementById("behold-lanes-pickers")); onThemeChange(() => window.dispatchEvent(new Event("behold-theme")));</script>`;
|
|
3757
5853
|
function laneStripScript(frames) {
|
|
3758
5854
|
return `<script>
|
|
@@ -3778,7 +5874,7 @@ const LF = ${safeJson(frames)};
|
|
|
3778
5874
|
const inNow = now !== undefined, inPrev = prev !== undefined;
|
|
3779
5875
|
return inNow !== inPrev || (inNow && inPrev && now !== prev);
|
|
3780
5876
|
}
|
|
3781
|
-
const color = s => s === "good" ? css("--managed", "#
|
|
5877
|
+
const color = s => s === "good" ? css("--managed", "#a6e3a1") : s === "warn" ? css("--foreign", "#f9e2af") : s === "accent" ? css("--pending", "#89b4fa") : css("--edge", "#56596d");
|
|
3782
5878
|
|
|
3783
5879
|
function draw() {
|
|
3784
5880
|
const dpr = window.devicePixelRatio || 1;
|
|
@@ -3787,22 +5883,22 @@ const LF = ${safeJson(frames)};
|
|
|
3787
5883
|
c.clearRect(0, 0, host.clientWidth, H); c.font = "12px ui-sans-serif, system-ui, sans-serif";
|
|
3788
5884
|
subs.forEach((s, r) => {
|
|
3789
5885
|
const y = padT + r * rowH + rowH / 2;
|
|
3790
|
-
c.fillStyle = offset[s] ? css("--foreign", "#
|
|
3791
|
-
c.strokeStyle = css("--line", "#
|
|
5886
|
+
c.fillStyle = offset[s] ? css("--foreign", "#f9e2af") : css("--muted", "#777a8c"); c.textAlign = "left"; c.fillText(s, 8, y + 4);
|
|
5887
|
+
c.strokeStyle = css("--line", "#3b3c4a"); c.beginPath(); c.moveTo(padL, y); c.lineTo(host.clientWidth - padR, y); c.stroke();
|
|
3792
5888
|
LF.forEach((f, i) => {
|
|
3793
5889
|
if (!f.byLexicon[s]) return;
|
|
3794
5890
|
// dot per substrate; brighter when a node in this substrate changed at i
|
|
3795
5891
|
const changed = Object.keys(f.status).some(id => f.lexicon[id] === s && changedAt(id, i));
|
|
3796
5892
|
const hi = focus && f.lexicon[focus] === s && changedAt(focus, i);
|
|
3797
|
-
c.fillStyle = hi ? css("--fg", "#
|
|
5893
|
+
c.fillStyle = hi ? css("--fg", "#cdd6f4") : changed ? color(mode(f, s)) : css("--line", "#3b3c4a");
|
|
3798
5894
|
c.beginPath(); c.arc(xOf(i, s), y, i === cur ? 5 : hi ? 4.5 : 3.5, 0, 7); c.fill();
|
|
3799
5895
|
});
|
|
3800
5896
|
});
|
|
3801
|
-
const px = baseX(cur); c.strokeStyle = css("--pending", "#
|
|
5897
|
+
const px = baseX(cur); c.strokeStyle = css("--pending", "#89b4fa"); c.lineWidth = 1.5;
|
|
3802
5898
|
c.beginPath(); c.moveTo(px, padT - 2); c.lineTo(px, padT + subs.length * rowH); c.stroke();
|
|
3803
|
-
if (pair != null) { const qx = baseX(pair); c.strokeStyle = css("--foreign", "#
|
|
5899
|
+
if (pair != null) { const qx = baseX(pair); c.strokeStyle = css("--foreign", "#f9e2af"); c.setLineDash([3,3]);
|
|
3804
5900
|
c.beginPath(); c.moveTo(qx, padT - 2); c.lineTo(qx, padT + subs.length * rowH); c.stroke(); c.setLineDash([]); }
|
|
3805
|
-
c.fillStyle = css("--fg", "#
|
|
5901
|
+
c.fillStyle = css("--fg", "#cdd6f4"); c.textAlign = "center"; c.fillText(LF[cur].name, px, padT + subs.length * rowH + 16);
|
|
3806
5902
|
document.getElementById("behold-lanes-meta").textContent =
|
|
3807
5903
|
LF.length + " frames \xB7 frame " + (cur + 1) + "/" + LF.length + (focus ? " \xB7 focus " + focus : "");
|
|
3808
5904
|
document.getElementById("behold-lanes-rt").style.display = anyOffset() ? "inline" : "none";
|
|
@@ -3907,9 +6003,108 @@ async function emulatorDown(projectDir) {
|
|
|
3907
6003
|
await runChantRaw(["emulator", "down"], projectDir);
|
|
3908
6004
|
}
|
|
3909
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
|
+
|
|
3910
6104
|
// src/server.ts
|
|
3911
|
-
var
|
|
3912
|
-
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;
|
|
3913
6108
|
function optsFromQuery(url, tierEnvVar, projectDir) {
|
|
3914
6109
|
const q = url.searchParams;
|
|
3915
6110
|
const opts = {};
|
|
@@ -3957,8 +6152,8 @@ function tierFailure(tier, message) {
|
|
|
3957
6152
|
}
|
|
3958
6153
|
function beholdVersion() {
|
|
3959
6154
|
try {
|
|
3960
|
-
const here =
|
|
3961
|
-
return JSON.parse(
|
|
6155
|
+
const here = dirname4(fileURLToPath3(import.meta.url));
|
|
6156
|
+
return JSON.parse(readFileSync12(join15(here, "..", "package.json"), "utf8")).version ?? "unknown";
|
|
3962
6157
|
} catch {
|
|
3963
6158
|
return "unknown";
|
|
3964
6159
|
}
|
|
@@ -4002,12 +6197,125 @@ async function captureFrame(projectDir, env, frames, broadcaster) {
|
|
|
4002
6197
|
return null;
|
|
4003
6198
|
}
|
|
4004
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
|
+
}
|
|
4005
6312
|
function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffer(), runner = new OpRunner({
|
|
4006
6313
|
projectDir: cfg.projectDir,
|
|
4007
6314
|
broadcaster,
|
|
4008
6315
|
onDone: (opEnv) => captureFrame(cfg.projectDir, opEnv ?? cfg.env, frames, broadcaster)
|
|
4009
6316
|
})) {
|
|
4010
6317
|
const app = new Hono();
|
|
6318
|
+
if (cfg.carveReport) carveRoutes(app, cfg.carveReport, cfg.carveDemo);
|
|
4011
6319
|
let beholdConfig = loadBeholdConfig(cfg.projectDir);
|
|
4012
6320
|
let tierEnvVar = beholdConfig.tiers?.envVar;
|
|
4013
6321
|
const boundK8sContext = async (env) => {
|
|
@@ -4019,13 +6327,14 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
|
|
|
4019
6327
|
return void 0;
|
|
4020
6328
|
}
|
|
4021
6329
|
};
|
|
6330
|
+
const estateSourceRoots = async (opts) => (await Promise.all((cfg.projectDirs ?? []).map(async (dir) => [await graphPath(dir, opts), dir]))).flat();
|
|
4022
6331
|
app.get("/healthz", (c) => c.json({ ok: true, projectDir: cfg.projectDir, env: cfg.env ?? null, frames: frames.size }));
|
|
4023
6332
|
app.get("/api/frames", (c) => c.json({ frames: frames.summaries() }));
|
|
4024
6333
|
app.get("/lanes", (c) => {
|
|
4025
6334
|
const all = frames.all();
|
|
4026
6335
|
if (all.length < 2) {
|
|
4027
6336
|
return c.html(
|
|
4028
|
-
`<!doctype html><meta charset=utf-8><script type="module">import { initTheme } from "/theme.js"; initTheme();</script><body style="font:14px system-ui;background:var(--bg,#
|
|
6337
|
+
`<!doctype html><meta charset=utf-8><script type="module">import { initTheme } from "/theme.js"; initTheme();</script><body style="font:14px system-ui;background:var(--bg,#1e1e2e);color:var(--muted,#777a8c);padding:2rem"><h3 style="color:var(--fg,#cdd6f4)">deployment lanes</h3><p>${all.length} frame(s) captured \u2014 need at least two to scrub.</p><p>Frames accrue when the estate moves: hit <b style="color:var(--fg,#cdd6f4)">\u21BB Refresh</b> (captures the current live state), run a <b style="color:var(--fg,#cdd6f4)">Sync</b>/Adopt, edit the source, or serve with <code>--poll</code> against a moving environment. Then reload.</p><p><a href="/" style="color:var(--pending,#58a6ff);text-decoration:none">\u2190 back to the graph</a></p></body>`
|
|
4029
6338
|
);
|
|
4030
6339
|
}
|
|
4031
6340
|
return c.html(renderLanes(all, frames.summaries()));
|
|
@@ -4091,9 +6400,9 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
|
|
|
4091
6400
|
return c.json({ started: true, name, ran: label });
|
|
4092
6401
|
});
|
|
4093
6402
|
app.post("/api/local/reset", (c) => {
|
|
4094
|
-
const down =
|
|
4095
|
-
const up =
|
|
4096
|
-
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)) {
|
|
4097
6406
|
return c.json({ error: "no local-down.sh / local-up.sh in scripts/local \u2014 reset is only for local emulator projects" }, 400);
|
|
4098
6407
|
}
|
|
4099
6408
|
if (!runner.bringUp("reset local emulator", "bash", ["-c", "bash scripts/local/local-down.sh && bash scripts/local/local-up.sh"], cfg.projectDir)) {
|
|
@@ -4170,26 +6479,29 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
|
|
|
4170
6479
|
app.post("/api/project/open", async (c) => {
|
|
4171
6480
|
if (cfg.previewMode) return c.json({ error: "switching projects is locked in preview mode" }, 403);
|
|
4172
6481
|
const body = await c.req.json().catch(() => ({}));
|
|
4173
|
-
const dir = typeof body.dir === "string" && body.dir.trim() ?
|
|
4174
|
-
if (!dir || !
|
|
4175
|
-
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"))) {
|
|
4176
6485
|
return c.json({ error: `${dir} doesn't look like a chant project \u2014 no chant.config.ts` }, 400);
|
|
4177
6486
|
}
|
|
6487
|
+
switchServedProject([dir]);
|
|
6488
|
+
return c.json({ ok: true, projectDir: dir });
|
|
6489
|
+
});
|
|
6490
|
+
const switchServedProject = (dirs, env) => {
|
|
4178
6491
|
addRecent(cfg.projectDir);
|
|
4179
|
-
cfg.projectDir =
|
|
4180
|
-
cfg.projectDirs = void 0;
|
|
4181
|
-
cfg.env =
|
|
4182
|
-
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]);
|
|
4183
6496
|
tierEnvVar = beholdConfig.tiers?.envVar;
|
|
4184
|
-
runner.retarget(
|
|
4185
|
-
addRecent(
|
|
4186
|
-
cfg.onProjectSwitch?.(
|
|
6497
|
+
runner.retarget(dirs[0]);
|
|
6498
|
+
for (const d of dirs) addRecent(d);
|
|
6499
|
+
cfg.onProjectSwitch?.(dirs[0]);
|
|
4187
6500
|
broadcaster.emit("changed");
|
|
4188
|
-
|
|
4189
|
-
});
|
|
6501
|
+
};
|
|
4190
6502
|
app.post("/api/project/reveal", async (c) => {
|
|
4191
6503
|
const body = await c.req.json().catch(() => ({}));
|
|
4192
|
-
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;
|
|
4193
6505
|
const known = /* @__PURE__ */ new Set([cfg.projectDir, ...cfg.projectDirs ?? [], ...listRecents().map((r) => r.dir)]);
|
|
4194
6506
|
if (!known.has(dir)) return c.json({ error: "not a served or recent project directory" }, 400);
|
|
4195
6507
|
const opener = process.platform === "darwin" ? "open" : process.platform === "win32" ? "explorer" : "xdg-open";
|
|
@@ -4200,6 +6512,144 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
|
|
|
4200
6512
|
return c.json({ error: err instanceof Error ? err.message : String(err) }, 500);
|
|
4201
6513
|
}
|
|
4202
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);
|
|
4203
6653
|
app.get(
|
|
4204
6654
|
"/api",
|
|
4205
6655
|
(c) => c.json({
|
|
@@ -4211,8 +6661,29 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
|
|
|
4211
6661
|
{ method: "GET", path: "/api/project", desc: "project info: dir, recents, environments, tiers, targets, stacks, preview lock" },
|
|
4212
6662
|
{ method: "POST", path: "/api/project/open", desc: "switch the served project: JSON body {dir} (validated; preview-locked)" },
|
|
4213
6663
|
{ method: "POST", path: "/api/project/reveal", desc: "open the OS file manager at a served/recent project dir: JSON body {dir?}" },
|
|
4214
|
-
{ 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
|
+
] : [],
|
|
4215
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)" },
|
|
4216
6687
|
{ method: "GET", path: "/api/diff", desc: "per-node live diff for ?env= \u2014 {env, nodes: {<id>: {observed, diff, health, fieldDrift}}}" },
|
|
4217
6688
|
{ method: "GET", path: "/api/reconcile", desc: "pending-change summary for ?env=" },
|
|
4218
6689
|
{ method: "GET", path: "/api/resources", desc: "component \u2192 declared resources" },
|
|
@@ -4244,10 +6715,31 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
|
|
|
4244
6715
|
let mode;
|
|
4245
6716
|
let metaEnv = cfg.env ?? null;
|
|
4246
6717
|
if (multi) {
|
|
4247
|
-
ir = await composeEstate(cfg.projectDirs, opts);
|
|
6718
|
+
ir = await composeEstate(cfg.projectDirs, logical ? { ...opts, detail: 3 } : opts);
|
|
4248
6719
|
ir = addValueMatchEdges(ir);
|
|
4249
6720
|
ir = addK8sDeclaredEdges(ir);
|
|
4250
|
-
|
|
6721
|
+
const estateContext = await boundK8sContext(metaEnv ?? void 0);
|
|
6722
|
+
ir = addClusterAnchorEdges(ir, estateContext);
|
|
6723
|
+
if (logical) {
|
|
6724
|
+
const logicalBefore = ir.nodes.length;
|
|
6725
|
+
const { ir: projected, byContainer } = projectTopology(ir, metaEnv ?? void 0, estateContext, await estateSourceRoots(opts));
|
|
6726
|
+
const { svg: svg2 } = renderArchitecture(projected, byContainer);
|
|
6727
|
+
const logicalNote = notesFor("logical", projected, void 0, logicalBefore);
|
|
6728
|
+
return c.json({
|
|
6729
|
+
ir: projected,
|
|
6730
|
+
svg: svg2,
|
|
6731
|
+
byContainer,
|
|
6732
|
+
meta: {
|
|
6733
|
+
projectDir: cfg.projectDir,
|
|
6734
|
+
env: metaEnv,
|
|
6735
|
+
tier: opts.tier ?? null,
|
|
6736
|
+
target: opts.target ?? null,
|
|
6737
|
+
mode: "logical",
|
|
6738
|
+
estate: cfg.projectDirs.length,
|
|
6739
|
+
...logicalNote ? { note: logicalNote } : {}
|
|
6740
|
+
}
|
|
6741
|
+
});
|
|
6742
|
+
}
|
|
4251
6743
|
} else if (components) {
|
|
4252
6744
|
ir = await componentGraphIr(cfg.projectDir, opts);
|
|
4253
6745
|
const env = opts.env ?? cfg.env;
|
|
@@ -4283,13 +6775,13 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
|
|
|
4283
6775
|
srcCompositeEdgesAttached = 0;
|
|
4284
6776
|
}
|
|
4285
6777
|
}
|
|
4286
|
-
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"))) {
|
|
4287
6779
|
return c.json(noProjectError(cfg.projectDir), 404);
|
|
4288
6780
|
}
|
|
4289
6781
|
const radial = new URL(c.req.url).searchParams.get("radial") === "1";
|
|
4290
6782
|
const { svg } = renderGraph(ir, multi ? { boxes: "byStack" } : { radial });
|
|
4291
6783
|
const srcZoom = components ? "components" : logical ? "logical" : opts.detail === 1 ? "composites" : opts.detail === 3 ? "attributes" : "resources";
|
|
4292
|
-
const estateLensNote = multi &&
|
|
6784
|
+
const estateLensNote = multi && components ? "the components lens doesn't apply to a composed estate yet \u2014 showing the composed entity graph" : void 0;
|
|
4293
6785
|
const srcNote = multi ? estateLensNote : notesFor(srcZoom, ir, srcCompositeEdgesAttached);
|
|
4294
6786
|
return c.json({
|
|
4295
6787
|
ir,
|
|
@@ -4388,32 +6880,56 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
|
|
|
4388
6880
|
return c.json({ error: "overlay needs an environment \u2014 pick one, or start behold with --env <name>" }, 400);
|
|
4389
6881
|
}
|
|
4390
6882
|
const logical = new URL(c.req.url).searchParams.get("logical") === "1";
|
|
6883
|
+
const runtime = new URL(c.req.url).searchParams.get("runtime") === "1";
|
|
4391
6884
|
try {
|
|
4392
6885
|
if (cfg.projectDirs && cfg.projectDirs.length > 1) {
|
|
4393
|
-
const
|
|
4394
|
-
const est = await composeEstateOverlay(cfg.projectDirs, { ...tierTargetOpts(query), detail
|
|
6886
|
+
const detail = logical || runtime ? 3 : query.detail;
|
|
6887
|
+
const est = await composeEstateOverlay(cfg.projectDirs, { ...tierTargetOpts(query), detail, env }, reclassifyOverlay);
|
|
4395
6888
|
if (est.dropped.length === est.total) {
|
|
4396
6889
|
return c.json({ error: `no project in the estate could be graphed \u2014 ${est.dropped.map((d) => `${d.name}: ${d.reason}`).join("; ")}` }, 500);
|
|
4397
6890
|
}
|
|
4398
|
-
|
|
4399
|
-
|
|
6891
|
+
const composed = est.ir;
|
|
6892
|
+
let ir2 = runtime ? attachRuntimeContainment(composed) : pruneRuntimeChildren(composed);
|
|
6893
|
+
if ((detail ?? 2) < 3) ir2 = pruneImports(ir2);
|
|
4400
6894
|
ir2 = addValueMatchEdges(ir2);
|
|
4401
6895
|
ir2 = addK8sDeclaredEdges(ir2);
|
|
4402
|
-
|
|
4403
|
-
|
|
4404
|
-
const lensNote = logical || runtime ? `the ${logical ? "logical" : "runtime"} lens doesn't apply to a composed estate yet \u2014 showing the composed entity overlay` : void 0;
|
|
6896
|
+
const boundContext2 = await boundK8sContext(env);
|
|
6897
|
+
ir2 = addClusterAnchorEdges(ir2, boundContext2);
|
|
4405
6898
|
const coverNote = est.unobserved.length || est.dropped.length ? `live observe covered ${est.observed} of ${est.total} projects \u2014 ` + [
|
|
4406
6899
|
...est.unobserved.map((u) => `${u.name}: ${u.reason} (painted unobserved)`),
|
|
4407
6900
|
...est.dropped.map((d) => `${d.name}: dropped (${d.reason})`)
|
|
4408
6901
|
].join("; ") : void 0;
|
|
4409
|
-
|
|
6902
|
+
if (logical) {
|
|
6903
|
+
const logicalBefore = ir2.nodes.length;
|
|
6904
|
+
const { ir: projected, byContainer } = projectTopology(ir2, env, boundContext2, await estateSourceRoots(query));
|
|
6905
|
+
const { svg: svg3 } = renderArchitecture(projected, byContainer);
|
|
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 ");
|
|
6912
|
+
return c.json({
|
|
6913
|
+
ir: projected,
|
|
6914
|
+
svg: svg3,
|
|
6915
|
+
byContainer,
|
|
6916
|
+
meta: { projectDir: cfg.projectDir, env, mode: "logical", estate: est.total, ...note3 ? { note: note3 } : {} }
|
|
6917
|
+
});
|
|
6918
|
+
}
|
|
6919
|
+
const { svg: svg2 } = renderGraph(ir2, { boxes: runtime ? "byContainer" : "byStack" });
|
|
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 ");
|
|
4410
6926
|
return c.json({
|
|
4411
6927
|
ir: ir2,
|
|
4412
6928
|
svg: svg2,
|
|
4413
6929
|
meta: { projectDir: cfg.projectDir, env, mode: "overlay", estate: est.total, ...note2 ? { note: note2 } : {} }
|
|
4414
6930
|
});
|
|
4415
6931
|
}
|
|
4416
|
-
const opts = { ...query, live: true, overlay: true, env, ...logical ? { detail: 3 } : {} };
|
|
6932
|
+
const opts = { ...query, live: true, overlay: true, env, ...logical || runtime ? { detail: 3 } : {} };
|
|
4417
6933
|
let ir = reclassifyOverlay(await graphIr(cfg.projectDir, opts));
|
|
4418
6934
|
const boundContext = await boundK8sContext(env);
|
|
4419
6935
|
ir = mergeClusterRoot(ir, await clusterRootGraphIr(cfg.projectDir, query), await runningK3dClusters());
|
|
@@ -4423,7 +6939,7 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
|
|
|
4423
6939
|
applyHelmArtifacts(ir, observed);
|
|
4424
6940
|
synthesizeHelmReleases(ir, observed, discoverReleaseUnits(cfg.projectDir));
|
|
4425
6941
|
}
|
|
4426
|
-
ir =
|
|
6942
|
+
ir = runtime ? attachRuntimeContainment(ir) : pruneRuntimeChildren(ir);
|
|
4427
6943
|
if (logical) {
|
|
4428
6944
|
const logicalBefore = ir.nodes.length;
|
|
4429
6945
|
const { ir: projected, byContainer } = projectTopology(addK8sDeclaredEdges(addValueMatchEdges(ir)), env, boundContext, [await graphPath(cfg.projectDir, opts), cfg.projectDir]);
|
|
@@ -4448,10 +6964,10 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
|
|
|
4448
6964
|
}
|
|
4449
6965
|
}
|
|
4450
6966
|
const { svg } = renderGraph(ir, { boxes: "byContainer", radial: new URL(c.req.url).searchParams.get("radial") === "1" });
|
|
4451
|
-
const zoom =
|
|
6967
|
+
const zoom = runtime ? "runtime" : query.detail === 1 ? "composites" : query.detail === 3 ? "attributes" : "resources";
|
|
4452
6968
|
const tierNote = tierMismatchNote(ir, beholdConfig.tiers, query.tier);
|
|
4453
6969
|
const nsNote = namespaceMismatchNote(ir.nodes);
|
|
4454
|
-
const zoomNotes = notesFor(zoom, ir, compositeEdgesAttached);
|
|
6970
|
+
const zoomNotes = notesFor(zoom, ir, compositeEdgesAttached, void 0, opts.detail ?? 2);
|
|
4455
6971
|
const note = [tierNote, nsNote, zoomNotes].filter(Boolean).join(" \xB7 ");
|
|
4456
6972
|
return c.json({ ir, svg, meta: { projectDir: cfg.projectDir, env, mode: "overlay", ...note ? { note } : {} } });
|
|
4457
6973
|
} catch (err) {
|
|
@@ -4512,10 +7028,12 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
|
|
|
4512
7028
|
const nodes = {};
|
|
4513
7029
|
for (const id of ids) {
|
|
4514
7030
|
const observed = nodeObserved(parsed, id);
|
|
7031
|
+
const verdict = classifyObservedHealth(observed);
|
|
4515
7032
|
nodes[id] = {
|
|
4516
7033
|
observed,
|
|
4517
7034
|
diff: nodeDiff(parsed, id),
|
|
4518
|
-
health:
|
|
7035
|
+
health: verdict.health,
|
|
7036
|
+
...verdict.detail ? { healthDetail: verdict.detail } : {},
|
|
4519
7037
|
// Field-level (per-manager) drift (#87, chant#1181) — null when no
|
|
4520
7038
|
// lexicon in this diff carries a `deep` section at all.
|
|
4521
7039
|
fieldDrift: nodeFieldDrift(parsed, id)
|
|
@@ -4539,12 +7057,14 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
|
|
|
4539
7057
|
return c.json({ error: "diff output was not JSON \u2014 chant may predate --live --json (needs 0.18.7+)" }, 500);
|
|
4540
7058
|
}
|
|
4541
7059
|
const observed = nodeObserved(parsed, node);
|
|
7060
|
+
const verdict = classifyObservedHealth(observed);
|
|
4542
7061
|
return c.json({
|
|
4543
7062
|
node,
|
|
4544
7063
|
env,
|
|
4545
7064
|
diff: nodeDiff(parsed, node),
|
|
4546
7065
|
observed,
|
|
4547
|
-
health:
|
|
7066
|
+
health: verdict.health,
|
|
7067
|
+
...verdict.detail ? { healthDetail: verdict.detail } : {},
|
|
4548
7068
|
// Field-level (per-manager) drift (#87, chant#1181) — null when no
|
|
4549
7069
|
// lexicon in this diff carries a `deep` section at all.
|
|
4550
7070
|
fieldDrift: nodeFieldDrift(parsed, node)
|
|
@@ -4593,38 +7113,38 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
|
|
|
4593
7113
|
}
|
|
4594
7114
|
return c.json({ started: true, component, env });
|
|
4595
7115
|
});
|
|
4596
|
-
const rel =
|
|
7116
|
+
const rel = relative3(process.cwd(), webRoot) || ".";
|
|
4597
7117
|
app.use("/*", serveStatic({ root: rel }));
|
|
4598
|
-
app.get("/", serveStatic({ path:
|
|
7118
|
+
app.get("/", serveStatic({ path: join15(rel, "index.html") }));
|
|
4599
7119
|
return app;
|
|
4600
7120
|
}
|
|
4601
|
-
async function
|
|
4602
|
-
|
|
4603
|
-
|
|
4604
|
-
|
|
4605
|
-
|
|
4606
|
-
if (emulators.length === 0) {
|
|
4607
|
-
process.stderr.write(
|
|
4608
|
-
"behold serve --local: no configured lexicon has a local emulator \u2014 serving without one.\n"
|
|
4609
|
-
);
|
|
4610
|
-
} else {
|
|
4611
|
-
Object.assign(process.env, mergedEnv(emulators));
|
|
4612
|
-
for (const e of emulators) {
|
|
4613
|
-
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.
|
|
4614
7126
|
`);
|
|
4615
|
-
|
|
4616
|
-
|
|
4617
|
-
|
|
4618
|
-
|
|
4619
|
-
process.
|
|
4620
|
-
|
|
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)}
|
|
4621
7138
|
Serving the source graph without the emulator \u2014 start Docker and restart to enable local deploys.
|
|
4622
7139
|
`
|
|
4623
|
-
|
|
4624
|
-
|
|
7140
|
+
);
|
|
7141
|
+
return [];
|
|
4625
7142
|
}
|
|
7143
|
+
}
|
|
7144
|
+
async function startServer(cfg) {
|
|
7145
|
+
if (cfg.local) cfg.emulators = await bootLocalEmulators(cfg.projectDir, "behold serve --local");
|
|
4626
7146
|
for (const dir of cfg.projectDirs ?? [cfg.projectDir]) {
|
|
4627
|
-
if (
|
|
7147
|
+
if (existsSync12(join15(dir, "chant.config.ts"))) addRecent(dir);
|
|
4628
7148
|
}
|
|
4629
7149
|
const broadcaster = new Broadcaster();
|
|
4630
7150
|
const frames = new FrameBuffer();
|
|
@@ -4666,8 +7186,10 @@ async function startServer(cfg) {
|
|
|
4666
7186
|
}
|
|
4667
7187
|
}
|
|
4668
7188
|
};
|
|
4669
|
-
|
|
4670
|
-
let
|
|
7189
|
+
const carve = !!cfg.carveReport;
|
|
7190
|
+
let stopWatch = carve ? () => {
|
|
7191
|
+
} : watchSource(cfg.projectDir, onEstateChange);
|
|
7192
|
+
let stopPoll = !carve && cfg.env && cfg.pollSecs ? startDriftPoll({
|
|
4671
7193
|
intervalMs: cfg.pollSecs * 1e3,
|
|
4672
7194
|
query: () => graphIr(cfg.projectDir, { live: true, overlay: true, env: cfg.env }),
|
|
4673
7195
|
onChange: onPollDrift,
|
|
@@ -4685,7 +7207,7 @@ async function startServer(cfg) {
|
|
|
4685
7207
|
`);
|
|
4686
7208
|
void capture();
|
|
4687
7209
|
};
|
|
4688
|
-
void capture();
|
|
7210
|
+
if (!carve) void capture();
|
|
4689
7211
|
let shuttingDown = false;
|
|
4690
7212
|
const shutdown = () => {
|
|
4691
7213
|
if (shuttingDown) return;
|
|
@@ -4699,6 +7221,19 @@ async function startServer(cfg) {
|
|
|
4699
7221
|
process.on("SIGINT", shutdown);
|
|
4700
7222
|
process.on("SIGTERM", shutdown);
|
|
4701
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
|
+
}
|
|
4702
7237
|
const poll = cfg.env && cfg.pollSecs ? `, polling drift every ${cfg.pollSecs}s` : "";
|
|
4703
7238
|
const auto = autoSync !== "off" ? ` auto-sync: ${autoSync}` : "";
|
|
4704
7239
|
const localTag = cfg.emulators && cfg.emulators.length ? ` local: ${cfg.emulators.map((e) => e.name).join(", ")} up (creds-free \u2014 deploys hit the emulator)` : "";
|
|
@@ -4729,53 +7264,17 @@ async function startServer(cfg) {
|
|
|
4729
7264
|
});
|
|
4730
7265
|
}
|
|
4731
7266
|
|
|
4732
|
-
// src/demos.ts
|
|
4733
|
-
import { readFileSync as readFileSync8 } from "node:fs";
|
|
4734
|
-
import { join as join11 } from "node:path";
|
|
4735
|
-
import { spawnSync } from "node:child_process";
|
|
4736
|
-
function loadDemoRegistry(pkgRoot) {
|
|
4737
|
-
let raw;
|
|
4738
|
-
try {
|
|
4739
|
-
raw = JSON.parse(readFileSync8(join11(pkgRoot, "demos.json"), "utf8"));
|
|
4740
|
-
} catch {
|
|
4741
|
-
return [];
|
|
4742
|
-
}
|
|
4743
|
-
const list = raw?.demos;
|
|
4744
|
-
if (!Array.isArray(list)) return [];
|
|
4745
|
-
return list.filter((e) => {
|
|
4746
|
-
const d = e;
|
|
4747
|
-
if (!d || typeof d.name !== "string" || !d.name || typeof d.description !== "string") return false;
|
|
4748
|
-
if (d.source === "bundled") {
|
|
4749
|
-
if (typeof d.dir !== "string" || !d.dir) return false;
|
|
4750
|
-
} else if (d.source === "git") {
|
|
4751
|
-
if (typeof d.repo !== "string" || !d.repo) return false;
|
|
4752
|
-
} else {
|
|
4753
|
-
return false;
|
|
4754
|
-
}
|
|
4755
|
-
if (!Array.isArray(d.requires) || d.requires.some((r) => typeof r !== "string")) return false;
|
|
4756
|
-
if (!d.serve || typeof d.serve !== "object") return false;
|
|
4757
|
-
if (d.serve.dirs !== void 0 && (!Array.isArray(d.serve.dirs) || d.serve.dirs.some((x) => typeof x !== "string") || !d.serve.dirs.length))
|
|
4758
|
-
return false;
|
|
4759
|
-
return true;
|
|
4760
|
-
});
|
|
4761
|
-
}
|
|
4762
|
-
function missingRequirements(entry) {
|
|
4763
|
-
const bins = entry.source === "git" && !entry.requires.includes("git") ? [...entry.requires, "git"] : entry.requires;
|
|
4764
|
-
const finder = process.platform === "win32" ? "where" : "which";
|
|
4765
|
-
return bins.filter((bin) => spawnSync(finder, [bin], { stdio: "ignore" }).status !== 0);
|
|
4766
|
-
}
|
|
4767
|
-
|
|
4768
7267
|
// src/export.ts
|
|
4769
|
-
import { mkdirSync as
|
|
4770
|
-
import { join as
|
|
4771
|
-
import { fileURLToPath 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";
|
|
7270
|
+
import { fileURLToPath as fileURLToPath4 } from "node:url";
|
|
4772
7271
|
var LENS_PARAMS = ["components", "detail", "env", "logical", "radial", "tier"];
|
|
4773
7272
|
function canonicalKey(path, params) {
|
|
4774
7273
|
const flat = params.get("components") === "1" || params.get("logical") === "1";
|
|
4775
7274
|
const q = LENS_PARAMS.filter((k) => params.has(k) && !(flat && (k === "detail" || k === "radial"))).map((k) => `${k}=${params.get(k)}`).join("&");
|
|
4776
7275
|
return q ? `${path}?${q}` : path;
|
|
4777
7276
|
}
|
|
4778
|
-
function
|
|
7277
|
+
function slug2(key) {
|
|
4779
7278
|
const base = key.replace(/^\//, "").replace(/[^a-zA-Z0-9=_.-]+/g, "_").slice(0, 120);
|
|
4780
7279
|
return `${base}.json`;
|
|
4781
7280
|
}
|
|
@@ -4815,7 +7314,7 @@ function captureKeys(axes) {
|
|
|
4815
7314
|
return [...keys];
|
|
4816
7315
|
}
|
|
4817
7316
|
function webDir() {
|
|
4818
|
-
return
|
|
7317
|
+
return join16(dirname5(fileURLToPath4(import.meta.url)), "..", "web");
|
|
4819
7318
|
}
|
|
4820
7319
|
function workerName(project, override) {
|
|
4821
7320
|
const raw = override ?? `behold-${basename(project)}`;
|
|
@@ -4823,19 +7322,19 @@ function workerName(project, override) {
|
|
|
4823
7322
|
return name || "behold-export";
|
|
4824
7323
|
}
|
|
4825
7324
|
async function runExport(cfg, outDir, opts = {}) {
|
|
4826
|
-
const app = createApp(cfg);
|
|
7325
|
+
const app = createApp({ ...cfg, layoutWrites: false });
|
|
4827
7326
|
const proj = await (await app.request("/api/project")).json();
|
|
4828
7327
|
const axes = { environments: proj.environments ?? [], tiers: proj.tiers ?? [] };
|
|
4829
|
-
const snapDir =
|
|
4830
|
-
|
|
7328
|
+
const snapDir = join16(outDir, "snapshots");
|
|
7329
|
+
mkdirSync4(snapDir, { recursive: true });
|
|
4831
7330
|
const keyToFile = {};
|
|
4832
7331
|
let ok = 0;
|
|
4833
7332
|
let failed = 0;
|
|
4834
7333
|
for (const key of captureKeys(axes)) {
|
|
4835
|
-
const res = await app.request(key);
|
|
7334
|
+
const res = await app.request(`${key}${key.includes("?") ? "&" : "?"}layout=1`);
|
|
4836
7335
|
const body = await res.text();
|
|
4837
|
-
const file =
|
|
4838
|
-
|
|
7336
|
+
const file = slug2(key);
|
|
7337
|
+
writeFileSync3(join16(snapDir, file), body);
|
|
4839
7338
|
keyToFile[key] = `snapshots/${file}`;
|
|
4840
7339
|
if (res.ok) ok++;
|
|
4841
7340
|
else failed++;
|
|
@@ -4847,21 +7346,21 @@ async function runExport(cfg, outDir, opts = {}) {
|
|
|
4847
7346
|
axes,
|
|
4848
7347
|
keyToFile
|
|
4849
7348
|
};
|
|
4850
|
-
|
|
4851
|
-
const html =
|
|
7349
|
+
writeFileSync3(join16(outDir, "manifest.json"), JSON.stringify(manifest, null, 2));
|
|
7350
|
+
const html = readFileSync13(join16(webDir(), "index.html"), "utf8").replace(
|
|
4852
7351
|
/<\/head>/i,
|
|
4853
7352
|
` <script>window.__BEHOLD_STATIC__ = true;</script>
|
|
4854
7353
|
</head>`
|
|
4855
7354
|
);
|
|
4856
|
-
|
|
4857
|
-
for (const f of
|
|
7355
|
+
writeFileSync3(join16(outDir, "index.html"), html);
|
|
7356
|
+
for (const f of readdirSync5(webDir())) {
|
|
4858
7357
|
if (f === "index.html") continue;
|
|
4859
|
-
copyFileSync(
|
|
7358
|
+
copyFileSync(join16(webDir(), f), join16(outDir, f));
|
|
4860
7359
|
}
|
|
4861
|
-
|
|
7360
|
+
writeFileSync3(join16(outDir, "README.md"), BUNDLE_README);
|
|
4862
7361
|
const name = workerName(cfg.projectDir, opts.name);
|
|
4863
|
-
|
|
4864
|
-
|
|
7362
|
+
writeFileSync3(
|
|
7363
|
+
join16(outDir, "wrangler.jsonc"),
|
|
4865
7364
|
JSON.stringify(
|
|
4866
7365
|
{ $schema: "node_modules/wrangler/config-schema.json", name, compatibility_date: "2025-06-01", assets: { directory: "." } },
|
|
4867
7366
|
null,
|
|
@@ -4908,14 +7407,248 @@ It's just files \u2014 GitHub Pages, S3, nginx, or Cloudflare Pages
|
|
|
4908
7407
|
(\`wrangler pages deploy .\`) all work.
|
|
4909
7408
|
`;
|
|
4910
7409
|
|
|
7410
|
+
// src/doctor.ts
|
|
7411
|
+
import { relative as relative4, resolve as resolve6 } from "node:path";
|
|
7412
|
+
var list2 = (xs) => xs.join(", ");
|
|
7413
|
+
var EMPTY_KUBECONFIG = { contexts: /* @__PURE__ */ new Map(), servers: /* @__PURE__ */ new Map() };
|
|
7414
|
+
function summarize(xs, max = 4) {
|
|
7415
|
+
return xs.length <= max ? list2(xs) : `${list2(xs.slice(0, max))}, +${xs.length - max} more`;
|
|
7416
|
+
}
|
|
7417
|
+
function labelFor(root, target, estate) {
|
|
7418
|
+
if (!estate) return "";
|
|
7419
|
+
const rel = relative4(root, target);
|
|
7420
|
+
return `${rel || "."}: `;
|
|
7421
|
+
}
|
|
7422
|
+
function chantCheck(root, targets, estate) {
|
|
7423
|
+
const floor = chantFloor();
|
|
7424
|
+
const resolutions = targets.map((t) => ({ target: t, res: resolveChant(t) }));
|
|
7425
|
+
const missing = resolutions.filter(({ res }) => res.source !== "project");
|
|
7426
|
+
const describe = ({ target, res }) => `${labelFor(root, target, estate)}${res.source === "project" ? `chant ${res.version ?? "(unknown version)"}` : "no chant installed"}`;
|
|
7427
|
+
if (missing.length) {
|
|
7428
|
+
return {
|
|
7429
|
+
name: "chant",
|
|
7430
|
+
status: "fail",
|
|
7431
|
+
detail: `${list2(resolutions.map(describe))} \u2014 behold shells the project's own chant`,
|
|
7432
|
+
fix: `Run \`npm install\` in ${list2(missing.map(({ target }) => relative4(root, target) || target))}`
|
|
7433
|
+
};
|
|
7434
|
+
}
|
|
7435
|
+
const stale = resolutions.filter(({ res }) => !meetsFloor(res.version, floor));
|
|
7436
|
+
const versions = list2(resolutions.map(describe));
|
|
7437
|
+
if (stale.length) {
|
|
7438
|
+
return {
|
|
7439
|
+
name: "chant",
|
|
7440
|
+
status: "warn",
|
|
7441
|
+
detail: `${versions} \u2014 below behold's floor ${floor}`,
|
|
7442
|
+
fix: "Bump @intentius/chant in the project (`npm install @intentius/chant@latest`) \u2014 older versions observe less."
|
|
7443
|
+
};
|
|
7444
|
+
}
|
|
7445
|
+
return { name: "chant", status: "pass", detail: `${versions}${floor ? ` (behold's floor ${floor})` : ""}` };
|
|
7446
|
+
}
|
|
7447
|
+
function lexiconCheck(root, declared, estate) {
|
|
7448
|
+
const missing = [];
|
|
7449
|
+
const found = [];
|
|
7450
|
+
for (const [target, lexicons] of declared) {
|
|
7451
|
+
const label = labelFor(root, target, estate);
|
|
7452
|
+
for (const r of resolveLexicons(target, lexicons)) {
|
|
7453
|
+
(r.installed ? found : missing).push(`${label}${r.lexicon}${r.installed && r.version ? ` ${r.version}` : ""}`);
|
|
7454
|
+
}
|
|
7455
|
+
}
|
|
7456
|
+
if (missing.length) {
|
|
7457
|
+
return {
|
|
7458
|
+
name: "lexicons",
|
|
7459
|
+
status: "fail",
|
|
7460
|
+
detail: `declared but not installed: ${list2(missing)}${found.length ? ` (installed: ${list2(found)})` : ""}`,
|
|
7461
|
+
fix: "Run `npm install` in the project \u2014 chant can't graph source importing a lexicon it can't resolve."
|
|
7462
|
+
};
|
|
7463
|
+
}
|
|
7464
|
+
if (!found.length) {
|
|
7465
|
+
return { name: "lexicons", status: "pass", detail: "none declared" };
|
|
7466
|
+
}
|
|
7467
|
+
return { name: "lexicons", status: "pass", detail: `${found.length} installed: ${list2(found)}` };
|
|
7468
|
+
}
|
|
7469
|
+
function envCheck(envs, serveArg) {
|
|
7470
|
+
if (!envs.length) {
|
|
7471
|
+
return {
|
|
7472
|
+
name: "envs",
|
|
7473
|
+
status: "warn",
|
|
7474
|
+
detail: "no environments declared \u2014 the declared graph serves, but there is no live overlay to pick",
|
|
7475
|
+
fix: "Declare `environments` (or `k8s.profiles`) in chant.config.ts, then serve with `--env <name>`."
|
|
7476
|
+
};
|
|
7477
|
+
}
|
|
7478
|
+
return { name: "envs", status: "pass", detail: `${list2(envs)} \u2014 \`behold serve ${serveArg} --env ${envs[0]}\` for the live overlay` };
|
|
7479
|
+
}
|
|
7480
|
+
function kubeCheck(lexicons, profiles, envs, kubeconfig) {
|
|
7481
|
+
if (!lexicons.includes("k8s") && !lexicons.includes("helm")) {
|
|
7482
|
+
return { name: "kube", status: "pass", detail: "no k8s or helm lexicon \u2014 no cluster binding to check" };
|
|
7483
|
+
}
|
|
7484
|
+
const ambient = kubeconfig.currentContext;
|
|
7485
|
+
if (!kubeconfig.contexts.size) {
|
|
7486
|
+
return {
|
|
7487
|
+
name: "kube",
|
|
7488
|
+
status: "warn",
|
|
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."
|
|
7491
|
+
};
|
|
7492
|
+
}
|
|
7493
|
+
const bound = envs.filter((e) => profiles[e]?.context);
|
|
7494
|
+
if (!bound.length) {
|
|
7495
|
+
return {
|
|
7496
|
+
name: "kube",
|
|
7497
|
+
status: "warn",
|
|
7498
|
+
detail: `no k8s.profiles binding \u2014 chant falls back to the ambient current-context${ambient ? ` (${ambient})` : " (none set)"}`,
|
|
7499
|
+
fix: "Declare `k8s.profiles.<env>.context` in chant.config.ts so the cluster the reads hit is explicit, not ambient."
|
|
7500
|
+
};
|
|
7501
|
+
}
|
|
7502
|
+
const lines = [];
|
|
7503
|
+
const unresolved = [];
|
|
7504
|
+
const mismatched = [];
|
|
7505
|
+
for (const env of bound) {
|
|
7506
|
+
const context = profiles[env].context;
|
|
7507
|
+
const target = resolveK8sTarget(profiles, env, kubeconfig);
|
|
7508
|
+
if (!target) {
|
|
7509
|
+
unresolved.push(`${env}: ${context}`);
|
|
7510
|
+
continue;
|
|
7511
|
+
}
|
|
7512
|
+
lines.push(`${env}: ${target.label} at ${target.endpoint}`);
|
|
7513
|
+
if (ambient && ambient !== target.label) mismatched.push(`${env}: ${target.label}`);
|
|
7514
|
+
}
|
|
7515
|
+
if (unresolved.length) {
|
|
7516
|
+
return {
|
|
7517
|
+
name: "kube",
|
|
7518
|
+
status: "warn",
|
|
7519
|
+
detail: `context not in the kubeconfig \u2014 ${list2(unresolved)}${lines.length ? `; bound: ${list2(lines)}` : ""}`,
|
|
7520
|
+
fix: `Add the context to your kubeconfig, or point k8s.profiles at one you have (${summarize([...kubeconfig.contexts.keys()])}).`
|
|
7521
|
+
};
|
|
7522
|
+
}
|
|
7523
|
+
if (mismatched.length) {
|
|
7524
|
+
return {
|
|
7525
|
+
name: "kube",
|
|
7526
|
+
status: "warn",
|
|
7527
|
+
detail: `bound ${list2(lines)}, but the ambient current-context is ${ambient} \u2014 chant refuses a live read on a mismatch`,
|
|
7528
|
+
fix: `Run \`kubectl config use-context ${mismatched[0].split(": ")[1]}\` before the live overlay.`
|
|
7529
|
+
};
|
|
7530
|
+
}
|
|
7531
|
+
return { name: "kube", status: "pass", detail: `bound ${list2(lines)}` };
|
|
7532
|
+
}
|
|
7533
|
+
function substrateCheck(substrates) {
|
|
7534
|
+
if (!substrates.length) return { name: "substrates", status: "pass", detail: "none needed by this project" };
|
|
7535
|
+
const detail = list2(substrates.map((s) => `${s.label} ${s.status}`));
|
|
7536
|
+
const down = substrates.filter((s) => s.status === "down" || s.status === "blocked");
|
|
7537
|
+
if (!down.length) return { name: "substrates", status: "pass", detail };
|
|
7538
|
+
const named = down.map((s) => `${s.label} (${s.bringUp ? `${s.bringUp.cmd} ${s.bringUp.args.join(" ")}` : s.detail})`);
|
|
7539
|
+
return {
|
|
7540
|
+
name: "substrates",
|
|
7541
|
+
status: "warn",
|
|
7542
|
+
detail,
|
|
7543
|
+
fix: `Bring up: ${list2(named)} \u2014 or \`behold serve <dir> --local\`, which boots the project's own emulators (needs Docker).`
|
|
7544
|
+
};
|
|
7545
|
+
}
|
|
7546
|
+
function opsCheck(root, ops, chantSource, estate) {
|
|
7547
|
+
const mcp = chantSource === "project" ? "chant MCP available (the project's own chant)" : "chant MCP unavailable until the project's chant is installed";
|
|
7548
|
+
if (!ops.length) {
|
|
7549
|
+
return {
|
|
7550
|
+
name: "ops",
|
|
7551
|
+
status: "warn",
|
|
7552
|
+
detail: `no committed Ops (*.op.ts) \u2014 behold can show drift but has nothing to trigger; ${mcp}`,
|
|
7553
|
+
fix: "Add an ApplyOp (code to cloud) or ReconcileOp (cloud to PR) under ops/ \u2014 behold triggers your Ops, it holds no apply creds."
|
|
7554
|
+
};
|
|
7555
|
+
}
|
|
7556
|
+
const names = ops.map((o) => `${labelFor(root, o.dir, estate)}${o.name} (${o.kind})`);
|
|
7557
|
+
return { name: "ops", status: "pass", detail: `${ops.length} committed: ${list2(names)}; ${mcp}` };
|
|
7558
|
+
}
|
|
7559
|
+
async function diagnose(dir, probes = {}) {
|
|
7560
|
+
const root = resolve6(dir);
|
|
7561
|
+
const shape = detectProjectShape(root);
|
|
7562
|
+
const behold = beholdVersion();
|
|
7563
|
+
if (shape.kind === "none") {
|
|
7564
|
+
return {
|
|
7565
|
+
behold,
|
|
7566
|
+
dir: root,
|
|
7567
|
+
kind: "none",
|
|
7568
|
+
ok: false,
|
|
7569
|
+
checks: [
|
|
7570
|
+
{
|
|
7571
|
+
name: "project",
|
|
7572
|
+
status: "fail",
|
|
7573
|
+
detail: `no chant.config.ts here, and no estate members declared (.behold.json \`members\`, or npm workspaces)`,
|
|
7574
|
+
fix: "Point behold at a chant project (`behold doctor <dir>`), or run `behold demo` for a bundled working example."
|
|
7575
|
+
}
|
|
7576
|
+
]
|
|
7577
|
+
};
|
|
7578
|
+
}
|
|
7579
|
+
const estate = shape.kind === "estate";
|
|
7580
|
+
const members = (shape.members ?? []).map((m) => resolve6(root, m));
|
|
7581
|
+
const targets = estate ? members : [root];
|
|
7582
|
+
const primary = targets[0];
|
|
7583
|
+
const projectCheck = estate ? {
|
|
7584
|
+
name: "project",
|
|
7585
|
+
status: "pass",
|
|
7586
|
+
detail: `estate of ${members.length} projects (${shape.membersFrom === "behold-config" ? ".behold.json members" : "npm workspaces"}): ${list2(
|
|
7587
|
+
members.map((m) => relative4(root, m))
|
|
7588
|
+
)}`
|
|
7589
|
+
} : { name: "project", status: "pass", detail: `chant project (${relative4(root, shape.configFile)})` };
|
|
7590
|
+
const infos = await Promise.all(targets.map(async (t) => ({ target: t, info: await detectProject(t) })));
|
|
7591
|
+
const declared = new Map(infos.map(({ target, info }) => [target, info.lexicons]));
|
|
7592
|
+
const lexicons = [...new Set(infos.flatMap(({ info }) => info.lexicons))];
|
|
7593
|
+
const envs = [...new Set(infos.flatMap(({ info }) => info.environments))];
|
|
7594
|
+
const profiles = {};
|
|
7595
|
+
for (const { info } of infos) for (const [env, p] of Object.entries(info.k8sProfiles ?? {})) profiles[env] ??= p;
|
|
7596
|
+
const kubeconfig = lexicons.includes("k8s") || lexicons.includes("helm") ? await (probes.loadKubeconfig ?? loadKubeconfig)() : EMPTY_KUBECONFIG;
|
|
7597
|
+
const kube = kubeCheck(lexicons, profiles, envs, kubeconfig);
|
|
7598
|
+
const boundContext = resolveK8sTarget(profiles, envs[0], kubeconfig)?.label;
|
|
7599
|
+
const substrates = await (probes.detectSubstrates ?? detectSubstrates)(primary, false, boundContext);
|
|
7600
|
+
const checks = [
|
|
7601
|
+
projectCheck,
|
|
7602
|
+
chantCheck(root, targets, estate),
|
|
7603
|
+
lexiconCheck(root, declared, estate),
|
|
7604
|
+
// An estate root is not itself servable — the hint has to name its members
|
|
7605
|
+
// (`behold serve a b c`, #31), which is what a stranger would otherwise
|
|
7606
|
+
// discover by having the root serve nothing.
|
|
7607
|
+
envCheck(envs, estate ? shape.members.map((m) => `${dir.replace(/\/$/, "")}/${m}`).join(" ") : dir),
|
|
7608
|
+
kube,
|
|
7609
|
+
substrateCheck(substrates),
|
|
7610
|
+
opsCheck(root, discoverEstateOps(targets), resolveChant(primary).source, estate)
|
|
7611
|
+
];
|
|
7612
|
+
return { behold, dir: root, kind: shape.kind, ok: !checks.some((c) => c.status === "fail"), checks };
|
|
7613
|
+
}
|
|
7614
|
+
function formatReport(report) {
|
|
7615
|
+
const lines = [`behold doctor ${report.behold} \u2014 ${report.dir}`, ""];
|
|
7616
|
+
for (const c of report.checks) {
|
|
7617
|
+
lines.push(` ${c.status.padEnd(4)} ${c.name.padEnd(10)} ${c.detail}`);
|
|
7618
|
+
if (c.fix) lines.push(` ${"fix".padEnd(10)} ${c.fix}`);
|
|
7619
|
+
}
|
|
7620
|
+
const tally = (s) => report.checks.filter((c) => c.status === s).length;
|
|
7621
|
+
lines.push("", ` ${tally("pass")} pass, ${tally("warn")} warn, ${tally("fail")} fail`, "");
|
|
7622
|
+
return lines.join("\n");
|
|
7623
|
+
}
|
|
7624
|
+
|
|
4911
7625
|
// src/cli.ts
|
|
4912
7626
|
var USAGE = `behold \u2014 a live control plane on chant (read-only core)
|
|
4913
7627
|
|
|
4914
7628
|
Usage:
|
|
4915
7629
|
behold demo [name] [target-dir] [--port <n>] [--list]
|
|
7630
|
+
behold doctor [project-dir] [--json]
|
|
4916
7631
|
behold preview [project-dir] [--port <n>] [--emulator]
|
|
4917
7632
|
behold export [project-dir] [--out <dir>] [--env <name>] [--name <worker>] [--emulator]
|
|
4918
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.
|
|
7644
|
+
|
|
7645
|
+
doctor Why won't this project serve well? A read-only diagnosis of
|
|
7646
|
+
everything behold needs \u2014 the project's kind, its own chant install
|
|
7647
|
+
and version, declared lexicons, the envs the picker will infer, the
|
|
7648
|
+
kube context chant binds versus the ambient one, substrate
|
|
7649
|
+
readiness, committed Ops \u2014 each line pass/warn/fail with a one-line
|
|
7650
|
+
fix. Defaults to the current directory. Exits 0 iff nothing failed;
|
|
7651
|
+
--json for machines. Starts no server and changes nothing.
|
|
4919
7652
|
|
|
4920
7653
|
demo The five-minute path from npm \u2014 no chant project needed. A catalog
|
|
4921
7654
|
of demo estates (behold demo --list): bundled ones copy out of the
|
|
@@ -4923,9 +7656,13 @@ Usage:
|
|
|
4923
7656
|
clone a public estate. Bare \`behold demo\` is the AWS example \u2014 an
|
|
4924
7657
|
S3 bucket + policy on a local emulator: blue = declared, click
|
|
4925
7658
|
Deploy, watch it turn green. \`behold demo k8s\` stands a workload
|
|
4926
|
-
up on a throwaway k3d cluster instead.
|
|
4927
|
-
|
|
4928
|
-
|
|
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.
|
|
4929
7666
|
|
|
4930
7667
|
export Capture the live estate into a self-contained, interactive STATIC
|
|
4931
7668
|
bundle (default ./behold-export) \u2014 every env/tier \xD7 zoom \xD7 radial,
|
|
@@ -4946,7 +7683,7 @@ Usage:
|
|
|
4946
7683
|
overlay, and rollback act on it).
|
|
4947
7684
|
|
|
4948
7685
|
Options:
|
|
4949
|
-
--port <n> Port (default 4600). preview/serve.
|
|
7686
|
+
--port <n> Port (default 4600). preview/serve/carve.
|
|
4950
7687
|
--env <name> Environment name \u2014 turns on the live drift overlay.
|
|
4951
7688
|
export/serve.
|
|
4952
7689
|
--poll <secs> Re-query live drift every <secs> and push updates (needs --env).
|
|
@@ -4977,13 +7714,15 @@ Options:
|
|
|
4977
7714
|
names, unlike --local; kept separate because Loom's own
|
|
4978
7715
|
\`scripts/local/local-up.sh\` Floci setup clashes on :4566
|
|
4979
7716
|
with chant's generic \`chant emulator up\`.
|
|
7717
|
+
--json doctor only: the report as JSON (stable keys) instead of
|
|
7718
|
+
the console lines \u2014 the AGENTS.md audience.
|
|
4980
7719
|
--out <dir> export only: output directory (default ./behold-export).
|
|
4981
7720
|
--name <worker> export only: Cloudflare Worker name in the generated
|
|
4982
7721
|
wrangler.jsonc.
|
|
4983
7722
|
-h, --help This text.
|
|
4984
7723
|
-v, --version Print the behold version.
|
|
4985
7724
|
`;
|
|
4986
|
-
async function
|
|
7725
|
+
async function run2(argv) {
|
|
4987
7726
|
const [cmd, ...rest] = argv;
|
|
4988
7727
|
if (!cmd || cmd === "-h" || cmd === "--help") {
|
|
4989
7728
|
process.stdout.write(USAGE);
|
|
@@ -4997,6 +7736,10 @@ async function run3(argv) {
|
|
|
4997
7736
|
await runDemo(rest);
|
|
4998
7737
|
return;
|
|
4999
7738
|
}
|
|
7739
|
+
if (cmd === "doctor") {
|
|
7740
|
+
await runDoctor(rest);
|
|
7741
|
+
return;
|
|
7742
|
+
}
|
|
5000
7743
|
if (cmd === "preview") {
|
|
5001
7744
|
await runPreview(rest);
|
|
5002
7745
|
return;
|
|
@@ -5005,6 +7748,10 @@ async function run3(argv) {
|
|
|
5005
7748
|
await runExportCmd(rest);
|
|
5006
7749
|
return;
|
|
5007
7750
|
}
|
|
7751
|
+
if (cmd === "carve") {
|
|
7752
|
+
await runCarve(rest);
|
|
7753
|
+
return;
|
|
7754
|
+
}
|
|
5008
7755
|
if (cmd !== "serve") {
|
|
5009
7756
|
process.stderr.write(`behold: unknown command '${cmd}'
|
|
5010
7757
|
|
|
@@ -5060,7 +7807,7 @@ ${USAGE}`);
|
|
|
5060
7807
|
process.stderr.write("behold serve: --auto-sync needs --env and --poll (it acts on polled drift)\n");
|
|
5061
7808
|
process.exit(2);
|
|
5062
7809
|
}
|
|
5063
|
-
const dirs = projectDirs.map((d) =>
|
|
7810
|
+
const dirs = projectDirs.map((d) => resolve7(d));
|
|
5064
7811
|
for (const d of dirs) warnIfNotChantProject(d);
|
|
5065
7812
|
await startServer({
|
|
5066
7813
|
projectDir: dirs[0],
|
|
@@ -5074,16 +7821,86 @@ ${USAGE}`);
|
|
|
5074
7821
|
});
|
|
5075
7822
|
}
|
|
5076
7823
|
function warnIfNotChantProject(dir) {
|
|
5077
|
-
|
|
7824
|
+
const shape = detectProjectShape(dir);
|
|
7825
|
+
if (shape.kind === "project") return;
|
|
7826
|
+
if (shape.kind === "estate") {
|
|
7827
|
+
process.stderr.write(
|
|
7828
|
+
`behold: warning \u2014 ${dir} is an estate root, not a chant project itself.
|
|
7829
|
+
Serve its members composed: behold serve ${shape.members.map((m) => join17(dir, m)).join(" ")}
|
|
7830
|
+
`
|
|
7831
|
+
);
|
|
7832
|
+
return;
|
|
7833
|
+
}
|
|
5078
7834
|
process.stderr.write(
|
|
5079
7835
|
`behold: warning \u2014 ${dir} has no chant.config.ts; this doesn't look like a chant project.
|
|
5080
|
-
No project yet? \`behold demo\` serves a bundled working example (needs Docker).
|
|
7836
|
+
\`behold doctor ${dir}\` says what's missing. No project yet? \`behold demo\` serves a bundled working example (needs Docker).
|
|
7837
|
+
`
|
|
7838
|
+
);
|
|
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}` : ""}
|
|
5081
7874
|
`
|
|
5082
7875
|
);
|
|
7876
|
+
await startServer({ projectDir: dirname6(reportPath), carveReport: reportPath, port });
|
|
7877
|
+
}
|
|
7878
|
+
async function runDoctor(rest) {
|
|
7879
|
+
let json = false;
|
|
7880
|
+
let dirArg;
|
|
7881
|
+
for (const a of rest) {
|
|
7882
|
+
if (a === "--json") json = true;
|
|
7883
|
+
else if (a === "-h" || a === "--help") return void process.stdout.write(USAGE);
|
|
7884
|
+
else if (!a.startsWith("-")) dirArg = a;
|
|
7885
|
+
else {
|
|
7886
|
+
process.stderr.write(`behold doctor: unexpected argument '${a}'
|
|
7887
|
+
`);
|
|
7888
|
+
process.exit(2);
|
|
7889
|
+
}
|
|
7890
|
+
}
|
|
7891
|
+
const dir = dirArg ?? ".";
|
|
7892
|
+
if (!existsSync13(resolve7(dir))) {
|
|
7893
|
+
process.stderr.write(`behold doctor: no such directory: ${resolve7(dir)}
|
|
7894
|
+
`);
|
|
7895
|
+
process.exit(2);
|
|
7896
|
+
}
|
|
7897
|
+
const report = await diagnose(dir);
|
|
7898
|
+
process.stdout.write(json ? JSON.stringify(report, null, 2) + "\n" : formatReport(report));
|
|
7899
|
+
if (!report.ok) process.exitCode = 1;
|
|
5083
7900
|
}
|
|
5084
7901
|
async function runDemo(rest) {
|
|
5085
|
-
const
|
|
5086
|
-
const registry = loadDemoRegistry(
|
|
7902
|
+
const pkgRoot2 = join17(dirname6(fileURLToPath5(import.meta.url)), "..");
|
|
7903
|
+
const registry = loadDemoRegistry(pkgRoot2);
|
|
5087
7904
|
let port = 4600;
|
|
5088
7905
|
let name;
|
|
5089
7906
|
let dirArg;
|
|
@@ -5134,64 +7951,93 @@ async function runDemo(rest) {
|
|
|
5134
7951
|
`);
|
|
5135
7952
|
process.exit(2);
|
|
5136
7953
|
}
|
|
5137
|
-
const target =
|
|
5138
|
-
|
|
5139
|
-
)
|
|
5140
|
-
|
|
5141
|
-
if (entry.source === "bundled") {
|
|
5142
|
-
const bundled = join13(pkgRoot, entry.dir);
|
|
5143
|
-
if (!existsSync10(bundled)) {
|
|
5144
|
-
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}
|
|
5145
7958
|
`);
|
|
5146
|
-
|
|
5147
|
-
|
|
5148
|
-
|
|
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.
|
|
5149
7966
|
`);
|
|
5150
|
-
|
|
5151
|
-
|
|
5152
|
-
|
|
5153
|
-
|
|
5154
|
-
|
|
5155
|
-
|
|
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
|
|
5156
7987
|
`);
|
|
5157
|
-
|
|
5158
|
-
|
|
5159
|
-
|
|
7988
|
+
const code = await spawnStep("npm", ["install"], project);
|
|
7989
|
+
if (code !== 0) {
|
|
7990
|
+
process.stderr.write(`behold demo carve: npm install failed in ${project}
|
|
5160
7991
|
`);
|
|
5161
|
-
|
|
5162
|
-
}
|
|
7992
|
+
process.exit(code || 1);
|
|
5163
7993
|
}
|
|
5164
|
-
} else {
|
|
5165
|
-
process.stdout.write(`behold demo ${entry.name} \u2192 reusing ${target}
|
|
5166
|
-
`);
|
|
5167
7994
|
}
|
|
5168
|
-
|
|
5169
|
-
|
|
5170
|
-
|
|
5171
|
-
const
|
|
5172
|
-
if (
|
|
5173
|
-
|
|
5174
|
-
|
|
5175
|
-
|
|
5176
|
-
|
|
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}`;
|
|
5177
8011
|
}
|
|
5178
|
-
|
|
5179
|
-
|
|
5180
|
-
|
|
5181
|
-
|
|
5182
|
-
|
|
5183
|
-
|
|
5184
|
-
|
|
5185
|
-
process.exit(r.status ?? 1);
|
|
5186
|
-
}
|
|
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
|
+
);
|
|
5187
8019
|
}
|
|
5188
|
-
|
|
8020
|
+
if (!existsSync13(serving)) {
|
|
8021
|
+
process.stderr.write(`behold demo carve: no carve report at ${serving}
|
|
5189
8022
|
`);
|
|
5190
|
-
|
|
5191
|
-
|
|
5192
|
-
|
|
5193
|
-
|
|
5194
|
-
|
|
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
|
+
});
|
|
5195
8041
|
}
|
|
5196
8042
|
function injectEmulatorEnv(env) {
|
|
5197
8043
|
process.env.LOOM_ENV ??= env ?? "local";
|
|
@@ -5215,8 +8061,8 @@ async function runPreview(rest) {
|
|
|
5215
8061
|
process.stderr.write("behold preview: --port must be a number\n");
|
|
5216
8062
|
process.exit(2);
|
|
5217
8063
|
}
|
|
5218
|
-
const projectDir =
|
|
5219
|
-
if (!
|
|
8064
|
+
const projectDir = resolve7(dirArg ?? process.cwd());
|
|
8065
|
+
if (!existsSync13(projectDir)) {
|
|
5220
8066
|
process.stderr.write(`behold preview: project not found at ${projectDir}
|
|
5221
8067
|
`);
|
|
5222
8068
|
process.exit(2);
|
|
@@ -5236,26 +8082,26 @@ async function runPreview(rest) {
|
|
|
5236
8082
|
await startServer({ projectDir, port, env: "local", previewMode: true });
|
|
5237
8083
|
}
|
|
5238
8084
|
async function runExportCmd(rest) {
|
|
5239
|
-
let outDir =
|
|
8085
|
+
let outDir = resolve7("behold-export");
|
|
5240
8086
|
let env;
|
|
5241
8087
|
let name;
|
|
5242
8088
|
let dirArg;
|
|
5243
8089
|
let emulator = false;
|
|
5244
8090
|
for (let i = 0; i < rest.length; i++) {
|
|
5245
8091
|
const a = rest[i];
|
|
5246
|
-
if (a === "--out") outDir =
|
|
8092
|
+
if (a === "--out") outDir = resolve7(rest[++i]);
|
|
5247
8093
|
else if (a === "--env") env = rest[++i];
|
|
5248
8094
|
else if (a === "--name") name = rest[++i];
|
|
5249
8095
|
else if (a === "--emulator") emulator = true;
|
|
5250
8096
|
else if (a === "-h" || a === "--help") return void process.stdout.write(USAGE);
|
|
5251
8097
|
else if (!a.startsWith("-")) dirArg = a;
|
|
5252
8098
|
}
|
|
5253
|
-
const projectDir =
|
|
8099
|
+
const projectDir = resolve7(dirArg ?? process.cwd());
|
|
5254
8100
|
if (emulator) {
|
|
5255
8101
|
injectEmulatorEnv(env);
|
|
5256
8102
|
env ??= "local";
|
|
5257
8103
|
}
|
|
5258
|
-
if (!
|
|
8104
|
+
if (!existsSync13(projectDir)) {
|
|
5259
8105
|
process.stderr.write(`behold export: project not found at ${projectDir}
|
|
5260
8106
|
`);
|
|
5261
8107
|
process.exit(2);
|
|
@@ -5266,18 +8112,18 @@ function isMainModule() {
|
|
|
5266
8112
|
const entry = process.argv[1];
|
|
5267
8113
|
if (!entry) return false;
|
|
5268
8114
|
try {
|
|
5269
|
-
return realpathSync(entry) === realpathSync(
|
|
8115
|
+
return realpathSync(entry) === realpathSync(fileURLToPath5(import.meta.url));
|
|
5270
8116
|
} catch {
|
|
5271
8117
|
return false;
|
|
5272
8118
|
}
|
|
5273
8119
|
}
|
|
5274
8120
|
if (isMainModule()) {
|
|
5275
|
-
|
|
8121
|
+
run2(process.argv.slice(2)).catch((err) => {
|
|
5276
8122
|
process.stderr.write(`behold: fatal: ${err?.message ?? err}
|
|
5277
8123
|
`);
|
|
5278
8124
|
process.exit(3);
|
|
5279
8125
|
});
|
|
5280
8126
|
}
|
|
5281
8127
|
export {
|
|
5282
|
-
|
|
8128
|
+
run2 as run
|
|
5283
8129
|
};
|