@intentius/behold 0.24.0 → 0.25.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 +100 -0
- package/dist/cli.js +2088 -473
- package/package.json +1 -1
- package/web/app.js +101 -2
- package/web/history-picker.js +85 -0
- package/web/history-picker.test.js +77 -0
- package/web/index.html +35 -0
- package/web/terragucci-estate.js +328 -0
- package/web/terragucci-estate.test.js +114 -0
- package/web/terragucci-lifecycle.js +143 -0
- package/web/terragucci-lifecycle.test.js +60 -0
- package/web/terragucci-progress.js +127 -0
- package/web/terragucci-progress.test.js +59 -0
- package/web/terragucci-timeline.js +118 -0
- package/web/terragucci-timeline.test.js +56 -0
package/dist/cli.js
CHANGED
|
@@ -119,17 +119,17 @@ var init_read_scheduler = __esm({
|
|
|
119
119
|
recordRefused();
|
|
120
120
|
return Promise.reject(new Error("Chant read queue is full; retry after current reads finish"));
|
|
121
121
|
}
|
|
122
|
-
let
|
|
122
|
+
let resolve29;
|
|
123
123
|
let reject;
|
|
124
124
|
const promise = new Promise((yes, no) => {
|
|
125
|
-
|
|
125
|
+
resolve29 = yes;
|
|
126
126
|
reject = no;
|
|
127
127
|
});
|
|
128
128
|
job = {
|
|
129
129
|
key,
|
|
130
130
|
run: run3,
|
|
131
131
|
promise,
|
|
132
|
-
resolve:
|
|
132
|
+
resolve: resolve29,
|
|
133
133
|
reject,
|
|
134
134
|
controller: new AbortController(),
|
|
135
135
|
users: 0,
|
|
@@ -143,7 +143,7 @@ var init_read_scheduler = __esm({
|
|
|
143
143
|
}
|
|
144
144
|
const shared2 = job;
|
|
145
145
|
shared2.users++;
|
|
146
|
-
const result = new Promise((
|
|
146
|
+
const result = new Promise((resolve29, reject) => {
|
|
147
147
|
let done = false;
|
|
148
148
|
const finish = (error, value, failed = false) => {
|
|
149
149
|
if (done) return;
|
|
@@ -151,7 +151,7 @@ var init_read_scheduler = __esm({
|
|
|
151
151
|
signal?.removeEventListener("abort", abort);
|
|
152
152
|
shared2.users--;
|
|
153
153
|
if (failed) reject(error);
|
|
154
|
-
else
|
|
154
|
+
else resolve29(value);
|
|
155
155
|
};
|
|
156
156
|
const abort = () => {
|
|
157
157
|
finish(signal?.reason, void 0, true);
|
|
@@ -240,8 +240,8 @@ function resolveSubstrateTargets(lexicons, env = process.env) {
|
|
|
240
240
|
}
|
|
241
241
|
function targetEnvOverrides(targets, chosen) {
|
|
242
242
|
const overrides = {};
|
|
243
|
-
for (const
|
|
244
|
-
overrides[
|
|
243
|
+
for (const t2 of targets) {
|
|
244
|
+
overrides[t2.envVar] = chosen ?? t2.endpoint;
|
|
245
245
|
}
|
|
246
246
|
return overrides;
|
|
247
247
|
}
|
|
@@ -329,13 +329,13 @@ var init_errors = __esm({
|
|
|
329
329
|
}
|
|
330
330
|
} catch {
|
|
331
331
|
}
|
|
332
|
-
const
|
|
333
|
-
return new _K8sApiError(statusCode, status?.reason,
|
|
332
|
+
const message2 = status?.message ?? firstLine(body) ?? "";
|
|
333
|
+
return new _K8sApiError(statusCode, status?.reason, message2, target, status);
|
|
334
334
|
}
|
|
335
335
|
};
|
|
336
336
|
K8sTransportError = class extends Error {
|
|
337
|
-
constructor(
|
|
338
|
-
super(target ? `${target}: ${
|
|
337
|
+
constructor(message2, target, options) {
|
|
338
|
+
super(target ? `${target}: ${message2}` : message2);
|
|
339
339
|
this.target = target;
|
|
340
340
|
this.name = "K8sTransportError";
|
|
341
341
|
if (options && "cause" in options) this.cause = options.cause;
|
|
@@ -366,20 +366,20 @@ var init_errors = __esm({
|
|
|
366
366
|
allowed;
|
|
367
367
|
};
|
|
368
368
|
FieldManagerError = class extends Error {
|
|
369
|
-
constructor(
|
|
370
|
-
super(`k8s: ${
|
|
369
|
+
constructor(message2) {
|
|
370
|
+
super(`k8s: ${message2}`);
|
|
371
371
|
this.name = "FieldManagerError";
|
|
372
372
|
}
|
|
373
373
|
};
|
|
374
374
|
KubeConfigError = class extends Error {
|
|
375
|
-
constructor(
|
|
376
|
-
super(`k8s: ${
|
|
375
|
+
constructor(message2) {
|
|
376
|
+
super(`k8s: ${message2}`);
|
|
377
377
|
this.name = "KubeConfigError";
|
|
378
378
|
}
|
|
379
379
|
};
|
|
380
380
|
UnknownResourceError = class extends Error {
|
|
381
|
-
constructor(selectorText2,
|
|
382
|
-
super(
|
|
381
|
+
constructor(selectorText2, message2) {
|
|
382
|
+
super(message2 ?? `k8s: the cluster's API discovery reports no resource matching "${selectorText2}"`);
|
|
383
383
|
this.selectorText = selectorText2;
|
|
384
384
|
this.name = "UnknownResourceError";
|
|
385
385
|
}
|
|
@@ -449,15 +449,15 @@ function renderConflictReport(report) {
|
|
|
449
449
|
grouped.set(conflict.manager, list3);
|
|
450
450
|
}
|
|
451
451
|
const count = new Set(conflicts.map((c) => c.field)).size;
|
|
452
|
-
const
|
|
452
|
+
const lines2 = [
|
|
453
453
|
`k8s: server-side apply of ${subject} was refused \u2014 ${count} ${count === 1 ? "field is" : "fields are"} owned by another field manager.`,
|
|
454
454
|
""
|
|
455
455
|
];
|
|
456
456
|
for (const [manager, fields] of [...grouped].sort(([a], [b]) => a.localeCompare(b))) {
|
|
457
|
-
|
|
458
|
-
for (const field of [...fields].sort())
|
|
457
|
+
lines2.push(` "${manager}" owns:`);
|
|
458
|
+
for (const field of [...fields].sort()) lines2.push(` ${field}`);
|
|
459
459
|
}
|
|
460
|
-
|
|
460
|
+
lines2.push(
|
|
461
461
|
"",
|
|
462
462
|
`chant applied as field manager "${fieldManager}". Taking these fields means the managers above`,
|
|
463
463
|
`stop owning them, and will contest them again on their next apply.`,
|
|
@@ -467,12 +467,12 @@ function renderConflictReport(report) {
|
|
|
467
467
|
" - re-run this apply with force-conflicts on, deliberately (the `force: true` activity argument,",
|
|
468
468
|
" or `forceConflicts: true` on ApplyOp), which transfers ownership to chant."
|
|
469
469
|
);
|
|
470
|
-
return
|
|
470
|
+
return lines2.join("\n");
|
|
471
471
|
}
|
|
472
|
-
function parseFieldConflicts(status,
|
|
472
|
+
function parseFieldConflicts(status, message2) {
|
|
473
473
|
const fromCauses = causesOf(status);
|
|
474
474
|
if (fromCauses.length > 0) return fromCauses;
|
|
475
|
-
return parseConflictMessage(
|
|
475
|
+
return parseConflictMessage(message2 ?? status?.message ?? "");
|
|
476
476
|
}
|
|
477
477
|
function causesOf(status) {
|
|
478
478
|
const details = status?.details;
|
|
@@ -494,17 +494,17 @@ function causesOf(status) {
|
|
|
494
494
|
}
|
|
495
495
|
return out;
|
|
496
496
|
}
|
|
497
|
-
function parseCauseMessage(
|
|
498
|
-
const manager = /conflicts? with "([^"]+)"/.exec(
|
|
499
|
-
const apiVersion = /\busing ([^\s:]+)\b/.exec(
|
|
497
|
+
function parseCauseMessage(message2) {
|
|
498
|
+
const manager = /conflicts? with "([^"]+)"/.exec(message2)?.[1];
|
|
499
|
+
const apiVersion = /\busing ([^\s:]+)\b/.exec(message2)?.[1];
|
|
500
500
|
return { ...manager ? { manager } : {}, ...apiVersion ? { apiVersion } : {} };
|
|
501
501
|
}
|
|
502
|
-
function parseConflictMessage(
|
|
503
|
-
if (!
|
|
502
|
+
function parseConflictMessage(message2) {
|
|
503
|
+
if (!message2) return [];
|
|
504
504
|
const out = [];
|
|
505
505
|
let manager;
|
|
506
506
|
let apiVersion;
|
|
507
|
-
for (const rawLine of
|
|
507
|
+
for (const rawLine of message2.split("\n")) {
|
|
508
508
|
const line = rawLine.trim();
|
|
509
509
|
if (line.length === 0) continue;
|
|
510
510
|
const header = /conflicts? with "([^"]+)"(?: using ([^\s:]+))?/.exec(line);
|
|
@@ -712,10 +712,10 @@ var init_kubeconfig = __esm({
|
|
|
712
712
|
// node_modules/@intentius/chant-k8s-client/src/watch.ts
|
|
713
713
|
function parseWatchFrames(chunk, carry = "") {
|
|
714
714
|
const combined = carry + chunk;
|
|
715
|
-
const
|
|
716
|
-
const rest =
|
|
715
|
+
const lines2 = combined.split("\n");
|
|
716
|
+
const rest = lines2.pop() ?? "";
|
|
717
717
|
const frames = [];
|
|
718
|
-
for (const line of
|
|
718
|
+
for (const line of lines2) {
|
|
719
719
|
const trimmed = line.trim();
|
|
720
720
|
if (!trimmed) continue;
|
|
721
721
|
let parsed;
|
|
@@ -748,9 +748,9 @@ async function* streamLines(source2) {
|
|
|
748
748
|
const decoder = new TextDecoder();
|
|
749
749
|
let carry = "";
|
|
750
750
|
const emit = function* (chunk) {
|
|
751
|
-
const
|
|
752
|
-
carry =
|
|
753
|
-
for (const line of
|
|
751
|
+
const lines2 = (carry + chunk).split("\n");
|
|
752
|
+
carry = lines2.pop() ?? "";
|
|
753
|
+
for (const line of lines2) yield line;
|
|
754
754
|
};
|
|
755
755
|
const decode = (value) => typeof value === "string" ? value : decoder.decode(value, { stream: true });
|
|
756
756
|
if (source2 && typeof source2.getReader === "function") {
|
|
@@ -1039,11 +1039,11 @@ async function createK8sClient(options = {}) {
|
|
|
1039
1039
|
}
|
|
1040
1040
|
return void 0;
|
|
1041
1041
|
}
|
|
1042
|
-
async function
|
|
1042
|
+
async function resolve29(selector, signal) {
|
|
1043
1043
|
return "apiVersion" in selector ? resolveByGvk(selector.apiVersion, selector.kind, signal) : resolveByResourceString(selector.resource, selector.group, signal);
|
|
1044
1044
|
}
|
|
1045
1045
|
async function resolveOrThrow(selector, signal) {
|
|
1046
|
-
const info = await
|
|
1046
|
+
const info = await resolve29(selector, signal);
|
|
1047
1047
|
if (!info) throw new UnknownResourceError(selectorText(selector));
|
|
1048
1048
|
return info;
|
|
1049
1049
|
}
|
|
@@ -1055,7 +1055,7 @@ async function createK8sClient(options = {}) {
|
|
|
1055
1055
|
return parts.join("/");
|
|
1056
1056
|
}
|
|
1057
1057
|
async function pathFor(ref2, signal) {
|
|
1058
|
-
const info = await
|
|
1058
|
+
const info = await resolve29({ apiVersion: ref2.apiVersion, kind: ref2.kind }, signal);
|
|
1059
1059
|
if (!info) return void 0;
|
|
1060
1060
|
return objectPath(info, ref2.name, ref2.namespace);
|
|
1061
1061
|
}
|
|
@@ -1148,10 +1148,10 @@ async function createK8sClient(options = {}) {
|
|
|
1148
1148
|
return page.metadata?.resourceVersion;
|
|
1149
1149
|
}
|
|
1150
1150
|
let ended = false;
|
|
1151
|
-
const end = (
|
|
1151
|
+
const end = (message2) => {
|
|
1152
1152
|
if (ended) return;
|
|
1153
1153
|
ended = true;
|
|
1154
|
-
if (
|
|
1154
|
+
if (message2 !== void 0) opts.onError?.(message2);
|
|
1155
1155
|
};
|
|
1156
1156
|
const done = (async () => {
|
|
1157
1157
|
try {
|
|
@@ -1166,11 +1166,11 @@ async function createK8sClient(options = {}) {
|
|
|
1166
1166
|
allowWatchBookmarks: "true"
|
|
1167
1167
|
};
|
|
1168
1168
|
if (resourceVersion) query.resourceVersion = resourceVersion;
|
|
1169
|
-
const { lines } = await openStream(listPath, query, controller.signal, target);
|
|
1169
|
+
const { lines: lines2 } = await openStream(listPath, query, controller.signal, target);
|
|
1170
1170
|
let expired = false;
|
|
1171
|
-
const iterator =
|
|
1172
|
-
const stopped = new Promise((
|
|
1173
|
-
const finish = () =>
|
|
1171
|
+
const iterator = lines2[Symbol.asyncIterator]();
|
|
1172
|
+
const stopped = new Promise((resolve30) => {
|
|
1173
|
+
const finish = () => resolve30({ done: true, value: void 0 });
|
|
1174
1174
|
if (controller.signal.aborted) finish();
|
|
1175
1175
|
else controller.signal.addEventListener("abort", finish, { once: true });
|
|
1176
1176
|
});
|
|
@@ -1197,11 +1197,11 @@ async function createK8sClient(options = {}) {
|
|
|
1197
1197
|
resourceVersion = await listResourceVersion();
|
|
1198
1198
|
continue;
|
|
1199
1199
|
}
|
|
1200
|
-
await new Promise((
|
|
1201
|
-
const timer = setTimeout(
|
|
1200
|
+
await new Promise((resolve30) => {
|
|
1201
|
+
const timer = setTimeout(resolve30, reopenDelayMs);
|
|
1202
1202
|
controller.signal.addEventListener("abort", () => {
|
|
1203
1203
|
clearTimeout(timer);
|
|
1204
|
-
|
|
1204
|
+
resolve30(void 0);
|
|
1205
1205
|
}, { once: true });
|
|
1206
1206
|
});
|
|
1207
1207
|
}
|
|
@@ -1319,7 +1319,7 @@ async function createK8sClient(options = {}) {
|
|
|
1319
1319
|
return {
|
|
1320
1320
|
provenance,
|
|
1321
1321
|
defaultNamespace,
|
|
1322
|
-
resolve:
|
|
1322
|
+
resolve: resolve29,
|
|
1323
1323
|
read,
|
|
1324
1324
|
pathFor,
|
|
1325
1325
|
readIfPresent,
|
|
@@ -1435,14 +1435,14 @@ function renderSegment(key) {
|
|
|
1435
1435
|
if (key.startsWith("k:")) return renderKeySegment(key.slice(2));
|
|
1436
1436
|
return void 0;
|
|
1437
1437
|
}
|
|
1438
|
-
function renderKeySegment(
|
|
1438
|
+
function renderKeySegment(json2) {
|
|
1439
1439
|
let parsed;
|
|
1440
1440
|
try {
|
|
1441
|
-
parsed = JSON.parse(
|
|
1441
|
+
parsed = JSON.parse(json2);
|
|
1442
1442
|
} catch {
|
|
1443
|
-
return `[${
|
|
1443
|
+
return `[${json2}]`;
|
|
1444
1444
|
}
|
|
1445
|
-
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) return `[${
|
|
1445
|
+
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) return `[${json2}]`;
|
|
1446
1446
|
const parts = Object.entries(parsed).map(
|
|
1447
1447
|
([name, value]) => `${name}=${JSON.stringify(value)}`
|
|
1448
1448
|
);
|
|
@@ -1581,13 +1581,13 @@ function parseLiveCheckOutput(run3, dir) {
|
|
|
1581
1581
|
const why = said4.find((l) => /^Error:/.test(l)) ?? said4[0] ?? `exit ${run3.code}`;
|
|
1582
1582
|
return refuse(`choudoufu could not read ${dir}: ${why}`, "Fix the configuration until `choudoufu live-check` prints a document, then reload.");
|
|
1583
1583
|
}
|
|
1584
|
-
let
|
|
1584
|
+
let json2;
|
|
1585
1585
|
try {
|
|
1586
|
-
|
|
1586
|
+
json2 = JSON.parse(text);
|
|
1587
1587
|
} catch (err) {
|
|
1588
1588
|
return refuse(`choudoufu live-check printed something that is not JSON: ${err instanceof Error ? err.message : String(err)}`, INSTALL);
|
|
1589
1589
|
}
|
|
1590
|
-
return parseLiveCheck(
|
|
1590
|
+
return parseLiveCheck(json2);
|
|
1591
1591
|
}
|
|
1592
1592
|
function tfFiles(dir) {
|
|
1593
1593
|
try {
|
|
@@ -1703,19 +1703,19 @@ function choudoufuBinary() {
|
|
|
1703
1703
|
return process.env.CHOUDOUFU_BIN || "choudoufu";
|
|
1704
1704
|
}
|
|
1705
1705
|
function parseChoudoufuVersion(stdout, bin = choudoufuBinary()) {
|
|
1706
|
-
let
|
|
1706
|
+
let json2;
|
|
1707
1707
|
try {
|
|
1708
|
-
|
|
1708
|
+
json2 = JSON.parse(stdout);
|
|
1709
1709
|
} catch {
|
|
1710
1710
|
return void 0;
|
|
1711
1711
|
}
|
|
1712
|
-
if (!isRecord(
|
|
1713
|
-
const forkField = "choudoufu_version" in
|
|
1712
|
+
if (!isRecord(json2)) return void 0;
|
|
1713
|
+
const forkField = "choudoufu_version" in json2;
|
|
1714
1714
|
return {
|
|
1715
1715
|
bin,
|
|
1716
|
-
version: typeof
|
|
1716
|
+
version: typeof json2.choudoufu_version === "string" ? json2.choudoufu_version : "",
|
|
1717
1717
|
forkField,
|
|
1718
|
-
...typeof
|
|
1718
|
+
...typeof json2.terraform_version === "string" ? { upstream: json2.terraform_version } : {}
|
|
1719
1719
|
};
|
|
1720
1720
|
}
|
|
1721
1721
|
function isDevBuild(version) {
|
|
@@ -1786,9 +1786,9 @@ var init_choudoufu_member = __esm({
|
|
|
1786
1786
|
COMMON_RUNG = "tag-governable";
|
|
1787
1787
|
versionCache = /* @__PURE__ */ new Map();
|
|
1788
1788
|
ChoudoufuReadError = class extends Error {
|
|
1789
|
-
constructor(
|
|
1790
|
-
super(`choudoufu live-check ${dir} exited 1: ${
|
|
1791
|
-
this.refusal =
|
|
1789
|
+
constructor(refusal2, dir) {
|
|
1790
|
+
super(`choudoufu live-check ${dir} exited 1: ${refusal2.error}`);
|
|
1791
|
+
this.refusal = refusal2;
|
|
1792
1792
|
}
|
|
1793
1793
|
refusal;
|
|
1794
1794
|
};
|
|
@@ -1837,16 +1837,16 @@ function readCarveReport(path, readFile2) {
|
|
|
1837
1837
|
HOW_TO_GET_ONE
|
|
1838
1838
|
);
|
|
1839
1839
|
}
|
|
1840
|
-
let
|
|
1840
|
+
let json2;
|
|
1841
1841
|
try {
|
|
1842
|
-
|
|
1842
|
+
json2 = JSON.parse(text);
|
|
1843
1843
|
} catch (err) {
|
|
1844
1844
|
return refuse2(
|
|
1845
1845
|
`${path} isn't valid JSON: ${err instanceof Error ? err.message : String(err)}`,
|
|
1846
1846
|
HOW_TO_GET_ONE
|
|
1847
1847
|
);
|
|
1848
1848
|
}
|
|
1849
|
-
return parseCarveReport(
|
|
1849
|
+
return parseCarveReport(json2);
|
|
1850
1850
|
}
|
|
1851
1851
|
function statusForBand(band, score) {
|
|
1852
1852
|
switch (band) {
|
|
@@ -2043,8 +2043,8 @@ function groupTerraformByRoot(ir) {
|
|
|
2043
2043
|
if (n.lexicon !== TERRAFORM_LEXICON) continue;
|
|
2044
2044
|
const root = typeof n.attrs.root === "string" && n.attrs.root ? n.attrs.root : n.id.includes("/") ? n.id.slice(0, n.id.indexOf("/")) : void 0;
|
|
2045
2045
|
if (!root) continue;
|
|
2046
|
-
const
|
|
2047
|
-
const prefix =
|
|
2046
|
+
const at2 = n.id.indexOf(`/${root}/`);
|
|
2047
|
+
const prefix = at2 > 0 ? n.id.slice(0, at2) : "";
|
|
2048
2048
|
const key = `${prefix} ${root}`;
|
|
2049
2049
|
(roots.get(key) ?? roots.set(key, { prefix, root, ids: [] }).get(key)).ids.push(n.id);
|
|
2050
2050
|
}
|
|
@@ -2415,9 +2415,9 @@ var init_terraform_member = __esm({
|
|
|
2415
2415
|
beholdDir = () => dirname2(fileURLToPath(import.meta.url));
|
|
2416
2416
|
spec = (peer) => `${peer.pkg}@${peer.range ?? "latest"}`;
|
|
2417
2417
|
TerraformReadError = class extends Error {
|
|
2418
|
-
constructor(
|
|
2419
|
-
super(`terraform reader ${dir} exited 1: ${
|
|
2420
|
-
this.refusal =
|
|
2418
|
+
constructor(refusal2, dir) {
|
|
2419
|
+
super(`terraform reader ${dir} exited 1: ${refusal2.error}`);
|
|
2420
|
+
this.refusal = refusal2;
|
|
2421
2421
|
}
|
|
2422
2422
|
refusal;
|
|
2423
2423
|
};
|
|
@@ -2485,8 +2485,8 @@ function indexRoster(addresses) {
|
|
|
2485
2485
|
const calls = /* @__PURE__ */ new Map();
|
|
2486
2486
|
for (const address of addresses) {
|
|
2487
2487
|
const { blockPath: blockPath2, ...instance } = decomposeAddress(address);
|
|
2488
|
-
const
|
|
2489
|
-
if (
|
|
2488
|
+
const at2 = blocks.get(blockPath2);
|
|
2489
|
+
if (at2) at2.push(instance);
|
|
2490
2490
|
else blocks.set(blockPath2, [instance]);
|
|
2491
2491
|
const segments = blockPath2.split("/");
|
|
2492
2492
|
for (let depth = 1; depth < segments.length; depth++) {
|
|
@@ -2506,8 +2506,8 @@ function endOf(path, index) {
|
|
|
2506
2506
|
const found = index.blocks.get(path) ?? (isModuleCall(path) ? index.calls.get(path) : void 0);
|
|
2507
2507
|
const byScope = /* @__PURE__ */ new Map();
|
|
2508
2508
|
for (const i of found ?? []) {
|
|
2509
|
-
const
|
|
2510
|
-
if (
|
|
2509
|
+
const at2 = byScope.get(i.scope);
|
|
2510
|
+
if (at2) at2.push(i);
|
|
2511
2511
|
else byScope.set(i.scope, [i]);
|
|
2512
2512
|
}
|
|
2513
2513
|
return byScope;
|
|
@@ -2545,7 +2545,7 @@ function joinLexiconEdges(edges, index, root = LEXICON_ROOT) {
|
|
|
2545
2545
|
for (const f of fromCards) add(f.address, byKey.get(f.key), e.viaAttr);
|
|
2546
2546
|
continue;
|
|
2547
2547
|
}
|
|
2548
|
-
for (const f of fromCards) for (const
|
|
2548
|
+
for (const f of fromCards) for (const t2 of toCards) add(f.address, t2.address, e.viaAttr);
|
|
2549
2549
|
}
|
|
2550
2550
|
}
|
|
2551
2551
|
return out;
|
|
@@ -2596,41 +2596,41 @@ var init_choudoufu_refs = __esm({
|
|
|
2596
2596
|
|
|
2597
2597
|
// src/choudoufu-live.ts
|
|
2598
2598
|
function said(run3, fallback) {
|
|
2599
|
-
const
|
|
2600
|
-
return
|
|
2599
|
+
const lines2 = stripAnsiLines(run3.stderr);
|
|
2600
|
+
return lines2.find((l) => /^Error:/.test(l)) ?? lines2[0] ?? fallback;
|
|
2601
2601
|
}
|
|
2602
2602
|
function parseLiveLs(run3, dir) {
|
|
2603
2603
|
if (run3.code === 127) return refuse3("choudoufu is not on PATH.", "Install choudoufu and put it on PATH.");
|
|
2604
2604
|
const text = run3.stdout.trim();
|
|
2605
2605
|
if (!text) return refuse3(`choudoufu live-ls could not list ${dir}: ${said(run3, `exit ${run3.code}`)}`, CREDS);
|
|
2606
|
-
let
|
|
2606
|
+
let json2;
|
|
2607
2607
|
try {
|
|
2608
|
-
|
|
2608
|
+
json2 = JSON.parse(text);
|
|
2609
2609
|
} catch {
|
|
2610
2610
|
return refuse3(`choudoufu live-ls printed something that is not JSON in ${dir}: ${said(run3, "unparseable stdout")}`, CREDS);
|
|
2611
2611
|
}
|
|
2612
|
-
if (!isRecord3(
|
|
2612
|
+
if (!isRecord3(json2) || typeof json2.estate !== "string" || !Array.isArray(json2.items)) {
|
|
2613
2613
|
return refuse3("That is not a live-ls document \u2014 no `estate` and `items`.", CREDS);
|
|
2614
2614
|
}
|
|
2615
|
-
if (typeof
|
|
2615
|
+
if (typeof json2.schemas !== "string") {
|
|
2616
2616
|
return refuse3("This live-ls document carries no `schemas` field: it was written by a choudoufu older than 0.16.0.", "Upgrade choudoufu to 0.16.0 or newer.");
|
|
2617
2617
|
}
|
|
2618
|
-
return { ok: true, doc:
|
|
2618
|
+
return { ok: true, doc: json2 };
|
|
2619
2619
|
}
|
|
2620
2620
|
function parseLivePlan(run3, dir) {
|
|
2621
2621
|
if (run3.code === 127) return refuse3("choudoufu is not on PATH.", "Install choudoufu and put it on PATH.");
|
|
2622
2622
|
const text = run3.stdout.trim();
|
|
2623
2623
|
if (!text) return refuse3(`choudoufu live-plan could not plan ${dir}: ${said(run3, `exit ${run3.code}`)}`, CREDS);
|
|
2624
|
-
let
|
|
2624
|
+
let json2;
|
|
2625
2625
|
try {
|
|
2626
|
-
|
|
2626
|
+
json2 = JSON.parse(text);
|
|
2627
2627
|
} catch {
|
|
2628
2628
|
return refuse3(`choudoufu live-plan printed something that is not JSON in ${dir}: ${said(run3, "unparseable stdout")}`, CREDS);
|
|
2629
2629
|
}
|
|
2630
|
-
if (!isRecord3(
|
|
2630
|
+
if (!isRecord3(json2) || typeof json2.estate !== "string" || !("bound" in json2)) {
|
|
2631
2631
|
return refuse3("That is not a live-plan document \u2014 no `estate` and `bound`.", CREDS);
|
|
2632
2632
|
}
|
|
2633
|
-
return { ok: true, doc:
|
|
2633
|
+
return { ok: true, doc: json2 };
|
|
2634
2634
|
}
|
|
2635
2635
|
function adoptLine(u) {
|
|
2636
2636
|
const composed = u.hint || u.adopt_command;
|
|
@@ -3191,11 +3191,11 @@ function memberSourceStamp(dir) {
|
|
|
3191
3191
|
const skipDirs = exclusions.get(root);
|
|
3192
3192
|
const h = createHash2("sha1");
|
|
3193
3193
|
let any = false;
|
|
3194
|
-
const walk = (
|
|
3195
|
-
const entries = readdirSync3(
|
|
3194
|
+
const walk = (at2) => {
|
|
3195
|
+
const entries = readdirSync3(at2, { withFileTypes: true }).sort((a, b) => a.name < b.name ? -1 : 1);
|
|
3196
3196
|
for (const e of entries) {
|
|
3197
3197
|
if (SKIP.has(e.name)) continue;
|
|
3198
|
-
const path = join5(
|
|
3198
|
+
const path = join5(at2, e.name);
|
|
3199
3199
|
if (e.isDirectory()) {
|
|
3200
3200
|
if (!skipDirs?.has(path)) walk(path);
|
|
3201
3201
|
continue;
|
|
@@ -3414,7 +3414,7 @@ function State(input, types) {
|
|
|
3414
3414
|
this.documents = [];
|
|
3415
3415
|
this.anchorMapTransactions = [];
|
|
3416
3416
|
}
|
|
3417
|
-
function generateError(state,
|
|
3417
|
+
function generateError(state, message2) {
|
|
3418
3418
|
const mark = {
|
|
3419
3419
|
name: null,
|
|
3420
3420
|
buffer: state.input.slice(0, -1),
|
|
@@ -3423,10 +3423,10 @@ function generateError(state, message) {
|
|
|
3423
3423
|
line: state.line,
|
|
3424
3424
|
column: state.position - state.lineStart
|
|
3425
3425
|
};
|
|
3426
|
-
return new YAMLException(
|
|
3426
|
+
return new YAMLException(message2, mark);
|
|
3427
3427
|
}
|
|
3428
|
-
function throwError(state,
|
|
3429
|
-
throw generateError(state,
|
|
3428
|
+
function throwError(state, message2) {
|
|
3429
|
+
throw generateError(state, message2);
|
|
3430
3430
|
}
|
|
3431
3431
|
function storeAnchor(state, name, value) {
|
|
3432
3432
|
const transactions = state.anchorMapTransactions;
|
|
@@ -4571,8 +4571,8 @@ var init_js_yaml_load = __esm({
|
|
|
4571
4571
|
STR_TYPE = { tag: "tag:yaml.org,2002:str", kind: "scalar", construct: (data) => data !== null ? data : "" };
|
|
4572
4572
|
SEQ_TYPE = { tag: "tag:yaml.org,2002:seq", kind: "sequence", construct: (data) => data !== null ? data : [] };
|
|
4573
4573
|
MAP_TYPE = { tag: "tag:yaml.org,2002:map", kind: "mapping", construct: (data) => data !== null ? data : {} };
|
|
4574
|
-
for (const
|
|
4575
|
-
|
|
4574
|
+
for (const t2 of [NULL_TYPE, INT_TYPE, FLOAT_TYPE, STR_TYPE, SEQ_TYPE, MAP_TYPE]) {
|
|
4575
|
+
t2.resolve ??= () => true;
|
|
4576
4576
|
}
|
|
4577
4577
|
_hasOwnProperty = Object.prototype.hasOwnProperty;
|
|
4578
4578
|
CONTEXT_FLOW_IN = 1;
|
|
@@ -4657,12 +4657,12 @@ function unshareAliases(value, seen = /* @__PURE__ */ new Set()) {
|
|
|
4657
4657
|
}
|
|
4658
4658
|
return value;
|
|
4659
4659
|
}
|
|
4660
|
-
function errorLine(
|
|
4660
|
+
function errorLine(lines2, err) {
|
|
4661
4661
|
let line = err.mark?.line ?? 0;
|
|
4662
4662
|
if (/multiline key may not be an implicit key/.test(err.reason)) {
|
|
4663
|
-
const indent =
|
|
4663
|
+
const indent = lines2[line]?.search(/\S/) ?? -1;
|
|
4664
4664
|
for (let k = line - 1; k >= 0; k--) {
|
|
4665
|
-
const prev =
|
|
4665
|
+
const prev = lines2[k];
|
|
4666
4666
|
if (prev.trim() === "" || prev.trim().startsWith("#")) continue;
|
|
4667
4667
|
if (prev.search(/\S/) !== indent || /:(?:\s|$)/.test(prev) || /^\s*-(?:\s|$)/.test(prev)) break;
|
|
4668
4668
|
line = k;
|
|
@@ -4696,6 +4696,13 @@ function parseYAML(content) {
|
|
|
4696
4696
|
const value = loadDocument(content);
|
|
4697
4697
|
return Array.isArray(value) ? {} : value;
|
|
4698
4698
|
}
|
|
4699
|
+
function parseYAMLDocument(content) {
|
|
4700
|
+
try {
|
|
4701
|
+
return JSON.parse(content);
|
|
4702
|
+
} catch {
|
|
4703
|
+
}
|
|
4704
|
+
return loadDocument(content);
|
|
4705
|
+
}
|
|
4699
4706
|
var YAMLParseError, DOCUMENT_MARKER, BOOL, TAGGED_SCALAR, TAGGED_SEQUENCE, MERGE, SCHEMA;
|
|
4700
4707
|
var init_yaml = __esm({
|
|
4701
4708
|
"node_modules/@intentius/chant/src/yaml.ts"() {
|
|
@@ -4703,8 +4710,8 @@ var init_yaml = __esm({
|
|
|
4703
4710
|
YAMLParseError = class extends Error {
|
|
4704
4711
|
/** 1-based line number within the text handed to the parser. */
|
|
4705
4712
|
line;
|
|
4706
|
-
constructor(line,
|
|
4707
|
-
super(`YAML line ${line}: ${
|
|
4713
|
+
constructor(line, message2) {
|
|
4714
|
+
super(`YAML line ${line}: ${message2}`);
|
|
4708
4715
|
this.name = "YAMLParseError";
|
|
4709
4716
|
this.line = line;
|
|
4710
4717
|
}
|
|
@@ -5576,9 +5583,9 @@ var init_chant = __esm({
|
|
|
5576
5583
|
});
|
|
5577
5584
|
|
|
5578
5585
|
// src/cli.ts
|
|
5579
|
-
import { resolve as
|
|
5580
|
-
import { realpathSync as realpathSync3, existsSync as
|
|
5581
|
-
import { fileURLToPath as
|
|
5586
|
+
import { resolve as resolve28, dirname as dirname17, join as join40 } from "node:path";
|
|
5587
|
+
import { realpathSync as realpathSync3, existsSync as existsSync25, readFileSync as readFileSync29, rmSync as rmSync4 } from "node:fs";
|
|
5588
|
+
import { fileURLToPath as fileURLToPath10 } from "node:url";
|
|
5582
5589
|
import { spawn as spawn8, spawnSync as spawnSync3 } from "node:child_process";
|
|
5583
5590
|
|
|
5584
5591
|
// src/server.ts
|
|
@@ -5629,14 +5636,14 @@ function contextBindsCluster(context, clusterName) {
|
|
|
5629
5636
|
}
|
|
5630
5637
|
|
|
5631
5638
|
// src/server.ts
|
|
5632
|
-
import { streamSSE } from "hono/streaming";
|
|
5639
|
+
import { streamSSE as streamSSE2 } from "hono/streaming";
|
|
5633
5640
|
import { serveStatic } from "@hono/node-server/serve-static";
|
|
5634
5641
|
import { serve } from "@hono/node-server";
|
|
5635
|
-
import { fileURLToPath as
|
|
5636
|
-
import { execFile as
|
|
5642
|
+
import { fileURLToPath as fileURLToPath8 } from "node:url";
|
|
5643
|
+
import { execFile as execFile4 } from "node:child_process";
|
|
5637
5644
|
import { promisify as promisify3 } from "node:util";
|
|
5638
|
-
import { existsSync as
|
|
5639
|
-
import { basename as
|
|
5645
|
+
import { existsSync as existsSync22, readFileSync as readFileSync25 } from "node:fs";
|
|
5646
|
+
import { basename as basename4, dirname as dirname14, join as join36, relative as relative9, resolve as resolve25, sep as sep8 } from "node:path";
|
|
5640
5647
|
|
|
5641
5648
|
// src/recents.ts
|
|
5642
5649
|
import { readFileSync, writeFileSync, mkdirSync, existsSync } from "node:fs";
|
|
@@ -5878,19 +5885,19 @@ function finishPipelineProgress(state, exitCode) {
|
|
|
5878
5885
|
}
|
|
5879
5886
|
|
|
5880
5887
|
// src/gh-run.ts
|
|
5881
|
-
var defaultGhExec = (args) => new Promise((
|
|
5888
|
+
var defaultGhExec = (args) => new Promise((resolve29) => {
|
|
5882
5889
|
let out = "";
|
|
5883
5890
|
let proc;
|
|
5884
5891
|
try {
|
|
5885
5892
|
proc = spawn3("gh", args, { stdio: ["ignore", "pipe", "pipe"] });
|
|
5886
5893
|
} catch {
|
|
5887
|
-
|
|
5894
|
+
resolve29({ code: 127, out: "" });
|
|
5888
5895
|
return;
|
|
5889
5896
|
}
|
|
5890
5897
|
proc.stdout.on("data", (d) => out += d);
|
|
5891
5898
|
proc.stderr.on("data", (d) => out += d);
|
|
5892
|
-
proc.on("error", () =>
|
|
5893
|
-
proc.on("close", (code) =>
|
|
5899
|
+
proc.on("error", () => resolve29({ code: 127, out }));
|
|
5900
|
+
proc.on("close", (code) => resolve29({ code: code ?? 1, out }));
|
|
5894
5901
|
});
|
|
5895
5902
|
async function ghReady(exec = defaultGhExec) {
|
|
5896
5903
|
const { code, out } = await exec(["auth", "status"]);
|
|
@@ -6152,10 +6159,10 @@ function readDispatchedRun(projectDir, root) {
|
|
|
6152
6159
|
return void 0;
|
|
6153
6160
|
}
|
|
6154
6161
|
}
|
|
6155
|
-
function concludeDispatchedRun(projectDir, runId,
|
|
6162
|
+
function concludeDispatchedRun(projectDir, runId, verdict3, root) {
|
|
6156
6163
|
const current = readDispatchedRun(projectDir, root);
|
|
6157
6164
|
if (!current || current.runId !== runId) return;
|
|
6158
|
-
saveDispatchedRun({ ...current, concluded: { verdict:
|
|
6165
|
+
saveDispatchedRun({ ...current, concluded: { verdict: verdict3, at: (/* @__PURE__ */ new Date()).toISOString() } }, root);
|
|
6159
6166
|
}
|
|
6160
6167
|
|
|
6161
6168
|
// src/operator.ts
|
|
@@ -6196,9 +6203,9 @@ function readOperatorStatus(run3) {
|
|
|
6196
6203
|
const err = strip(run3.stderr);
|
|
6197
6204
|
const out = strip(run3.stdout).trim();
|
|
6198
6205
|
if (run3.code !== 0) {
|
|
6199
|
-
const
|
|
6200
|
-
const first =
|
|
6201
|
-
if (/Unknown command|Unknown operator subcommand/i.test(
|
|
6206
|
+
const message2 = err.trim() || out || `chant operator status exited ${run3.code}`;
|
|
6207
|
+
const first = message2.split("\n")[0].replace(/^error:\s*/i, "");
|
|
6208
|
+
if (/Unknown command|Unknown operator subcommand/i.test(message2)) {
|
|
6202
6209
|
return {
|
|
6203
6210
|
ok: false,
|
|
6204
6211
|
refusal: {
|
|
@@ -6285,7 +6292,7 @@ var rec = (v) => isRecord5(v) ? v : void 0;
|
|
|
6285
6292
|
var str = (v) => typeof v === "string" && v ? v : void 0;
|
|
6286
6293
|
var labelsOf = (n) => rec(rec(n.attrs?.metadata)?.labels) ?? {};
|
|
6287
6294
|
function operatorIdentity(home) {
|
|
6288
|
-
const names = [...new Set(home.ticks.flatMap((
|
|
6295
|
+
const names = [...new Set(home.ticks.flatMap((t2) => t2.serviceAccount ? [t2.serviceAccount] : []))].sort();
|
|
6289
6296
|
if (names.length === 0) return { kind: "unknown" };
|
|
6290
6297
|
if (names.length === 1) return { kind: "one", name: names[0] };
|
|
6291
6298
|
return { kind: "many", count: names.length };
|
|
@@ -6341,7 +6348,7 @@ function markOperatorHome(ir) {
|
|
|
6341
6348
|
role: "home",
|
|
6342
6349
|
namespace: home.namespace,
|
|
6343
6350
|
...home.stack ? { stack: home.stack } : {},
|
|
6344
|
-
ticks: home.ticks.map((
|
|
6351
|
+
ticks: home.ticks.map((t2) => t2.op),
|
|
6345
6352
|
identity: operatorIdentity(home)
|
|
6346
6353
|
});
|
|
6347
6354
|
}
|
|
@@ -6404,10 +6411,10 @@ function operatorStrip(rows, now = Date.now()) {
|
|
|
6404
6411
|
}
|
|
6405
6412
|
var OPERATOR_INTERVAL_MS = 6e4;
|
|
6406
6413
|
var TICK_VERDICT_TTL_MS = 15 * OPERATOR_INTERVAL_MS;
|
|
6407
|
-
function tickFreshness(
|
|
6408
|
-
const
|
|
6409
|
-
if (!Number.isFinite(
|
|
6410
|
-
const age = now -
|
|
6414
|
+
function tickFreshness(at2, now = Date.now()) {
|
|
6415
|
+
const t2 = at2 ? Date.parse(at2) : NaN;
|
|
6416
|
+
if (!Number.isFinite(t2)) return { ageMs: null, fresh: false };
|
|
6417
|
+
const age = now - t2;
|
|
6411
6418
|
if (age < 0) return { ageMs: null, fresh: false };
|
|
6412
6419
|
return { ageMs: age, fresh: age <= TICK_VERDICT_TTL_MS };
|
|
6413
6420
|
}
|
|
@@ -6437,8 +6444,8 @@ function tickComponentVerdicts(rows) {
|
|
|
6437
6444
|
if (!tick || !Array.isArray(tick.components)) continue;
|
|
6438
6445
|
const verdicts = [];
|
|
6439
6446
|
for (const value of tick.components) {
|
|
6440
|
-
const
|
|
6441
|
-
if (
|
|
6447
|
+
const verdict3 = readVerdict(value);
|
|
6448
|
+
if (verdict3) verdicts.push(verdict3);
|
|
6442
6449
|
}
|
|
6443
6450
|
out.push({
|
|
6444
6451
|
op: row.op,
|
|
@@ -6451,14 +6458,14 @@ function tickComponentVerdicts(rows) {
|
|
|
6451
6458
|
return out;
|
|
6452
6459
|
}
|
|
6453
6460
|
function verdictsForEnv(all, env) {
|
|
6454
|
-
const when = (
|
|
6455
|
-
const ms = Date.parse(
|
|
6461
|
+
const when = (t2) => {
|
|
6462
|
+
const ms = Date.parse(t2.at);
|
|
6456
6463
|
return Number.isFinite(ms) ? ms : -Infinity;
|
|
6457
6464
|
};
|
|
6458
6465
|
let best = null;
|
|
6459
|
-
for (const
|
|
6460
|
-
if (
|
|
6461
|
-
if (!best || when(
|
|
6466
|
+
for (const t2 of all) {
|
|
6467
|
+
if (t2.env !== env) continue;
|
|
6468
|
+
if (!best || when(t2) > when(best)) best = t2;
|
|
6462
6469
|
}
|
|
6463
6470
|
return best;
|
|
6464
6471
|
}
|
|
@@ -6541,9 +6548,9 @@ function readOperatorLog(run3) {
|
|
|
6541
6548
|
const err = strip(run3.stderr);
|
|
6542
6549
|
const out = strip(run3.stdout).trim();
|
|
6543
6550
|
if (run3.code !== 0) {
|
|
6544
|
-
const
|
|
6545
|
-
const first =
|
|
6546
|
-
if (/Unknown command|Unknown operator subcommand|Unknown flag/i.test(
|
|
6551
|
+
const message2 = err.trim() || out || `chant operator log exited ${run3.code}`;
|
|
6552
|
+
const first = message2.split("\n")[0].replace(/^error:\s*/i, "");
|
|
6553
|
+
if (/Unknown command|Unknown operator subcommand|Unknown flag/i.test(message2)) {
|
|
6547
6554
|
return {
|
|
6548
6555
|
ok: false,
|
|
6549
6556
|
refusal: {
|
|
@@ -6627,15 +6634,15 @@ function operatorTimeline(entries) {
|
|
|
6627
6634
|
url: approvalLink(r.url) ?? null
|
|
6628
6635
|
};
|
|
6629
6636
|
}
|
|
6630
|
-
const
|
|
6637
|
+
const t2 = entry.record;
|
|
6631
6638
|
return {
|
|
6632
6639
|
kind: "tick",
|
|
6633
|
-
at:
|
|
6634
|
-
op:
|
|
6635
|
-
env: typeof
|
|
6636
|
-
id: typeof
|
|
6637
|
-
log:
|
|
6638
|
-
gated: (Array.isArray(
|
|
6640
|
+
at: t2.timestamp,
|
|
6641
|
+
op: t2.op,
|
|
6642
|
+
env: typeof t2.env === "string" ? t2.env : null,
|
|
6643
|
+
id: typeof t2.id === "string" && t2.id ? t2.id : null,
|
|
6644
|
+
log: t2.log,
|
|
6645
|
+
gated: (Array.isArray(t2.outcomes) ? t2.outcomes : []).filter((o) => o && o.action === "gated" && typeof o.gateName === "string").map((o) => ({
|
|
6639
6646
|
rule: o.ruleId,
|
|
6640
6647
|
op: typeof o.op === "string" ? o.op : null,
|
|
6641
6648
|
gate: o.gateName,
|
|
@@ -6668,9 +6675,9 @@ var initialOperatorState = {
|
|
|
6668
6675
|
code: null,
|
|
6669
6676
|
readAt: null
|
|
6670
6677
|
};
|
|
6671
|
-
function operatorRead(state, result,
|
|
6678
|
+
function operatorRead(state, result, at2, now = Date.now()) {
|
|
6672
6679
|
if (!result.ok) {
|
|
6673
|
-
return { ...state, strip: null, gates: [], verdicts: [], note: result.refusal.error, code: result.refusal.code, readAt:
|
|
6680
|
+
return { ...state, strip: null, gates: [], verdicts: [], note: result.refusal.error, code: result.refusal.code, readAt: at2 };
|
|
6674
6681
|
}
|
|
6675
6682
|
return {
|
|
6676
6683
|
...state,
|
|
@@ -6679,7 +6686,7 @@ function operatorRead(state, result, at, now = Date.now()) {
|
|
|
6679
6686
|
verdicts: tickComponentVerdicts(result.rows),
|
|
6680
6687
|
note: null,
|
|
6681
6688
|
code: null,
|
|
6682
|
-
readAt:
|
|
6689
|
+
readAt: at2
|
|
6683
6690
|
};
|
|
6684
6691
|
}
|
|
6685
6692
|
function operatorNote(state, now = Date.now()) {
|
|
@@ -6695,10 +6702,10 @@ function operatorNote(state, now = Date.now()) {
|
|
|
6695
6702
|
bits.push(
|
|
6696
6703
|
ticked ? `last tick per loop \u2014 the status surface answers with one tick each, and the history is its own read (chant#2029) reached from the strip` : `no tick recorded yet on any loop`
|
|
6697
6704
|
);
|
|
6698
|
-
const named = state.verdicts.reduce((n,
|
|
6705
|
+
const named = state.verdicts.reduce((n, t2) => n + t2.verdicts.length, 0);
|
|
6699
6706
|
if (named) {
|
|
6700
|
-
const stale = state.verdicts.filter((
|
|
6701
|
-
const painted = named - stale.reduce((n,
|
|
6707
|
+
const stale = state.verdicts.filter((t2) => t2.verdicts.length && !tickFreshness(t2.at, now).fresh);
|
|
6708
|
+
const painted = named - stale.reduce((n, t2) => n + t2.verdicts.length, 0);
|
|
6702
6709
|
bits.push(
|
|
6703
6710
|
painted ? `${painted} component verdict${painted === 1 ? "" : "s"} from that tick \u2014 joined onto the component DAG by component name, under the live read (never over it) and only while the tick is younger than ${Math.round(TICK_VERDICT_TTL_MS / 6e4)}m` : `${named} component verdict${named === 1 ? "" : "s"} from that tick, all older than ${Math.round(TICK_VERDICT_TTL_MS / 6e4)}m \u2014 named on the node, not painted: a colour would read as the estate's state now`
|
|
6704
6711
|
);
|
|
@@ -7538,9 +7545,9 @@ var JSON_FLAGS = {
|
|
|
7538
7545
|
"graph --intent": ["--json"]
|
|
7539
7546
|
};
|
|
7540
7547
|
var WorkspaceReadError = class extends Error {
|
|
7541
|
-
constructor(
|
|
7542
|
-
super(`${
|
|
7543
|
-
this.refusal =
|
|
7548
|
+
constructor(refusal2) {
|
|
7549
|
+
super(`${refusal2.error} ${refusal2.remedy}`);
|
|
7550
|
+
this.refusal = refusal2;
|
|
7544
7551
|
}
|
|
7545
7552
|
refusal;
|
|
7546
7553
|
};
|
|
@@ -8017,10 +8024,10 @@ function joinNamespaceBindings(members, isDir = realIsDir) {
|
|
|
8017
8024
|
if (!winner || tied) continue;
|
|
8018
8025
|
const prev = best.get(winner.dir);
|
|
8019
8026
|
if (prev === "ambiguous") continue;
|
|
8020
|
-
const
|
|
8021
|
-
if (!prev) best.set(winner.dir, { score: winner.score, join:
|
|
8027
|
+
const join41 = { dir: winner.dir, namespace: b.namespace, path: b.path, declaredBy: b.declaredBy };
|
|
8028
|
+
if (!prev) best.set(winner.dir, { score: winner.score, join: join41 });
|
|
8022
8029
|
else if (prev.join.namespace !== b.namespace) best.set(winner.dir, "ambiguous");
|
|
8023
|
-
else if (winner.score > prev.score) best.set(winner.dir, { score: winner.score, join:
|
|
8030
|
+
else if (winner.score > prev.score) best.set(winner.dir, { score: winner.score, join: join41 });
|
|
8024
8031
|
}
|
|
8025
8032
|
return [...best.values()].filter((v) => v !== "ambiguous").map((v) => v.join);
|
|
8026
8033
|
}
|
|
@@ -9260,9 +9267,9 @@ function projectLogical(ir) {
|
|
|
9260
9267
|
if (!arr.includes(c)) arr.push(c);
|
|
9261
9268
|
};
|
|
9262
9269
|
const ensureSubnet = (subnetId, vpcId) => {
|
|
9263
|
-
const
|
|
9264
|
-
if (vpcId) child(vpcTitle(vpcId),
|
|
9265
|
-
return
|
|
9270
|
+
const t2 = subnetTitle(subnetId);
|
|
9271
|
+
if (vpcId) child(vpcTitle(vpcId), t2);
|
|
9272
|
+
return t2;
|
|
9266
9273
|
};
|
|
9267
9274
|
const byComponent = /* @__PURE__ */ new Map();
|
|
9268
9275
|
for (const n of headline) (byComponent.get(componentOf(n)) ?? byComponent.set(componentOf(n), []).get(componentOf(n))).push(n);
|
|
@@ -9784,13 +9791,13 @@ function readOpIr(path, readFile2) {
|
|
|
9784
9791
|
} catch (err) {
|
|
9785
9792
|
return refuse5(`Couldn't read ${path}: ${err instanceof Error ? err.message : String(err)}`, HOW_TO_EMIT);
|
|
9786
9793
|
}
|
|
9787
|
-
let
|
|
9794
|
+
let json2;
|
|
9788
9795
|
try {
|
|
9789
|
-
|
|
9796
|
+
json2 = JSON.parse(text);
|
|
9790
9797
|
} catch (err) {
|
|
9791
9798
|
return refuse5(`${path} isn't valid JSON: ${err instanceof Error ? err.message : String(err)}`, HOW_TO_EMIT);
|
|
9792
9799
|
}
|
|
9793
|
-
return parseOpIr(
|
|
9800
|
+
return parseOpIr(json2);
|
|
9794
9801
|
}
|
|
9795
9802
|
function receiptName(step) {
|
|
9796
9803
|
const r = step.receipt;
|
|
@@ -10539,7 +10546,7 @@ function applyBadges(boxes, badges, marks) {
|
|
|
10539
10546
|
function addBandBoxes(layout, grids, sizes) {
|
|
10540
10547
|
const boxes = layout.groups;
|
|
10541
10548
|
if (!boxes || grids.size === 0) return;
|
|
10542
|
-
const
|
|
10549
|
+
const at2 = new Map((layout.nodes ?? []).map((n) => [n.id, n]));
|
|
10543
10550
|
const added = [];
|
|
10544
10551
|
for (const box of boxes) {
|
|
10545
10552
|
const grid = box.id !== void 0 ? grids.get(box.id) : void 0;
|
|
@@ -10547,7 +10554,7 @@ function addBandBoxes(layout, grids, sizes) {
|
|
|
10547
10554
|
for (const band of grid.bands) {
|
|
10548
10555
|
if (band.key === void 0) continue;
|
|
10549
10556
|
const cards = band.ids.flatMap((id) => {
|
|
10550
|
-
const p =
|
|
10557
|
+
const p = at2.get(id);
|
|
10551
10558
|
const s = sizes.get(id) ?? { w: NODE_W, h: NODE_H };
|
|
10552
10559
|
return p ? [{ x: p.x, y: p.y, w: s.w, h: s.h }] : [];
|
|
10553
10560
|
});
|
|
@@ -11088,10 +11095,10 @@ function packBoxComponents(layout, ir, boxes, bandOf) {
|
|
|
11088
11095
|
shelfH = 0;
|
|
11089
11096
|
}
|
|
11090
11097
|
for (const n of c.ns) {
|
|
11091
|
-
const
|
|
11092
|
-
if (!
|
|
11093
|
-
n.x = originX + shelfX +
|
|
11094
|
-
n.y = originY + shelfY +
|
|
11098
|
+
const at2 = c.local.get(n.id);
|
|
11099
|
+
if (!at2) continue;
|
|
11100
|
+
n.x = originX + shelfX + at2.x;
|
|
11101
|
+
n.y = originY + shelfY + at2.y;
|
|
11095
11102
|
ids2.push(n.id);
|
|
11096
11103
|
}
|
|
11097
11104
|
shelfX += c.w + COMPONENT_GAP;
|
|
@@ -11500,14 +11507,14 @@ async function applyIntoFloci(fromDir, step) {
|
|
|
11500
11507
|
if (init !== 0) return `terraform init exited ${init} (provider downloads need network)`;
|
|
11501
11508
|
const apply = await step(
|
|
11502
11509
|
"terraform",
|
|
11503
|
-
["apply", "-input=false", "-auto-approve", "-no-color", ...LIVE_TARGETS.map((
|
|
11510
|
+
["apply", "-input=false", "-auto-approve", "-no-color", ...LIVE_TARGETS.map((t2) => `-target=${t2}`)],
|
|
11504
11511
|
fromDir
|
|
11505
11512
|
);
|
|
11506
11513
|
if (apply !== 0) return `terraform apply exited ${apply}`;
|
|
11507
11514
|
return void 0;
|
|
11508
11515
|
}
|
|
11509
11516
|
async function runLivePlan(fromDir) {
|
|
11510
|
-
const r = await capture("terraform", ["plan", "-input=false", "-no-color", "-detailed-exitcode", ...LIVE_TARGETS.map((
|
|
11517
|
+
const r = await capture("terraform", ["plan", "-input=false", "-no-color", "-detailed-exitcode", ...LIVE_TARGETS.map((t2) => `-target=${t2}`)], fromDir);
|
|
11511
11518
|
if (r.code !== 0 && r.code !== 2) {
|
|
11512
11519
|
const tail = r.stdout.trim().split("\n").slice(-8).join("\n");
|
|
11513
11520
|
return { error: `terraform plan failed (exit ${r.code}):
|
|
@@ -11917,22 +11924,22 @@ function parseGateLine(line) {
|
|
|
11917
11924
|
}
|
|
11918
11925
|
function readRunStatus(op, run3) {
|
|
11919
11926
|
if (run3.code !== 0) {
|
|
11920
|
-
const
|
|
11921
|
-
if (/not available in local mode|@temporalio\/client is not installed/.test(
|
|
11927
|
+
const message2 = strip2(run3.stderr).trim() || strip2(run3.stdout).trim() || `chant run status exited ${run3.code}`;
|
|
11928
|
+
if (/not available in local mode|@temporalio\/client is not installed/.test(message2)) {
|
|
11922
11929
|
return {
|
|
11923
11930
|
ok: false,
|
|
11924
11931
|
refusal: {
|
|
11925
|
-
error: `No Temporal run state for "${op}" \u2014 ${
|
|
11932
|
+
error: `No Temporal run state for "${op}" \u2014 ${message2.split("\n")[0].replace(/^error:\s*/, "")}`,
|
|
11926
11933
|
code: "no-temporal",
|
|
11927
11934
|
remedy: "Gate state and per-step run records come from a durable run. Configure a Temporal profile in chant.config.ts (and `npm install @temporalio/client`), then run the Op with --temporal."
|
|
11928
11935
|
}
|
|
11929
11936
|
};
|
|
11930
11937
|
}
|
|
11931
|
-
if (/not found|NotFound/.test(
|
|
11938
|
+
if (/not found|NotFound/.test(message2)) {
|
|
11932
11939
|
return {
|
|
11933
11940
|
ok: false,
|
|
11934
11941
|
refusal: {
|
|
11935
|
-
error: `No run of "${op}" to report \u2014 ${
|
|
11942
|
+
error: `No run of "${op}" to report \u2014 ${message2.split("\n")[0].replace(/^error:\s*/, "")}`,
|
|
11936
11943
|
code: "no-run",
|
|
11937
11944
|
remedy: "Start the Op (Run) and the playhead fills in as each step settles."
|
|
11938
11945
|
}
|
|
@@ -11941,7 +11948,7 @@ function readRunStatus(op, run3) {
|
|
|
11941
11948
|
return {
|
|
11942
11949
|
ok: false,
|
|
11943
11950
|
refusal: {
|
|
11944
|
-
error: `chant run status ${op} failed: ${
|
|
11951
|
+
error: `chant run status ${op} failed: ${message2.split("\n")[0].replace(/^error:\s*/, "")}`,
|
|
11945
11952
|
code: "run-status",
|
|
11946
11953
|
remedy: "Run `chant run status " + op + " --temporal` in the project to see the whole error."
|
|
11947
11954
|
}
|
|
@@ -11978,8 +11985,8 @@ var initialRunState = {
|
|
|
11978
11985
|
endedAt: null,
|
|
11979
11986
|
lost: null
|
|
11980
11987
|
};
|
|
11981
|
-
function runStarted(op, mode,
|
|
11982
|
-
return { ...initialRunState, op, mode, status: "running", startedAt:
|
|
11988
|
+
function runStarted(op, mode, at2) {
|
|
11989
|
+
return { ...initialRunState, op, mode, status: "running", startedAt: at2 };
|
|
11983
11990
|
}
|
|
11984
11991
|
function runRecord(state, record) {
|
|
11985
11992
|
return { ...state, records: [...state.records, record] };
|
|
@@ -11987,14 +11994,14 @@ function runRecord(state, record) {
|
|
|
11987
11994
|
function runGate(state, result) {
|
|
11988
11995
|
return result.ok ? { ...state, gate: result.read.gate, gateNote: null } : { ...state, gate: null, gateNote: result.refusal.error };
|
|
11989
11996
|
}
|
|
11990
|
-
function runEnded(state, exitCode,
|
|
11997
|
+
function runEnded(state, exitCode, at2) {
|
|
11991
11998
|
if (state.status !== "running") return state;
|
|
11992
|
-
if (exitCode === 0) return { ...state, status: "ok", endedAt:
|
|
11993
|
-
if (state.records.some((r) => r.status === "fail")) return { ...state, status: "failed", endedAt:
|
|
11999
|
+
if (exitCode === 0) return { ...state, status: "ok", endedAt: at2 };
|
|
12000
|
+
if (state.records.some((r) => r.status === "fail")) return { ...state, status: "failed", endedAt: at2 };
|
|
11994
12001
|
return {
|
|
11995
12002
|
...state,
|
|
11996
12003
|
status: "lost",
|
|
11997
|
-
endedAt:
|
|
12004
|
+
endedAt: at2,
|
|
11998
12005
|
lost: `the progress stream ended (exit ${exitCode}) after ${state.records.length} settled step${state.records.length === 1 ? "" : "s"} without a failed step \u2014 behold does not know whether the Op failed or the stream broke, so the playhead stays at the last settled step.`
|
|
11999
12006
|
};
|
|
12000
12007
|
}
|
|
@@ -12081,11 +12088,11 @@ var RAN = { ok: "ok", fail: "failed", skipped: "skipped" };
|
|
|
12081
12088
|
function tookLabel(ms) {
|
|
12082
12089
|
return ms < 1e3 ? `${ms}ms` : `${(ms / 1e3).toFixed(1)}s`;
|
|
12083
12090
|
}
|
|
12084
|
-
function paintPlayhead(ir,
|
|
12085
|
-
if (!
|
|
12091
|
+
function paintPlayhead(ir, join41, gate) {
|
|
12092
|
+
if (!join41.byNode.size && !join41.at) return ir;
|
|
12086
12093
|
const nodes = ir.nodes.map((node) => {
|
|
12087
|
-
const run3 =
|
|
12088
|
-
const here =
|
|
12094
|
+
const run3 = join41.byNode.get(node.id);
|
|
12095
|
+
const here = join41.at === node.id;
|
|
12089
12096
|
if (!run3 && !here) return node;
|
|
12090
12097
|
const isGate = node.attrs?._step === "gate";
|
|
12091
12098
|
const attrs = { ...node.attrs };
|
|
@@ -12100,7 +12107,7 @@ function paintPlayhead(ir, join35, gate) {
|
|
|
12100
12107
|
if (here) {
|
|
12101
12108
|
attrs._playhead = true;
|
|
12102
12109
|
attrs._status = isGate && gate ? "warn" : "accent";
|
|
12103
|
-
attrs.run = isGate && gate ? `waiting for approval since ${gate.since}` :
|
|
12110
|
+
attrs.run = isGate && gate ? `waiting for approval since ${gate.since}` : join41.atReason ?? "here";
|
|
12104
12111
|
if (isGate && gate?.description) attrs.gateDescription = gate.description;
|
|
12105
12112
|
}
|
|
12106
12113
|
return { ...node, attrs };
|
|
@@ -12122,11 +12129,11 @@ function pendingGateCard(op, gate, ir) {
|
|
|
12122
12129
|
}
|
|
12123
12130
|
};
|
|
12124
12131
|
}
|
|
12125
|
-
function playheadNote(state,
|
|
12132
|
+
function playheadNote(state, join41) {
|
|
12126
12133
|
if (!state.op) {
|
|
12127
12134
|
return "No run state: nothing has been run from this behold session, so every step paints as declared.";
|
|
12128
12135
|
}
|
|
12129
|
-
const settled =
|
|
12136
|
+
const settled = join41 ? join41.byNode.size : state.records.length;
|
|
12130
12137
|
const bits = [];
|
|
12131
12138
|
const head = state.status === "idle" ? `Run state for ${state.op}: no per-step records \u2014 this behold session didn't start the run, so only the gate query answers` : `Run: ${state.op} (${state.mode === "temporal" ? "durable" : "local executor"}) \u2014 ${settled} step${settled === 1 ? "" : "s"} settled`;
|
|
12132
12139
|
if (state.status === "running") {
|
|
@@ -12142,9 +12149,9 @@ function playheadNote(state, join35) {
|
|
|
12142
12149
|
} else if (state.gateNote) {
|
|
12143
12150
|
bits.push(`gate state unavailable \u2014 ${state.gateNote}`);
|
|
12144
12151
|
}
|
|
12145
|
-
if (
|
|
12152
|
+
if (join41?.unplaced.length) {
|
|
12146
12153
|
bits.push(
|
|
12147
|
-
`${
|
|
12154
|
+
`${join41.unplaced.length} record${join41.unplaced.length === 1 ? "" : "s"} matched no declared step and ${join41.unplaced.length === 1 ? "is" : "are"} not painted`
|
|
12148
12155
|
);
|
|
12149
12156
|
}
|
|
12150
12157
|
if (!bits.length) return `${head}.`;
|
|
@@ -12164,15 +12171,15 @@ function extractPrUrl(line) {
|
|
|
12164
12171
|
init_project();
|
|
12165
12172
|
|
|
12166
12173
|
// src/diff.ts
|
|
12167
|
-
function nodeObserved(
|
|
12168
|
-
for (const lex of Object.values(
|
|
12174
|
+
function nodeObserved(json2, nodeId) {
|
|
12175
|
+
for (const lex of Object.values(json2.lexicons ?? {})) {
|
|
12169
12176
|
const o = lex.observed?.[nodeId];
|
|
12170
12177
|
if (o) return o;
|
|
12171
12178
|
}
|
|
12172
12179
|
return null;
|
|
12173
12180
|
}
|
|
12174
|
-
function nodeDiff(
|
|
12175
|
-
for (const lex of Object.values(
|
|
12181
|
+
function nodeDiff(json2, nodeId) {
|
|
12182
|
+
for (const lex of Object.values(json2.lexicons ?? {})) {
|
|
12176
12183
|
const r = lex.resources;
|
|
12177
12184
|
if (!r) continue;
|
|
12178
12185
|
const drift = r.driftedSinceSnapshot?.find((d) => d.name === nodeId);
|
|
@@ -12199,9 +12206,9 @@ function nodeDiff(json, nodeId) {
|
|
|
12199
12206
|
}
|
|
12200
12207
|
return null;
|
|
12201
12208
|
}
|
|
12202
|
-
function nodeFieldDrift(
|
|
12209
|
+
function nodeFieldDrift(json2, nodeId) {
|
|
12203
12210
|
let sawDeep = false;
|
|
12204
|
-
for (const lex of Object.values(
|
|
12211
|
+
for (const lex of Object.values(json2.lexicons ?? {})) {
|
|
12205
12212
|
const deep = lex.deep;
|
|
12206
12213
|
if (!deep) continue;
|
|
12207
12214
|
sawDeep = true;
|
|
@@ -12277,8 +12284,8 @@ function argoErrorMessage(tree) {
|
|
|
12277
12284
|
}
|
|
12278
12285
|
const type = str4(rec4(c)?.type);
|
|
12279
12286
|
if (!type || !ARGO_ERROR_CONDITION.test(type)) continue;
|
|
12280
|
-
const
|
|
12281
|
-
if (
|
|
12287
|
+
const message2 = str4(rec4(c)?.message);
|
|
12288
|
+
if (message2) return message2;
|
|
12282
12289
|
}
|
|
12283
12290
|
return void 0;
|
|
12284
12291
|
}
|
|
@@ -12302,8 +12309,8 @@ function argoVerdict(o) {
|
|
|
12302
12309
|
);
|
|
12303
12310
|
if (verdicts.length === 0) return void 0;
|
|
12304
12311
|
const pair = [health ? `health=${health}` : void 0, sync ? `sync=${sync}` : void 0].filter(Boolean).join(", ");
|
|
12305
|
-
const
|
|
12306
|
-
const detail =
|
|
12312
|
+
const message2 = argoErrorMessage(tree);
|
|
12313
|
+
const detail = message2 ? `${pair}: ${message2}` : pair;
|
|
12307
12314
|
for (const rung of ["degraded", "progressing", "unknown"]) {
|
|
12308
12315
|
if (verdicts.includes(rung)) return { health: rung, detail };
|
|
12309
12316
|
}
|
|
@@ -12391,8 +12398,8 @@ function unhappyCondition(o, type) {
|
|
|
12391
12398
|
const unhappy = type === "ReplicaFailure" ? status === "True" : status === "False";
|
|
12392
12399
|
if (!unhappy) continue;
|
|
12393
12400
|
const reason = str4(cond.reason);
|
|
12394
|
-
const
|
|
12395
|
-
return `${type}${reason ? `=${reason}` : ""}${
|
|
12401
|
+
const message2 = str4(cond.message);
|
|
12402
|
+
return `${type}${reason ? `=${reason}` : ""}${message2 ? `: ${message2}` : ""}`;
|
|
12396
12403
|
}
|
|
12397
12404
|
}
|
|
12398
12405
|
return void 0;
|
|
@@ -12646,8 +12653,8 @@ var OpRunner = class {
|
|
|
12646
12653
|
* it opens the ops lens and re-pulls the lens on the broadcast, so an
|
|
12647
12654
|
* unchanged answer that still broadcast would have the two chasing each other.
|
|
12648
12655
|
*/
|
|
12649
|
-
noteOperator(result,
|
|
12650
|
-
return this.foldOperator(operatorRead(this.lastOperatorState, result,
|
|
12656
|
+
noteOperator(result, at2 = (/* @__PURE__ */ new Date()).toISOString()) {
|
|
12657
|
+
return this.foldOperator(operatorRead(this.lastOperatorState, result, at2));
|
|
12651
12658
|
}
|
|
12652
12659
|
/** Compare-and-broadcast, ignoring `readAt` — the instant behold asked changes
|
|
12653
12660
|
* on every poll and is not itself news; broadcasting on it would make the
|
|
@@ -12821,19 +12828,19 @@ import { join as join20 } from "node:path";
|
|
|
12821
12828
|
import { platform } from "node:os";
|
|
12822
12829
|
var DOCKER_DOWN_DETAIL = "docker is down";
|
|
12823
12830
|
function probe(cmd, args) {
|
|
12824
|
-
return new Promise((
|
|
12831
|
+
return new Promise((resolve29) => {
|
|
12825
12832
|
let out = "";
|
|
12826
12833
|
let proc;
|
|
12827
12834
|
try {
|
|
12828
12835
|
proc = spawn5(cmd, args, { stdio: ["ignore", "pipe", "pipe"] });
|
|
12829
12836
|
} catch {
|
|
12830
|
-
|
|
12837
|
+
resolve29({ code: 127, out: "" });
|
|
12831
12838
|
return;
|
|
12832
12839
|
}
|
|
12833
12840
|
proc.stdout.on("data", (d) => out += d);
|
|
12834
12841
|
proc.stderr.on("data", (d) => out += d);
|
|
12835
|
-
proc.on("error", () =>
|
|
12836
|
-
proc.on("close", (code) =>
|
|
12842
|
+
proc.on("error", () => resolve29({ code: 127, out }));
|
|
12843
|
+
proc.on("close", (code) => resolve29({ code: code ?? 1, out }));
|
|
12837
12844
|
});
|
|
12838
12845
|
}
|
|
12839
12846
|
async function dockerAvailable(run3) {
|
|
@@ -13124,10 +13131,10 @@ function validateBehaviourBlock(v) {
|
|
|
13124
13131
|
if (errorRate === void 0) return bad("errorRate is not a fraction 0..1");
|
|
13125
13132
|
const res = rec5(b.resilience);
|
|
13126
13133
|
const failure = str5(res?.failure);
|
|
13127
|
-
const
|
|
13134
|
+
const verdict3 = res?.verdict;
|
|
13128
13135
|
if (!failure) return bad("resilience.failure missing \u2014 a verdict with no named failure says nothing");
|
|
13129
|
-
if (
|
|
13130
|
-
return bad(`resilience.verdict ${JSON.stringify(
|
|
13136
|
+
if (verdict3 !== "survives" && verdict3 !== "degrades" && verdict3 !== "fails") {
|
|
13137
|
+
return bad(`resilience.verdict ${JSON.stringify(verdict3)} is not survives/degrades/fails`);
|
|
13131
13138
|
}
|
|
13132
13139
|
const note = str5(res?.note);
|
|
13133
13140
|
let rightSize;
|
|
@@ -13154,7 +13161,7 @@ function validateBehaviourBlock(v) {
|
|
|
13154
13161
|
cost: { perHour, currency },
|
|
13155
13162
|
headroom: { ...cpu !== void 0 ? { cpu } : {}, ...latency !== void 0 ? { latency } : {} },
|
|
13156
13163
|
errorRate,
|
|
13157
|
-
resilience: { failure, verdict:
|
|
13164
|
+
resilience: { failure, verdict: verdict3, ...note ? { note } : {} },
|
|
13158
13165
|
...rightSize ? { rightSize } : {},
|
|
13159
13166
|
provenance: { engine, version, tolerance, basis }
|
|
13160
13167
|
}
|
|
@@ -13179,9 +13186,9 @@ function validateBehaviourMeta(v) {
|
|
|
13179
13186
|
if (!traffic) return bad("meta.at.traffic missing");
|
|
13180
13187
|
let total;
|
|
13181
13188
|
if (m.total !== void 0) {
|
|
13182
|
-
const
|
|
13183
|
-
const perHour = num(
|
|
13184
|
-
const currency = str5(
|
|
13189
|
+
const t2 = rec5(m.total);
|
|
13190
|
+
const perHour = num(t2?.perHour);
|
|
13191
|
+
const currency = str5(t2?.currency);
|
|
13185
13192
|
if (perHour === void 0 || !currency) return bad("meta.total is not {perHour, currency}");
|
|
13186
13193
|
total = { perHour, currency };
|
|
13187
13194
|
}
|
|
@@ -13315,14 +13322,14 @@ function attachBehaviour(ir, members, env, readFile2 = realRead) {
|
|
|
13315
13322
|
const first = metas[0]?.meta ?? {};
|
|
13316
13323
|
const engines = [...new Set(metas.map((m) => `${m.meta.engine} ${m.meta.version}`))];
|
|
13317
13324
|
if (engines.length > 1) diagnostics.push(`priced by ${engines.length} engines (${engines.join(", ")}) \u2014 every figure carries its own provenance`);
|
|
13318
|
-
const trafficLevels = [...new Set(metas.map((m) => m.meta.at?.traffic).filter((
|
|
13325
|
+
const trafficLevels = [...new Set(metas.map((m) => m.meta.at?.traffic).filter((t2) => !!t2))];
|
|
13319
13326
|
if (trafficLevels.length > 1) diagnostics.push(`priced at ${trafficLevels.length} traffic levels (${trafficLevels.join("; ")}) \u2014 the sums add figures stated at different levels`);
|
|
13320
13327
|
const meta = {
|
|
13321
13328
|
...first.engine ? { engine: first.engine } : {},
|
|
13322
13329
|
...first.version ? { version: first.version } : {},
|
|
13323
13330
|
...first.at ? { at: first.at } : {}
|
|
13324
13331
|
};
|
|
13325
|
-
const engineTotal = metas.map((m) => m.meta.total).find((
|
|
13332
|
+
const engineTotal = metas.map((m) => m.meta.total).find((t2) => !!t2);
|
|
13326
13333
|
if (engineTotal) {
|
|
13327
13334
|
meta.total = engineTotal;
|
|
13328
13335
|
if (metas.filter((m) => m.meta.total).length > 1) diagnostics.push(FIRST_TOTAL_SHOWN);
|
|
@@ -13518,8 +13525,8 @@ function attachBehaviourDelta(ir, members, behaviour) {
|
|
|
13518
13525
|
const stated = rows.filter((r) => r.live?.basis === "total").length;
|
|
13519
13526
|
if (live2 && stated > 1) {
|
|
13520
13527
|
delete out.total;
|
|
13521
|
-
const
|
|
13522
|
-
if (
|
|
13528
|
+
const at2 = diagnostics.indexOf(FIRST_TOTAL_SHOWN);
|
|
13529
|
+
if (at2 >= 0) diagnostics.splice(at2, 1);
|
|
13523
13530
|
}
|
|
13524
13531
|
if (live2) out.live = live2;
|
|
13525
13532
|
if (declared) out.declared = declared;
|
|
@@ -13699,26 +13706,26 @@ var isRecord10 = (v) => typeof v === "object" && v !== null && !Array.isArray(v)
|
|
|
13699
13706
|
var refuse7 = (error, remedy) => ({ ok: false, refusal: { error, code: "choudoufu-live-check", remedy } });
|
|
13700
13707
|
var CREDS2 = "Give behold the ambient read-only AWS credentials the estate's `plan` needs (AWS_PROFILE, or AWS_ENDPOINT_URL for an emulator), then re-check live with plan.";
|
|
13701
13708
|
function said3(run3, fallback) {
|
|
13702
|
-
const
|
|
13703
|
-
return
|
|
13709
|
+
const lines2 = stripAnsiLines(run3.stderr);
|
|
13710
|
+
return lines2.find((l) => /^Error:/.test(l)) ?? lines2[0] ?? fallback;
|
|
13704
13711
|
}
|
|
13705
13712
|
function parsePlanShow(run3, dir) {
|
|
13706
13713
|
if (run3.code === 127) return refuse7("choudoufu is not on PATH.", "Install choudoufu and put it on PATH.");
|
|
13707
13714
|
const text = run3.stdout.trim();
|
|
13708
13715
|
if (!text) return refuse7(`choudoufu show could not read the plan for ${dir}: ${said3(run3, `exit ${run3.code}`)}`, CREDS2);
|
|
13709
|
-
let
|
|
13716
|
+
let json2;
|
|
13710
13717
|
try {
|
|
13711
|
-
|
|
13718
|
+
json2 = JSON.parse(text);
|
|
13712
13719
|
} catch {
|
|
13713
13720
|
return refuse7(`choudoufu show printed something that is not JSON for ${dir}: ${said3(run3, "unparseable stdout")}`, CREDS2);
|
|
13714
13721
|
}
|
|
13715
|
-
if (!isRecord10(
|
|
13722
|
+
if (!isRecord10(json2) || typeof json2.format_version !== "string") {
|
|
13716
13723
|
return refuse7("That is not a plan document \u2014 no `format_version`.", CREDS2);
|
|
13717
13724
|
}
|
|
13718
|
-
if (
|
|
13725
|
+
if (json2.resource_changes !== void 0 && json2.resource_changes !== null && !Array.isArray(json2.resource_changes)) {
|
|
13719
13726
|
return refuse7("That plan document's `resource_changes` is not a list.", CREDS2);
|
|
13720
13727
|
}
|
|
13721
|
-
return { ok: true, doc:
|
|
13728
|
+
return { ok: true, doc: json2 };
|
|
13722
13729
|
}
|
|
13723
13730
|
var KNOWN_AFTER_APPLY = "(known after apply)";
|
|
13724
13731
|
var SENSITIVE = "(sensitive)";
|
|
@@ -13847,16 +13854,16 @@ function parseLiveMv(run3, what) {
|
|
|
13847
13854
|
if (run3.code === 127) return refuse8("choudoufu is not on PATH.", "Install choudoufu and put it on PATH.");
|
|
13848
13855
|
const text = run3.stdout.trim();
|
|
13849
13856
|
if (!text) return refuse8(`choudoufu live-mv printed no document for ${what} (exit ${run3.code}).`, "Run the line by hand to see choudoufu's own words.");
|
|
13850
|
-
let
|
|
13857
|
+
let json2;
|
|
13851
13858
|
try {
|
|
13852
|
-
|
|
13859
|
+
json2 = JSON.parse(text);
|
|
13853
13860
|
} catch {
|
|
13854
13861
|
return refuse8(`choudoufu live-mv printed something that is not JSON for ${what}.`, "Run the line by hand to see choudoufu's own words.");
|
|
13855
13862
|
}
|
|
13856
|
-
if (!isRecord11(
|
|
13863
|
+
if (!isRecord11(json2) || !isRecord11(json2.from) || !isRecord11(json2.to) || typeof json2.dry_run !== "boolean") {
|
|
13857
13864
|
return refuse8(`That is not a live-mv document (${what}).`, "Upgrade choudoufu to 0.16.0 or newer.");
|
|
13858
13865
|
}
|
|
13859
|
-
return { ok: true, doc:
|
|
13866
|
+
return { ok: true, doc: json2 };
|
|
13860
13867
|
}
|
|
13861
13868
|
function handoffLine(m) {
|
|
13862
13869
|
return `choudoufu live-mv -from-estate=${m.from} ${m.address} ${m.new_address ?? m.address}`;
|
|
@@ -14212,16 +14219,16 @@ import { existsSync as existsSync16 } from "node:fs";
|
|
|
14212
14219
|
import { dirname as dirname6, join as join24, resolve as resolve14 } from "node:path";
|
|
14213
14220
|
var TERRAGUCCI_CONFIG_NAMES = ["terragucci.yml", "terragucci.yaml", "terragucci.json", "terragucci.ts"];
|
|
14214
14221
|
function terragucciConfig(dir) {
|
|
14215
|
-
let
|
|
14222
|
+
let at2 = resolve14(dir);
|
|
14216
14223
|
for (; ; ) {
|
|
14217
14224
|
for (const name of TERRAGUCCI_CONFIG_NAMES) {
|
|
14218
|
-
const file = join24(
|
|
14225
|
+
const file = join24(at2, name);
|
|
14219
14226
|
if (existsSync16(file)) return file;
|
|
14220
14227
|
}
|
|
14221
|
-
if (existsSync16(join24(
|
|
14222
|
-
const up = dirname6(
|
|
14223
|
-
if (up ===
|
|
14224
|
-
|
|
14228
|
+
if (existsSync16(join24(at2, ".git"))) return void 0;
|
|
14229
|
+
const up = dirname6(at2);
|
|
14230
|
+
if (up === at2) return void 0;
|
|
14231
|
+
at2 = up;
|
|
14225
14232
|
}
|
|
14226
14233
|
}
|
|
14227
14234
|
function servedTerragucciConfig(dirs) {
|
|
@@ -14288,7 +14295,7 @@ function labelFor(root, target, estate) {
|
|
|
14288
14295
|
function chantCheck(root, targets, estate) {
|
|
14289
14296
|
if (!targets.length) return { name: "chant", status: "pass", detail: "no chant members \u2014 nothing for behold to shell" };
|
|
14290
14297
|
const floor = chantFloor();
|
|
14291
|
-
const resolutions = targets.map((
|
|
14298
|
+
const resolutions = targets.map((t2) => ({ target: t2, res: resolveChant(t2) }));
|
|
14292
14299
|
const missing = resolutions.filter(({ res }) => res.source !== "project");
|
|
14293
14300
|
const describe = ({ target, res }) => `${labelFor(root, target, estate)}${res.source === "project" ? `chant ${res.version ?? "(unknown version)"}` : "no chant installed"}`;
|
|
14294
14301
|
if (missing.length) {
|
|
@@ -14335,7 +14342,7 @@ function lexiconCheck(root, declared, estate) {
|
|
|
14335
14342
|
}
|
|
14336
14343
|
function targetCheck(declared) {
|
|
14337
14344
|
const lexicons = [...new Set([...declared.values()].flat())].sort();
|
|
14338
|
-
const routable = new Set(SUBSTRATE_TARGET_VARS.map((
|
|
14345
|
+
const routable = new Set(SUBSTRATE_TARGET_VARS.map((t2) => t2.lexicon));
|
|
14339
14346
|
const unlisted = lexicons.filter((l) => !routable.has(l) && !(l in NO_AMBIENT_TARGET));
|
|
14340
14347
|
if (!lexicons.length) return { name: "targets", status: "pass", detail: "no lexicon declared \u2014 nothing to aim" };
|
|
14341
14348
|
if (!unlisted.length) {
|
|
@@ -14387,7 +14394,7 @@ function kubeCheck(lexicons, profiles, envs, kubeconfig) {
|
|
|
14387
14394
|
fix: "Declare `k8s.profiles.<env>.context` in chant.config.ts so the cluster the reads hit is explicit, not ambient."
|
|
14388
14395
|
};
|
|
14389
14396
|
}
|
|
14390
|
-
const
|
|
14397
|
+
const lines2 = [];
|
|
14391
14398
|
const unresolved = [];
|
|
14392
14399
|
const mismatched = [];
|
|
14393
14400
|
for (const env of bound) {
|
|
@@ -14397,14 +14404,14 @@ function kubeCheck(lexicons, profiles, envs, kubeconfig) {
|
|
|
14397
14404
|
unresolved.push(`${env}: ${context}`);
|
|
14398
14405
|
continue;
|
|
14399
14406
|
}
|
|
14400
|
-
|
|
14407
|
+
lines2.push(`${env}: ${target.label} at ${target.endpoint}`);
|
|
14401
14408
|
if (ambient && ambient !== target.label) mismatched.push(`${env}: ${target.label}`);
|
|
14402
14409
|
}
|
|
14403
14410
|
if (unresolved.length) {
|
|
14404
14411
|
return {
|
|
14405
14412
|
name: "kube",
|
|
14406
14413
|
status: "warn",
|
|
14407
|
-
detail: `context not in the kubeconfig \u2014 ${list2(unresolved)}${
|
|
14414
|
+
detail: `context not in the kubeconfig \u2014 ${list2(unresolved)}${lines2.length ? `; bound: ${list2(lines2)}` : ""}`,
|
|
14408
14415
|
fix: `Add the context to your kubeconfig, or point k8s.profiles at one you have (${summarize([...kubeconfig.contexts.keys()])}).`
|
|
14409
14416
|
};
|
|
14410
14417
|
}
|
|
@@ -14412,11 +14419,11 @@ function kubeCheck(lexicons, profiles, envs, kubeconfig) {
|
|
|
14412
14419
|
return {
|
|
14413
14420
|
name: "kube",
|
|
14414
14421
|
status: "warn",
|
|
14415
|
-
detail: `bound ${list2(
|
|
14422
|
+
detail: `bound ${list2(lines2)}, but the ambient current-context is ${ambient} \u2014 chant refuses a live read on a mismatch`,
|
|
14416
14423
|
fix: `Run \`kubectl config use-context ${mismatched[0].split(": ")[1]}\` before the live overlay.`
|
|
14417
14424
|
};
|
|
14418
14425
|
}
|
|
14419
|
-
return { name: "kube", status: "pass", detail: `bound ${list2(
|
|
14426
|
+
return { name: "kube", status: "pass", detail: `bound ${list2(lines2)}` };
|
|
14420
14427
|
}
|
|
14421
14428
|
function substrateCheck(substrates) {
|
|
14422
14429
|
if (!substrates.length) return { name: "substrates", status: "pass", detail: "none needed by this project" };
|
|
@@ -14584,7 +14591,7 @@ async function diagnose(dir, probes = {}) {
|
|
|
14584
14591
|
detail: `${estateDetail}; invalid: ${list2(invalidDetail)}`,
|
|
14585
14592
|
fix: kindsFix
|
|
14586
14593
|
} : { name: "project", status: "pass", detail: `${emptyDeclaration}${estateDetail}` } : { name: "project", status: "pass", detail: `${emptyDeclaration}chant project (${relative6(root, shape.configFile)})` };
|
|
14587
|
-
const infos = await Promise.all(chantTargets.map(async (
|
|
14594
|
+
const infos = await Promise.all(chantTargets.map(async (t2) => ({ target: t2, info: await detectProject(t2) })));
|
|
14588
14595
|
const declared = new Map(infos.map(({ target, info }) => [target, info.lexicons]));
|
|
14589
14596
|
const lexicons = [...new Set(infos.flatMap(({ info }) => info.lexicons))];
|
|
14590
14597
|
const envs = [...new Set(infos.flatMap(({ info }) => info.environments))];
|
|
@@ -14619,14 +14626,14 @@ async function diagnose(dir, probes = {}) {
|
|
|
14619
14626
|
return { behold, dir: root, kind: shape.kind, ok: !checks.some((c) => c.status === "fail"), checks };
|
|
14620
14627
|
}
|
|
14621
14628
|
function formatReport(report) {
|
|
14622
|
-
const
|
|
14629
|
+
const lines2 = [`behold doctor ${report.behold} \u2014 ${report.dir}`, ""];
|
|
14623
14630
|
for (const c of report.checks) {
|
|
14624
|
-
|
|
14625
|
-
if (c.fix)
|
|
14631
|
+
lines2.push(` ${c.status.padEnd(4)} ${c.name.padEnd(10)} ${c.detail}`);
|
|
14632
|
+
if (c.fix) lines2.push(` ${"fix".padEnd(10)} ${c.fix}`);
|
|
14626
14633
|
}
|
|
14627
14634
|
const tally = (s) => report.checks.filter((c) => c.status === s).length;
|
|
14628
|
-
|
|
14629
|
-
return
|
|
14635
|
+
lines2.push("", ` ${tally("pass")} pass, ${tally("warn")} warn, ${tally("fail")} fail`, "");
|
|
14636
|
+
return lines2.join("\n");
|
|
14630
14637
|
}
|
|
14631
14638
|
|
|
14632
14639
|
// src/server.ts
|
|
@@ -14922,11 +14929,11 @@ function renderLanes(frames, summaries) {
|
|
|
14922
14929
|
// src/emulator.ts
|
|
14923
14930
|
init_chant();
|
|
14924
14931
|
function parseEmulators(stdout) {
|
|
14925
|
-
const
|
|
14926
|
-
for (let i =
|
|
14927
|
-
if (!
|
|
14932
|
+
const lines2 = stdout.split(/\r?\n/).map((l) => l.trim()).filter(Boolean);
|
|
14933
|
+
for (let i = lines2.length - 1; i >= 0; i--) {
|
|
14934
|
+
if (!lines2[i].startsWith("{")) continue;
|
|
14928
14935
|
try {
|
|
14929
|
-
const parsed = JSON.parse(
|
|
14936
|
+
const parsed = JSON.parse(lines2[i]);
|
|
14930
14937
|
if (Array.isArray(parsed.emulators)) return parsed.emulators;
|
|
14931
14938
|
} catch {
|
|
14932
14939
|
}
|
|
@@ -14956,7 +14963,7 @@ async function emulatorDown(projectDir) {
|
|
|
14956
14963
|
|
|
14957
14964
|
// src/terragucci-view.ts
|
|
14958
14965
|
import { execFileSync as execFileSync2 } from "node:child_process";
|
|
14959
|
-
import { dirname as
|
|
14966
|
+
import { dirname as dirname10, join as join32, relative as relative7, sep as sep6 } from "node:path";
|
|
14960
14967
|
init_terraform_member();
|
|
14961
14968
|
|
|
14962
14969
|
// src/terragucci-reports.ts
|
|
@@ -14970,9 +14977,9 @@ var INDEX_SCHEMA = "terragucci.report-index/v1";
|
|
|
14970
14977
|
var ESTATE_SCHEMA = "terragucci.estate/v1";
|
|
14971
14978
|
var MAX_RUNS_PER_STAGE = 12;
|
|
14972
14979
|
var TerragucciReadError = class extends Error {
|
|
14973
|
-
constructor(
|
|
14974
|
-
super(
|
|
14975
|
-
this.refusal =
|
|
14980
|
+
constructor(refusal2) {
|
|
14981
|
+
super(refusal2.error);
|
|
14982
|
+
this.refusal = refusal2;
|
|
14976
14983
|
}
|
|
14977
14984
|
refusal;
|
|
14978
14985
|
};
|
|
@@ -15020,7 +15027,7 @@ function schemaStamp(dist) {
|
|
|
15020
15027
|
function compileSchemas(dist) {
|
|
15021
15028
|
const version = JSON.parse(readFileSync18(join26(dist, "..", "package.json"), "utf8")).version ?? "";
|
|
15022
15029
|
const ajv = new Ajv2020({ strict: false, allErrors: false });
|
|
15023
|
-
const
|
|
15030
|
+
const compiled3 = Object.fromEntries(
|
|
15024
15031
|
Object.keys(SCHEMA_FILES).map((n) => [n, ajv.compile(JSON.parse(readFileSync18(join26(dist, SCHEMA_FILES[n]), "utf8")))])
|
|
15025
15032
|
);
|
|
15026
15033
|
return {
|
|
@@ -15028,8 +15035,8 @@ function compileSchemas(dist) {
|
|
|
15028
15035
|
check(name, doc) {
|
|
15029
15036
|
const id = isRecord14(doc) ? doc.schema : void 0;
|
|
15030
15037
|
if (id !== SCHEMA_IDS[name]) return `its schema is ${JSON.stringify(id)}, not ${SCHEMA_IDS[name]}`;
|
|
15031
|
-
if (
|
|
15032
|
-
const e =
|
|
15038
|
+
if (compiled3[name](doc)) return void 0;
|
|
15039
|
+
const e = compiled3[name].errors?.[0];
|
|
15033
15040
|
return `${SCHEMA_FILES[name]} rejects it: ${e?.instancePath || "/"} ${e?.message ?? "is invalid"}`;
|
|
15034
15041
|
}
|
|
15035
15042
|
};
|
|
@@ -15272,12 +15279,12 @@ function joinTerragucci(read, nodes, roots) {
|
|
|
15272
15279
|
for (const n of nodes) {
|
|
15273
15280
|
const root = n.attrs?.root;
|
|
15274
15281
|
if (typeof root !== "string") continue;
|
|
15275
|
-
const
|
|
15276
|
-
if (
|
|
15277
|
-
const member = n.id.slice(0,
|
|
15282
|
+
const at2 = n.id.indexOf(`/${root}/`);
|
|
15283
|
+
if (at2 < 0) continue;
|
|
15284
|
+
const member = n.id.slice(0, at2);
|
|
15278
15285
|
const key = `${member}\0${root}`;
|
|
15279
15286
|
if (!byRoot.has(key)) byRoot.set(key, /* @__PURE__ */ new Map());
|
|
15280
|
-
byRoot.get(key).set(n.id.slice(
|
|
15287
|
+
byRoot.get(key).set(n.id.slice(at2 + root.length + 2), n.id);
|
|
15281
15288
|
}
|
|
15282
15289
|
const cards = {};
|
|
15283
15290
|
const unmatchedChanges = [];
|
|
@@ -15333,8 +15340,8 @@ var S3Error = class extends Error {
|
|
|
15333
15340
|
status;
|
|
15334
15341
|
/** S3's error code (`AccessDenied`, `NoSuchBucket`, ...), when the body carried one. */
|
|
15335
15342
|
code;
|
|
15336
|
-
constructor(
|
|
15337
|
-
super(
|
|
15343
|
+
constructor(message2, answer) {
|
|
15344
|
+
super(message2);
|
|
15338
15345
|
if (answer) {
|
|
15339
15346
|
this.status = answer.status;
|
|
15340
15347
|
const code = xmlText(answer.body, "Code");
|
|
@@ -15391,8 +15398,8 @@ async function assumeRole(w, fetchFn) {
|
|
|
15391
15398
|
}
|
|
15392
15399
|
var sha256 = (b) => createHash4("sha256").update(b).digest("hex");
|
|
15393
15400
|
var hmac = (key, s) => createHmac("sha256", key).update(s).digest();
|
|
15394
|
-
var objectUrl = (
|
|
15395
|
-
function sign(
|
|
15401
|
+
var objectUrl = (t2, key) => `${t2.endpoint ? `${t2.endpoint}/${t2.bucket}` : `https://${t2.bucket}.s3.${t2.region}.amazonaws.com`}/${encodePath(key)}`;
|
|
15402
|
+
function sign(t2, method, rawUrl, extra, payload, now) {
|
|
15396
15403
|
const url = new URL(rawUrl);
|
|
15397
15404
|
const amzDate = now.toISOString().replace(/[-:]/g, "").replace(/\.\d{3}/, "");
|
|
15398
15405
|
const day = amzDate.slice(0, 8);
|
|
@@ -15401,17 +15408,17 @@ function sign(t, method, rawUrl, extra, payload, now) {
|
|
|
15401
15408
|
"x-amz-content-sha256": payload,
|
|
15402
15409
|
"x-amz-date": amzDate,
|
|
15403
15410
|
...Object.fromEntries(Object.entries(extra).map(([k, v]) => [k.toLowerCase(), v])),
|
|
15404
|
-
...
|
|
15411
|
+
...t2.sessionToken ? { "x-amz-security-token": t2.sessionToken } : {}
|
|
15405
15412
|
};
|
|
15406
15413
|
const names = Object.keys(headers).sort();
|
|
15407
15414
|
const query = [...url.searchParams].map(([k, v]) => `${encodeStrict(k)}=${encodeStrict(v)}`).sort().join("&");
|
|
15408
15415
|
const canonical = [method, url.pathname, query, ...names.map((n) => `${n}:${headers[n].trim()}`), "", names.join(";"), payload].join("\n");
|
|
15409
|
-
const scope = `${day}/${
|
|
15416
|
+
const scope = `${day}/${t2.region}/s3/aws4_request`;
|
|
15410
15417
|
const toSign = ["AWS4-HMAC-SHA256", amzDate, scope, sha256(canonical)].join("\n");
|
|
15411
|
-
const key4 = hmac(hmac(hmac(hmac(`AWS4${
|
|
15418
|
+
const key4 = hmac(hmac(hmac(hmac(`AWS4${t2.secretAccessKey}`, day), t2.region), "s3"), "aws4_request");
|
|
15412
15419
|
const signature = createHmac("sha256", key4).update(toSign).digest("hex");
|
|
15413
15420
|
const { host: _host, ...sent } = headers;
|
|
15414
|
-
return { ...sent, authorization: `AWS4-HMAC-SHA256 Credential=${
|
|
15421
|
+
return { ...sent, authorization: `AWS4-HMAC-SHA256 Credential=${t2.accessKeyId}/${scope}, SignedHeaders=${names.join(";")}, Signature=${signature}` };
|
|
15415
15422
|
}
|
|
15416
15423
|
var S3Object = class {
|
|
15417
15424
|
constructor(target, fetchFn = fetch, now = () => /* @__PURE__ */ new Date()) {
|
|
@@ -15424,45 +15431,61 @@ var S3Object = class {
|
|
|
15424
15431
|
now;
|
|
15425
15432
|
creds;
|
|
15426
15433
|
async signer() {
|
|
15427
|
-
const
|
|
15428
|
-
if (!("webIdentity" in
|
|
15429
|
-
this.creds ??= assumeRole(
|
|
15434
|
+
const t2 = this.target;
|
|
15435
|
+
if (!("webIdentity" in t2)) return t2;
|
|
15436
|
+
this.creds ??= assumeRole(t2.webIdentity, this.fetchFn).catch((e) => {
|
|
15430
15437
|
this.creds = void 0;
|
|
15431
15438
|
throw e;
|
|
15432
15439
|
});
|
|
15433
|
-
return { bucket:
|
|
15440
|
+
return { bucket: t2.bucket, ...t2.endpoint ? { endpoint: t2.endpoint } : {}, region: t2.region, ...await this.creds };
|
|
15434
15441
|
}
|
|
15435
15442
|
/** The object's text, or undefined when there is none. */
|
|
15436
15443
|
async get(key) {
|
|
15437
|
-
const
|
|
15438
|
-
const url = objectUrl(
|
|
15439
|
-
const res = await this.fetchFn(url, { method: "GET", headers: sign(
|
|
15444
|
+
const t2 = await this.signer();
|
|
15445
|
+
const url = objectUrl(t2, key);
|
|
15446
|
+
const res = await this.fetchFn(url, { method: "GET", headers: sign(t2, "GET", url, {}, sha256(""), this.now()) });
|
|
15440
15447
|
if (res.status === 404) return void 0;
|
|
15441
15448
|
if (!res.ok) {
|
|
15442
15449
|
const body = await res.text();
|
|
15443
|
-
throw new S3Error(`GET s3://${
|
|
15450
|
+
throw new S3Error(`GET s3://${t2.bucket}/${key}: ${res.status} ${body.slice(0, 300)}`, { status: res.status, body });
|
|
15444
15451
|
}
|
|
15445
15452
|
return res.text();
|
|
15446
15453
|
}
|
|
15454
|
+
/**
|
|
15455
|
+
* #505: a conditional HEAD of the object. `same` when its ETag is still
|
|
15456
|
+
* `ifNoneMatch` (S3 answers 304), its ETag otherwise, undefined when there
|
|
15457
|
+
* is no such object.
|
|
15458
|
+
*/
|
|
15459
|
+
async head(key, ifNoneMatch) {
|
|
15460
|
+
const t2 = await this.signer();
|
|
15461
|
+
const url = objectUrl(t2, key);
|
|
15462
|
+
const extra = ifNoneMatch ? { "if-none-match": ifNoneMatch } : {};
|
|
15463
|
+
const res = await this.fetchFn(url, { method: "HEAD", headers: sign(t2, "HEAD", url, extra, sha256(""), this.now()) });
|
|
15464
|
+
if (res.status === 304) return { same: true };
|
|
15465
|
+
if (res.status === 404) return void 0;
|
|
15466
|
+
if (!res.ok) throw new S3Error(`HEAD s3://${t2.bucket}/${key}: ${res.status}`, { status: res.status, body: "" });
|
|
15467
|
+
const etag = res.headers?.get("etag") ?? void 0;
|
|
15468
|
+
return { same: false, ...etag ? { etag } : {} };
|
|
15469
|
+
}
|
|
15447
15470
|
/** Write an object, with a Cache-Control header when one is given. */
|
|
15448
15471
|
async put(key, body, contentType2, cacheControl) {
|
|
15449
|
-
const
|
|
15450
|
-
const url = objectUrl(
|
|
15472
|
+
const t2 = await this.signer();
|
|
15473
|
+
const url = objectUrl(t2, key);
|
|
15451
15474
|
const extra = { "content-type": contentType2, ...cacheControl ? { "cache-control": cacheControl } : {} };
|
|
15452
|
-
const res = await this.fetchFn(url, { method: "PUT", headers: sign(
|
|
15475
|
+
const res = await this.fetchFn(url, { method: "PUT", headers: sign(t2, "PUT", url, extra, sha256(body), this.now()), body });
|
|
15453
15476
|
if (!res.ok) {
|
|
15454
15477
|
const text = await res.text();
|
|
15455
|
-
throw new S3Error(`PUT s3://${
|
|
15478
|
+
throw new S3Error(`PUT s3://${t2.bucket}/${key}: ${res.status} ${text.slice(0, 300)}`, { status: res.status, body: text });
|
|
15456
15479
|
}
|
|
15457
15480
|
}
|
|
15458
15481
|
/** Delete an object. A missing one is not an error. */
|
|
15459
15482
|
async delete(key) {
|
|
15460
|
-
const
|
|
15461
|
-
const url = objectUrl(
|
|
15462
|
-
const res = await this.fetchFn(url, { method: "DELETE", headers: sign(
|
|
15483
|
+
const t2 = await this.signer();
|
|
15484
|
+
const url = objectUrl(t2, key);
|
|
15485
|
+
const res = await this.fetchFn(url, { method: "DELETE", headers: sign(t2, "DELETE", url, {}, sha256(""), this.now()) });
|
|
15463
15486
|
if (res.ok || res.status === 404) return;
|
|
15464
15487
|
const text = await res.text();
|
|
15465
|
-
throw new S3Error(`DELETE s3://${
|
|
15488
|
+
throw new S3Error(`DELETE s3://${t2.bucket}/${key}: ${res.status} ${text.slice(0, 300)}`, { status: res.status, body: text });
|
|
15466
15489
|
}
|
|
15467
15490
|
};
|
|
15468
15491
|
|
|
@@ -15534,12 +15557,1037 @@ function terragucciSource(spec2, opts = {}) {
|
|
|
15534
15557
|
};
|
|
15535
15558
|
}
|
|
15536
15559
|
|
|
15560
|
+
// src/terragucci-timeline.ts
|
|
15561
|
+
import { readFileSync as readFileSync20, statSync as statSync9 } from "node:fs";
|
|
15562
|
+
import { createRequire as createRequire4 } from "node:module";
|
|
15563
|
+
import { dirname as dirname8, join as join28, resolve as resolve18 } from "node:path";
|
|
15564
|
+
import { fileURLToPath as fileURLToPath5 } from "node:url";
|
|
15565
|
+
import { Ajv2020 as Ajv20202 } from "ajv/dist/2020.js";
|
|
15566
|
+
var AUDIT_SCHEMA = "terragucci.audit/v1";
|
|
15567
|
+
var AUDIT_KEY = "audit.jsonl";
|
|
15568
|
+
var AUDIT_SCHEMA_FILE = "audit.schema.json";
|
|
15569
|
+
var WHAT_IS_ROOT = /* @__PURE__ */ new Set(["override-requested", "override", "override-revoked", "unlock", "state-export"]);
|
|
15570
|
+
var TIMELINE_LIMIT = 200;
|
|
15571
|
+
var TIMELINE_MAX = 1e3;
|
|
15572
|
+
var TIMELINE_TTL_MS = 3e4;
|
|
15573
|
+
var isRecord15 = (v) => v !== null && typeof v === "object" && !Array.isArray(v);
|
|
15574
|
+
var str10 = (v) => typeof v === "string";
|
|
15575
|
+
function structuralAudit(doc) {
|
|
15576
|
+
if (!isRecord15(doc)) return "it is not a JSON object";
|
|
15577
|
+
if (doc.schema !== AUDIT_SCHEMA) return `its schema is ${JSON.stringify(doc.schema)}, not ${AUDIT_SCHEMA}`;
|
|
15578
|
+
for (const f of ["id", "kind", "project", "at", "what", "result"]) if (!str10(doc[f])) return `its ${f} is not a string`;
|
|
15579
|
+
if (Number.isNaN(Date.parse(doc.at))) return `its at, ${JSON.stringify(doc.at)}, is not a time`;
|
|
15580
|
+
if (doc.who !== null && !str10(doc.who)) return "its who is neither a string nor null";
|
|
15581
|
+
if (doc.digest !== null && !str10(doc.digest)) return "its digest is neither a string nor null";
|
|
15582
|
+
if (!isRecord15(doc.evidence) || !str10(doc.evidence.source)) return "its evidence has no source";
|
|
15583
|
+
if (doc.detail !== void 0 && !isRecord15(doc.detail)) return "its detail is not an object";
|
|
15584
|
+
return void 0;
|
|
15585
|
+
}
|
|
15586
|
+
function compileAuditSchema(schema, version) {
|
|
15587
|
+
const v = new Ajv20202({ strict: false, allErrors: false }).compile(schema);
|
|
15588
|
+
return {
|
|
15589
|
+
validation: { by: "schema", terragucci: version },
|
|
15590
|
+
check(doc) {
|
|
15591
|
+
const id = isRecord15(doc) ? doc.schema : void 0;
|
|
15592
|
+
if (id !== AUDIT_SCHEMA) return `its schema is ${JSON.stringify(id)}, not ${AUDIT_SCHEMA}`;
|
|
15593
|
+
if (v(doc)) return void 0;
|
|
15594
|
+
const e = v.errors?.[0];
|
|
15595
|
+
return `${AUDIT_SCHEMA_FILE} rejects it: ${e?.instancePath || "/"} ${e?.message ?? "is invalid"}`;
|
|
15596
|
+
}
|
|
15597
|
+
};
|
|
15598
|
+
}
|
|
15599
|
+
var compiled = /* @__PURE__ */ new Map();
|
|
15600
|
+
function auditValidator(dirs = []) {
|
|
15601
|
+
for (const from of [...dirs, dirname8(fileURLToPath5(import.meta.url))]) {
|
|
15602
|
+
let dist;
|
|
15603
|
+
try {
|
|
15604
|
+
dist = dirname8(createRequire4(join28(resolve18(from), "noop.js")).resolve("@intentius/terragucci/report.schema.json"));
|
|
15605
|
+
} catch {
|
|
15606
|
+
continue;
|
|
15607
|
+
}
|
|
15608
|
+
const pkg = join28(dist, "..", "package.json");
|
|
15609
|
+
const file = join28(dist, AUDIT_SCHEMA_FILE);
|
|
15610
|
+
let stamp;
|
|
15611
|
+
try {
|
|
15612
|
+
stamp = [dist, ...[pkg, file].map((f) => {
|
|
15613
|
+
const st = statSync9(f);
|
|
15614
|
+
return `${st.mtimeMs}:${st.size}`;
|
|
15615
|
+
})].join("\0");
|
|
15616
|
+
} catch {
|
|
15617
|
+
continue;
|
|
15618
|
+
}
|
|
15619
|
+
const hit = compiled.get(stamp);
|
|
15620
|
+
if (hit) return hit;
|
|
15621
|
+
let v;
|
|
15622
|
+
try {
|
|
15623
|
+
const version = JSON.parse(readFileSync20(pkg, "utf8")).version ?? "";
|
|
15624
|
+
v = compileAuditSchema(JSON.parse(readFileSync20(file, "utf8")), version);
|
|
15625
|
+
} catch {
|
|
15626
|
+
continue;
|
|
15627
|
+
}
|
|
15628
|
+
for (const k of compiled.keys()) if (k.startsWith(`${dist}\0`)) compiled.delete(k);
|
|
15629
|
+
compiled.set(stamp, v);
|
|
15630
|
+
return v;
|
|
15631
|
+
}
|
|
15632
|
+
return { validation: { by: "structural" }, check: structuralAudit };
|
|
15633
|
+
}
|
|
15634
|
+
var refuse11 = (error, remedy) => {
|
|
15635
|
+
throw new TerragucciReadError({ error, code: "terragucci-report", remedy });
|
|
15636
|
+
};
|
|
15637
|
+
function parseAudit(text, location, v) {
|
|
15638
|
+
const out = [];
|
|
15639
|
+
const lines2 = text.split("\n");
|
|
15640
|
+
for (const [i, raw] of lines2.entries()) {
|
|
15641
|
+
const line = raw.trim();
|
|
15642
|
+
if (!line) continue;
|
|
15643
|
+
let doc;
|
|
15644
|
+
try {
|
|
15645
|
+
doc = JSON.parse(line);
|
|
15646
|
+
} catch {
|
|
15647
|
+
refuse11(`${AUDIT_KEY} in ${location}, line ${i + 1}, is not JSON.`, "Check that terragucci audit wrote it; its record is one JSON object per line.");
|
|
15648
|
+
}
|
|
15649
|
+
const why = v.check(doc);
|
|
15650
|
+
if (why) {
|
|
15651
|
+
refuse11(
|
|
15652
|
+
`${AUDIT_KEY} in ${location}, line ${i + 1}, is not a terragucci ${AUDIT_SCHEMA} entry: ${why}.`,
|
|
15653
|
+
v.validation.by === "schema" ? `It was checked against @intentius/terragucci ${v.validation.terragucci}'s ${AUDIT_SCHEMA_FILE}. Check that this terragucci wrote it, or upgrade behold.` : "Install @intentius/terragucci beside behold to check it against terragucci's own JSON Schema, and check that terragucci wrote it."
|
|
15654
|
+
);
|
|
15655
|
+
}
|
|
15656
|
+
out.push(doc);
|
|
15657
|
+
}
|
|
15658
|
+
return out;
|
|
15659
|
+
}
|
|
15660
|
+
function ownRoots(e) {
|
|
15661
|
+
const roots = /* @__PURE__ */ new Set();
|
|
15662
|
+
const listed = e.detail?.roots;
|
|
15663
|
+
if (Array.isArray(listed)) {
|
|
15664
|
+
for (const r of listed) {
|
|
15665
|
+
if (str10(r)) roots.add(r);
|
|
15666
|
+
else if (isRecord15(r) && str10(r.root)) roots.add(r.root);
|
|
15667
|
+
else if (isRecord15(r) && str10(r.path)) roots.add(r.path);
|
|
15668
|
+
}
|
|
15669
|
+
}
|
|
15670
|
+
if (WHAT_IS_ROOT.has(e.kind)) roots.add(e.what);
|
|
15671
|
+
return [...roots];
|
|
15672
|
+
}
|
|
15673
|
+
function reportKey(e) {
|
|
15674
|
+
const k = e.evidence.key;
|
|
15675
|
+
if (e.evidence.source !== "report" || !str10(k) || !k.endsWith("/report.json")) return void 0;
|
|
15676
|
+
const inner = k.indexOf(`/${e.project}/`);
|
|
15677
|
+
const from = k.startsWith(`${e.project}/`) ? 0 : inner < 0 ? -1 : inner + 1;
|
|
15678
|
+
if (from < 0) return void 0;
|
|
15679
|
+
return `${k.slice(from, -"report.json".length)}report.html`;
|
|
15680
|
+
}
|
|
15681
|
+
var at = (iso) => Date.parse(iso);
|
|
15682
|
+
function timelineEntries(entries, project) {
|
|
15683
|
+
const mine = entries.map((e, i) => ({ e, i })).filter(({ e }) => e.project === project);
|
|
15684
|
+
const gate = (e) => `${e.what}\0${e.digest ?? ""}`;
|
|
15685
|
+
const gateRoots = /* @__PURE__ */ new Map();
|
|
15686
|
+
const gateReport = /* @__PURE__ */ new Map();
|
|
15687
|
+
for (const { e } of mine) {
|
|
15688
|
+
if (WHAT_IS_ROOT.has(e.kind)) continue;
|
|
15689
|
+
const roots = ownRoots(e);
|
|
15690
|
+
if (roots.length) {
|
|
15691
|
+
const s = gateRoots.get(gate(e)) ?? /* @__PURE__ */ new Set();
|
|
15692
|
+
for (const r of roots) s.add(r);
|
|
15693
|
+
gateRoots.set(gate(e), s);
|
|
15694
|
+
}
|
|
15695
|
+
const key = reportKey(e);
|
|
15696
|
+
const prev = gateReport.get(gate(e));
|
|
15697
|
+
if (key && (!prev || at(e.at) >= prev.at)) gateReport.set(gate(e), { at: at(e.at), key });
|
|
15698
|
+
}
|
|
15699
|
+
const out = mine.map(({ e, i }) => {
|
|
15700
|
+
const d = e.detail ?? {};
|
|
15701
|
+
let roots = ownRoots(e);
|
|
15702
|
+
if (!roots.length && (e.kind === "approval" || e.kind === "approval-revoked")) roots = [...gateRoots.get(gate(e)) ?? []];
|
|
15703
|
+
const waveNo = typeof d.wave === "number" ? d.wave : /^wave-(\d+)$/.exec(e.what)?.[1];
|
|
15704
|
+
const report = reportKey(e) ?? (WHAT_IS_ROOT.has(e.kind) ? void 0 : gateReport.get(gate(e))?.key);
|
|
15705
|
+
const pr = d.pull_request;
|
|
15706
|
+
const entry = {
|
|
15707
|
+
id: e.id,
|
|
15708
|
+
kind: e.kind,
|
|
15709
|
+
at: e.at,
|
|
15710
|
+
who: e.who,
|
|
15711
|
+
what: e.what,
|
|
15712
|
+
...waveNo !== void 0 ? { wave: Number(waveNo) } : {},
|
|
15713
|
+
digest: e.digest,
|
|
15714
|
+
result: e.result,
|
|
15715
|
+
roots,
|
|
15716
|
+
run: {
|
|
15717
|
+
...str10(d.commit) ? { commit: d.commit } : {},
|
|
15718
|
+
...str10(e.evidence.job_url) ? { job_url: e.evidence.job_url } : {},
|
|
15719
|
+
...str10(d.run_id) ? { run_id: d.run_id } : {},
|
|
15720
|
+
...str10(pr) || typeof pr === "number" ? { pull_request: String(pr) } : {},
|
|
15721
|
+
...report ? { report } : {}
|
|
15722
|
+
},
|
|
15723
|
+
evidence: e.evidence,
|
|
15724
|
+
...e.detail ? { detail: e.detail } : {}
|
|
15725
|
+
};
|
|
15726
|
+
return { entry, i };
|
|
15727
|
+
});
|
|
15728
|
+
out.sort((a, b) => at(b.entry.at) - at(a.entry.at) || b.i - a.i);
|
|
15729
|
+
return out.map((x) => x.entry);
|
|
15730
|
+
}
|
|
15731
|
+
async function readTimeline(source2, opts = {}) {
|
|
15732
|
+
const v = opts.validator ?? auditValidator();
|
|
15733
|
+
let text;
|
|
15734
|
+
try {
|
|
15735
|
+
text = await source2.read(AUDIT_KEY);
|
|
15736
|
+
} catch (e) {
|
|
15737
|
+
refuse11(
|
|
15738
|
+
`Could not read ${AUDIT_KEY} from ${source2.location}: ${e instanceof Error ? e.message : String(e)}`,
|
|
15739
|
+
source2.kind === "s3" ? "behold reads the bucket with your aws CLI or the environment's credentials: check GetObject on <prefix>/audit.jsonl." : "Check the source serves the reports prefix."
|
|
15740
|
+
);
|
|
15741
|
+
}
|
|
15742
|
+
const base = { source: source2.location, kind: source2.kind, validation: v.validation, read: (opts.now ?? /* @__PURE__ */ new Date()).toISOString() };
|
|
15743
|
+
if (text === void 0) return { ...base, present: false, project: opts.project ?? opts.checkoutProject ?? null, projects: [], lines: 0, entries: [] };
|
|
15744
|
+
const all = parseAudit(text, source2.location, v);
|
|
15745
|
+
const projects = [...new Set(all.map((e) => e.project))].sort();
|
|
15746
|
+
let project;
|
|
15747
|
+
if (opts.project) project = opts.project;
|
|
15748
|
+
else if (projects.length <= 1) project = projects[0] ?? opts.checkoutProject ?? null;
|
|
15749
|
+
else if (opts.checkoutProject && projects.includes(opts.checkoutProject)) project = opts.checkoutProject;
|
|
15750
|
+
else {
|
|
15751
|
+
project = refuse11(
|
|
15752
|
+
`${source2.location}/${AUDIT_KEY} holds ${projects.length} projects, and none is this checkout's${opts.checkoutProject ? ` (${opts.checkoutProject})` : ""}.`,
|
|
15753
|
+
`Name one with --terragucci-project: ${projects.join(", ")}.`
|
|
15754
|
+
);
|
|
15755
|
+
}
|
|
15756
|
+
return { ...base, present: true, project, projects, lines: all.length, entries: project ? timelineEntries(all, project) : [] };
|
|
15757
|
+
}
|
|
15758
|
+
var absentWords = (location) => `${location} has no ${AUDIT_KEY}: terragucci writes its audit record at the top of the reports prefix when the estate job runs \`terragucci audit\`. This says nothing about whether anything happened.`;
|
|
15759
|
+
function timelineAnswer(r, q = {}) {
|
|
15760
|
+
const limit = q.limit ?? TIMELINE_LIMIT;
|
|
15761
|
+
const matched = q.root ? r.entries.filter((e) => e.roots.includes(q.root)) : r.entries;
|
|
15762
|
+
return {
|
|
15763
|
+
source: r.source,
|
|
15764
|
+
kind: r.kind,
|
|
15765
|
+
project: r.project,
|
|
15766
|
+
validation: r.validation,
|
|
15767
|
+
read: r.read,
|
|
15768
|
+
record: { key: AUDIT_KEY, present: r.present },
|
|
15769
|
+
...r.present ? {} : { absent: absentWords(r.source) },
|
|
15770
|
+
files: "/api/terragucci/file?key=",
|
|
15771
|
+
...q.root ? { root: q.root } : {},
|
|
15772
|
+
total: matched.length,
|
|
15773
|
+
limit,
|
|
15774
|
+
entries: matched.slice(0, limit)
|
|
15775
|
+
};
|
|
15776
|
+
}
|
|
15777
|
+
function terragucciTimelineRoute(app, source2, opts = {}) {
|
|
15778
|
+
let cached;
|
|
15779
|
+
app.get("/api/terragucci/timeline", async (c) => {
|
|
15780
|
+
const q = new URL(c.req.url).searchParams;
|
|
15781
|
+
const rawLimit = q.get("limit");
|
|
15782
|
+
let limit;
|
|
15783
|
+
if (rawLimit !== null) {
|
|
15784
|
+
limit = Number(rawLimit);
|
|
15785
|
+
if (!Number.isInteger(limit) || limit < 1 || limit > TIMELINE_MAX) {
|
|
15786
|
+
return c.json({ error: `limit must be a whole number from 1 to ${TIMELINE_MAX}, not ${JSON.stringify(rawLimit)}`, code: "terragucci-timeline" }, 400);
|
|
15787
|
+
}
|
|
15788
|
+
}
|
|
15789
|
+
const root = q.get("root") || void 0;
|
|
15790
|
+
const checkout = opts.checkoutProject?.();
|
|
15791
|
+
const key = checkout ?? "";
|
|
15792
|
+
const now = Date.now();
|
|
15793
|
+
if (q.get("fresh") === "1" || !cached || cached.key !== key || now - cached.at >= TIMELINE_TTL_MS) {
|
|
15794
|
+
const p = readTimeline(source2, {
|
|
15795
|
+
...opts.project ? { project: opts.project } : {},
|
|
15796
|
+
...checkout ? { checkoutProject: checkout } : {},
|
|
15797
|
+
...opts.validator ? { validator: opts.validator() } : {},
|
|
15798
|
+
...opts.now ? { now: opts.now() } : {}
|
|
15799
|
+
});
|
|
15800
|
+
cached = { at: now, key, read: p };
|
|
15801
|
+
p.catch(() => cached = void 0);
|
|
15802
|
+
}
|
|
15803
|
+
let r;
|
|
15804
|
+
try {
|
|
15805
|
+
r = await cached.read;
|
|
15806
|
+
} catch (e) {
|
|
15807
|
+
if (e instanceof TerragucciReadError) return c.json(e.refusal, 422);
|
|
15808
|
+
throw e;
|
|
15809
|
+
}
|
|
15810
|
+
return c.json(timelineAnswer(r, { ...root ? { root } : {}, ...limit ? { limit } : {} }));
|
|
15811
|
+
});
|
|
15812
|
+
}
|
|
15813
|
+
|
|
15814
|
+
// src/terragucci-poll.ts
|
|
15815
|
+
import { createHash as createHash6 } from "node:crypto";
|
|
15816
|
+
import { stat } from "node:fs/promises";
|
|
15817
|
+
import { join as join30, resolve as resolve20 } from "node:path";
|
|
15818
|
+
import { streamSSE } from "hono/streaming";
|
|
15819
|
+
|
|
15820
|
+
// src/terragucci-lifecycle.ts
|
|
15821
|
+
init_scratch();
|
|
15822
|
+
import { execFile as execFile3 } from "node:child_process";
|
|
15823
|
+
import { createHash as createHash5 } from "node:crypto";
|
|
15824
|
+
import { existsSync as existsSync18, mkdirSync as mkdirSync6, readdirSync as readdirSync10 } from "node:fs";
|
|
15825
|
+
import { tmpdir as tmpdir3 } from "node:os";
|
|
15826
|
+
import { basename as basename3, isAbsolute as isAbsolute4, join as join29, resolve as resolve19 } from "node:path";
|
|
15827
|
+
var LIFECYCLE_BRANCH = "chant/lifecycle";
|
|
15828
|
+
var LEDGER_PATH = "_gates/tf-apply.jsonl";
|
|
15829
|
+
var APPLIED_PATH = "_gates/tf-apply/applied.jsonl";
|
|
15830
|
+
var WAVE_RECORDS_DIR = "_gates/tf-apply";
|
|
15831
|
+
var LOCKS_PATH = "_locks/tf-apply.json";
|
|
15832
|
+
var CHECKOUT_REF = `refs/remotes/origin/${LIFECYCLE_BRANCH}`;
|
|
15833
|
+
var CACHE_REF = `refs/heads/${LIFECYCLE_BRANCH}`;
|
|
15834
|
+
var CACHE_PREFIX = "behold-lifecycle-";
|
|
15835
|
+
var lines = (text) => text.split("\n").map((l) => l.trim()).filter(Boolean);
|
|
15836
|
+
var json = (line) => {
|
|
15837
|
+
try {
|
|
15838
|
+
const r = JSON.parse(line);
|
|
15839
|
+
return r && typeof r === "object" && !Array.isArray(r) ? r : void 0;
|
|
15840
|
+
} catch {
|
|
15841
|
+
return void 0;
|
|
15842
|
+
}
|
|
15843
|
+
};
|
|
15844
|
+
function parseLedger(text) {
|
|
15845
|
+
const out = { pending: [], resolutions: [] };
|
|
15846
|
+
for (const line of lines(text)) {
|
|
15847
|
+
const r = json(line);
|
|
15848
|
+
if (!r) continue;
|
|
15849
|
+
if (r.version !== 1 || typeof r.op !== "string" || typeof r.gate !== "string" || typeof r.timestamp !== "string") continue;
|
|
15850
|
+
if (r.planDigest !== void 0 && typeof r.planDigest !== "string") continue;
|
|
15851
|
+
if (r.kind === "pending") {
|
|
15852
|
+
if (typeof r.expiresAt === "string") out.pending.push(r);
|
|
15853
|
+
} else if (typeof r.resolvedBy === "string") {
|
|
15854
|
+
out.resolutions.push(r);
|
|
15855
|
+
}
|
|
15856
|
+
}
|
|
15857
|
+
return out;
|
|
15858
|
+
}
|
|
15859
|
+
function parseApplied(text) {
|
|
15860
|
+
const out = { applied: [], finished: [] };
|
|
15861
|
+
for (const line of lines(text)) {
|
|
15862
|
+
const r = json(line);
|
|
15863
|
+
if (!r || r.version !== 1 || typeof r.gate !== "string" || typeof r.planDigest !== "string") continue;
|
|
15864
|
+
if (r.kind === "applied" && typeof r.approvedAt === "string") out.applied.push(r);
|
|
15865
|
+
else if (r.kind === "finished" && typeof r.applied === "string" && (r.result === "applied" || r.result === "failed")) out.finished.push(r);
|
|
15866
|
+
}
|
|
15867
|
+
return out;
|
|
15868
|
+
}
|
|
15869
|
+
function parseLocks(text) {
|
|
15870
|
+
const doc = json(text.trim());
|
|
15871
|
+
const locks = {};
|
|
15872
|
+
if (doc?.version !== 1 || !doc.locks || typeof doc.locks !== "object" || Array.isArray(doc.locks)) return locks;
|
|
15873
|
+
for (const [root, l] of Object.entries(doc.locks)) {
|
|
15874
|
+
if (Number.isInteger(l?.pr) && typeof l.by === "string" && typeof l.at === "string" && typeof l.head === "string")
|
|
15875
|
+
locks[root] = { pr: l.pr, by: l.by, at: l.at, head: l.head, ...l.via === "lock" ? { via: "lock" } : {}, ...l.stage === "plan" ? { stage: "plan" } : {} };
|
|
15876
|
+
}
|
|
15877
|
+
return locks;
|
|
15878
|
+
}
|
|
15879
|
+
var DIGEST = /^(?:jcs1-)?sha256:([0-9a-f]{64})$/;
|
|
15880
|
+
function samePlanDigest(a, b) {
|
|
15881
|
+
if (a === b) return true;
|
|
15882
|
+
if (a === void 0 || b === void 0) return false;
|
|
15883
|
+
const x = DIGEST.exec(a);
|
|
15884
|
+
const y = DIGEST.exec(b);
|
|
15885
|
+
return x !== null && y !== null && x[1] === y[1];
|
|
15886
|
+
}
|
|
15887
|
+
var t = (iso) => new Date(iso).getTime();
|
|
15888
|
+
var newest = (xs, at2) => xs.reduce((b, x) => !b || t(at2(x)) >= t(at2(b)) ? x : b, void 0);
|
|
15889
|
+
function gateEntries(ledger, records, source2, now) {
|
|
15890
|
+
const gates = /* @__PURE__ */ new Set();
|
|
15891
|
+
for (const r of [...ledger.pending, ...ledger.resolutions, ...ledger.applied]) if (/^wave-\d+$/.test(r.gate)) gates.add(r.gate);
|
|
15892
|
+
const out = [];
|
|
15893
|
+
for (const gate of gates) {
|
|
15894
|
+
const wave = Number(gate.slice("wave-".length));
|
|
15895
|
+
const pending = newest(ledger.pending.filter((p) => p.gate === gate), (p) => p.timestamp);
|
|
15896
|
+
const since = pending ? t(pending.timestamp) : 0;
|
|
15897
|
+
const resolutions = ledger.resolutions.filter((r) => r.gate === gate && t(r.timestamp) >= since);
|
|
15898
|
+
const approval = pending ? newest(resolutions.filter((r) => samePlanDigest(r.planDigest, pending.planDigest)), (r) => r.timestamp) : newest(resolutions, (r) => r.timestamp);
|
|
15899
|
+
const digest = pending?.planDigest ?? approval?.planDigest;
|
|
15900
|
+
const applied = approval ? newest(ledger.applied.filter((a) => a.gate === gate && samePlanDigest(a.planDigest, approval.planDigest) && t(a.approvedAt) >= t(approval.timestamp)), (a) => a.timestamp) : !pending ? newest(ledger.applied.filter((a) => a.gate === gate), (a) => a.timestamp) : void 0;
|
|
15901
|
+
const finished = applied ? newest(ledger.finished.filter((f) => f.gate === gate && samePlanDigest(f.planDigest, applied.planDigest) && f.applied === applied.timestamp), (f) => f.timestamp) : void 0;
|
|
15902
|
+
const state = applied ? finished?.result === "failed" ? "failed" : "applied" : approval ? "approved" : pending && t(pending.expiresAt) <= now.getTime() ? "expired" : "waiting";
|
|
15903
|
+
out.push({
|
|
15904
|
+
gate,
|
|
15905
|
+
wave,
|
|
15906
|
+
state,
|
|
15907
|
+
...digest ? { digest } : {},
|
|
15908
|
+
...pending ? {
|
|
15909
|
+
pending: {
|
|
15910
|
+
timestamp: pending.timestamp,
|
|
15911
|
+
expiresAt: pending.expiresAt,
|
|
15912
|
+
...pending.planDigest ? { planDigest: pending.planDigest } : {},
|
|
15913
|
+
...pending.commit ? { commit: pending.commit } : {},
|
|
15914
|
+
...pending.runId ? { runId: pending.runId } : {},
|
|
15915
|
+
...pending.url ? { url: pending.url } : {},
|
|
15916
|
+
...pending.description ? { description: pending.description } : {}
|
|
15917
|
+
}
|
|
15918
|
+
} : {},
|
|
15919
|
+
...approval ? {
|
|
15920
|
+
approval: {
|
|
15921
|
+
by: approval.resolvedBy,
|
|
15922
|
+
at: approval.timestamp,
|
|
15923
|
+
...approval.planDigest ? { planDigest: approval.planDigest } : {},
|
|
15924
|
+
signed: !!approval.seal,
|
|
15925
|
+
...approval.via ? { via: approval.via } : {},
|
|
15926
|
+
...approval.pr !== void 0 ? { pr: approval.pr } : {},
|
|
15927
|
+
...approval.note ? { note: approval.note } : {}
|
|
15928
|
+
}
|
|
15929
|
+
} : {},
|
|
15930
|
+
...applied ? {
|
|
15931
|
+
applied: {
|
|
15932
|
+
at: applied.timestamp,
|
|
15933
|
+
approvedAt: applied.approvedAt,
|
|
15934
|
+
approvedBy: applied.approvedBy,
|
|
15935
|
+
...applied.runId ? { runId: applied.runId } : {},
|
|
15936
|
+
...applied.commit ? { commit: applied.commit } : {},
|
|
15937
|
+
...finished ? { result: finished.result, finished: finished.timestamp } : {}
|
|
15938
|
+
}
|
|
15939
|
+
} : {},
|
|
15940
|
+
records: records.filter((r) => r.wave === wave).map(({ digest: d, path }) => ({ digest: d, path })),
|
|
15941
|
+
...state === "waiting" ? { command: `npx terragucci approve ${gate}${digest ? ` --plan ${digest}` : ""}` } : {},
|
|
15942
|
+
source: source2
|
|
15943
|
+
});
|
|
15944
|
+
}
|
|
15945
|
+
return out.sort((a, b) => a.wave - b.wave);
|
|
15946
|
+
}
|
|
15947
|
+
function waveRecords(paths) {
|
|
15948
|
+
const out = [];
|
|
15949
|
+
for (const path of paths) {
|
|
15950
|
+
const m = /^_gates\/tf-apply\/wave-(\d+)\/([^/]+)\.json$/.exec(path);
|
|
15951
|
+
if (m) out.push({ wave: Number(m[1]), digest: m[2].replace("_", ":"), path });
|
|
15952
|
+
}
|
|
15953
|
+
return out;
|
|
15954
|
+
}
|
|
15955
|
+
var realGit = (args, o = {}) => new Promise((res) => {
|
|
15956
|
+
execFile3(
|
|
15957
|
+
process.env.GIT_BIN || "git",
|
|
15958
|
+
args,
|
|
15959
|
+
{ encoding: "utf8", maxBuffer: 64 * 1024 * 1024, timeout: o.timeoutMs ?? 6e4, env: { ...process.env, GIT_TERMINAL_PROMPT: "0", GIT_OPTIONAL_LOCKS: "0" } },
|
|
15960
|
+
(err, stdout, stderr) => {
|
|
15961
|
+
const code = err ? typeof err.code === "number" ? err.code : -1 : 0;
|
|
15962
|
+
res({ code, stdout: String(stdout ?? ""), stderr: String(stderr ?? "") || (err ? err.message : "") });
|
|
15963
|
+
}
|
|
15964
|
+
);
|
|
15965
|
+
});
|
|
15966
|
+
var redact = (s) => s.replace(/(\b[a-z][a-z0-9+.-]*:\/\/)[^/@\s]+@/gi, "$1");
|
|
15967
|
+
function cacheDir(origin, root = tmpdir3()) {
|
|
15968
|
+
return join29(root, `${CACHE_PREFIX}${createHash5("sha256").update(origin).digest("hex").slice(0, 16)}`);
|
|
15969
|
+
}
|
|
15970
|
+
var inflight = /* @__PURE__ */ new Map();
|
|
15971
|
+
function serially(key, fn) {
|
|
15972
|
+
const prev = inflight.get(key) ?? Promise.resolve();
|
|
15973
|
+
const next = prev.catch(() => void 0).then(fn);
|
|
15974
|
+
inflight.set(key, next);
|
|
15975
|
+
void next.finally(() => {
|
|
15976
|
+
if (inflight.get(key) === next) inflight.delete(key);
|
|
15977
|
+
}).catch(() => void 0);
|
|
15978
|
+
return next;
|
|
15979
|
+
}
|
|
15980
|
+
function fetchable(url, checkout) {
|
|
15981
|
+
if (/^[a-z][a-z0-9+.-]*:\/\//i.test(url) || /^[^/]+@[^:/]+:/.test(url) || isAbsolute4(url)) return url;
|
|
15982
|
+
return resolve19(checkout, url);
|
|
15983
|
+
}
|
|
15984
|
+
async function fetchIntoCache(git, origin, cache5) {
|
|
15985
|
+
assertScratch(basename3(cache5));
|
|
15986
|
+
return serially(cache5, async () => {
|
|
15987
|
+
if (!existsSync18(join29(cache5, "HEAD"))) {
|
|
15988
|
+
if (existsSync18(cache5) && readdirSync10(cache5).length > 0) return { ok: false, error: `${cache5} exists and is not behold's lifecycle cache; behold leaves it alone` };
|
|
15989
|
+
mkdirSync6(cache5, { recursive: true });
|
|
15990
|
+
const init = await git(["init", "--bare", "-q", cache5]);
|
|
15991
|
+
if (init.code !== 0) return { ok: false, error: `could not make the cache ${cache5}: ${init.stderr.trim()}` };
|
|
15992
|
+
}
|
|
15993
|
+
const f = await git(["--git-dir", cache5, "fetch", "-q", "--no-tags", origin, `+refs/heads/${LIFECYCLE_BRANCH}:${CACHE_REF}`]);
|
|
15994
|
+
if (f.code === 0) return { ok: true, found: true };
|
|
15995
|
+
if (/couldn't find remote ref|could not find remote ref/i.test(f.stderr)) return { ok: true, found: false };
|
|
15996
|
+
return { ok: false, error: redact(f.stderr.trim() || `git fetch exited ${f.code}`) };
|
|
15997
|
+
});
|
|
15998
|
+
}
|
|
15999
|
+
async function readAt(git, repo, ref2, now) {
|
|
16000
|
+
const tip = await git([...repo, "log", "-1", "--format=%H%x00%cI", ref2, "--"]);
|
|
16001
|
+
if (tip.code !== 0 || !tip.stdout.trim()) return void 0;
|
|
16002
|
+
const [sha, committed] = tip.stdout.trim().split("\0");
|
|
16003
|
+
const show = async (path) => {
|
|
16004
|
+
const r = await git([...repo, "show", `${sha}:${path}`]);
|
|
16005
|
+
return r.code === 0 ? r.stdout : "";
|
|
16006
|
+
};
|
|
16007
|
+
const changed = async (...paths) => {
|
|
16008
|
+
const r = await git([...repo, "log", "-1", "--format=%H%x00%cI", sha, "--", ...paths]);
|
|
16009
|
+
const [c, at2] = r.code === 0 && r.stdout.trim() ? r.stdout.trim().split("\0") : [sha, committed];
|
|
16010
|
+
return { branch: LIFECYCLE_BRANCH, commit: c, committed: at2, path: paths.join(" ") };
|
|
16011
|
+
};
|
|
16012
|
+
const [ledgerText, appliedText, locksText, tree] = await Promise.all([
|
|
16013
|
+
show(LEDGER_PATH),
|
|
16014
|
+
show(APPLIED_PATH),
|
|
16015
|
+
show(LOCKS_PATH),
|
|
16016
|
+
git([...repo, "ls-tree", "-r", "--name-only", sha, "--", `${WAVE_RECORDS_DIR}/`])
|
|
16017
|
+
]);
|
|
16018
|
+
const ledger = { ...parseLedger(ledgerText), ...parseApplied(appliedText) };
|
|
16019
|
+
const records = waveRecords(tree.code === 0 ? lines(tree.stdout) : []);
|
|
16020
|
+
const gates = ledger.pending.length || ledger.resolutions.length || ledger.applied.length ? gateEntries(ledger, records, await changed(LEDGER_PATH, APPLIED_PATH), now) : [];
|
|
16021
|
+
const lockMap = parseLocks(locksText);
|
|
16022
|
+
const lockSource = Object.keys(lockMap).length ? await changed(LOCKS_PATH) : void 0;
|
|
16023
|
+
const locks = Object.entries(lockMap).sort(([a], [b]) => a < b ? -1 : 1).map(([root, l]) => ({ root, ...l, source: lockSource }));
|
|
16024
|
+
return { commit: { sha, committed }, gates, locks };
|
|
16025
|
+
}
|
|
16026
|
+
async function readLifecycle(checkout, opts = {}) {
|
|
16027
|
+
const git = opts.git ?? realGit;
|
|
16028
|
+
const now = (opts.now ?? (() => /* @__PURE__ */ new Date()))();
|
|
16029
|
+
const base = { branch: LIFECYCLE_BRANCH, read: { at: now.toISOString() } };
|
|
16030
|
+
const empty = { gates: [], locks: [] };
|
|
16031
|
+
const remote = await git(["-C", checkout, "remote", "get-url", "origin"]);
|
|
16032
|
+
const url = remote.code === 0 ? remote.stdout.trim() : "";
|
|
16033
|
+
let fetchError;
|
|
16034
|
+
let origin;
|
|
16035
|
+
if (!url) {
|
|
16036
|
+
fetchError = "the checkout has no origin remote";
|
|
16037
|
+
} else {
|
|
16038
|
+
const from = fetchable(url, checkout);
|
|
16039
|
+
origin = redact(from);
|
|
16040
|
+
const cache5 = cacheDir(from, opts.cacheRoot);
|
|
16041
|
+
let f;
|
|
16042
|
+
try {
|
|
16043
|
+
f = await fetchIntoCache(git, from, cache5);
|
|
16044
|
+
} catch (e) {
|
|
16045
|
+
f = { ok: false, error: e instanceof Error ? e.message : String(e) };
|
|
16046
|
+
}
|
|
16047
|
+
if (f.ok && !f.found) {
|
|
16048
|
+
return { ...base, found: false, via: "fetch", origin, fetch: { ok: true }, ...empty, note: `${origin} has no ${LIFECYCLE_BRANCH} branch: no wave has asked for an approval and no root is locked yet.` };
|
|
16049
|
+
}
|
|
16050
|
+
if (f.ok) {
|
|
16051
|
+
const lane2 = await readAt(git, ["--git-dir", cache5], CACHE_REF, now);
|
|
16052
|
+
if (lane2) return { ...base, found: true, via: "fetch", origin, fetch: { ok: true }, ...lane2, note: `${LIFECYCLE_BRANCH} as fetched from ${origin} at ${base.read.at}.` };
|
|
16053
|
+
fetchError = `fetched ${LIFECYCLE_BRANCH}, but behold's cache could not read it`;
|
|
16054
|
+
} else {
|
|
16055
|
+
fetchError = f.error;
|
|
16056
|
+
}
|
|
16057
|
+
}
|
|
16058
|
+
const lane = await readAt(git, ["-C", checkout], CHECKOUT_REF, now);
|
|
16059
|
+
if (lane) {
|
|
16060
|
+
return {
|
|
16061
|
+
...base,
|
|
16062
|
+
found: true,
|
|
16063
|
+
via: "checkout-ref",
|
|
16064
|
+
...origin ? { origin } : {},
|
|
16065
|
+
fetch: { ok: false, error: fetchError },
|
|
16066
|
+
...lane,
|
|
16067
|
+
note: `behold could not fetch ${LIFECYCLE_BRANCH} (${fetchError}), so this is the checkout's ${CHECKOUT_REF}, as of its last git fetch; its tip was committed ${lane.commit.committed}.`
|
|
16068
|
+
};
|
|
16069
|
+
}
|
|
16070
|
+
return {
|
|
16071
|
+
...base,
|
|
16072
|
+
found: false,
|
|
16073
|
+
via: "none",
|
|
16074
|
+
...origin ? { origin } : {},
|
|
16075
|
+
fetch: { ok: false, error: fetchError },
|
|
16076
|
+
...empty,
|
|
16077
|
+
note: `behold could not fetch ${LIFECYCLE_BRANCH} (${fetchError}) and the checkout has no ${CHECKOUT_REF}, so the gates and locks are unknown.`
|
|
16078
|
+
};
|
|
16079
|
+
}
|
|
16080
|
+
|
|
16081
|
+
// src/terragucci-poll.ts
|
|
16082
|
+
var TERRAGUCCI_POLL_SECS = 30;
|
|
16083
|
+
var INDEX_KEY = "index.json";
|
|
16084
|
+
var trim2 = (s) => s.replace(/^\/+|\/+$/g, "");
|
|
16085
|
+
var joinKey2 = (...p) => p.map(trim2).filter(Boolean).join("/");
|
|
16086
|
+
var verdict2 = (prev, tag) => ({ ...tag ? { tag } : {}, changed: prev !== void 0 && tag !== prev });
|
|
16087
|
+
function indexProbe(spec2, opts = {}) {
|
|
16088
|
+
const name = opts.key ?? INDEX_KEY;
|
|
16089
|
+
const s3 = /^s3:\/\/([^/]+)\/?(.*)$/.exec(spec2);
|
|
16090
|
+
if (s3) {
|
|
16091
|
+
const bucket = s3[1];
|
|
16092
|
+
const key = joinKey2(s3[2] ?? "", name);
|
|
16093
|
+
const env = opts.env ?? process.env;
|
|
16094
|
+
if (!opts.aws && hasEnvCredentials(env)) {
|
|
16095
|
+
const client = new S3Object(s3FromEnv(bucket, env), opts.fetch);
|
|
16096
|
+
return async (prev) => {
|
|
16097
|
+
const h = await client.head(key, prev);
|
|
16098
|
+
if (!h) return verdict2(prev, void 0);
|
|
16099
|
+
return h.same ? { tag: prev, changed: false } : verdict2(prev, h.etag);
|
|
16100
|
+
};
|
|
16101
|
+
}
|
|
16102
|
+
const aws = opts.aws ?? realAws;
|
|
16103
|
+
return async (prev) => {
|
|
16104
|
+
const r = await aws(["s3api", "head-object", "--bucket", bucket, "--key", key, ...prev ? ["--if-none-match", prev] : [], "--output", "json"]);
|
|
16105
|
+
if (r.code === 0) {
|
|
16106
|
+
let etag;
|
|
16107
|
+
try {
|
|
16108
|
+
etag = JSON.parse(r.stdout).ETag;
|
|
16109
|
+
} catch {
|
|
16110
|
+
etag = void 0;
|
|
16111
|
+
}
|
|
16112
|
+
return verdict2(prev, etag);
|
|
16113
|
+
}
|
|
16114
|
+
if (/\(304\)|Not Modified/i.test(r.stderr)) return { tag: prev, changed: false };
|
|
16115
|
+
if (/\(404\)|Not Found|NoSuchKey/i.test(r.stderr)) return verdict2(prev, void 0);
|
|
16116
|
+
throw new Error(`aws s3api head-object s3://${bucket}/${key} failed: ${r.stderr.trim() || `exit ${r.code}`}`);
|
|
16117
|
+
};
|
|
16118
|
+
}
|
|
16119
|
+
if (/^https?:\/\//.test(spec2)) {
|
|
16120
|
+
const url = `${spec2.replace(/\/+$/, "")}/${name}`;
|
|
16121
|
+
const get = opts.fetch ?? fetch;
|
|
16122
|
+
return async (prev) => {
|
|
16123
|
+
const res = await get(url, { headers: prev ? { "if-none-match": prev } : {} });
|
|
16124
|
+
if (res.status === 304) return { tag: prev, changed: false };
|
|
16125
|
+
if (res.status === 404 || res.status === 403) return verdict2(prev, void 0);
|
|
16126
|
+
if (!res.ok) throw new Error(`GET ${url} answered ${res.status}`);
|
|
16127
|
+
const etag = res.headers.get("etag") ?? `sha256:${createHash6("sha256").update(await res.text()).digest("hex")}`;
|
|
16128
|
+
return verdict2(prev, etag);
|
|
16129
|
+
};
|
|
16130
|
+
}
|
|
16131
|
+
const file = join30(resolve20(opts.cwd ?? process.cwd(), spec2), name);
|
|
16132
|
+
return async (prev) => {
|
|
16133
|
+
try {
|
|
16134
|
+
const s = await stat(file);
|
|
16135
|
+
return verdict2(prev, `mtime:${s.mtimeMs}`);
|
|
16136
|
+
} catch (e) {
|
|
16137
|
+
if (e.code === "ENOENT") return verdict2(prev, void 0);
|
|
16138
|
+
throw e;
|
|
16139
|
+
}
|
|
16140
|
+
};
|
|
16141
|
+
}
|
|
16142
|
+
function lifecycleKey(a) {
|
|
16143
|
+
const { read: _read, note: _note, ...rest } = a;
|
|
16144
|
+
return JSON.stringify(rest);
|
|
16145
|
+
}
|
|
16146
|
+
var TerragucciPoller = class {
|
|
16147
|
+
constructor(o) {
|
|
16148
|
+
this.o = o;
|
|
16149
|
+
}
|
|
16150
|
+
o;
|
|
16151
|
+
listeners = /* @__PURE__ */ new Set();
|
|
16152
|
+
timer;
|
|
16153
|
+
running;
|
|
16154
|
+
indexTag;
|
|
16155
|
+
lastKey;
|
|
16156
|
+
/** Bumped on every start and stop, so a tick that outlives a stop schedules nothing. */
|
|
16157
|
+
generation = 0;
|
|
16158
|
+
get active() {
|
|
16159
|
+
return this.listeners.size > 0;
|
|
16160
|
+
}
|
|
16161
|
+
subscribe(fn) {
|
|
16162
|
+
this.listeners.add(fn);
|
|
16163
|
+
if (this.listeners.size === 1) this.start();
|
|
16164
|
+
return () => {
|
|
16165
|
+
if (!this.listeners.delete(fn)) return;
|
|
16166
|
+
if (this.listeners.size === 0) this.stop();
|
|
16167
|
+
};
|
|
16168
|
+
}
|
|
16169
|
+
start() {
|
|
16170
|
+
this.generation++;
|
|
16171
|
+
this.indexTag = void 0;
|
|
16172
|
+
const last = this.o.lastLifecycle?.();
|
|
16173
|
+
this.lastKey = last ? lifecycleKey(last) : void 0;
|
|
16174
|
+
this.schedule(0);
|
|
16175
|
+
}
|
|
16176
|
+
stop() {
|
|
16177
|
+
this.generation++;
|
|
16178
|
+
clearTimeout(this.timer);
|
|
16179
|
+
this.timer = void 0;
|
|
16180
|
+
}
|
|
16181
|
+
schedule(ms) {
|
|
16182
|
+
const gen = this.generation;
|
|
16183
|
+
clearTimeout(this.timer);
|
|
16184
|
+
this.timer = setTimeout(() => {
|
|
16185
|
+
if (gen !== this.generation) return;
|
|
16186
|
+
this.running = this.tick().finally(() => {
|
|
16187
|
+
this.running = void 0;
|
|
16188
|
+
if (gen === this.generation && this.active) this.schedule(this.o.intervalMs);
|
|
16189
|
+
});
|
|
16190
|
+
}, ms);
|
|
16191
|
+
}
|
|
16192
|
+
send(e) {
|
|
16193
|
+
for (const fn of [...this.listeners]) fn(e);
|
|
16194
|
+
}
|
|
16195
|
+
/** One round: the index, then the lane. Exposed for a test; the timer calls it. */
|
|
16196
|
+
async tick() {
|
|
16197
|
+
const now = () => (this.o.now ?? (() => /* @__PURE__ */ new Date()))().toISOString();
|
|
16198
|
+
const reports = {};
|
|
16199
|
+
try {
|
|
16200
|
+
const p = await this.o.probe(this.indexTag);
|
|
16201
|
+
if (p.tag) reports.tag = p.tag;
|
|
16202
|
+
if (p.changed) {
|
|
16203
|
+
this.o.onReportsChanged?.();
|
|
16204
|
+
this.send({ type: "reports", data: { at: now(), ...p.tag ? { tag: p.tag } : {} } });
|
|
16205
|
+
}
|
|
16206
|
+
this.indexTag = p.tag;
|
|
16207
|
+
} catch (e) {
|
|
16208
|
+
reports.error = e instanceof Error ? e.message : String(e);
|
|
16209
|
+
}
|
|
16210
|
+
let progress;
|
|
16211
|
+
if (this.o.progress) {
|
|
16212
|
+
try {
|
|
16213
|
+
const { answer, ...asked } = await this.o.progress();
|
|
16214
|
+
progress = asked;
|
|
16215
|
+
if (answer !== void 0) this.send({ type: "progress", data: answer });
|
|
16216
|
+
} catch (e) {
|
|
16217
|
+
progress = { asked: true, error: e instanceof Error ? e.message : String(e) };
|
|
16218
|
+
}
|
|
16219
|
+
}
|
|
16220
|
+
const lifecycle = {};
|
|
16221
|
+
try {
|
|
16222
|
+
const a = await this.o.lifecycle();
|
|
16223
|
+
this.o.onLifecycle?.(a);
|
|
16224
|
+
if (a.commit) lifecycle.commit = a.commit.sha;
|
|
16225
|
+
lifecycle.via = a.via;
|
|
16226
|
+
const key = lifecycleKey(a);
|
|
16227
|
+
if (key !== this.lastKey) {
|
|
16228
|
+
this.lastKey = key;
|
|
16229
|
+
this.send({ type: "lifecycle", data: a });
|
|
16230
|
+
}
|
|
16231
|
+
} catch (e) {
|
|
16232
|
+
lifecycle.error = e instanceof Error ? e.message : String(e);
|
|
16233
|
+
}
|
|
16234
|
+
this.send({ type: "polled", data: { at: now(), reports, lifecycle, ...progress ? { progress } : {} } });
|
|
16235
|
+
}
|
|
16236
|
+
/** For a test: wait for a tick in flight. */
|
|
16237
|
+
async settled() {
|
|
16238
|
+
await this.running;
|
|
16239
|
+
}
|
|
16240
|
+
};
|
|
16241
|
+
var LIFECYCLE_READ_TTL_MS = 3e4;
|
|
16242
|
+
function terragucciLaneRoutes(app, o, base, forget) {
|
|
16243
|
+
let cached;
|
|
16244
|
+
let last;
|
|
16245
|
+
const fresh = () => {
|
|
16246
|
+
const b = base();
|
|
16247
|
+
const p = o.readLane ? o.readLane(b) : readLifecycle(b, { ...o.now ? { now: o.now } : {}, ...o.lifecycle });
|
|
16248
|
+
cached = { at: Date.now(), base: b, read: p };
|
|
16249
|
+
p.then((a) => last = a).catch(() => cached = void 0);
|
|
16250
|
+
return p;
|
|
16251
|
+
};
|
|
16252
|
+
const read = (force) => {
|
|
16253
|
+
const b = base();
|
|
16254
|
+
if (!force && cached && cached.base === b && Date.now() - cached.at < LIFECYCLE_READ_TTL_MS) return cached.read;
|
|
16255
|
+
return fresh();
|
|
16256
|
+
};
|
|
16257
|
+
const pollSecs = o.pollSecs ?? TERRAGUCCI_POLL_SECS;
|
|
16258
|
+
const poller = new TerragucciPoller({
|
|
16259
|
+
intervalMs: pollSecs * 1e3,
|
|
16260
|
+
probe: o.probe ?? indexProbe(o.source, { ...o.aws ? { aws: o.aws } : {}, ...o.fetch ? { fetch: o.fetch } : {} }),
|
|
16261
|
+
lifecycle: fresh,
|
|
16262
|
+
lastLifecycle: () => last,
|
|
16263
|
+
onReportsChanged: forget,
|
|
16264
|
+
...o.progress ? { progress: o.progress } : {},
|
|
16265
|
+
...o.now ? { now: o.now } : {}
|
|
16266
|
+
});
|
|
16267
|
+
app.get("/api/terragucci/lifecycle", async (c) => c.json(await read(new URL(c.req.url).searchParams.get("fresh") === "1")));
|
|
16268
|
+
app.get(
|
|
16269
|
+
"/api/terragucci/events",
|
|
16270
|
+
(c) => streamSSE(c, async (stream) => {
|
|
16271
|
+
await stream.writeSSE({ event: "hello", data: JSON.stringify({ pollSecs }) });
|
|
16272
|
+
const unsubscribe = pollSecs > 0 ? poller.subscribe((e) => void stream.writeSSE({ event: e.type, data: JSON.stringify(e.data) }).catch(() => void 0)) : () => {
|
|
16273
|
+
};
|
|
16274
|
+
stream.onAbort(unsubscribe);
|
|
16275
|
+
while (!stream.aborted) {
|
|
16276
|
+
await stream.writeSSE({ event: "ping", data: "" });
|
|
16277
|
+
await stream.sleep(3e4);
|
|
16278
|
+
}
|
|
16279
|
+
unsubscribe();
|
|
16280
|
+
})
|
|
16281
|
+
);
|
|
16282
|
+
return poller;
|
|
16283
|
+
}
|
|
16284
|
+
|
|
16285
|
+
// src/terragucci-progress.ts
|
|
16286
|
+
import { readFileSync as readFileSync21, statSync as statSync10 } from "node:fs";
|
|
16287
|
+
import { createRequire as createRequire5 } from "node:module";
|
|
16288
|
+
import { dirname as dirname9, join as join31, resolve as resolve21 } from "node:path";
|
|
16289
|
+
import { fileURLToPath as fileURLToPath6 } from "node:url";
|
|
16290
|
+
import { Ajv2020 as Ajv20203 } from "ajv/dist/2020.js";
|
|
16291
|
+
var RUN_SCHEMA = "terragucci.run/v1";
|
|
16292
|
+
var RUN_SCHEMA_FILE = "run.schema.json";
|
|
16293
|
+
var PROGRESS_TTL_MS = 3e4;
|
|
16294
|
+
var PROGRESS_STATUSES = ["done", "in-flight", "waiting", "not-applied"];
|
|
16295
|
+
var isRecord16 = (v) => v !== null && typeof v === "object" && !Array.isArray(v);
|
|
16296
|
+
var str11 = (v) => typeof v === "string";
|
|
16297
|
+
function structuralRun(doc) {
|
|
16298
|
+
if (!isRecord16(doc)) return "it is not a JSON object";
|
|
16299
|
+
if (doc.schema !== RUN_SCHEMA) return `its schema is ${JSON.stringify(doc.schema)}, not ${RUN_SCHEMA}`;
|
|
16300
|
+
for (const f of ["project", "commit", "updated"]) if (!str11(doc[f])) return `its ${f} is not a string`;
|
|
16301
|
+
if (!Array.isArray(doc.waves)) return "it has no waves array";
|
|
16302
|
+
for (const [i, w] of doc.waves.entries()) {
|
|
16303
|
+
if (!isRecord16(w) || typeof w.number !== "number" || !str11(w.state) || !str11(w.gate) || !Array.isArray(w.roots)) return `wave ${i} lacks number, state, gate or roots`;
|
|
16304
|
+
if (w.progress === void 0) continue;
|
|
16305
|
+
const p = w.progress;
|
|
16306
|
+
if (!isRecord16(p) || !str11(p.read) || !Array.isArray(p.resources)) return `wave ${String(w.number)}'s progress lacks read or resources`;
|
|
16307
|
+
for (const r of p.resources) {
|
|
16308
|
+
if (!isRecord16(r) || !str11(r.root) || !str11(r.address) || !str11(r.action)) return `a resource of wave ${String(w.number)}'s progress lacks root, address or action`;
|
|
16309
|
+
if (!PROGRESS_STATUSES.includes(r.status)) return `a resource of wave ${String(w.number)}'s progress has status ${JSON.stringify(r.status)}`;
|
|
16310
|
+
if (r.done_at !== void 0 && !str11(r.done_at)) return `a resource of wave ${String(w.number)}'s progress has a done_at that is not a string`;
|
|
16311
|
+
}
|
|
16312
|
+
}
|
|
16313
|
+
return void 0;
|
|
16314
|
+
}
|
|
16315
|
+
function compileRunSchema(schema, version) {
|
|
16316
|
+
const v = new Ajv20203({ strict: false, allErrors: false }).compile(schema);
|
|
16317
|
+
return {
|
|
16318
|
+
validation: { by: "schema", terragucci: version },
|
|
16319
|
+
check(doc) {
|
|
16320
|
+
const id = isRecord16(doc) ? doc.schema : void 0;
|
|
16321
|
+
if (id !== RUN_SCHEMA) return `its schema is ${JSON.stringify(id)}, not ${RUN_SCHEMA}`;
|
|
16322
|
+
if (v(doc)) return void 0;
|
|
16323
|
+
const e = v.errors?.[0];
|
|
16324
|
+
return `${RUN_SCHEMA_FILE} rejects it: ${e?.instancePath || "/"} ${e?.message ?? "is invalid"}`;
|
|
16325
|
+
}
|
|
16326
|
+
};
|
|
16327
|
+
}
|
|
16328
|
+
var compiled2 = /* @__PURE__ */ new Map();
|
|
16329
|
+
function runValidator(dirs = []) {
|
|
16330
|
+
for (const from of [...dirs, dirname9(fileURLToPath6(import.meta.url))]) {
|
|
16331
|
+
let dist;
|
|
16332
|
+
try {
|
|
16333
|
+
dist = dirname9(createRequire5(join31(resolve21(from), "noop.js")).resolve("@intentius/terragucci/report.schema.json"));
|
|
16334
|
+
} catch {
|
|
16335
|
+
continue;
|
|
16336
|
+
}
|
|
16337
|
+
const pkg = join31(dist, "..", "package.json");
|
|
16338
|
+
const file = join31(dist, RUN_SCHEMA_FILE);
|
|
16339
|
+
let stamp;
|
|
16340
|
+
try {
|
|
16341
|
+
stamp = [dist, ...[pkg, file].map((f) => {
|
|
16342
|
+
const st = statSync10(f);
|
|
16343
|
+
return `${st.mtimeMs}:${st.size}`;
|
|
16344
|
+
})].join("\0");
|
|
16345
|
+
} catch {
|
|
16346
|
+
continue;
|
|
16347
|
+
}
|
|
16348
|
+
const hit = compiled2.get(stamp);
|
|
16349
|
+
if (hit) return hit;
|
|
16350
|
+
let v;
|
|
16351
|
+
try {
|
|
16352
|
+
const version = JSON.parse(readFileSync21(pkg, "utf8")).version ?? "";
|
|
16353
|
+
v = compileRunSchema(JSON.parse(readFileSync21(file, "utf8")), version);
|
|
16354
|
+
} catch {
|
|
16355
|
+
continue;
|
|
16356
|
+
}
|
|
16357
|
+
for (const k of compiled2.keys()) if (k.startsWith(`${dist}\0`)) compiled2.delete(k);
|
|
16358
|
+
compiled2.set(stamp, v);
|
|
16359
|
+
return v;
|
|
16360
|
+
}
|
|
16361
|
+
return { validation: { by: "structural" }, check: structuralRun };
|
|
16362
|
+
}
|
|
16363
|
+
var refuse12 = (error, remedy) => {
|
|
16364
|
+
throw new TerragucciReadError({ error, code: "terragucci-report", remedy });
|
|
16365
|
+
};
|
|
16366
|
+
var newestFirst2 = (a, b) => a.finished < b.finished ? 1 : a.finished > b.finished ? -1 : 0;
|
|
16367
|
+
function newestApply(rows, project) {
|
|
16368
|
+
return rows.filter((r) => r.project === project && r.stage === "tf-apply").sort(newestFirst2)[0];
|
|
16369
|
+
}
|
|
16370
|
+
function runKey(row) {
|
|
16371
|
+
const segs = row.path.split("/").filter(Boolean);
|
|
16372
|
+
const at2 = segs.lastIndexOf(row.commit);
|
|
16373
|
+
const base = at2 >= 2 && /^\d{4}$/.test(segs[at2 - 2]) && /^\d{2}$/.test(segs[at2 - 1]) ? segs.slice(0, at2 - 2) : row.project.split("/").filter(Boolean);
|
|
16374
|
+
return [...base, "runs", row.commit, "run.json"].join("/");
|
|
16375
|
+
}
|
|
16376
|
+
var zero = () => ({ done: 0, "in-flight": 0, waiting: 0, "not-applied": 0 });
|
|
16377
|
+
var MARK_ORDER = ["not-applied", "in-flight", "waiting", "done"];
|
|
16378
|
+
function unsettled(run3) {
|
|
16379
|
+
return run3.waves.some((w) => w.state === "applying" || (w.progress?.resources ?? []).some((r) => r.status === "in-flight" || r.status === "waiting"));
|
|
16380
|
+
}
|
|
16381
|
+
function joinProgress(run3, nodes, roots) {
|
|
16382
|
+
const byRoot = /* @__PURE__ */ new Map();
|
|
16383
|
+
for (const n of nodes) {
|
|
16384
|
+
const root = n.attrs?.root;
|
|
16385
|
+
if (typeof root !== "string") continue;
|
|
16386
|
+
const at2 = n.id.indexOf(`/${root}/`);
|
|
16387
|
+
if (at2 < 0) continue;
|
|
16388
|
+
const key = `${n.id.slice(0, at2)}\0${root}`;
|
|
16389
|
+
if (!byRoot.has(key)) byRoot.set(key, /* @__PURE__ */ new Map());
|
|
16390
|
+
byRoot.get(key).set(n.id.slice(at2 + root.length + 2), n.id);
|
|
16391
|
+
}
|
|
16392
|
+
const rootByPath = new Map(roots.map((r) => [r.path, r]));
|
|
16393
|
+
const waves = [];
|
|
16394
|
+
const cards = {};
|
|
16395
|
+
const unmatched = [];
|
|
16396
|
+
for (const w of run3.waves) {
|
|
16397
|
+
if (!w.progress) continue;
|
|
16398
|
+
const counts = zero();
|
|
16399
|
+
const resources = [];
|
|
16400
|
+
const onCard = /* @__PURE__ */ new Map();
|
|
16401
|
+
for (const r of w.progress.resources) {
|
|
16402
|
+
counts[r.status]++;
|
|
16403
|
+
const root = rootByPath.get(r.root);
|
|
16404
|
+
const card = root ? byRoot.get(`${root.member}\0${root.name}`)?.get(blockPath(r.address)) : void 0;
|
|
16405
|
+
const own = { root: r.root, address: r.address, action: r.action, status: r.status, ...r.done_at ? { done_at: r.done_at } : {} };
|
|
16406
|
+
resources.push({ ...own, ...card ? { card } : {} });
|
|
16407
|
+
if (!card) {
|
|
16408
|
+
unmatched.push({ root: r.root, address: r.address });
|
|
16409
|
+
continue;
|
|
16410
|
+
}
|
|
16411
|
+
const m = onCard.get(card) ?? { wave: w.number, state: w.state, read: w.progress.read, status: "done", counts: zero(), resources: [] };
|
|
16412
|
+
m.counts[r.status]++;
|
|
16413
|
+
m.resources.push(own);
|
|
16414
|
+
onCard.set(card, m);
|
|
16415
|
+
}
|
|
16416
|
+
for (const [card, m] of onCard) {
|
|
16417
|
+
m.status = MARK_ORDER.find((s) => m.counts[s] > 0) ?? "done";
|
|
16418
|
+
(cards[card] ??= []).push(m);
|
|
16419
|
+
}
|
|
16420
|
+
waves.push({ number: w.number, state: w.state, gate: w.gate, read: w.progress.read, counts, resources });
|
|
16421
|
+
}
|
|
16422
|
+
return { waves, cards, unmatched, watching: unsettled(run3) };
|
|
16423
|
+
}
|
|
16424
|
+
var ProgressGraphError = class extends Error {
|
|
16425
|
+
};
|
|
16426
|
+
var FILES = "/api/terragucci/file?key=";
|
|
16427
|
+
var progressKey = (a) => JSON.stringify([a.run, a.waves, a.unmatched]);
|
|
16428
|
+
var TerragucciProgress = class {
|
|
16429
|
+
constructor(o) {
|
|
16430
|
+
this.o = o;
|
|
16431
|
+
}
|
|
16432
|
+
o;
|
|
16433
|
+
cached;
|
|
16434
|
+
/** The run view the poll watches: its key, the tag last seen, and the answer last given. */
|
|
16435
|
+
watch;
|
|
16436
|
+
probes = /* @__PURE__ */ new Map();
|
|
16437
|
+
now() {
|
|
16438
|
+
return this.o.now?.() ?? /* @__PURE__ */ new Date();
|
|
16439
|
+
}
|
|
16440
|
+
/** The answer, from the 30 s cache unless `fresh`. */
|
|
16441
|
+
answer(fresh = false) {
|
|
16442
|
+
const key = this.o.checkoutProject?.() ?? "";
|
|
16443
|
+
const at2 = Date.now();
|
|
16444
|
+
if (!fresh && this.cached && this.cached.key === key && at2 - this.cached.at < PROGRESS_TTL_MS) return this.cached.answer;
|
|
16445
|
+
const p = this.read();
|
|
16446
|
+
this.cached = { at: at2, key, answer: p };
|
|
16447
|
+
p.catch(() => this.cached = void 0);
|
|
16448
|
+
return p;
|
|
16449
|
+
}
|
|
16450
|
+
/** Drop the cached answer: the index changed, so the newest commit may have. */
|
|
16451
|
+
forget() {
|
|
16452
|
+
this.cached = void 0;
|
|
16453
|
+
}
|
|
16454
|
+
async read() {
|
|
16455
|
+
const v = this.o.validators?.();
|
|
16456
|
+
const src = this.o.source;
|
|
16457
|
+
let text;
|
|
16458
|
+
try {
|
|
16459
|
+
text = await src.read("index.json");
|
|
16460
|
+
} catch (e) {
|
|
16461
|
+
refuse12(`Could not read index.json from ${src.location}: ${e instanceof Error ? e.message : String(e)}`, "Check the source serves terragucci's reports prefix.");
|
|
16462
|
+
}
|
|
16463
|
+
if (text === void 0) refuse12(`${src.location} has no index.json.`, "Point --terragucci at terragucci's report directory or bucket prefix, the one holding index.json.");
|
|
16464
|
+
let doc;
|
|
16465
|
+
try {
|
|
16466
|
+
doc = JSON.parse(text);
|
|
16467
|
+
} catch {
|
|
16468
|
+
refuse12(`index.json in ${src.location} is not JSON.`, "Point --terragucci at a terragucci reports directory or bucket prefix, the one holding index.json.");
|
|
16469
|
+
}
|
|
16470
|
+
const why = v?.index.check("index", doc);
|
|
16471
|
+
if (why) refuse12(`index.json in ${src.location} is not a terragucci terragucci.report-index/v1 document: ${why}.`, "Check that terragucci wrote it.");
|
|
16472
|
+
const rows = doc.reports;
|
|
16473
|
+
const checkout = this.o.checkoutProject?.();
|
|
16474
|
+
const project = pickProject(rows, { ...this.o.project ? { project: this.o.project } : {}, ...checkout ? { checkoutProject: checkout } : {} }, src);
|
|
16475
|
+
const row = newestApply(rows, project);
|
|
16476
|
+
if (!row) {
|
|
16477
|
+
this.watch = void 0;
|
|
16478
|
+
return {
|
|
16479
|
+
...this.base(project, v?.run.validation),
|
|
16480
|
+
run: null,
|
|
16481
|
+
absent: `index.json lists no tf-apply run of ${project} yet, so there is no run view to read.`,
|
|
16482
|
+
waves: [],
|
|
16483
|
+
cards: {},
|
|
16484
|
+
unmatched: [],
|
|
16485
|
+
watching: false
|
|
16486
|
+
};
|
|
16487
|
+
}
|
|
16488
|
+
const key = runKey(row);
|
|
16489
|
+
const a = await this.readRun(project, row.commit, key, v?.run);
|
|
16490
|
+
this.watch = { key, commit: row.commit, project, last: a };
|
|
16491
|
+
return a;
|
|
16492
|
+
}
|
|
16493
|
+
base(project, validation) {
|
|
16494
|
+
return { source: this.o.source.location, kind: this.o.source.kind, project, validation: validation ?? { by: "structural" }, read: this.now().toISOString(), files: FILES };
|
|
16495
|
+
}
|
|
16496
|
+
/** One GET of run.json, checked and joined. */
|
|
16497
|
+
async readRun(project, commit, key, v) {
|
|
16498
|
+
const src = this.o.source;
|
|
16499
|
+
const validator = v ?? runValidator();
|
|
16500
|
+
let text;
|
|
16501
|
+
try {
|
|
16502
|
+
text = await src.read(key);
|
|
16503
|
+
} catch (e) {
|
|
16504
|
+
refuse12(
|
|
16505
|
+
`Could not read ${key} from ${src.location}: ${e instanceof Error ? e.message : String(e)}`,
|
|
16506
|
+
src.kind === "s3" ? "behold reads the bucket with your aws CLI or the environment's credentials: check GetObject on <prefix>/*/runs/*/run.json." : "Check the source serves the reports prefix."
|
|
16507
|
+
);
|
|
16508
|
+
}
|
|
16509
|
+
const html = key.replace(/run\.json$/, "run.html");
|
|
16510
|
+
if (text === void 0) {
|
|
16511
|
+
return {
|
|
16512
|
+
...this.base(project, validator.validation),
|
|
16513
|
+
run: { commit, key, present: false },
|
|
16514
|
+
absent: `${src.location} has no ${key}: terragucci writes the run view when reports.bucket is set, and a wave's progress per resource only for a choudoufu wave (terragucci 0.4.7 and newer). This says nothing about whether the apply moved.`,
|
|
16515
|
+
waves: [],
|
|
16516
|
+
cards: {},
|
|
16517
|
+
unmatched: [],
|
|
16518
|
+
watching: false
|
|
16519
|
+
};
|
|
16520
|
+
}
|
|
16521
|
+
let doc;
|
|
16522
|
+
try {
|
|
16523
|
+
doc = JSON.parse(text);
|
|
16524
|
+
} catch {
|
|
16525
|
+
refuse12(`${key} in ${src.location} is not JSON.`, "Check that terragucci wrote it.");
|
|
16526
|
+
}
|
|
16527
|
+
const why = validator.check(doc);
|
|
16528
|
+
if (why) {
|
|
16529
|
+
refuse12(
|
|
16530
|
+
`${key} in ${src.location} is not a terragucci ${RUN_SCHEMA} document: ${why}.`,
|
|
16531
|
+
validator.validation.by === "schema" ? `It was checked against @intentius/terragucci ${validator.validation.terragucci}'s ${RUN_SCHEMA_FILE}. Check that this terragucci wrote it, or upgrade behold.` : "Install @intentius/terragucci beside behold to check it against terragucci's own JSON Schema, and check that terragucci wrote it."
|
|
16532
|
+
);
|
|
16533
|
+
}
|
|
16534
|
+
const run3 = doc;
|
|
16535
|
+
let ctx;
|
|
16536
|
+
try {
|
|
16537
|
+
ctx = await this.o.context();
|
|
16538
|
+
} catch (e) {
|
|
16539
|
+
throw new ProgressGraphError(`behold could not read the checkout's roots to place the progress on: ${e instanceof Error ? e.message : String(e)}`);
|
|
16540
|
+
}
|
|
16541
|
+
return {
|
|
16542
|
+
...this.base(project, validator.validation),
|
|
16543
|
+
run: { commit, key, present: true, updated: run3.updated, html },
|
|
16544
|
+
...joinProgress(run3, ctx.nodes, ctx.roots)
|
|
16545
|
+
};
|
|
16546
|
+
}
|
|
16547
|
+
/**
|
|
16548
|
+
* One tick of the #505 poll: ask whether the watched run.json changed, only
|
|
16549
|
+
* while it is unsettled. A first ask after a read has no tag to compare, so
|
|
16550
|
+
* it reads the view again rather than miss a write made in between.
|
|
16551
|
+
*/
|
|
16552
|
+
async poll() {
|
|
16553
|
+
const w = this.watch;
|
|
16554
|
+
if (!w || !w.last.watching) return { asked: false };
|
|
16555
|
+
let probe2 = this.probes.get(w.key);
|
|
16556
|
+
if (!probe2) {
|
|
16557
|
+
probe2 = this.o.probe(w.key);
|
|
16558
|
+
this.probes.set(w.key, probe2);
|
|
16559
|
+
}
|
|
16560
|
+
const p = await probe2(w.tag);
|
|
16561
|
+
const changed = p.changed || w.tag === void 0;
|
|
16562
|
+
w.tag = p.tag;
|
|
16563
|
+
if (!changed) return { asked: true, key: w.key, changed: false };
|
|
16564
|
+
const v = this.o.validators?.();
|
|
16565
|
+
const a = await this.readRun(w.project, w.commit, w.key, v?.run);
|
|
16566
|
+
if (this.watch !== w) return { asked: true, key: w.key, changed: true };
|
|
16567
|
+
const moved = progressKey(a) !== progressKey(w.last);
|
|
16568
|
+
w.last = a;
|
|
16569
|
+
this.cached = { at: Date.now(), key: this.o.checkoutProject?.() ?? "", answer: Promise.resolve(a) };
|
|
16570
|
+
return { asked: true, key: w.key, changed: true, ...moved ? { answer: a } : {} };
|
|
16571
|
+
}
|
|
16572
|
+
};
|
|
16573
|
+
function terragucciProgressRoute(app, progress) {
|
|
16574
|
+
app.get("/api/terragucci/progress", async (c) => {
|
|
16575
|
+
try {
|
|
16576
|
+
return c.json(await progress.answer(new URL(c.req.url).searchParams.get("fresh") === "1"));
|
|
16577
|
+
} catch (e) {
|
|
16578
|
+
if (e instanceof TerragucciReadError) return c.json(e.refusal, 422);
|
|
16579
|
+
if (e instanceof ProgressGraphError) return c.json({ error: e.message, code: "terragucci-graph", remedy: "`behold doctor` names what the Terraform read needs." }, 422);
|
|
16580
|
+
throw e;
|
|
16581
|
+
}
|
|
16582
|
+
});
|
|
16583
|
+
}
|
|
16584
|
+
|
|
15537
16585
|
// src/terragucci-view.ts
|
|
15538
16586
|
var TERRAGUCCI_READ_TTL_MS = 3e4;
|
|
15539
16587
|
function repoBase(dirs) {
|
|
15540
16588
|
for (const d of dirs) {
|
|
15541
16589
|
const cfg = terragucciConfig(d);
|
|
15542
|
-
if (cfg) return
|
|
16590
|
+
if (cfg) return dirname10(cfg);
|
|
15543
16591
|
}
|
|
15544
16592
|
try {
|
|
15545
16593
|
return execFileSync2("git", ["-C", dirs[0], "rev-parse", "--show-toplevel"], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
|
|
@@ -15572,7 +16620,7 @@ function checkoutRoots(dirs, base = repoBase(dirs)) {
|
|
|
15572
16620
|
for (const m of estateMembers(dirs)) {
|
|
15573
16621
|
if (m.kind !== "terraform") continue;
|
|
15574
16622
|
for (const r of discoverTerraformRoots(m.dir).roots) {
|
|
15575
|
-
const path = relative7(base,
|
|
16623
|
+
const path = relative7(base, join32(m.dir, r.dir)).split(sep6).join("/") || ".";
|
|
15576
16624
|
out.push({ member: m.name, name: r.name, path });
|
|
15577
16625
|
}
|
|
15578
16626
|
}
|
|
@@ -15599,6 +16647,28 @@ function terragucciRoutes(app, opts, dirs) {
|
|
|
15599
16647
|
p.catch(() => cached = void 0);
|
|
15600
16648
|
return p;
|
|
15601
16649
|
};
|
|
16650
|
+
const progress = new TerragucciProgress({
|
|
16651
|
+
source: source2,
|
|
16652
|
+
...opts.project ? { project: opts.project } : {},
|
|
16653
|
+
checkoutProject: () => checkoutProject(repoBase(dirs())),
|
|
16654
|
+
validators: () => ({ index: terragucciValidator(dirs()), run: runValidator(dirs()) }),
|
|
16655
|
+
context: async () => {
|
|
16656
|
+
const served2 = dirs();
|
|
16657
|
+
return { nodes: (await composeEstate(served2, { detail: 3 })).nodes, roots: checkoutRoots(served2) };
|
|
16658
|
+
},
|
|
16659
|
+
probe: (key) => indexProbe(opts.source, { key, ...opts.aws ? { aws: opts.aws } : {}, ...opts.fetch ? { fetch: opts.fetch } : {} }),
|
|
16660
|
+
...opts.now ? { now: opts.now } : {}
|
|
16661
|
+
});
|
|
16662
|
+
terragucciProgressRoute(app, progress);
|
|
16663
|
+
terragucciLaneRoutes(
|
|
16664
|
+
app,
|
|
16665
|
+
{ source: opts.source, ...opts.pollSecs !== void 0 ? { pollSecs: opts.pollSecs } : {}, ...opts.aws ? { aws: opts.aws } : {}, ...opts.fetch ? { fetch: opts.fetch } : {}, ...opts.now ? { now: opts.now } : {}, progress: () => progress.poll(), ...opts.lane },
|
|
16666
|
+
() => repoBase(dirs()),
|
|
16667
|
+
() => {
|
|
16668
|
+
cached = void 0;
|
|
16669
|
+
progress.forget();
|
|
16670
|
+
}
|
|
16671
|
+
);
|
|
15602
16672
|
app.get("/api/terragucci", async (c) => {
|
|
15603
16673
|
const served2 = dirs();
|
|
15604
16674
|
const roots = checkoutRoots(served2);
|
|
@@ -15629,6 +16699,12 @@ function terragucciRoutes(app, opts, dirs) {
|
|
|
15629
16699
|
...marks
|
|
15630
16700
|
});
|
|
15631
16701
|
});
|
|
16702
|
+
terragucciTimelineRoute(app, source2, {
|
|
16703
|
+
...opts.project ? { project: opts.project } : {},
|
|
16704
|
+
checkoutProject: () => checkoutProject(repoBase(dirs())),
|
|
16705
|
+
validator: () => auditValidator(dirs()),
|
|
16706
|
+
...opts.now ? { now: opts.now } : {}
|
|
16707
|
+
});
|
|
15632
16708
|
app.get("/api/terragucci/file", async (c) => {
|
|
15633
16709
|
const key = new URL(c.req.url).searchParams.get("key") ?? "";
|
|
15634
16710
|
if (!FILE_KEY.test(key)) return c.json({ error: `not a report file: ${JSON.stringify(key)}`, code: "terragucci-file" }, 400);
|
|
@@ -15647,30 +16723,372 @@ function terragucciRoutes(app, opts, dirs) {
|
|
|
15647
16723
|
});
|
|
15648
16724
|
}
|
|
15649
16725
|
|
|
16726
|
+
// src/terragucci-estate.ts
|
|
16727
|
+
init_yaml();
|
|
16728
|
+
import { existsSync as existsSync19, readFileSync as readFileSync22, statSync as statSync11 } from "node:fs";
|
|
16729
|
+
import { createRequire as createRequire6 } from "node:module";
|
|
16730
|
+
import { dirname as dirname11, extname, join as join33, resolve as resolve22 } from "node:path";
|
|
16731
|
+
import { fileURLToPath as fileURLToPath7 } from "node:url";
|
|
16732
|
+
import { Ajv2020 as Ajv20204 } from "ajv/dist/2020.js";
|
|
16733
|
+
var RUN_SCHEMA2 = "terragucci.run/v1";
|
|
16734
|
+
function configProjects(file) {
|
|
16735
|
+
let text;
|
|
16736
|
+
try {
|
|
16737
|
+
text = readFileSync22(file, "utf8");
|
|
16738
|
+
} catch {
|
|
16739
|
+
return void 0;
|
|
16740
|
+
}
|
|
16741
|
+
if (extname(file) === ".ts") return /(^|[\s,{])projects\s*:\s*\{/.test(text) ? [] : void 0;
|
|
16742
|
+
let doc;
|
|
16743
|
+
try {
|
|
16744
|
+
doc = parseYAMLDocument(text);
|
|
16745
|
+
} catch {
|
|
16746
|
+
return void 0;
|
|
16747
|
+
}
|
|
16748
|
+
const projects = doc && typeof doc === "object" && !Array.isArray(doc) ? doc.projects : void 0;
|
|
16749
|
+
if (!projects || typeof projects !== "object" || Array.isArray(projects)) return void 0;
|
|
16750
|
+
return Object.keys(projects);
|
|
16751
|
+
}
|
|
16752
|
+
function controlRepo(dirs, roots = () => checkoutRoots(dirs).length) {
|
|
16753
|
+
const config = servedTerragucciConfig(dirs);
|
|
16754
|
+
if (!config) return void 0;
|
|
16755
|
+
const projects = configProjects(config);
|
|
16756
|
+
if (!projects) return void 0;
|
|
16757
|
+
let n;
|
|
16758
|
+
try {
|
|
16759
|
+
n = roots();
|
|
16760
|
+
} catch {
|
|
16761
|
+
n = 0;
|
|
16762
|
+
}
|
|
16763
|
+
return n === 0 ? { config, projects } : void 0;
|
|
16764
|
+
}
|
|
16765
|
+
var isRecord17 = (v) => v !== null && typeof v === "object" && !Array.isArray(v);
|
|
16766
|
+
var str12 = (v) => typeof v === "string";
|
|
16767
|
+
var int = (v) => typeof v === "number" && Number.isInteger(v);
|
|
16768
|
+
function structuralRun2(doc) {
|
|
16769
|
+
if (!isRecord17(doc)) return "it is not a JSON object";
|
|
16770
|
+
if (doc.schema !== RUN_SCHEMA2) return `its schema is ${JSON.stringify(doc.schema)}, not ${RUN_SCHEMA2}`;
|
|
16771
|
+
if (!str12(doc.project) || !str12(doc.commit) || !str12(doc.updated)) return "it lacks project, commit or updated";
|
|
16772
|
+
if (!Array.isArray(doc.roots) || !Array.isArray(doc.waves)) return "it has no roots or waves array";
|
|
16773
|
+
for (const [i, r] of doc.roots.entries()) {
|
|
16774
|
+
if (!isRecord17(r) || !str12(r.root) || !int(r.wave) || !Array.isArray(r.reads) || !r.reads.every(str12)) return `root ${i} lacks root, wave or reads`;
|
|
16775
|
+
if (r.state !== void 0 && !(isRecord17(r.state) && str12(r.state.key))) return `root ${String(r.root)}'s state has no key`;
|
|
16776
|
+
if (r.external !== void 0 && !(Array.isArray(r.external) && r.external.every((x) => isRecord17(x) && str12(x.data) && str12(x.key)))) return `root ${String(r.root)}'s external reads lack data or key`;
|
|
16777
|
+
}
|
|
16778
|
+
for (const [i, w] of doc.waves.entries()) {
|
|
16779
|
+
if (!isRecord17(w) || !int(w.number) || !Array.isArray(w.roots) || !Array.isArray(w.reads) || !str12(w.state) || !str12(w.gate)) return `wave ${i} lacks number, roots, reads, state or gate`;
|
|
16780
|
+
}
|
|
16781
|
+
return void 0;
|
|
16782
|
+
}
|
|
16783
|
+
var compiledRun = /* @__PURE__ */ new Map();
|
|
16784
|
+
function runViewValidator(dirs = []) {
|
|
16785
|
+
for (const from of [...dirs, dirname11(fileURLToPath7(import.meta.url))]) {
|
|
16786
|
+
let entry;
|
|
16787
|
+
try {
|
|
16788
|
+
entry = createRequire6(join33(resolve22(from), "noop.js")).resolve("@intentius/terragucci/report.schema.json");
|
|
16789
|
+
} catch {
|
|
16790
|
+
continue;
|
|
16791
|
+
}
|
|
16792
|
+
const dist = dirname11(entry);
|
|
16793
|
+
const schema = join33(dist, "run.schema.json");
|
|
16794
|
+
let stamp;
|
|
16795
|
+
try {
|
|
16796
|
+
const st = statSync11(schema);
|
|
16797
|
+
const pkg = statSync11(join33(dist, "..", "package.json"));
|
|
16798
|
+
stamp = `${schema}\0${st.mtimeMs}:${st.size}\0${pkg.mtimeMs}`;
|
|
16799
|
+
} catch {
|
|
16800
|
+
continue;
|
|
16801
|
+
}
|
|
16802
|
+
const hit = compiledRun.get(stamp);
|
|
16803
|
+
if (hit) return hit;
|
|
16804
|
+
let v;
|
|
16805
|
+
try {
|
|
16806
|
+
const version = JSON.parse(readFileSync22(join33(dist, "..", "package.json"), "utf8")).version ?? "";
|
|
16807
|
+
const validate = new Ajv20204({ strict: false, allErrors: false }).compile(JSON.parse(readFileSync22(schema, "utf8")));
|
|
16808
|
+
v = {
|
|
16809
|
+
validation: { by: "schema", terragucci: version },
|
|
16810
|
+
check(doc) {
|
|
16811
|
+
const id = isRecord17(doc) ? doc.schema : void 0;
|
|
16812
|
+
if (id !== RUN_SCHEMA2) return `its schema is ${JSON.stringify(id)}, not ${RUN_SCHEMA2}`;
|
|
16813
|
+
if (validate(doc)) return void 0;
|
|
16814
|
+
const e = validate.errors?.[0];
|
|
16815
|
+
return `run.schema.json rejects it: ${e?.instancePath || "/"} ${e?.message ?? "is invalid"}`;
|
|
16816
|
+
}
|
|
16817
|
+
};
|
|
16818
|
+
} catch {
|
|
16819
|
+
continue;
|
|
16820
|
+
}
|
|
16821
|
+
for (const k of compiledRun.keys()) if (k.startsWith(`${schema}\0`)) compiledRun.delete(k);
|
|
16822
|
+
compiledRun.set(stamp, v);
|
|
16823
|
+
return v;
|
|
16824
|
+
}
|
|
16825
|
+
return { validation: { by: "structural" }, check: structuralRun2 };
|
|
16826
|
+
}
|
|
16827
|
+
var refusal = (error, remedy) => ({ error, code: "terragucci-report", remedy });
|
|
16828
|
+
var refuse13 = (error, remedy) => {
|
|
16829
|
+
throw new TerragucciReadError(refusal(error, remedy));
|
|
16830
|
+
};
|
|
16831
|
+
var trimSlashes = (s) => s.replace(/^\/+|\/+$/g, "");
|
|
16832
|
+
function runKey2(p, commit) {
|
|
16833
|
+
const page = p.run_view?.commit === commit ? p.run_view.page : void 0;
|
|
16834
|
+
if (page && !/^[a-z]+:\/\//i.test(page) && !page.startsWith("/") && !page.split("/").includes("..") && page.endsWith("/run.html")) {
|
|
16835
|
+
return `${trimSlashes(page.slice(0, -"run.html".length))}/run.json`;
|
|
16836
|
+
}
|
|
16837
|
+
return [trimSlashes(p.project), "runs", commit, "run.json"].join("/");
|
|
16838
|
+
}
|
|
16839
|
+
var sameState = (a, b) => a.key === b.key && (!a.bucket || !b.bucket || a.bucket === b.bucket);
|
|
16840
|
+
function controlEdges(views) {
|
|
16841
|
+
const edges = [];
|
|
16842
|
+
const has = (f, t2) => edges.some((e) => e.from.project === f.project && e.from.root === f.root && e.to.project === t2.project && e.to.root === t2.root);
|
|
16843
|
+
for (const v of views) for (const r of v.roots) for (const u of r.reads) edges.push({ from: { project: v.project, root: u }, to: { project: v.project, root: r.root }, cross: false });
|
|
16844
|
+
const held = views.flatMap((v) => v.roots.filter((r) => r.state).map((r) => ({ project: v.project, root: r.root, state: r.state })));
|
|
16845
|
+
for (const v of views) {
|
|
16846
|
+
for (const r of v.roots) {
|
|
16847
|
+
for (const x of r.external ?? []) {
|
|
16848
|
+
for (const h of held) {
|
|
16849
|
+
if (h.project === v.project || !sameState(h.state, x)) continue;
|
|
16850
|
+
const from = { project: h.project, root: h.root };
|
|
16851
|
+
const to = { project: v.project, root: r.root };
|
|
16852
|
+
if (!has(from, to)) edges.push({ from, to, cross: true });
|
|
16853
|
+
}
|
|
16854
|
+
}
|
|
16855
|
+
}
|
|
16856
|
+
}
|
|
16857
|
+
return edges;
|
|
16858
|
+
}
|
|
16859
|
+
function siblingCheckout(project, base, projectOf = checkoutProject) {
|
|
16860
|
+
const name = project.split("/").filter(Boolean).pop();
|
|
16861
|
+
if (!name) return void 0;
|
|
16862
|
+
const dir = join33(dirname11(resolve22(base)), name);
|
|
16863
|
+
if (dir === resolve22(base) || !existsSync19(join33(dir, ".git"))) return void 0;
|
|
16864
|
+
return projectOf(dir) === project ? dir : void 0;
|
|
16865
|
+
}
|
|
16866
|
+
var shellWord = (s) => /^[A-Za-z0-9_./:@%+=,-]+$/.test(s) ? s : `'${s.replace(/'/g, `'\\''`)}'`;
|
|
16867
|
+
async function readControlEstate(source2, config, opts = {}) {
|
|
16868
|
+
const v = opts.validator ?? terragucciValidator();
|
|
16869
|
+
const rv = opts.runValidator ?? runViewValidator();
|
|
16870
|
+
let gets = 0;
|
|
16871
|
+
const get = async (key) => {
|
|
16872
|
+
gets++;
|
|
16873
|
+
return source2.read(key);
|
|
16874
|
+
};
|
|
16875
|
+
let text;
|
|
16876
|
+
try {
|
|
16877
|
+
text = await get("estate.json");
|
|
16878
|
+
} catch (e) {
|
|
16879
|
+
refuse13(`Could not read estate.json from ${source2.location}: ${e instanceof Error ? e.message : String(e)}`, "Check the source is the top of terragucci's reports prefix and that GetObject on estate.json is allowed.");
|
|
16880
|
+
}
|
|
16881
|
+
if (text === void 0) {
|
|
16882
|
+
refuse13(
|
|
16883
|
+
`${source2.location} has no estate.json, so a control repo's projects have nothing to be drawn from.`,
|
|
16884
|
+
"Point --terragucci at the top of the reports prefix, where `terragucci estate` writes estate.json, or run `terragucci estate` once."
|
|
16885
|
+
);
|
|
16886
|
+
}
|
|
16887
|
+
let doc;
|
|
16888
|
+
try {
|
|
16889
|
+
doc = JSON.parse(text);
|
|
16890
|
+
} catch {
|
|
16891
|
+
refuse13(`estate.json in ${source2.location} is not JSON.`, "Point --terragucci at the top of terragucci's reports prefix.");
|
|
16892
|
+
}
|
|
16893
|
+
const why = v.check("estate", doc);
|
|
16894
|
+
if (why) {
|
|
16895
|
+
refuse13(
|
|
16896
|
+
`estate.json in ${source2.location} is not a terragucci terragucci.estate/v1 document: ${why}.`,
|
|
16897
|
+
v.validation.by === "schema" ? `It was checked against @intentius/terragucci ${v.validation.terragucci}'s estate.schema.json. Check that this terragucci wrote it, or upgrade behold.` : "Install @intentius/terragucci beside behold to check it against terragucci's own JSON Schemas, and check that terragucci wrote it."
|
|
16898
|
+
);
|
|
16899
|
+
}
|
|
16900
|
+
const estate = doc;
|
|
16901
|
+
const estateDated = { key: "estate.json", at: estate.generated };
|
|
16902
|
+
const views = [];
|
|
16903
|
+
const projects = [];
|
|
16904
|
+
const configured = new Set(opts.configured ?? []);
|
|
16905
|
+
for (const p of estate.projects) {
|
|
16906
|
+
const commit = p.run_view?.commit ?? p.apply?.commit;
|
|
16907
|
+
let run3;
|
|
16908
|
+
let missing;
|
|
16909
|
+
let refused2;
|
|
16910
|
+
if (!commit) {
|
|
16911
|
+
missing = p.status === "ok" ? "no apply has run, so there is no run view" : p.status === "no-index" ? "no runs in the bucket yet" : `its index could not be read${p.error ? `: ${p.error}` : ""}`;
|
|
16912
|
+
} else {
|
|
16913
|
+
const key = runKey2(p, commit);
|
|
16914
|
+
let body;
|
|
16915
|
+
try {
|
|
16916
|
+
body = await get(key);
|
|
16917
|
+
} catch (e) {
|
|
16918
|
+
refused2 = refusal(`Could not read ${key} from ${source2.location}: ${e instanceof Error ? e.message : String(e)}`, "Check GetObject on <prefix>/*/runs/*/run.json is allowed.");
|
|
16919
|
+
}
|
|
16920
|
+
if (!refused2 && body === void 0) missing = `no run view at ${key}`;
|
|
16921
|
+
if (!refused2 && body !== void 0) {
|
|
16922
|
+
let parsed;
|
|
16923
|
+
let problem;
|
|
16924
|
+
try {
|
|
16925
|
+
parsed = JSON.parse(body);
|
|
16926
|
+
problem = rv.check(parsed);
|
|
16927
|
+
} catch {
|
|
16928
|
+
problem = "it is not JSON";
|
|
16929
|
+
}
|
|
16930
|
+
if (!problem && parsed.project !== p.project) problem = `it is ${JSON.stringify(parsed.project)}'s run view, not ${p.project}'s`;
|
|
16931
|
+
if (!problem && parsed.commit !== commit) problem = `it is the run view of ${parsed.commit}, not ${commit}`;
|
|
16932
|
+
if (problem) {
|
|
16933
|
+
refused2 = refusal(
|
|
16934
|
+
`${key} in ${source2.location} is not a terragucci ${RUN_SCHEMA2} document: ${problem}.`,
|
|
16935
|
+
rv.validation.by === "schema" ? `It was checked against @intentius/terragucci ${rv.validation.terragucci}'s run.schema.json. Check that this terragucci wrote it, or upgrade behold.` : "Install @intentius/terragucci beside behold to check it against terragucci's own run.schema.json, and check that terragucci wrote it."
|
|
16936
|
+
);
|
|
16937
|
+
} else {
|
|
16938
|
+
const view = parsed;
|
|
16939
|
+
views.push(view);
|
|
16940
|
+
run3 = {
|
|
16941
|
+
commit,
|
|
16942
|
+
updated: view.updated,
|
|
16943
|
+
from: p.run_view?.commit === commit ? "run_view" : "apply",
|
|
16944
|
+
key,
|
|
16945
|
+
page: key.replace(/run\.json$/, "run.html"),
|
|
16946
|
+
roots: view.roots.map((r) => ({
|
|
16947
|
+
root: r.root,
|
|
16948
|
+
wave: r.wave,
|
|
16949
|
+
reads: r.reads,
|
|
16950
|
+
...r.state ? { state: r.state } : {},
|
|
16951
|
+
...r.external?.length ? { external: r.external } : {}
|
|
16952
|
+
})),
|
|
16953
|
+
waves: view.waves.map((w) => ({
|
|
16954
|
+
number: w.number,
|
|
16955
|
+
roots: w.roots,
|
|
16956
|
+
reads: w.reads,
|
|
16957
|
+
state: w.state,
|
|
16958
|
+
gate: w.gate,
|
|
16959
|
+
...w.policy ? { policy: w.policy } : {},
|
|
16960
|
+
...w.approval ? { approval: w.approval } : {},
|
|
16961
|
+
...w.digest !== void 0 ? { digest: w.digest } : {},
|
|
16962
|
+
...w.command ? { command: w.command } : {},
|
|
16963
|
+
...w.updated ? { updated: w.updated } : {}
|
|
16964
|
+
})),
|
|
16965
|
+
dated: { key, at: view.updated }
|
|
16966
|
+
};
|
|
16967
|
+
}
|
|
16968
|
+
}
|
|
16969
|
+
}
|
|
16970
|
+
const waiting = p.waiting.map((w) => {
|
|
16971
|
+
const fromRun = run3 && run3.commit === w.commit ? run3.waves.find((x) => x.number === w.wave) : void 0;
|
|
16972
|
+
const digest = fromRun?.digest;
|
|
16973
|
+
return {
|
|
16974
|
+
wave: w.wave,
|
|
16975
|
+
commit: w.commit,
|
|
16976
|
+
since: w.since,
|
|
16977
|
+
age_seconds: w.age_seconds,
|
|
16978
|
+
...w.report ? { report: w.report } : {},
|
|
16979
|
+
...digest !== void 0 ? { digest } : {},
|
|
16980
|
+
command: fromRun?.command ?? `npx terragucci approve wave-${w.wave}${digest ? ` --plan ${digest}` : " --plan <digest>"}`,
|
|
16981
|
+
dated: estateDated
|
|
16982
|
+
};
|
|
16983
|
+
});
|
|
16984
|
+
const at2 = opts.base ? siblingCheckout(p.project, opts.base, opts.projectOf) : void 0;
|
|
16985
|
+
projects.push({
|
|
16986
|
+
project: p.project,
|
|
16987
|
+
status: p.status,
|
|
16988
|
+
...p.error ? { error: p.error } : {},
|
|
16989
|
+
...p.index ? { index: p.index } : {},
|
|
16990
|
+
counts: { drifted: p.drifted, failed: p.failed, waiting: p.waiting.length, dated: estateDated },
|
|
16991
|
+
waiting,
|
|
16992
|
+
...run3 ? { run: run3 } : {},
|
|
16993
|
+
...missing ? { missing } : {},
|
|
16994
|
+
...refused2 ? { refused: refused2 } : {},
|
|
16995
|
+
...at2 ? { checkout: { path: at2, command: `behold serve ${shellWord(at2)} --terragucci ${shellWord(source2.location)}` } } : {},
|
|
16996
|
+
configured: configured.has(p.project)
|
|
16997
|
+
});
|
|
16998
|
+
}
|
|
16999
|
+
for (const name of opts.configured ?? []) {
|
|
17000
|
+
if (projects.some((p) => p.project === name)) continue;
|
|
17001
|
+
const at2 = opts.base ? siblingCheckout(name, opts.base, opts.projectOf) : void 0;
|
|
17002
|
+
projects.push({
|
|
17003
|
+
project: name,
|
|
17004
|
+
status: "not-in-estate",
|
|
17005
|
+
waiting: [],
|
|
17006
|
+
missing: `estate.json (generated ${estate.generated}) does not list it`,
|
|
17007
|
+
...at2 ? { checkout: { path: at2, command: `behold serve ${shellWord(at2)} --terragucci ${shellWord(source2.location)}` } } : {},
|
|
17008
|
+
configured: true
|
|
17009
|
+
});
|
|
17010
|
+
}
|
|
17011
|
+
return {
|
|
17012
|
+
source: source2.location,
|
|
17013
|
+
kind: source2.kind,
|
|
17014
|
+
config,
|
|
17015
|
+
validation: { estate: v.validation, run: rv.validation },
|
|
17016
|
+
read: (opts.now ?? /* @__PURE__ */ new Date()).toISOString(),
|
|
17017
|
+
estate: { key: "estate.json", html: "estate.html", generated: estate.generated },
|
|
17018
|
+
gets,
|
|
17019
|
+
projects,
|
|
17020
|
+
edges: controlEdges(views),
|
|
17021
|
+
files: "/api/terragucci/file?key="
|
|
17022
|
+
};
|
|
17023
|
+
}
|
|
17024
|
+
function terragucciEstateRoutes(app, opts, dirs) {
|
|
17025
|
+
const source2 = terragucciSource(opts.source, { ...opts.aws ? { aws: opts.aws } : {}, ...opts.fetch ? { fetch: opts.fetch } : {} });
|
|
17026
|
+
let cached;
|
|
17027
|
+
app.get("/api/terragucci/estate", async (c) => {
|
|
17028
|
+
const served2 = dirs();
|
|
17029
|
+
const control = controlRepo(served2);
|
|
17030
|
+
if (!control) {
|
|
17031
|
+
return c.json(
|
|
17032
|
+
{
|
|
17033
|
+
error: "This is not a terragucci control repo: its terragucci config lists no `projects:`, or the checkout has Terraform roots of its own.",
|
|
17034
|
+
code: "terragucci-not-control",
|
|
17035
|
+
remedy: "GET /api/terragucci answers a repo's own roots; this route answers a control repo, the one whose terragucci.yml lists `projects:`."
|
|
17036
|
+
},
|
|
17037
|
+
404
|
|
17038
|
+
);
|
|
17039
|
+
}
|
|
17040
|
+
const key = served2.join("\0");
|
|
17041
|
+
const now = Date.now();
|
|
17042
|
+
const fresh = new URL(c.req.url).searchParams.get("fresh") === "1";
|
|
17043
|
+
if (fresh || !cached || cached.key !== key || now - cached.at >= TERRAGUCCI_READ_TTL_MS) {
|
|
17044
|
+
const base = repoBase(served2);
|
|
17045
|
+
const p = readControlEstate(source2, control.config, {
|
|
17046
|
+
validator: terragucciValidator(served2),
|
|
17047
|
+
runValidator: runViewValidator(served2),
|
|
17048
|
+
configured: control.projects,
|
|
17049
|
+
base,
|
|
17050
|
+
...opts.now ? { now: opts.now() } : {}
|
|
17051
|
+
});
|
|
17052
|
+
cached = { at: now, key, read: p };
|
|
17053
|
+
p.catch(() => cached = void 0);
|
|
17054
|
+
}
|
|
17055
|
+
try {
|
|
17056
|
+
return c.json(await cached.read);
|
|
17057
|
+
} catch (e) {
|
|
17058
|
+
if (e instanceof TerragucciReadError) return c.json(e.refusal, 422);
|
|
17059
|
+
throw e;
|
|
17060
|
+
}
|
|
17061
|
+
});
|
|
17062
|
+
}
|
|
17063
|
+
function controlRepoInfo(dirs) {
|
|
17064
|
+
const c = controlRepo(dirs);
|
|
17065
|
+
return c ? { config: c.config, projects: c.projects } : void 0;
|
|
17066
|
+
}
|
|
17067
|
+
|
|
15650
17068
|
// src/demo-pins.ts
|
|
15651
17069
|
init_terraform_member();
|
|
15652
|
-
import { existsSync as
|
|
15653
|
-
import { dirname as
|
|
15654
|
-
var
|
|
17070
|
+
import { existsSync as existsSync20, lstatSync as lstatSync2, mkdirSync as mkdirSync7, readFileSync as readFileSync23, readlinkSync as readlinkSync2, symlinkSync as symlinkSync2, writeFileSync as writeFileSync6 } from "node:fs";
|
|
17071
|
+
import { dirname as dirname12, join as join34, resolve as resolve23 } from "node:path";
|
|
17072
|
+
var isRecord18 = (v) => v !== null && typeof v === "object" && !Array.isArray(v);
|
|
15655
17073
|
function linkDemoPins(work, probes = {}) {
|
|
15656
|
-
const file =
|
|
15657
|
-
if (!
|
|
17074
|
+
const file = join34(work, "chant.workspace.json");
|
|
17075
|
+
if (!existsSync20(file)) return { ok: true, linked: [] };
|
|
15658
17076
|
let decl;
|
|
15659
17077
|
try {
|
|
15660
|
-
decl = JSON.parse(
|
|
17078
|
+
decl = JSON.parse(readFileSync23(file, "utf8"));
|
|
15661
17079
|
} catch {
|
|
15662
17080
|
return { ok: true, linked: [] };
|
|
15663
17081
|
}
|
|
15664
|
-
if (!
|
|
17082
|
+
if (!isRecord18(decl) || !Array.isArray(decl.pins)) return { ok: true, linked: [] };
|
|
15665
17083
|
const find = probes.resolve ?? ((pkg) => resolvedPackage(pkg));
|
|
15666
17084
|
const linked = [];
|
|
15667
17085
|
let rewrite = false;
|
|
15668
17086
|
for (const pin of decl.pins) {
|
|
15669
|
-
if (!
|
|
17087
|
+
if (!isRecord18(pin) || typeof pin.package !== "string") continue;
|
|
15670
17088
|
const pkg = pin.package;
|
|
15671
17089
|
if (pkg === TERRAFORM_LEXICON_PKG) {
|
|
15672
|
-
const
|
|
15673
|
-
if (
|
|
17090
|
+
const refusal2 = (probes.reader ?? terraformReaderState)().refusal;
|
|
17091
|
+
if (refusal2) return { ok: false, error: refusal2.error, remedy: refusal2.remedy };
|
|
15674
17092
|
}
|
|
15675
17093
|
const found = find(pkg);
|
|
15676
17094
|
if (!found) {
|
|
@@ -15680,9 +17098,9 @@ function linkDemoPins(work, probes = {}) {
|
|
|
15680
17098
|
remedy: `Install ${pkg} beside behold, then load the demo again.`
|
|
15681
17099
|
};
|
|
15682
17100
|
}
|
|
15683
|
-
const link =
|
|
17101
|
+
const link = join34(work, "node_modules", ...pkg.split("/"));
|
|
15684
17102
|
if (!present(link)) {
|
|
15685
|
-
|
|
17103
|
+
mkdirSync7(dirname12(link), { recursive: true });
|
|
15686
17104
|
symlinkSync2(found.dir, link, "junction");
|
|
15687
17105
|
} else if (!linksTo(link, found.dir)) {
|
|
15688
17106
|
continue;
|
|
@@ -15708,26 +17126,26 @@ function present(path) {
|
|
|
15708
17126
|
}
|
|
15709
17127
|
function linksTo(link, target) {
|
|
15710
17128
|
try {
|
|
15711
|
-
return lstatSync2(link).isSymbolicLink() &&
|
|
17129
|
+
return lstatSync2(link).isSymbolicLink() && resolve23(dirname12(link), readlinkSync2(link)) === resolve23(target);
|
|
15712
17130
|
} catch {
|
|
15713
17131
|
return false;
|
|
15714
17132
|
}
|
|
15715
17133
|
}
|
|
15716
17134
|
|
|
15717
17135
|
// src/demos.ts
|
|
15718
|
-
import { readFileSync as
|
|
15719
|
-
import { dirname as
|
|
17136
|
+
import { readFileSync as readFileSync24, existsSync as existsSync21, cpSync, mkdirSync as mkdirSync8 } from "node:fs";
|
|
17137
|
+
import { dirname as dirname13, isAbsolute as isAbsolute5, join as join35, relative as relative8, resolve as resolve24, sep as sep7 } from "node:path";
|
|
15720
17138
|
import { spawn as spawn7, spawnSync as spawnSync2 } from "node:child_process";
|
|
15721
17139
|
function readCatalog(file, catalog) {
|
|
15722
17140
|
let raw;
|
|
15723
17141
|
try {
|
|
15724
|
-
raw = JSON.parse(
|
|
17142
|
+
raw = JSON.parse(readFileSync24(file, "utf8"));
|
|
15725
17143
|
} catch {
|
|
15726
17144
|
return [];
|
|
15727
17145
|
}
|
|
15728
17146
|
const list3 = raw?.demos;
|
|
15729
17147
|
if (!Array.isArray(list3)) return [];
|
|
15730
|
-
const catalogDir =
|
|
17148
|
+
const catalogDir = dirname13(resolve24(file));
|
|
15731
17149
|
return list3.filter((e) => validEntry(e)).map((e) => ({ ...e, catalog, catalogDir }));
|
|
15732
17150
|
}
|
|
15733
17151
|
function validEntry(e) {
|
|
@@ -15761,10 +17179,10 @@ function validEntry(e) {
|
|
|
15761
17179
|
return true;
|
|
15762
17180
|
}
|
|
15763
17181
|
function loadDemoRegistry(pkgRoot3) {
|
|
15764
|
-
const merged = readCatalog(
|
|
17182
|
+
const merged = readCatalog(join35(pkgRoot3, "demos.json"), "demos");
|
|
15765
17183
|
const seen = new Set(merged.map((e) => e.name));
|
|
15766
17184
|
const named = process.env.BEHOLD_WORKBENCH;
|
|
15767
|
-
for (const e of [...readCatalog(
|
|
17185
|
+
for (const e of [...readCatalog(join35(pkgRoot3, "workbench.json"), "workbench"), ...named ? readCatalog(resolve24(named), "workbench") : []]) {
|
|
15768
17186
|
if (seen.has(e.name)) {
|
|
15769
17187
|
process.stderr.write(`behold: workbench entry "${e.name}" collides with a demo already in the catalog \u2014 dropped
|
|
15770
17188
|
`);
|
|
@@ -15777,17 +17195,17 @@ function loadDemoRegistry(pkgRoot3) {
|
|
|
15777
17195
|
}
|
|
15778
17196
|
function demoLocalPath(entry) {
|
|
15779
17197
|
if (entry.source !== "local" || !entry.path) return void 0;
|
|
15780
|
-
return
|
|
17198
|
+
return isAbsolute5(entry.path) ? entry.path : resolve24(entry.catalogDir ?? process.cwd(), entry.path);
|
|
15781
17199
|
}
|
|
15782
17200
|
function missingRequirements(entry) {
|
|
15783
17201
|
const bins = entry.source === "git" && !entry.requires.includes("git") ? [...entry.requires, "git"] : entry.requires;
|
|
15784
17202
|
const finder = process.platform === "win32" ? "where" : "which";
|
|
15785
17203
|
const missing = bins.filter((bin) => {
|
|
15786
|
-
if (bin === "choudoufu" && process.env.CHOUDOUFU_BIN &&
|
|
17204
|
+
if (bin === "choudoufu" && process.env.CHOUDOUFU_BIN && existsSync21(process.env.CHOUDOUFU_BIN)) return false;
|
|
15787
17205
|
return spawnSync2(finder, [bin], { stdio: "ignore" }).status !== 0;
|
|
15788
17206
|
});
|
|
15789
17207
|
const local = demoLocalPath(entry);
|
|
15790
|
-
if (local && !
|
|
17208
|
+
if (local && !existsSync21(local)) missing.push(`${entry.path} (not checked out)`);
|
|
15791
17209
|
return missing;
|
|
15792
17210
|
}
|
|
15793
17211
|
function fetchesFromNetwork(entry) {
|
|
@@ -15796,9 +17214,9 @@ function fetchesFromNetwork(entry) {
|
|
|
15796
17214
|
function demoTargetDir(entry, cwd = process.cwd()) {
|
|
15797
17215
|
const inPlace = entry.inPlace ? demoLocalPath(entry) : void 0;
|
|
15798
17216
|
if (inPlace) return inPlace;
|
|
15799
|
-
const legacy =
|
|
15800
|
-
if (entry.name === "writes" &&
|
|
15801
|
-
return
|
|
17217
|
+
const legacy = resolve24(cwd, "behold-demo");
|
|
17218
|
+
if (entry.name === "writes" && existsSync21(legacy)) return legacy;
|
|
17219
|
+
return resolve24(cwd, "behold-demos", entry.name);
|
|
15802
17220
|
}
|
|
15803
17221
|
async function loadDemo(entry, opts) {
|
|
15804
17222
|
const { pkgRoot: pkgRoot3, target } = opts;
|
|
@@ -15807,12 +17225,12 @@ async function loadDemo(entry, opts) {
|
|
|
15807
17225
|
const inPlace = entry.source === "local" && entry.inPlace;
|
|
15808
17226
|
const work = inPlace ? local : target;
|
|
15809
17227
|
if (inPlace) {
|
|
15810
|
-
if (!
|
|
17228
|
+
if (!existsSync21(work)) return { ok: false, error: `${entry.path} is not checked out (looked in ${work})` };
|
|
15811
17229
|
say(`serving ${work} where it is \u2014 nothing copied`);
|
|
15812
|
-
} else if (!
|
|
17230
|
+
} else if (!existsSync21(target)) {
|
|
15813
17231
|
if (entry.source === "bundled" || entry.source === "local" && local) {
|
|
15814
|
-
const from = entry.source === "bundled" ?
|
|
15815
|
-
if (!
|
|
17232
|
+
const from = entry.source === "bundled" ? join35(pkgRoot3, entry.dir) : local;
|
|
17233
|
+
if (!existsSync21(from)) return { ok: false, error: entry.source === "bundled" ? `this install has no bundled ${entry.dir}` : `${entry.path} is not checked out (looked in ${from})` };
|
|
15816
17234
|
say(`copying to ${target} (it's yours \u2014 edit it)`);
|
|
15817
17235
|
try {
|
|
15818
17236
|
cpSync(from, target, {
|
|
@@ -15825,7 +17243,7 @@ async function loadDemo(entry, opts) {
|
|
|
15825
17243
|
} else if (entry.source === "local") {
|
|
15826
17244
|
say(`rendering into ${target} (empty \u2014 the setup writes the estate)`);
|
|
15827
17245
|
try {
|
|
15828
|
-
|
|
17246
|
+
mkdirSync8(target, { recursive: true });
|
|
15829
17247
|
} catch (err) {
|
|
15830
17248
|
return { ok: false, error: `could not make ${target}: ${err instanceof Error ? err.message : String(err)}` };
|
|
15831
17249
|
}
|
|
@@ -15843,7 +17261,7 @@ async function loadDemo(entry, opts) {
|
|
|
15843
17261
|
if (!pins.ok) return { ok: false, error: `${pins.error} ${pins.remedy}` };
|
|
15844
17262
|
for (const p of pins.linked) say(`linked ${p.pkg} ${p.version} into the copy${p.was ? ` (the declaration pinned ${p.was}; now ${p.version})` : ""}`);
|
|
15845
17263
|
}
|
|
15846
|
-
if (!inPlace &&
|
|
17264
|
+
if (!inPlace && existsSync21(join35(work, "package.json")) && !existsSync21(join35(work, "node_modules"))) {
|
|
15847
17265
|
say("npm install\u2026");
|
|
15848
17266
|
if (await runStep("npm", ["install"], { cwd: work, shell: process.platform === "win32" }) !== 0) {
|
|
15849
17267
|
return { ok: false, error: `npm install failed in ${work}` };
|
|
@@ -15860,7 +17278,7 @@ async function loadDemo(entry, opts) {
|
|
|
15860
17278
|
return { ok: false, error: `setup failed (${entry.setup})` };
|
|
15861
17279
|
}
|
|
15862
17280
|
}
|
|
15863
|
-
return { ok: true, serveDirs: entry.serve.dirs?.length ? entry.serve.dirs.map((d) =>
|
|
17281
|
+
return { ok: true, serveDirs: entry.serve.dirs?.length ? entry.serve.dirs.map((d) => join35(work, d)) : [work] };
|
|
15864
17282
|
}
|
|
15865
17283
|
function runStep(cmd, args, opts = {}) {
|
|
15866
17284
|
return new Promise((res) => {
|
|
@@ -15871,9 +17289,9 @@ function runStep(cmd, args, opts = {}) {
|
|
|
15871
17289
|
}
|
|
15872
17290
|
|
|
15873
17291
|
// src/server.ts
|
|
15874
|
-
var pkgRoot =
|
|
15875
|
-
var webRoot =
|
|
15876
|
-
var execFileP = async (cmd, args) => (await promisify3(
|
|
17292
|
+
var pkgRoot = join36(dirname14(fileURLToPath8(import.meta.url)), "..");
|
|
17293
|
+
var webRoot = join36(pkgRoot, "web");
|
|
17294
|
+
var execFileP = async (cmd, args) => (await promisify3(execFile4)(cmd, args, { encoding: "utf8", timeout: 1e4 })).stdout;
|
|
15877
17295
|
function optsFromQuery(url, tierEnvVar, projectDir) {
|
|
15878
17296
|
const q = url.searchParams;
|
|
15879
17297
|
const opts = {};
|
|
@@ -15913,17 +17331,17 @@ async function knownComponents(projectDir, opts) {
|
|
|
15913
17331
|
return void 0;
|
|
15914
17332
|
}
|
|
15915
17333
|
}
|
|
15916
|
-
function tierFailure(tier,
|
|
17334
|
+
function tierFailure(tier, message2) {
|
|
15917
17335
|
return {
|
|
15918
17336
|
code: "tier",
|
|
15919
|
-
error: `chant couldn't evaluate the "${tier}" tier here: ${
|
|
17337
|
+
error: `chant couldn't evaluate the "${tier}" tier here: ${message2}`,
|
|
15920
17338
|
remedy: `A non-default tier (e.g. a production-only one) can need parameters \u2014 real credentials, a different target \u2014 this environment doesn't have. Pick a different tier to see its graph.`
|
|
15921
17339
|
};
|
|
15922
17340
|
}
|
|
15923
17341
|
function beholdVersion() {
|
|
15924
17342
|
try {
|
|
15925
|
-
const here =
|
|
15926
|
-
return JSON.parse(
|
|
17343
|
+
const here = dirname14(fileURLToPath8(import.meta.url));
|
|
17344
|
+
return JSON.parse(readFileSync25(join36(here, "..", "package.json"), "utf8")).version ?? "unknown";
|
|
15927
17345
|
} catch {
|
|
15928
17346
|
return "unknown";
|
|
15929
17347
|
}
|
|
@@ -15937,8 +17355,8 @@ function noProjectError(projectDir) {
|
|
|
15937
17355
|
}
|
|
15938
17356
|
function errorResponse(c, opts, err) {
|
|
15939
17357
|
if (err instanceof TerraformReadError) return c.json(err.refusal, 500);
|
|
15940
|
-
const
|
|
15941
|
-
const failure = err instanceof ChantCliError ? err.failure : classifyChantFailure(
|
|
17358
|
+
const message2 = err instanceof Error ? err.message : String(err);
|
|
17359
|
+
const failure = err instanceof ChantCliError ? err.failure : classifyChantFailure(message2);
|
|
15942
17360
|
const routeError = opts.tier && failure.code !== "not-installed" ? tierFailure(opts.tier, failure.message) : { error: failure.message, code: failure.code, remedy: failure.remedy };
|
|
15943
17361
|
return c.json(routeError, 500);
|
|
15944
17362
|
}
|
|
@@ -15947,13 +17365,13 @@ function deployAxes(tierEnvVar, lexicons = [], k8sTarget) {
|
|
|
15947
17365
|
if (tierEnvVar && process.env[tierEnvVar]) axes.tier = process.env[tierEnvVar];
|
|
15948
17366
|
const targets = [...resolveSubstrateTargets(lexicons), ...k8sTarget ? [k8sTarget] : []];
|
|
15949
17367
|
if (targets.length === 1) axes.target = targets[0].endpoint;
|
|
15950
|
-
else if (targets.length > 1) axes.target = targets.map((
|
|
17368
|
+
else if (targets.length > 1) axes.target = targets.map((t2) => `${t2.label}=${t2.endpoint}`).join(" ");
|
|
15951
17369
|
return axes;
|
|
15952
17370
|
}
|
|
15953
17371
|
function deployTargets(lexicons = [], k8sTarget) {
|
|
15954
|
-
return [...resolveSubstrateTargets(lexicons), ...k8sTarget ? [k8sTarget] : []].map((
|
|
15955
|
-
name:
|
|
15956
|
-
endpoint:
|
|
17372
|
+
return [...resolveSubstrateTargets(lexicons), ...k8sTarget ? [k8sTarget] : []].map((t2) => ({
|
|
17373
|
+
name: t2.label,
|
|
17374
|
+
endpoint: t2.endpoint
|
|
15957
17375
|
}));
|
|
15958
17376
|
}
|
|
15959
17377
|
async function captureFrame(projectDir, env, frames, broadcaster) {
|
|
@@ -15970,8 +17388,8 @@ async function captureFrame(projectDir, env, frames, broadcaster) {
|
|
|
15970
17388
|
}
|
|
15971
17389
|
}
|
|
15972
17390
|
function carveRoutes(app, reportPath, demo) {
|
|
15973
|
-
const load2 = () => readCarveReport(reportPath, (p) =>
|
|
15974
|
-
const carveDirs = [
|
|
17391
|
+
const load2 = () => readCarveReport(reportPath, (p) => readFileSync25(p, "utf8"));
|
|
17392
|
+
const carveDirs = [dirname14(reportPath), ...demo ? [demo.out] : []];
|
|
15975
17393
|
const carveStates = () => readCarveStates(carveDirs, nodeCarveIo, 0);
|
|
15976
17394
|
const emittedNodes = async (states) => {
|
|
15977
17395
|
if (!demo || states.size === 0) return /* @__PURE__ */ new Map();
|
|
@@ -16195,7 +17613,7 @@ async function readoptDispatchedRun(cfg, runner) {
|
|
|
16195
17613
|
onLine: io.line,
|
|
16196
17614
|
onProgress: io.progress,
|
|
16197
17615
|
onAdopted: ({ runId, url }) => saveDispatchedRun({ ...run3, runId, ...url ? { url } : {} }),
|
|
16198
|
-
onConcluded: ({ verdict:
|
|
17616
|
+
onConcluded: ({ verdict: verdict3 }) => concludeDispatchedRun(cfg.projectDir, run3.runId, verdict3)
|
|
16199
17617
|
});
|
|
16200
17618
|
});
|
|
16201
17619
|
if (!started) return { outcome: "refused", reason: `busy \u2014 ${runner.running} is running`, run: run3 };
|
|
@@ -16214,7 +17632,7 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
|
|
|
16214
17632
|
const app = new Hono();
|
|
16215
17633
|
const guard = { allowedHosts: allowedHostsFrom(cfg.allowedHosts ?? []), ...cfg.host ? { boundHost: cfg.host } : {} };
|
|
16216
17634
|
app.use("*", async (c, next) => {
|
|
16217
|
-
const
|
|
17635
|
+
const verdict3 = guardRequest(
|
|
16218
17636
|
{
|
|
16219
17637
|
method: c.req.method,
|
|
16220
17638
|
host: c.req.header("host") ?? new URL(c.req.url).host,
|
|
@@ -16223,7 +17641,7 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
|
|
|
16223
17641
|
},
|
|
16224
17642
|
guard
|
|
16225
17643
|
);
|
|
16226
|
-
if (!
|
|
17644
|
+
if (!verdict3.ok) return c.json({ error: verdict3.error, code: "request-refused" }, verdict3.status);
|
|
16227
17645
|
await next();
|
|
16228
17646
|
});
|
|
16229
17647
|
setChoudoufuRunner(cfg.choudoufu?.run);
|
|
@@ -16242,6 +17660,7 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
|
|
|
16242
17660
|
await next();
|
|
16243
17661
|
});
|
|
16244
17662
|
if (cfg.terragucci) terragucciRoutes(app, cfg.terragucci, () => cfg.projectDirs ?? [cfg.projectDir]);
|
|
17663
|
+
if (cfg.terragucci) terragucciEstateRoutes(app, cfg.terragucci, () => cfg.projectDirs ?? [cfg.projectDir]);
|
|
16245
17664
|
if (cfg.carveReport) carveRoutes(app, cfg.carveReport, cfg.carveDemo);
|
|
16246
17665
|
const predictDeps = cfg.behaviour ?? { read: graphIr };
|
|
16247
17666
|
let beholdConfig = loadBeholdConfig(cfg.projectDir);
|
|
@@ -16354,9 +17773,9 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
|
|
|
16354
17773
|
return c.json({ started: true, name, ran: label });
|
|
16355
17774
|
});
|
|
16356
17775
|
app.post("/api/local/reset", (c) => {
|
|
16357
|
-
const down =
|
|
16358
|
-
const up =
|
|
16359
|
-
if (!
|
|
17776
|
+
const down = join36(cfg.projectDir, "scripts/local/local-down.sh");
|
|
17777
|
+
const up = join36(cfg.projectDir, "scripts/local/local-up.sh");
|
|
17778
|
+
if (!existsSync22(down) || !existsSync22(up)) {
|
|
16360
17779
|
return c.json({ error: "no local-down.sh / local-up.sh in scripts/local \u2014 reset is only for local emulator projects" }, 400);
|
|
16361
17780
|
}
|
|
16362
17781
|
if (!runner.bringUp("reset local emulator", "bash", ["-c", "bash scripts/local/local-down.sh && bash scripts/local/local-up.sh"], cfg.projectDir)) {
|
|
@@ -16377,7 +17796,7 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
|
|
|
16377
17796
|
const dirs = [];
|
|
16378
17797
|
const declared = [];
|
|
16379
17798
|
for (const f of discoverOpIrs(estateDirs)) {
|
|
16380
|
-
const parsed = readOpIr(f.path, (p) =>
|
|
17799
|
+
const parsed = readOpIr(f.path, (p) => readFileSync25(p, "utf8"));
|
|
16381
17800
|
if (!parsed.ok) continue;
|
|
16382
17801
|
const mine = declaredConvergeOps([parsed.ir]);
|
|
16383
17802
|
if (!mine.length) continue;
|
|
@@ -16526,7 +17945,7 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
|
|
|
16526
17945
|
});
|
|
16527
17946
|
app.get(
|
|
16528
17947
|
"/api/events",
|
|
16529
|
-
(c) =>
|
|
17948
|
+
(c) => streamSSE2(c, async (stream) => {
|
|
16530
17949
|
const unsubscribe = broadcaster.subscribe((type, data) => {
|
|
16531
17950
|
void stream.writeSSE({ event: type, data: data || String(Date.now()) });
|
|
16532
17951
|
});
|
|
@@ -16595,6 +18014,11 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
|
|
|
16595
18014
|
...tgConfig() ? { terragucci: { config: tgConfig(), ...terragucciRefusal(tgConfig(), "deploying from behold") } } : {},
|
|
16596
18015
|
// #490: the page asks /api/terragucci for the marks when this is set.
|
|
16597
18016
|
...cfg.terragucci ? { terragucciReports: { source: cfg.terragucci.source, ...cfg.terragucci.project ? { project: cfg.terragucci.project } : {} } } : {},
|
|
18017
|
+
// #509: a control repo (`projects:`, no roots): the page asks /api/terragucci/estate.
|
|
18018
|
+
...(() => {
|
|
18019
|
+
const control = cfg.terragucci ? controlRepoInfo(cfg.projectDirs ?? [cfg.projectDir]) : void 0;
|
|
18020
|
+
return control ? { terragucciControl: control } : {};
|
|
18021
|
+
})(),
|
|
16598
18022
|
// #477: who `chant approve` records when behold runs it. The approve
|
|
16599
18023
|
// buttons say it before the click.
|
|
16600
18024
|
approver: localApprover(),
|
|
@@ -16651,9 +18075,9 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
|
|
|
16651
18075
|
app.post("/api/project/open", async (c) => {
|
|
16652
18076
|
if (cfg.previewMode) return c.json({ error: "switching projects is locked in preview mode" }, 403);
|
|
16653
18077
|
const body = await c.req.json().catch(() => ({}));
|
|
16654
|
-
const dir = typeof body.dir === "string" && body.dir.trim() ?
|
|
16655
|
-
if (!dir || !
|
|
16656
|
-
if (!
|
|
18078
|
+
const dir = typeof body.dir === "string" && body.dir.trim() ? resolve25(body.dir.trim()) : "";
|
|
18079
|
+
if (!dir || !existsSync22(dir)) return c.json({ error: `no such directory: ${dir || "(no dir given)"}` }, 400);
|
|
18080
|
+
if (!existsSync22(join36(dir, "chant.config.ts"))) {
|
|
16657
18081
|
return c.json({ error: `${dir} doesn't look like a chant project \u2014 no chant.config.ts` }, 400);
|
|
16658
18082
|
}
|
|
16659
18083
|
switchServedProject([dir]);
|
|
@@ -16676,7 +18100,7 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
|
|
|
16676
18100
|
};
|
|
16677
18101
|
app.post("/api/project/reveal", async (c) => {
|
|
16678
18102
|
const body = await c.req.json().catch(() => ({}));
|
|
16679
|
-
const dir = typeof body.dir === "string" && body.dir.trim() ?
|
|
18103
|
+
const dir = typeof body.dir === "string" && body.dir.trim() ? resolve25(body.dir.trim()) : cfg.projectDir;
|
|
16680
18104
|
const known = /* @__PURE__ */ new Set([cfg.projectDir, ...cfg.projectDirs ?? [], ...listRecents().map((r) => r.dir)]);
|
|
16681
18105
|
if (!known.has(dir)) return c.json({ error: "not a served or recent project directory" }, 400);
|
|
16682
18106
|
const opener = process.platform === "darwin" ? "open" : process.platform === "win32" ? "explorer" : "xdg-open";
|
|
@@ -16704,7 +18128,7 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
|
|
|
16704
18128
|
fetches: fetchesFromNetwork(e),
|
|
16705
18129
|
...e.repo ? { repo: e.repo } : {},
|
|
16706
18130
|
target,
|
|
16707
|
-
loaded:
|
|
18131
|
+
loaded: existsSync22(target),
|
|
16708
18132
|
satisfiable: missing.length === 0,
|
|
16709
18133
|
// #254: the carve walkthrough runs fine here — it just isn't a project
|
|
16710
18134
|
// to switch INTO. Carve mode claims `/api/graph` and `/api/project` at
|
|
@@ -16956,7 +18380,7 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
|
|
|
16956
18380
|
}
|
|
16957
18381
|
const parsedOps = [];
|
|
16958
18382
|
for (const f of files) {
|
|
16959
|
-
const parsed = readOpIr(f.path, (p) =>
|
|
18383
|
+
const parsed = readOpIr(f.path, (p) => readFileSync25(p, "utf8"));
|
|
16960
18384
|
if (!parsed.ok) return c.json(parsed.refusal, 422);
|
|
16961
18385
|
parsedOps.push(parsed.ir);
|
|
16962
18386
|
}
|
|
@@ -16969,8 +18393,8 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
|
|
|
16969
18393
|
}
|
|
16970
18394
|
const runState = runner.runState;
|
|
16971
18395
|
const ranOp = runState.op ? parsedOps.find((o) => o.name === runState.op) : void 0;
|
|
16972
|
-
const
|
|
16973
|
-
const opsIr =
|
|
18396
|
+
const join41 = ranOp ? joinRun(ranOp, runState.records, runState.gate) : null;
|
|
18397
|
+
const opsIr = join41 ? paintPlayhead(declaredIr, join41, runState.gate) : declaredIr;
|
|
16974
18398
|
const { svg: svg2 } = renderGraph(opsIr, { boxes: "byStack" });
|
|
16975
18399
|
const operator = runner.noteDeclaredConvergeOps(declaredConvergeOps(parsedOps));
|
|
16976
18400
|
return c.json({
|
|
@@ -16983,7 +18407,7 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
|
|
|
16983
18407
|
target: null,
|
|
16984
18408
|
mode: "ops",
|
|
16985
18409
|
ops: parsedOps.length,
|
|
16986
|
-
note: `${opsNote(parsedOps, opsIr)} ${playheadNote(runState,
|
|
18410
|
+
note: `${opsNote(parsedOps, opsIr)} ${playheadNote(runState, join41)} ${operatorNote(operator)}`,
|
|
16987
18411
|
// The operator strip's own model: the ConvergeOps this project
|
|
16988
18412
|
// declares, plus whatever the last status read said. `declared`
|
|
16989
18413
|
// without a `strip` is the honest starting state — a loop exists and
|
|
@@ -17094,7 +18518,7 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
|
|
|
17094
18518
|
srcCompositeEdgesAttached = 0;
|
|
17095
18519
|
}
|
|
17096
18520
|
}
|
|
17097
|
-
if (!multi && !components && !logical && !opts.lens && !ownKindVia && ir.nodes.length === 0 && !
|
|
18521
|
+
if (!multi && !components && !logical && !opts.lens && !ownKindVia && ir.nodes.length === 0 && !existsSync22(join36(cfg.projectDir, "chant.config.ts"))) {
|
|
17098
18522
|
return c.json(noProjectError(cfg.projectDir), 404);
|
|
17099
18523
|
}
|
|
17100
18524
|
const radial = new URL(c.req.url).searchParams.get("radial") === "1";
|
|
@@ -17199,8 +18623,8 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
|
|
|
17199
18623
|
adoptedId = runId;
|
|
17200
18624
|
saveDispatchedRun({ ...base, runId, ...url ? { url } : {} });
|
|
17201
18625
|
},
|
|
17202
|
-
onConcluded: ({ verdict:
|
|
17203
|
-
if (adoptedId !== void 0) concludeDispatchedRun(cfg.projectDir, adoptedId,
|
|
18626
|
+
onConcluded: ({ verdict: verdict3 }) => {
|
|
18627
|
+
if (adoptedId !== void 0) concludeDispatchedRun(cfg.projectDir, adoptedId, verdict3);
|
|
17204
18628
|
}
|
|
17205
18629
|
})
|
|
17206
18630
|
);
|
|
@@ -17575,12 +18999,12 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
|
|
|
17575
18999
|
}
|
|
17576
19000
|
for (const id of ids) {
|
|
17577
19001
|
const observed = nodeObserved(parsed, id);
|
|
17578
|
-
const
|
|
19002
|
+
const verdict3 = classifyObservedHealth(observed);
|
|
17579
19003
|
nodes[id] = {
|
|
17580
19004
|
observed,
|
|
17581
19005
|
diff: nodeDiff(parsed, id),
|
|
17582
|
-
health:
|
|
17583
|
-
...
|
|
19006
|
+
health: verdict3.health,
|
|
19007
|
+
...verdict3.detail ? { healthDetail: verdict3.detail } : {},
|
|
17584
19008
|
// Field-level (per-manager) drift (#87, chant#1181) — null when no
|
|
17585
19009
|
// lexicon in this diff carries a `deep` section at all.
|
|
17586
19010
|
fieldDrift: nodeFieldDrift(parsed, id)
|
|
@@ -17590,9 +19014,9 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
|
|
|
17590
19014
|
});
|
|
17591
19015
|
const choudoufuPlanPath = (plan) => {
|
|
17592
19016
|
if (!plan) return { error: "no plan named", code: "choudoufu_no_plan", remedy: "pass ?plan=<path to a carve.json inside a served choudoufu member> \u2014 GET /api/project lists the ones found" };
|
|
17593
|
-
const abs =
|
|
19017
|
+
const abs = resolve25(cfg.projectDir, plan);
|
|
17594
19018
|
const members = (cfg.projectDirs ?? [cfg.projectDir]).filter((d) => memberKindOf(d) === "choudoufu");
|
|
17595
|
-
const inside = members.some((d) => abs.startsWith(
|
|
19019
|
+
const inside = members.some((d) => abs.startsWith(resolve25(d) + sep8));
|
|
17596
19020
|
if (!inside) return { error: `${plan} is not inside a served choudoufu member`, code: "choudoufu_plan_outside", remedy: "a plan lives in the estate it moves; point ?plan= at a carve.json under one of the served members" };
|
|
17597
19021
|
return { path: abs };
|
|
17598
19022
|
};
|
|
@@ -17664,14 +19088,14 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
|
|
|
17664
19088
|
return c.json({ error: "diff output was not JSON \u2014 chant may predate --live --json (needs 0.18.7+)" }, 500);
|
|
17665
19089
|
}
|
|
17666
19090
|
const observed = nodeObserved(parsed, node);
|
|
17667
|
-
const
|
|
19091
|
+
const verdict3 = classifyObservedHealth(observed);
|
|
17668
19092
|
return c.json({
|
|
17669
19093
|
node,
|
|
17670
19094
|
env,
|
|
17671
19095
|
diff: nodeDiff(parsed, node),
|
|
17672
19096
|
observed,
|
|
17673
|
-
health:
|
|
17674
|
-
...
|
|
19097
|
+
health: verdict3.health,
|
|
19098
|
+
...verdict3.detail ? { healthDetail: verdict3.detail } : {},
|
|
17675
19099
|
// Field-level (per-manager) drift (#87, chant#1181) — null when no
|
|
17676
19100
|
// lexicon in this diff carries a `deep` section at all.
|
|
17677
19101
|
fieldDrift: nodeFieldDrift(parsed, node)
|
|
@@ -17728,7 +19152,7 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
|
|
|
17728
19152
|
if (!c.req.path.startsWith("/api/") && c.res.ok && !c.res.headers.has("cache-control")) c.res.headers.set("cache-control", "no-cache");
|
|
17729
19153
|
});
|
|
17730
19154
|
app.use("/*", serveStatic({ root: rel }));
|
|
17731
|
-
app.get("/", serveStatic({ path:
|
|
19155
|
+
app.get("/", serveStatic({ path: join36(rel, "index.html") }));
|
|
17732
19156
|
return app;
|
|
17733
19157
|
}
|
|
17734
19158
|
async function bootLocalEmulators(dir, who) {
|
|
@@ -17757,7 +19181,7 @@ async function bootLocalEmulators(dir, who) {
|
|
|
17757
19181
|
async function startServer(cfg) {
|
|
17758
19182
|
if (cfg.local) cfg.emulators = await bootLocalEmulators(cfg.projectDir, "behold serve --local");
|
|
17759
19183
|
for (const dir of cfg.projectDirs ?? [cfg.projectDir]) {
|
|
17760
|
-
if (
|
|
19184
|
+
if (existsSync22(join36(dir, "chant.config.ts"))) addRecent(dir);
|
|
17761
19185
|
}
|
|
17762
19186
|
const broadcaster = new Broadcaster();
|
|
17763
19187
|
const frames = new FrameBuffer();
|
|
@@ -17790,7 +19214,7 @@ async function startServer(cfg) {
|
|
|
17790
19214
|
invalidateOverlay(memberDir);
|
|
17791
19215
|
onEstateChange(memberDir);
|
|
17792
19216
|
};
|
|
17793
|
-
const memberTag = (dir) => (cfg.projectDirs?.length ?? 0) > 1 ? ` [${
|
|
19217
|
+
const memberTag = (dir) => (cfg.projectDirs?.length ?? 0) > 1 ? ` [${basename4(dir)}]` : "";
|
|
17794
19218
|
const onPollDrift = (dir, movedLexicons) => {
|
|
17795
19219
|
broadcaster.emit("changed", dir);
|
|
17796
19220
|
if (autoSync === "off") return;
|
|
@@ -17805,7 +19229,7 @@ async function startServer(cfg) {
|
|
|
17805
19229
|
const suspended = autoSync === "pull-request" ? suspendedByRollback(await openRollbackBranches(dir, cfg.env), movedLexicons) : /* @__PURE__ */ new Set();
|
|
17806
19230
|
const { routed, local } = splitForgeRouted(discoverEstateOps(cfg.projectDirs ?? [cfg.projectDir]), executorDesignation);
|
|
17807
19231
|
for (const { op: o, designation: d } of routed) {
|
|
17808
|
-
broadcaster.emit("op", `\u27F3 auto-sync (${autoSync})${memberTag(dir)} declined ${o.name}: env ${o.env} deploys through ${"invalid" in d ? "a forge" : d.forge} per ${
|
|
19232
|
+
broadcaster.emit("op", `\u27F3 auto-sync (${autoSync})${memberTag(dir)} declined ${o.name}: env ${o.env} deploys through ${"invalid" in d ? "a forge" : d.forge} per ${basename4(o.dir)}/.behold.json \u2014 never applied from here`);
|
|
17809
19233
|
}
|
|
17810
19234
|
const { picks, declined } = pickAutoSyncOps(
|
|
17811
19235
|
autoSync,
|
|
@@ -17930,8 +19354,8 @@ init_chant();
|
|
|
17930
19354
|
|
|
17931
19355
|
// src/workspace-convert.ts
|
|
17932
19356
|
init_chant();
|
|
17933
|
-
import { existsSync as
|
|
17934
|
-
import { basename as
|
|
19357
|
+
import { existsSync as existsSync23, mkdirSync as mkdirSync9, readFileSync as readFileSync26, renameSync as renameSync2, writeFileSync as writeFileSync7 } from "node:fs";
|
|
19358
|
+
import { basename as basename5, dirname as dirname15, join as join37, relative as relative10, resolve as resolve26, sep as sep9 } from "node:path";
|
|
17935
19359
|
import { shortStackNames as shortStackNames2 } from "@intentius/pinhole";
|
|
17936
19360
|
init_project();
|
|
17937
19361
|
init_terraform_member();
|
|
@@ -17948,28 +19372,28 @@ function unique(name, taken) {
|
|
|
17948
19372
|
}
|
|
17949
19373
|
var posix = (p) => p.split(sep9).join("/");
|
|
17950
19374
|
function installedVersion(pkg, dir) {
|
|
17951
|
-
for (let
|
|
17952
|
-
const manifest =
|
|
17953
|
-
if (
|
|
19375
|
+
for (let at2 = resolve26(dir); ; at2 = dirname15(at2)) {
|
|
19376
|
+
const manifest = join37(at2, "node_modules", ...pkg.split("/"), "package.json");
|
|
19377
|
+
if (existsSync23(manifest)) {
|
|
17954
19378
|
try {
|
|
17955
|
-
const v = JSON.parse(
|
|
19379
|
+
const v = JSON.parse(readFileSync26(manifest, "utf8")).version;
|
|
17956
19380
|
return typeof v === "string" ? v : void 0;
|
|
17957
19381
|
} catch {
|
|
17958
19382
|
return void 0;
|
|
17959
19383
|
}
|
|
17960
19384
|
}
|
|
17961
|
-
if (
|
|
19385
|
+
if (dirname15(at2) === at2) return void 0;
|
|
17962
19386
|
}
|
|
17963
19387
|
}
|
|
17964
19388
|
function planConversion(rootArg) {
|
|
17965
|
-
const root =
|
|
17966
|
-
const existing = DECLARATION_FILES.find((f) =>
|
|
19389
|
+
const root = resolve26(rootArg);
|
|
19390
|
+
const existing = DECLARATION_FILES.find((f) => existsSync23(join37(root, f)));
|
|
17967
19391
|
if (existing) return { ok: false, error: `${root} already has ${existing}; there is nothing to convert.` };
|
|
17968
19392
|
const shape = detectProjectShape(root);
|
|
17969
19393
|
if (shape.kind !== "estate" || !shape.members?.length || shape.membersFrom === "itself") {
|
|
17970
19394
|
return { ok: false, error: `${root} is not an estate root with a member list, so there is no member list to convert.` };
|
|
17971
19395
|
}
|
|
17972
|
-
const abs = shape.members.map((m) =>
|
|
19396
|
+
const abs = shape.members.map((m) => resolve26(root, m.dir));
|
|
17973
19397
|
const shorts = shortStackNames2(abs);
|
|
17974
19398
|
const taken = /* @__PURE__ */ new Set();
|
|
17975
19399
|
const members = [];
|
|
@@ -17984,10 +19408,10 @@ function planConversion(rootArg) {
|
|
|
17984
19408
|
const scan = discoverTerraformRoots(abs[i]);
|
|
17985
19409
|
for (const r of scan.roots) {
|
|
17986
19410
|
const name2 = unique(declarationName(r.name), taken);
|
|
17987
|
-
members.push({ name: name2, dir: r.dir === "." ? dir : posix(
|
|
19411
|
+
members.push({ name: name2, dir: r.dir === "." ? dir : posix(join37(dir, r.dir)), kind: "terraform" });
|
|
17988
19412
|
moves.push({ from: `${short}/${r.name}/`, to: `${name2}/${name2}/` });
|
|
17989
19413
|
}
|
|
17990
|
-
for (const s of scan.skipped) notes.push(`${posix(
|
|
19414
|
+
for (const s of scan.skipped) notes.push(`${posix(join37(dir, s.dir))} is not declared: ${s.why}.`);
|
|
17991
19415
|
if (scan.roots.length > 1) notes.push(`${dir} held ${scan.roots.length} Terraform roots; each is its own member now, since a chant terraform member is one root.`);
|
|
17992
19416
|
return;
|
|
17993
19417
|
}
|
|
@@ -18008,14 +19432,14 @@ function planConversion(rootArg) {
|
|
|
18008
19432
|
else notes.push(`${TERRAFORM_LEXICON2} is not installed where chant looks from ${root}; install it there and add it to "pins", or chant cannot read the terraform and choudoufu members.`);
|
|
18009
19433
|
}
|
|
18010
19434
|
const declaration = {
|
|
18011
|
-
name: declarationName(
|
|
19435
|
+
name: declarationName(basename5(root)),
|
|
18012
19436
|
schema: 1,
|
|
18013
19437
|
...pins.length ? { pins } : {},
|
|
18014
19438
|
members
|
|
18015
19439
|
};
|
|
18016
19440
|
const firstLayout = layoutPath(abs[0]);
|
|
18017
19441
|
moves.sort((a, b) => b.from.length - a.from.length);
|
|
18018
|
-
return { ok: true, plan: { root, from: shape.membersFrom === "behold-config" ? "behold-config" : "workspaces", declaration, moves, ...
|
|
19442
|
+
return { ok: true, plan: { root, from: shape.membersFrom === "behold-config" ? "behold-config" : "workspaces", declaration, moves, ...existsSync23(firstLayout) ? { layoutFrom: firstLayout } : {}, notes } };
|
|
18019
19443
|
}
|
|
18020
19444
|
function moveLayoutIds(file, moves) {
|
|
18021
19445
|
const lenses = {};
|
|
@@ -18030,18 +19454,18 @@ function moveLayoutIds(file, moves) {
|
|
|
18030
19454
|
return { version: 1, lenses };
|
|
18031
19455
|
}
|
|
18032
19456
|
function applyConversion(plan) {
|
|
18033
|
-
const declarationFile =
|
|
18034
|
-
if (
|
|
19457
|
+
const declarationFile = join37(plan.root, DECLARATION_FILES[0]);
|
|
19458
|
+
if (existsSync23(declarationFile)) throw new Error(`${declarationFile} appeared while converting; nothing was written`);
|
|
18035
19459
|
writeFileSync7(declarationFile, `${JSON.stringify(plan.declaration, null, 2)}
|
|
18036
19460
|
`);
|
|
18037
19461
|
let layout;
|
|
18038
19462
|
const target = layoutPath(plan.root);
|
|
18039
19463
|
if (!plan.layoutFrom) layout = { skipped: "no saved layout to move" };
|
|
18040
|
-
else if (
|
|
18041
|
-
else if (
|
|
19464
|
+
else if (resolve26(plan.layoutFrom) === resolve26(target)) layout = { skipped: "the saved layout is already in the root" };
|
|
19465
|
+
else if (existsSync23(target)) layout = { skipped: `${target} already exists; the layout in ${plan.layoutFrom} was left alone` };
|
|
18042
19466
|
else {
|
|
18043
|
-
const moved = moveLayoutIds(readLayoutFile(
|
|
18044
|
-
|
|
19467
|
+
const moved = moveLayoutIds(readLayoutFile(dirname15(dirname15(plan.layoutFrom))), plan.moves);
|
|
19468
|
+
mkdirSync9(join37(plan.root, LAYOUT_DIR), { recursive: true });
|
|
18045
19469
|
const tmp = `${target}.tmp`;
|
|
18046
19470
|
writeFileSync7(tmp, `${JSON.stringify(moved, null, 2)}
|
|
18047
19471
|
`);
|
|
@@ -18052,11 +19476,11 @@ function applyConversion(plan) {
|
|
|
18052
19476
|
}
|
|
18053
19477
|
|
|
18054
19478
|
// src/export.ts
|
|
18055
|
-
import { mkdirSync as
|
|
19479
|
+
import { mkdirSync as mkdirSync10, writeFileSync as writeFileSync8, copyFileSync, cpSync as cpSync2, readFileSync as readFileSync27, readdirSync as readdirSync11, existsSync as existsSync24, realpathSync as realpathSync2 } from "node:fs";
|
|
18056
19480
|
import { homedir as homedir3 } from "node:os";
|
|
18057
|
-
import { join as
|
|
18058
|
-
import { createHash as
|
|
18059
|
-
import { fileURLToPath as
|
|
19481
|
+
import { join as join38, dirname as dirname16, basename as basename6, relative as relative11, resolve as resolve27, sep as sep10 } from "node:path";
|
|
19482
|
+
import { createHash as createHash7 } from "node:crypto";
|
|
19483
|
+
import { fileURLToPath as fileURLToPath9 } from "node:url";
|
|
18060
19484
|
var LENS_PARAMS = ["collapse", "components", "detail", "env", "logical", "ops", "radial", "stacks", "tier"];
|
|
18061
19485
|
function canonicalKey(path, params) {
|
|
18062
19486
|
const flat = params.get("components") === "1" || params.get("logical") === "1" || params.get("ops") === "1" || params.get("stacks") === "1";
|
|
@@ -18065,7 +19489,7 @@ function canonicalKey(path, params) {
|
|
|
18065
19489
|
}
|
|
18066
19490
|
function slug3(key, body) {
|
|
18067
19491
|
const base = key.replace(/^\//, "").replace(/[^a-zA-Z0-9=_.-]+/g, "_").slice(0, 120);
|
|
18068
|
-
return `${base}.${
|
|
19492
|
+
return `${base}.${createHash7("sha256").update(body).digest("hex").slice(0, 16)}.json`;
|
|
18069
19493
|
}
|
|
18070
19494
|
var CONTENT_ADDRESSED = /^snapshots\/[A-Za-z0-9=_.-]+\.[0-9a-f]{16}\.json$/;
|
|
18071
19495
|
function captureKeys(axes) {
|
|
@@ -18076,6 +19500,7 @@ function captureKeys(axes) {
|
|
|
18076
19500
|
add("/api/ops", {});
|
|
18077
19501
|
if (axes.ops) add("/api/graph", { ops: "1", entities: "1" });
|
|
18078
19502
|
if (axes.terragucci) add("/api/terragucci", {});
|
|
19503
|
+
if (axes.terragucci) add("/api/terragucci/timeline", {});
|
|
18079
19504
|
const tiers = axes.tiers && axes.tiers.length ? axes.tiers : [""];
|
|
18080
19505
|
const envs = ["", ...axes.environments];
|
|
18081
19506
|
for (const env of envs) {
|
|
@@ -18106,31 +19531,31 @@ function captureKeys(axes) {
|
|
|
18106
19531
|
return [...keys];
|
|
18107
19532
|
}
|
|
18108
19533
|
function webDir() {
|
|
18109
|
-
return
|
|
19534
|
+
return join38(dirname16(fileURLToPath9(import.meta.url)), "..", "web");
|
|
18110
19535
|
}
|
|
18111
19536
|
function pkgRoot2() {
|
|
18112
|
-
return
|
|
19537
|
+
return join38(dirname16(fileURLToPath9(import.meta.url)), "..");
|
|
18113
19538
|
}
|
|
18114
19539
|
var NOTICE_FILES = ["LICENSE", "THIRD_PARTY.md"];
|
|
18115
19540
|
function copyNotices(outDir, root = pkgRoot2()) {
|
|
18116
19541
|
const copied = [];
|
|
18117
19542
|
for (const f of NOTICE_FILES) {
|
|
18118
|
-
if (!
|
|
18119
|
-
copyFileSync(
|
|
19543
|
+
if (!existsSync24(join38(root, f))) continue;
|
|
19544
|
+
copyFileSync(join38(root, f), join38(outDir, f));
|
|
18120
19545
|
copied.push(f);
|
|
18121
19546
|
}
|
|
18122
|
-
const lic =
|
|
18123
|
-
if (
|
|
18124
|
-
|
|
18125
|
-
for (const f of
|
|
18126
|
-
copyFileSync(
|
|
19547
|
+
const lic = join38(root, "licenses");
|
|
19548
|
+
if (existsSync24(lic)) {
|
|
19549
|
+
mkdirSync10(join38(outDir, "licenses"), { recursive: true });
|
|
19550
|
+
for (const f of readdirSync11(lic)) {
|
|
19551
|
+
copyFileSync(join38(lic, f), join38(outDir, "licenses", f));
|
|
18127
19552
|
copied.push(`licenses/${f}`);
|
|
18128
19553
|
}
|
|
18129
19554
|
}
|
|
18130
19555
|
return copied;
|
|
18131
19556
|
}
|
|
18132
19557
|
function workerName(project, override) {
|
|
18133
|
-
const raw = override ?? `behold-${
|
|
19558
|
+
const raw = override ?? `behold-${basename6(project)}`;
|
|
18134
19559
|
const name = raw.toLowerCase().replace(/[^a-z0-9-]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 63);
|
|
18135
19560
|
return name || "behold-export";
|
|
18136
19561
|
}
|
|
@@ -18145,8 +19570,8 @@ async function runExport(cfg, outDir, opts = {}) {
|
|
|
18145
19570
|
...proj.terragucciReports ? { terragucci: true } : {}
|
|
18146
19571
|
};
|
|
18147
19572
|
const scrub = exportScrubber(cfg);
|
|
18148
|
-
const snapDir =
|
|
18149
|
-
|
|
19573
|
+
const snapDir = join38(outDir, "snapshots");
|
|
19574
|
+
mkdirSync10(snapDir, { recursive: true });
|
|
18150
19575
|
const keyToFile = {};
|
|
18151
19576
|
let ok = 0;
|
|
18152
19577
|
let failed = 0;
|
|
@@ -18154,45 +19579,46 @@ async function runExport(cfg, outDir, opts = {}) {
|
|
|
18154
19579
|
const res = await app.request(`${key}${key.includes("?") ? "&" : "?"}layout=1`);
|
|
18155
19580
|
const body = scrub(shapeSnapshot(key, await res.text(), opts));
|
|
18156
19581
|
const file = slug3(key, body);
|
|
18157
|
-
writeFileSync8(
|
|
19582
|
+
writeFileSync8(join38(snapDir, file), body);
|
|
18158
19583
|
keyToFile[key] = `snapshots/${file}`;
|
|
18159
19584
|
if (res.ok) ok++;
|
|
18160
19585
|
else failed++;
|
|
18161
19586
|
if (key === "/api/terragucci" && !res.ok) {
|
|
18162
|
-
const
|
|
18163
|
-
throw new Error(`terragucci reports not read: ${
|
|
18164
|
-
${
|
|
19587
|
+
const refusal2 = JSON.parse(body);
|
|
19588
|
+
throw new Error(`terragucci reports not read: ${refusal2.error ?? res.status}${refusal2.remedy ? `
|
|
19589
|
+
${refusal2.remedy}` : ""}`);
|
|
18165
19590
|
}
|
|
18166
19591
|
}
|
|
18167
19592
|
const manifest = {
|
|
18168
19593
|
static: true,
|
|
18169
19594
|
capturedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
18170
|
-
projectDir:
|
|
19595
|
+
projectDir: basename6(cfg.projectDir),
|
|
18171
19596
|
axes,
|
|
18172
19597
|
keyToFile
|
|
18173
19598
|
};
|
|
18174
|
-
writeFileSync8(
|
|
18175
|
-
const html =
|
|
19599
|
+
writeFileSync8(join38(outDir, "manifest.json"), JSON.stringify(manifest, null, 2));
|
|
19600
|
+
const html = readFileSync27(join38(webDir(), "index.html"), "utf8").replace(
|
|
18176
19601
|
/<\/head>/i,
|
|
18177
19602
|
` <script>window.__BEHOLD_STATIC__ = true;</script>
|
|
18178
19603
|
</head>`
|
|
18179
19604
|
);
|
|
18180
|
-
writeFileSync8(
|
|
18181
|
-
for (const f of
|
|
19605
|
+
writeFileSync8(join38(outDir, "index.html"), html);
|
|
19606
|
+
for (const f of readdirSync11(webDir())) {
|
|
18182
19607
|
if (f === "index.html" || f.endsWith(".test.js")) continue;
|
|
18183
|
-
cpSync2(
|
|
19608
|
+
cpSync2(join38(webDir(), f), join38(outDir, f), { recursive: true });
|
|
18184
19609
|
}
|
|
18185
|
-
writeFileSync8(
|
|
19610
|
+
writeFileSync8(join38(outDir, "README.md"), BUNDLE_README);
|
|
18186
19611
|
copyNotices(outDir);
|
|
18187
19612
|
const name = workerName(cfg.projectDir, opts.name);
|
|
18188
19613
|
writeFileSync8(
|
|
18189
|
-
|
|
19614
|
+
join38(outDir, "wrangler.jsonc"),
|
|
18190
19615
|
JSON.stringify(
|
|
18191
19616
|
{ $schema: "node_modules/wrangler/config-schema.json", name, compatibility_date: "2025-06-01", assets: { directory: "." } },
|
|
18192
19617
|
null,
|
|
18193
19618
|
2
|
|
18194
19619
|
) + "\n"
|
|
18195
19620
|
);
|
|
19621
|
+
writeFileSync8(join38(outDir, "manifest.json"), JSON.stringify({ ...manifest, files: bundleFiles(outDir).filter((f) => f !== "manifest.json") }, null, 2));
|
|
18196
19622
|
process.stdout.write(
|
|
18197
19623
|
`behold export \u2192 ${outDir}
|
|
18198
19624
|
${ok} snapshots${failed ? ` (${failed} endpoint error(s) captured as-is)` : ""}
|
|
@@ -18201,11 +19627,11 @@ async function runExport(cfg, outDir, opts = {}) {
|
|
|
18201
19627
|
`
|
|
18202
19628
|
);
|
|
18203
19629
|
}
|
|
18204
|
-
var
|
|
19630
|
+
var isRecord19 = (v) => v !== null && typeof v === "object" && !Array.isArray(v);
|
|
18205
19631
|
function dropSource(v) {
|
|
18206
19632
|
if (Array.isArray(v)) return v.forEach(dropSource);
|
|
18207
|
-
if (!
|
|
18208
|
-
if (
|
|
19633
|
+
if (!isRecord19(v)) return;
|
|
19634
|
+
if (isRecord19(v.attrs)) delete v.attrs.source;
|
|
18209
19635
|
for (const x of Object.values(v)) dropSource(x);
|
|
18210
19636
|
}
|
|
18211
19637
|
function shapeSnapshot(key, body, opts = {}) {
|
|
@@ -18215,22 +19641,22 @@ function shapeSnapshot(key, body, opts = {}) {
|
|
|
18215
19641
|
} catch {
|
|
18216
19642
|
return body;
|
|
18217
19643
|
}
|
|
18218
|
-
if (!
|
|
19644
|
+
if (!isRecord19(doc)) return body;
|
|
18219
19645
|
if (key === "/api/project") {
|
|
18220
19646
|
delete doc.recents;
|
|
18221
19647
|
delete doc.approver;
|
|
18222
19648
|
}
|
|
18223
|
-
if (key === "/api/terragucci") doc.files = opts.reportsBase ?? VIEWS_REPORTS_BASE;
|
|
19649
|
+
if (key === "/api/terragucci" || key === "/api/terragucci/timeline") doc.files = opts.reportsBase ?? VIEWS_REPORTS_BASE;
|
|
18224
19650
|
if (opts.noSource) dropSource(doc);
|
|
18225
19651
|
return JSON.stringify(doc);
|
|
18226
19652
|
}
|
|
18227
19653
|
function exportScrubber(cfg, home = homedir3()) {
|
|
18228
19654
|
const dirs = [cfg.projectDir, ...cfg.projectDirs ?? [], ...cfg.workspace ? [cfg.workspace.root] : []];
|
|
18229
19655
|
const tg = cfg.terragucci?.source;
|
|
18230
|
-
if (tg && !/^[a-z0-9]+:\/\//i.test(tg)) dirs.push(
|
|
19656
|
+
if (tg && !/^[a-z0-9]+:\/\//i.test(tg)) dirs.push(resolve27(tg));
|
|
18231
19657
|
const pairs = /* @__PURE__ */ new Map();
|
|
18232
19658
|
for (const d of dirs) {
|
|
18233
|
-
for (const p of [
|
|
19659
|
+
for (const p of [resolve27(d), safeRealpath(d)]) if (p && p !== "/") pairs.set(p, basename6(p));
|
|
18234
19660
|
}
|
|
18235
19661
|
const ordered = [...pairs].sort((a, b) => b[0].length - a[0].length);
|
|
18236
19662
|
return (text) => {
|
|
@@ -18269,8 +19695,8 @@ function publishTarget(spec2) {
|
|
|
18269
19695
|
const prefix = m[2];
|
|
18270
19696
|
const parts = prefix.split("/");
|
|
18271
19697
|
if (parts.some((p) => p === "" || p === "." || p === "..")) return { error: `--publish ${spec2}: the prefix has an empty, . or .. segment` };
|
|
18272
|
-
const
|
|
18273
|
-
if (
|
|
19698
|
+
const at2 = parts.lastIndexOf("views");
|
|
19699
|
+
if (at2 < 0 || at2 !== parts.length - 2) {
|
|
18274
19700
|
return { error: `--publish writes only to a views/<name> directory, such as s3://${m[1]}/${parts.filter((p) => p !== "views").join("/")}/views/behold: a bundle's index.html would overwrite the prefix's own pages anywhere else` };
|
|
18275
19701
|
}
|
|
18276
19702
|
return { bucket: m[1], prefix };
|
|
@@ -18284,16 +19710,19 @@ function manifestFiles(text) {
|
|
|
18284
19710
|
} catch {
|
|
18285
19711
|
return [];
|
|
18286
19712
|
}
|
|
18287
|
-
const map =
|
|
19713
|
+
const map = isRecord19(doc) && isRecord19(doc.keyToFile) ? doc.keyToFile : {};
|
|
18288
19714
|
return Object.values(map).filter((f) => typeof f === "string" && /^snapshots\/[A-Za-z0-9=_.-]+$/.test(f) && !f.includes(".."));
|
|
18289
19715
|
}
|
|
19716
|
+
function bundleFiles(outDir) {
|
|
19717
|
+
return readdirSync11(outDir, { recursive: true, withFileTypes: true }).filter((d) => d.isFile()).map((d) => relative11(outDir, join38(d.parentPath, d.name)).split(sep10).join("/")).sort();
|
|
19718
|
+
}
|
|
18290
19719
|
async function publishBundle(outDir, target, client) {
|
|
18291
|
-
const files =
|
|
19720
|
+
const files = bundleFiles(outDir);
|
|
18292
19721
|
const warnings = [];
|
|
18293
|
-
const
|
|
19722
|
+
const at2 = (f) => `${target.prefix}/${f}`;
|
|
18294
19723
|
let previous = [];
|
|
18295
19724
|
try {
|
|
18296
|
-
const text = await client.get(
|
|
19725
|
+
const text = await client.get(at2("manifest.json"));
|
|
18297
19726
|
if (text !== void 0) previous = manifestFiles(text);
|
|
18298
19727
|
} catch (e) {
|
|
18299
19728
|
const note = e instanceof S3Error && e.status === 403 ? " (S3 answers 403 for a missing object too when the credentials lack s3:ListBucket)" : "";
|
|
@@ -18301,18 +19730,18 @@ async function publishBundle(outDir, target, client) {
|
|
|
18301
19730
|
}
|
|
18302
19731
|
const last = ["manifest.json", "index.html"];
|
|
18303
19732
|
const order = [...files.filter((f) => !last.includes(f)), ...last.filter((f) => files.includes(f))];
|
|
18304
|
-
for (const f of order) await client.put(
|
|
19733
|
+
for (const f of order) await client.put(at2(f), readFileSync27(join38(outDir, f)), contentType(f), CONTENT_ADDRESSED.test(f) ? IMMUTABLE : NO_CACHE);
|
|
18305
19734
|
const wrote = new Set(files);
|
|
18306
19735
|
const removed = [];
|
|
18307
19736
|
for (const f of previous) {
|
|
18308
19737
|
if (wrote.has(f)) continue;
|
|
18309
19738
|
try {
|
|
18310
|
-
await client.delete(
|
|
18311
|
-
removed.push(
|
|
19739
|
+
await client.delete(at2(f));
|
|
19740
|
+
removed.push(at2(f));
|
|
18312
19741
|
} catch (e) {
|
|
18313
|
-
const
|
|
19742
|
+
const denied2 = e instanceof S3Error && (e.status === 403 || e.code === "AccessDenied");
|
|
18314
19743
|
warnings.push(
|
|
18315
|
-
|
|
19744
|
+
denied2 ? `the previous export's files were left in s3://${target.bucket}/${target.prefix}/: deleting them needs s3:DeleteObject on that prefix, which these credentials lack` : `the previous export's files were left in s3://${target.bucket}/${target.prefix}/: ${e instanceof Error ? e.message : String(e)}`
|
|
18316
19745
|
);
|
|
18317
19746
|
break;
|
|
18318
19747
|
}
|
|
@@ -18351,6 +19780,143 @@ It's just files \u2014 GitHub Pages, S3, nginx, or Cloudflare Pages
|
|
|
18351
19780
|
(\`wrangler pages deploy .\`) all work.
|
|
18352
19781
|
`;
|
|
18353
19782
|
|
|
19783
|
+
// src/publish-snapshots.ts
|
|
19784
|
+
import { execFileSync as execFileSync3 } from "node:child_process";
|
|
19785
|
+
import { readFileSync as readFileSync28 } from "node:fs";
|
|
19786
|
+
import { join as join39 } from "node:path";
|
|
19787
|
+
var DEFAULT_KEEP = 30;
|
|
19788
|
+
var SNAPSHOT_REPORTS_BASE = "../../../";
|
|
19789
|
+
var COMMIT = /^[0-9a-f]{7,64}$/;
|
|
19790
|
+
function resolveCommit(dir, given) {
|
|
19791
|
+
const git = (...args) => execFileSync3("git", ["-C", dir, ...args], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
|
|
19792
|
+
let sha;
|
|
19793
|
+
if (given !== void 0) {
|
|
19794
|
+
sha = given.toLowerCase();
|
|
19795
|
+
if (!COMMIT.test(sha)) return { error: `--commit takes a commit's hex digits (7 to 64 of them), not ${given}` };
|
|
19796
|
+
} else {
|
|
19797
|
+
try {
|
|
19798
|
+
sha = git("rev-parse", "HEAD").toLowerCase();
|
|
19799
|
+
} catch {
|
|
19800
|
+
return { error: `--publish files the view under the served checkout's commit, and ${dir} is not a git checkout with a commit: pass --commit <sha>` };
|
|
19801
|
+
}
|
|
19802
|
+
if (!COMMIT.test(sha)) return { error: `git rev-parse HEAD in ${dir} answered ${sha}: pass --commit <sha>` };
|
|
19803
|
+
}
|
|
19804
|
+
try {
|
|
19805
|
+
const at2 = git("show", "-s", "--format=%cI", `${sha}^{commit}`);
|
|
19806
|
+
return at2 ? { sha, at: new Date(at2).toISOString() } : { sha };
|
|
19807
|
+
} catch {
|
|
19808
|
+
return { sha };
|
|
19809
|
+
}
|
|
19810
|
+
}
|
|
19811
|
+
var isRecord20 = (v) => v !== null && typeof v === "object" && !Array.isArray(v);
|
|
19812
|
+
function parseHistory(text) {
|
|
19813
|
+
const doc = JSON.parse(text);
|
|
19814
|
+
if (!Array.isArray(doc)) throw new Error("history.json is not an array");
|
|
19815
|
+
const str13 = (v) => typeof v === "string" ? v : "";
|
|
19816
|
+
return doc.filter((e) => isRecord20(e) && typeof e.commit === "string" && COMMIT.test(e.commit)).map((e) => ({ commit: e.commit, at: str13(e.at), project: str13(e.project), generated: str13(e.generated) }));
|
|
19817
|
+
}
|
|
19818
|
+
function nextHistory(old, entry, keep) {
|
|
19819
|
+
const all = [entry, ...old.filter((e) => e.commit !== entry.commit)];
|
|
19820
|
+
const seen = /* @__PURE__ */ new Set();
|
|
19821
|
+
const unique2 = all.filter((e) => seen.has(e.commit) ? false : (seen.add(e.commit), true));
|
|
19822
|
+
return { history: unique2.slice(0, keep), dropped: unique2.slice(keep).map((e) => e.commit) };
|
|
19823
|
+
}
|
|
19824
|
+
var BUNDLE_PATH = /^[A-Za-z0-9=_.@+-]+(\/[A-Za-z0-9=_.@+-]+)*$/;
|
|
19825
|
+
var insideBundle = (f) => typeof f === "string" && BUNDLE_PATH.test(f) && !f.split("/").some((p) => p === "." || p === "..");
|
|
19826
|
+
function commitFiles(manifestText) {
|
|
19827
|
+
const doc = JSON.parse(manifestText);
|
|
19828
|
+
if (!isRecord20(doc)) return ["manifest.json"];
|
|
19829
|
+
const listed = Array.isArray(doc.files) ? doc.files.filter(insideBundle) : [];
|
|
19830
|
+
const snaps = isRecord20(doc.keyToFile) ? Object.values(doc.keyToFile).filter(insideBundle) : [];
|
|
19831
|
+
const rest = [.../* @__PURE__ */ new Set([...listed, ...snaps])].filter((f) => f !== "index.html" && f !== "manifest.json").sort();
|
|
19832
|
+
return ["index.html", ...rest, "manifest.json"];
|
|
19833
|
+
}
|
|
19834
|
+
function redirectPage() {
|
|
19835
|
+
return `<!doctype html>
|
|
19836
|
+
<html lang="en">
|
|
19837
|
+
<head>
|
|
19838
|
+
<meta charset="utf-8">
|
|
19839
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
19840
|
+
<title>behold</title>
|
|
19841
|
+
<script>
|
|
19842
|
+
(function () {
|
|
19843
|
+
var path = location.pathname;
|
|
19844
|
+
var base = /\\/$|\\.html?$/.test(path) ? "" : path.split("/").pop() + "/";
|
|
19845
|
+
fetch(base + "latest.json", { cache: "no-cache" })
|
|
19846
|
+
.then(function (r) { if (!r.ok) throw new Error("latest.json: " + r.status); return r.json(); })
|
|
19847
|
+
.then(function (l) {
|
|
19848
|
+
if (!l || typeof l.commit !== "string" || !${COMMIT}.test(l.commit)) throw new Error("latest.json names no commit");
|
|
19849
|
+
location.replace(base + l.commit + "/index.html" + location.search + location.hash);
|
|
19850
|
+
})
|
|
19851
|
+
.catch(function (e) { document.getElementById("m").textContent = "No published view to open: " + e.message; });
|
|
19852
|
+
})();
|
|
19853
|
+
</script>
|
|
19854
|
+
</head>
|
|
19855
|
+
<body style="font: 13px ui-monospace, monospace; padding: 16px">
|
|
19856
|
+
<p id="m">Opening the newest view...</p>
|
|
19857
|
+
<noscript><p>This page opens the newest view named in latest.json and needs JavaScript.</p></noscript>
|
|
19858
|
+
</body>
|
|
19859
|
+
</html>
|
|
19860
|
+
`;
|
|
19861
|
+
}
|
|
19862
|
+
var message = (e) => e instanceof Error ? e.message : String(e);
|
|
19863
|
+
var denied = (e) => e instanceof S3Error && (e.status === 403 || e.code === "AccessDenied");
|
|
19864
|
+
async function publishSnapshot(outDir, target, commit, client, opts = {}) {
|
|
19865
|
+
const sha = commit.sha.toLowerCase();
|
|
19866
|
+
if (!COMMIT.test(sha)) throw new Error(`a commit is 7 to 64 hex digits, not ${commit.sha}`);
|
|
19867
|
+
const keep = opts.keep ?? DEFAULT_KEEP;
|
|
19868
|
+
if (!Number.isInteger(keep) || keep < 1) throw new Error(`--keep takes a whole number of at least 1, not ${keep}`);
|
|
19869
|
+
const at2 = (f) => `${target.prefix}/${f}`;
|
|
19870
|
+
const local = JSON.parse(readFileSync28(join39(outDir, "manifest.json"), "utf8"));
|
|
19871
|
+
const generated = local.capturedAt ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
19872
|
+
const entry = { commit: sha, at: commit.at ?? generated, project: opts.project ?? local.projectDir ?? "", generated };
|
|
19873
|
+
const bundle = await publishBundle(outDir, { bucket: target.bucket, prefix: at2(sha) }, client);
|
|
19874
|
+
const warnings = [...bundle.warnings];
|
|
19875
|
+
const removed = [...bundle.removed];
|
|
19876
|
+
let old = [];
|
|
19877
|
+
try {
|
|
19878
|
+
const text = await client.get(at2("history.json"));
|
|
19879
|
+
if (text !== void 0) old = parseHistory(text);
|
|
19880
|
+
} catch (e) {
|
|
19881
|
+
const note = e instanceof S3Error && e.status === 403 ? " (S3 answers 403 for a missing object too when the credentials lack s3:ListBucket)" : "";
|
|
19882
|
+
warnings.push(`the previous history.json could not be read, so it starts again from ${sha} and older commits are left in place: ${message(e)}${note}`);
|
|
19883
|
+
}
|
|
19884
|
+
const { history, dropped } = nextHistory(old, entry, keep);
|
|
19885
|
+
await client.put(at2("history.json"), JSON.stringify(history, null, 2) + "\n", "application/json", NO_CACHE);
|
|
19886
|
+
await client.put(at2("latest.json"), JSON.stringify({ commit: sha }) + "\n", "application/json", NO_CACHE);
|
|
19887
|
+
await client.put(at2("index.html"), redirectPage(), "text/html; charset=utf-8", NO_CACHE);
|
|
19888
|
+
const pruned = [];
|
|
19889
|
+
prune: for (const c of dropped) {
|
|
19890
|
+
if (c === sha) continue;
|
|
19891
|
+
let files;
|
|
19892
|
+
try {
|
|
19893
|
+
const text = await client.get(at2(`${c}/manifest.json`));
|
|
19894
|
+
if (text === void 0) {
|
|
19895
|
+
pruned.push(c);
|
|
19896
|
+
continue;
|
|
19897
|
+
}
|
|
19898
|
+
files = commitFiles(text);
|
|
19899
|
+
} catch (e) {
|
|
19900
|
+
warnings.push(`${c} left history.json but its files were left in s3://${target.bucket}/${at2(c)}/: its manifest.json could not be read: ${message(e)}`);
|
|
19901
|
+
continue;
|
|
19902
|
+
}
|
|
19903
|
+
for (const f of files) {
|
|
19904
|
+
try {
|
|
19905
|
+
await client.delete(at2(`${c}/${f}`));
|
|
19906
|
+
removed.push(at2(`${c}/${f}`));
|
|
19907
|
+
} catch (e) {
|
|
19908
|
+
const left = dropped.slice(dropped.indexOf(c)).join(", ");
|
|
19909
|
+
warnings.push(
|
|
19910
|
+
denied(e) ? `commits past --keep ${keep} left history.json but their files were left in s3://${target.bucket}/${target.prefix}/ (${left}): deleting them needs s3:DeleteObject on that prefix, which these credentials lack` : `commits past --keep ${keep} left history.json but their files were left in s3://${target.bucket}/${target.prefix}/ (${left}): ${message(e)}`
|
|
19911
|
+
);
|
|
19912
|
+
break prune;
|
|
19913
|
+
}
|
|
19914
|
+
}
|
|
19915
|
+
pruned.push(c);
|
|
19916
|
+
}
|
|
19917
|
+
return { commit: sha, files: bundle.files, removed, dropped, pruned, history: history.length, warnings };
|
|
19918
|
+
}
|
|
19919
|
+
|
|
18354
19920
|
// src/cli.ts
|
|
18355
19921
|
init_project();
|
|
18356
19922
|
init_member_kind();
|
|
@@ -18366,7 +19932,7 @@ Usage:
|
|
|
18366
19932
|
behold preview [project-dir] [--port <n>] [--emulator]
|
|
18367
19933
|
behold export [project-dir] [--out <dir>] [--env <name>] [--name <worker>] [--emulator]
|
|
18368
19934
|
[--terragucci <src> [--terragucci-project <p>] [--reports-base <rel>]] [--no-source]
|
|
18369
|
-
[--publish s3://<bucket>/<prefix>/views/<name>]
|
|
19935
|
+
[--publish s3://<bucket>/<prefix>/views/<name> [--commit <sha>] [--keep <n>]]
|
|
18370
19936
|
behold serve <project-dir\u2026> [--port <n>] [--host <addr>] [--allow-host <name,\u2026>] [--hud <url>] [--env <name>] [--poll <secs>] [--local]
|
|
18371
19937
|
behold carve <report.json> [--port <n>]
|
|
18372
19938
|
|
|
@@ -18446,6 +20012,13 @@ Options:
|
|
|
18446
20012
|
--terragucci-project <host/path>
|
|
18447
20013
|
serve/export: which project, when the reports hold several
|
|
18448
20014
|
and none is this checkout's git remote.
|
|
20015
|
+
--terragucci-poll <secs>
|
|
20016
|
+
serve with --terragucci only: while a page is open, ask
|
|
20017
|
+
whether index.json changed (If-None-Match, or its mtime)
|
|
20018
|
+
and fetch chant/lifecycle (the gates and locks) every
|
|
20019
|
+
<secs> (default 30), and push what moved to the page.
|
|
20020
|
+
0 turns it off. The fetch goes into behold's own cache
|
|
20021
|
+
(<tmpdir>/behold-lifecycle-*), never into the checkout.
|
|
18449
20022
|
--env <name> Environment name \u2014 turns on the live drift overlay.
|
|
18450
20023
|
export/serve.
|
|
18451
20024
|
--poll <secs> Re-query live drift every <secs> and push updates (needs --env).
|
|
@@ -18493,9 +20066,17 @@ Options:
|
|
|
18493
20066
|
(terragucci never writes there), so a bundle never
|
|
18494
20067
|
overwrites a page it did not make. The one cloud write
|
|
18495
20068
|
behold makes, and only when an export asks for it.
|
|
20069
|
+
Each commit's view goes to <name>/<commit>/; history.json,
|
|
20070
|
+
latest.json and a redirecting index.html sit beside them.
|
|
20071
|
+
--commit <sha> export with --publish only: the commit to file the view
|
|
20072
|
+
under (default: the served checkout's HEAD).
|
|
20073
|
+
--keep <n> export with --publish only: how many commits history.json
|
|
20074
|
+
keeps (default 30); older commits' files are deleted as
|
|
20075
|
+
their manifest.json names them, best effort.
|
|
18496
20076
|
--reports-base <r> export with --terragucci only: where a report link points
|
|
18497
20077
|
from the bundle, relative to it (default ../../, for a
|
|
18498
|
-
bundle
|
|
20078
|
+
bundle at <prefix>/views/behold/; ../../../ with
|
|
20079
|
+
--publish, whose bundle sits one commit deeper).
|
|
18499
20080
|
--name <worker> export only: Cloudflare Worker name in the generated
|
|
18500
20081
|
wrangler.jsonc.
|
|
18501
20082
|
-h, --help This text.
|
|
@@ -18547,6 +20128,7 @@ ${USAGE}`);
|
|
|
18547
20128
|
let hudArg = process.env.BEHOLD_HUD_URL || void 0;
|
|
18548
20129
|
let terragucci;
|
|
18549
20130
|
let terragucciProject;
|
|
20131
|
+
let terragucciPoll;
|
|
18550
20132
|
const allowHosts = [];
|
|
18551
20133
|
for (let i = 0; i < rest.length; i++) {
|
|
18552
20134
|
const a = rest[i];
|
|
@@ -18556,6 +20138,7 @@ ${USAGE}`);
|
|
|
18556
20138
|
else if (a === "--hud") hudArg = rest[++i];
|
|
18557
20139
|
else if (a === "--terragucci") terragucci = rest[++i];
|
|
18558
20140
|
else if (a === "--terragucci-project") terragucciProject = rest[++i];
|
|
20141
|
+
else if (a === "--terragucci-poll") terragucciPoll = Number(rest[++i]);
|
|
18559
20142
|
else if (a === "--env") env = rest[++i];
|
|
18560
20143
|
else if (a === "--poll") pollSecs = Number(rest[++i]);
|
|
18561
20144
|
else if (a === "--local") local = true;
|
|
@@ -18610,7 +20193,15 @@ ${USAGE}`);
|
|
|
18610
20193
|
process.stderr.write("behold serve: --terragucci-project needs --terragucci\n");
|
|
18611
20194
|
process.exit(2);
|
|
18612
20195
|
}
|
|
18613
|
-
|
|
20196
|
+
if (terragucciPoll !== void 0 && (!Number.isFinite(terragucciPoll) || terragucciPoll < 0)) {
|
|
20197
|
+
process.stderr.write("behold serve: --terragucci-poll must be a number of seconds, 0 for off\n");
|
|
20198
|
+
process.exit(2);
|
|
20199
|
+
}
|
|
20200
|
+
if (terragucciPoll !== void 0 && terragucci === void 0) {
|
|
20201
|
+
process.stderr.write("behold serve: --terragucci-poll needs --terragucci\n");
|
|
20202
|
+
process.exit(2);
|
|
20203
|
+
}
|
|
20204
|
+
const dirs = projectDirs.map((d) => resolve28(d));
|
|
18614
20205
|
const target = await serveTarget(dirs, "serve");
|
|
18615
20206
|
await startServer({
|
|
18616
20207
|
...target,
|
|
@@ -18622,7 +20213,7 @@ ${USAGE}`);
|
|
|
18622
20213
|
...host ? { host } : {},
|
|
18623
20214
|
...allowHosts.length ? { allowedHosts: allowHosts } : {},
|
|
18624
20215
|
...target.workspace && hud ? { hud } : {},
|
|
18625
|
-
...terragucci ? { terragucci: { source: terragucci, ...terragucciProject ? { project: terragucciProject } : {} } } : {}
|
|
20216
|
+
...terragucci ? { terragucci: { source: terragucci, ...terragucciProject ? { project: terragucciProject } : {}, ...terragucciPoll !== void 0 ? { pollSecs: terragucciPoll } : {} } } : {}
|
|
18626
20217
|
});
|
|
18627
20218
|
}
|
|
18628
20219
|
async function serveTarget(dirs, verb) {
|
|
@@ -18676,7 +20267,7 @@ function warnIfNotChantProject(dir) {
|
|
|
18676
20267
|
if (shape.kind === "estate") {
|
|
18677
20268
|
process.stderr.write(
|
|
18678
20269
|
`behold: warning \u2014 ${dir} is an estate root, not a chant project itself.
|
|
18679
|
-
Serve its members composed: behold serve ${shape.members.map((m) =>
|
|
20270
|
+
Serve its members composed: behold serve ${shape.members.map((m) => join40(dir, m.dir)).join(" ")}
|
|
18680
20271
|
`
|
|
18681
20272
|
);
|
|
18682
20273
|
return;
|
|
@@ -18711,8 +20302,8 @@ async function runCarve(rest) {
|
|
|
18711
20302
|
);
|
|
18712
20303
|
process.exit(2);
|
|
18713
20304
|
}
|
|
18714
|
-
const reportPath =
|
|
18715
|
-
const parsed = readCarveReport(reportPath, (p) =>
|
|
20305
|
+
const reportPath = resolve28(fileArg);
|
|
20306
|
+
const parsed = readCarveReport(reportPath, (p) => readFileSync29(p, "utf8"));
|
|
18716
20307
|
if (!parsed.ok) {
|
|
18717
20308
|
process.stderr.write(`behold carve: ${parsed.refusal.error}
|
|
18718
20309
|
${parsed.refusal.remedy}
|
|
@@ -18723,14 +20314,14 @@ async function runCarve(rest) {
|
|
|
18723
20314
|
`behold carve \u2014 ${parsed.report.resources.length} resource(s)/module(s) ranked${parsed.report.from ? ` from ${parsed.report.from}` : ""}
|
|
18724
20315
|
`
|
|
18725
20316
|
);
|
|
18726
|
-
await startServer({ projectDir:
|
|
20317
|
+
await startServer({ projectDir: dirname17(reportPath), carveReport: reportPath, port });
|
|
18727
20318
|
}
|
|
18728
20319
|
async function runDoctor(rest) {
|
|
18729
|
-
let
|
|
20320
|
+
let json2 = false;
|
|
18730
20321
|
let fix = false;
|
|
18731
20322
|
let dirArg;
|
|
18732
20323
|
for (const a of rest) {
|
|
18733
|
-
if (a === "--json")
|
|
20324
|
+
if (a === "--json") json2 = true;
|
|
18734
20325
|
else if (a === "--fix") fix = true;
|
|
18735
20326
|
else if (a === "-h" || a === "--help") return void process.stdout.write(USAGE);
|
|
18736
20327
|
else if (!a.startsWith("-")) dirArg = a;
|
|
@@ -18741,14 +20332,14 @@ async function runDoctor(rest) {
|
|
|
18741
20332
|
}
|
|
18742
20333
|
}
|
|
18743
20334
|
const dir = dirArg ?? ".";
|
|
18744
|
-
if (!
|
|
18745
|
-
process.stderr.write(`behold doctor: no such directory: ${
|
|
20335
|
+
if (!existsSync25(resolve28(dir))) {
|
|
20336
|
+
process.stderr.write(`behold doctor: no such directory: ${resolve28(dir)}
|
|
18746
20337
|
`);
|
|
18747
20338
|
process.exit(2);
|
|
18748
20339
|
}
|
|
18749
20340
|
if (fix) return runDoctorFix(dir);
|
|
18750
20341
|
const report = await diagnose(dir);
|
|
18751
|
-
process.stdout.write(
|
|
20342
|
+
process.stdout.write(json2 ? JSON.stringify(report, null, 2) + "\n" : formatReport(report));
|
|
18752
20343
|
if (!report.ok) process.exitCode = 1;
|
|
18753
20344
|
}
|
|
18754
20345
|
async function runDoctorFix(dir) {
|
|
@@ -18778,7 +20369,7 @@ ${check.stdout || check.stderr}
|
|
|
18778
20369
|
if (check.code !== 0) process.exitCode = 1;
|
|
18779
20370
|
}
|
|
18780
20371
|
async function runDemo(rest) {
|
|
18781
|
-
const pkgRoot3 =
|
|
20372
|
+
const pkgRoot3 = join40(dirname17(fileURLToPath10(import.meta.url)), "..");
|
|
18782
20373
|
const registry2 = loadDemoRegistry(pkgRoot3);
|
|
18783
20374
|
let port = 4600;
|
|
18784
20375
|
let live2 = false;
|
|
@@ -18841,7 +20432,7 @@ async function runDemo(rest) {
|
|
|
18841
20432
|
`);
|
|
18842
20433
|
process.exit(2);
|
|
18843
20434
|
}
|
|
18844
|
-
const target = dirArg ?
|
|
20435
|
+
const target = dirArg ? resolve28(dirArg) : demoTargetDir(entry);
|
|
18845
20436
|
const loaded = await loadDemo(entry, { pkgRoot: pkgRoot3, target, log: (line) => process.stdout.write(line + "\n") });
|
|
18846
20437
|
if (!loaded.ok) {
|
|
18847
20438
|
process.stderr.write(`behold demo ${entry.name}: ${loaded.error}
|
|
@@ -18873,11 +20464,11 @@ function spawnStep(cmd, args, cwd) {
|
|
|
18873
20464
|
});
|
|
18874
20465
|
}
|
|
18875
20466
|
async function serveCarveDemo(target, carve, port, live2 = false) {
|
|
18876
|
-
const
|
|
18877
|
-
const project =
|
|
18878
|
-
const from =
|
|
18879
|
-
const state = carve.state ?
|
|
18880
|
-
const committed =
|
|
20467
|
+
const at2 = (rel) => resolve28(target, rel);
|
|
20468
|
+
const project = at2(carve.project);
|
|
20469
|
+
const from = at2(carve.from);
|
|
20470
|
+
const state = carve.state ? at2(carve.state) : void 0;
|
|
20471
|
+
const committed = at2(carve.report);
|
|
18881
20472
|
let liveInfo;
|
|
18882
20473
|
if (live2) {
|
|
18883
20474
|
for (const bin of ["docker", "terraform"]) {
|
|
@@ -18913,7 +20504,7 @@ async function serveCarveDemo(target, carve, port, live2 = false) {
|
|
|
18913
20504
|
};
|
|
18914
20505
|
process.stdout.write("behold demo carve --live \u2192 the tfstate is real now; the advisor reads what terraform wrote.\n");
|
|
18915
20506
|
}
|
|
18916
|
-
if (
|
|
20507
|
+
if (existsSync25(join40(project, "package.json")) && !existsSync25(join40(project, "node_modules"))) {
|
|
18917
20508
|
process.stdout.write(`behold demo carve \u2192 npm install in ${carve.project}/ (the chant this walkthrough shells)\u2026
|
|
18918
20509
|
`);
|
|
18919
20510
|
const code = await spawnStep("npm", ["install"], project);
|
|
@@ -18924,13 +20515,13 @@ async function serveCarveDemo(target, carve, port, live2 = false) {
|
|
|
18924
20515
|
}
|
|
18925
20516
|
}
|
|
18926
20517
|
let degraded;
|
|
18927
|
-
if (!
|
|
20518
|
+
if (!existsSync25(join40(target, "node_modules", ...HCL_PARSER_PKG.split("/")))) {
|
|
18928
20519
|
process.stdout.write(`behold demo carve \u2192 npm install ${HCL_PARSER_PKG} (chant's HCL parser, ~2MB, once)\u2026
|
|
18929
20520
|
`);
|
|
18930
20521
|
const code = await spawnStep("npm", ["install", "--no-save", "--no-package-lock", HCL_PARSER_PKG], target);
|
|
18931
20522
|
if (code !== 0) degraded = `couldn't install ${HCL_PARSER_PKG} (chant's HCL parser) \u2014 no network?`;
|
|
18932
20523
|
}
|
|
18933
|
-
const report =
|
|
20524
|
+
const report = at2("carve-report.json");
|
|
18934
20525
|
if (!degraded) {
|
|
18935
20526
|
const bin = resolveChant(project).bin;
|
|
18936
20527
|
process.stdout.write("behold demo carve \u2192 chant carve advise (read-only; emits nothing)\u2026\n");
|
|
@@ -18941,7 +20532,7 @@ async function serveCarveDemo(target, carve, port, live2 = false) {
|
|
|
18941
20532
|
);
|
|
18942
20533
|
if (code !== 0) degraded = `chant carve advise exited ${code}`;
|
|
18943
20534
|
}
|
|
18944
|
-
const serving = !degraded &&
|
|
20535
|
+
const serving = !degraded && existsSync25(report) ? report : committed;
|
|
18945
20536
|
if (degraded) {
|
|
18946
20537
|
process.stderr.write(
|
|
18947
20538
|
`behold demo carve: ${degraded}
|
|
@@ -18949,7 +20540,7 @@ async function serveCarveDemo(target, carve, port, live2 = false) {
|
|
|
18949
20540
|
`
|
|
18950
20541
|
);
|
|
18951
20542
|
}
|
|
18952
|
-
if (!
|
|
20543
|
+
if (!existsSync25(serving)) {
|
|
18953
20544
|
process.stderr.write(`behold demo carve: no carve report at ${serving}
|
|
18954
20545
|
`);
|
|
18955
20546
|
process.exit(2);
|
|
@@ -18965,7 +20556,7 @@ async function serveCarveDemo(target, carve, port, live2 = false) {
|
|
|
18965
20556
|
from,
|
|
18966
20557
|
...state ? { state } : {},
|
|
18967
20558
|
project,
|
|
18968
|
-
out:
|
|
20559
|
+
out: at2(carve.out),
|
|
18969
20560
|
...liveInfo ? { live: liveInfo } : {},
|
|
18970
20561
|
...degraded ? { degraded: `${degraded} \u2014 showing the committed report shipped with the demo.` } : {}
|
|
18971
20562
|
},
|
|
@@ -18994,8 +20585,8 @@ async function runPreview(rest) {
|
|
|
18994
20585
|
process.stderr.write("behold preview: --port must be a number\n");
|
|
18995
20586
|
process.exit(2);
|
|
18996
20587
|
}
|
|
18997
|
-
const projectDir =
|
|
18998
|
-
if (!
|
|
20588
|
+
const projectDir = resolve28(dirArg ?? process.cwd());
|
|
20589
|
+
if (!existsSync25(projectDir)) {
|
|
18999
20590
|
process.stderr.write(`behold preview: project not found at ${projectDir}
|
|
19000
20591
|
`);
|
|
19001
20592
|
process.exit(2);
|
|
@@ -19015,7 +20606,7 @@ async function runPreview(rest) {
|
|
|
19015
20606
|
await startServer({ projectDir, port, env: "local", previewMode: true });
|
|
19016
20607
|
}
|
|
19017
20608
|
async function runExportCmd(rest) {
|
|
19018
|
-
let outDir =
|
|
20609
|
+
let outDir = resolve28("behold-export");
|
|
19019
20610
|
let env;
|
|
19020
20611
|
let name;
|
|
19021
20612
|
let dirArg;
|
|
@@ -19024,10 +20615,12 @@ async function runExportCmd(rest) {
|
|
|
19024
20615
|
let terragucciProject;
|
|
19025
20616
|
let reportsBase;
|
|
19026
20617
|
let publish;
|
|
20618
|
+
let commitArg;
|
|
20619
|
+
let keepArg;
|
|
19027
20620
|
let noSource = false;
|
|
19028
20621
|
for (let i = 0; i < rest.length; i++) {
|
|
19029
20622
|
const a = rest[i];
|
|
19030
|
-
if (a === "--out") outDir =
|
|
20623
|
+
if (a === "--out") outDir = resolve28(rest[++i]);
|
|
19031
20624
|
else if (a === "--env") env = rest[++i];
|
|
19032
20625
|
else if (a === "--name") name = rest[++i];
|
|
19033
20626
|
else if (a === "--emulator") emulator = true;
|
|
@@ -19036,6 +20629,8 @@ async function runExportCmd(rest) {
|
|
|
19036
20629
|
else if (a === "--reports-base") reportsBase = rest[++i];
|
|
19037
20630
|
else if (a === "--no-source") noSource = true;
|
|
19038
20631
|
else if (a === "--publish") publish = rest[++i];
|
|
20632
|
+
else if (a === "--commit") commitArg = rest[++i];
|
|
20633
|
+
else if (a === "--keep") keepArg = rest[++i];
|
|
19039
20634
|
else if (a === "-h" || a === "--help") return void process.stdout.write(USAGE);
|
|
19040
20635
|
else if (!a.startsWith("-")) dirArg = a;
|
|
19041
20636
|
}
|
|
@@ -19046,6 +20641,16 @@ async function runExportCmd(rest) {
|
|
|
19046
20641
|
const destination = publish === void 0 ? void 0 : publishTarget(publish);
|
|
19047
20642
|
if (destination && "error" in destination) {
|
|
19048
20643
|
process.stderr.write(`behold export: ${destination.error}
|
|
20644
|
+
`);
|
|
20645
|
+
process.exit(2);
|
|
20646
|
+
}
|
|
20647
|
+
if ((commitArg !== void 0 || keepArg !== void 0) && !destination) {
|
|
20648
|
+
process.stderr.write("behold export: --commit and --keep need --publish\n");
|
|
20649
|
+
process.exit(2);
|
|
20650
|
+
}
|
|
20651
|
+
const keep = keepArg === void 0 ? DEFAULT_KEEP : Number(keepArg);
|
|
20652
|
+
if (!Number.isInteger(keep) || keep < 1) {
|
|
20653
|
+
process.stderr.write(`behold export: --keep takes a whole number of at least 1, not ${keepArg}
|
|
19049
20654
|
`);
|
|
19050
20655
|
process.exit(2);
|
|
19051
20656
|
}
|
|
@@ -19055,12 +20660,18 @@ async function runExportCmd(rest) {
|
|
|
19055
20660
|
);
|
|
19056
20661
|
process.exit(2);
|
|
19057
20662
|
}
|
|
19058
|
-
const projectDir =
|
|
20663
|
+
const projectDir = resolve28(dirArg ?? process.cwd());
|
|
20664
|
+
const commit = destination ? resolveCommit(projectDir, commitArg) : void 0;
|
|
20665
|
+
if (commit && "error" in commit) {
|
|
20666
|
+
process.stderr.write(`behold export: ${commit.error}
|
|
20667
|
+
`);
|
|
20668
|
+
process.exit(2);
|
|
20669
|
+
}
|
|
19059
20670
|
if (emulator) {
|
|
19060
20671
|
injectEmulatorEnv(env);
|
|
19061
20672
|
env ??= "local";
|
|
19062
20673
|
}
|
|
19063
|
-
if (!
|
|
20674
|
+
if (!existsSync25(projectDir)) {
|
|
19064
20675
|
process.stderr.write(`behold export: project not found at ${projectDir}
|
|
19065
20676
|
`);
|
|
19066
20677
|
process.exit(2);
|
|
@@ -19074,12 +20685,16 @@ async function runExportCmd(rest) {
|
|
|
19074
20685
|
...terragucci ? { terragucci: { source: terragucci, ...terragucciProject ? { project: terragucciProject } : {} } } : {}
|
|
19075
20686
|
},
|
|
19076
20687
|
outDir,
|
|
19077
|
-
{
|
|
20688
|
+
{
|
|
20689
|
+
...name ? { name } : {},
|
|
20690
|
+
...noSource ? { noSource } : {},
|
|
20691
|
+
...reportsBase !== void 0 ? { reportsBase } : destination ? { reportsBase: SNAPSHOT_REPORTS_BASE } : {}
|
|
20692
|
+
}
|
|
19078
20693
|
);
|
|
19079
|
-
if (destination && !("error" in destination)) {
|
|
19080
|
-
const done = await
|
|
20694
|
+
if (destination && !("error" in destination) && commit && !("error" in commit)) {
|
|
20695
|
+
const done = await publishSnapshot(outDir, destination, commit, new S3Object(s3FromEnv(destination.bucket)), { keep });
|
|
19081
20696
|
process.stdout.write(
|
|
19082
|
-
` Published: ${done.files} files to s3://${destination.bucket}/${destination.prefix}
|
|
20697
|
+
` Published: ${done.files} files to s3://${destination.bucket}/${destination.prefix}/${done.commit}/, latest and history (${done.history} of ${keep} kept) updated` + (done.removed.length ? `, ${done.removed.length} stale file${done.removed.length === 1 ? "" : "s"} removed` : "") + (done.dropped.length ? `, ${done.dropped.length} commit${done.dropped.length === 1 ? "" : "s"} past --keep dropped` : "") + "\n"
|
|
19083
20698
|
);
|
|
19084
20699
|
for (const w of done.warnings) process.stderr.write(` warning: ${w}
|
|
19085
20700
|
`);
|
|
@@ -19089,7 +20704,7 @@ function isMainModule() {
|
|
|
19089
20704
|
const entry = process.argv[1];
|
|
19090
20705
|
if (!entry) return false;
|
|
19091
20706
|
try {
|
|
19092
|
-
return realpathSync3(entry) === realpathSync3(
|
|
20707
|
+
return realpathSync3(entry) === realpathSync3(fileURLToPath10(import.meta.url));
|
|
19093
20708
|
} catch {
|
|
19094
20709
|
return false;
|
|
19095
20710
|
}
|