@intentius/behold 0.19.0 → 0.21.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 +90 -5
- package/README.md +54 -1
- package/demos.json +3 -2
- package/dist/cli.js +1327 -303
- package/example-argo-estate/README.md +8 -8
- package/example-argo-estate/app-a/package.json +2 -2
- package/example-argo-estate/app-b/package.json +2 -2
- package/example-argo-estate/chant.workspace.json +27 -0
- package/example-argo-estate/control-plane/package.json +2 -2
- package/example-argo-estate/package-lock.json +133 -56
- package/example-carve/README.md +14 -14
- package/example-carve/app/package-lock.json +72 -9
- package/example-carve/app/package.json +2 -2
- package/example-carve/legacy-tf/versions.tf +1 -1
- package/example-choudoufu-estate/README.md +4 -4
- package/example-choudoufu-estate/chant.workspace.json +32 -0
- package/example-choudoufu-estate/scripts/choudoufu-up.sh +21 -0
- package/example-flux-estate/README.md +5 -5
- package/example-flux-estate/app-a/package.json +2 -2
- package/example-flux-estate/app-b/package.json +2 -2
- package/example-flux-estate/chant.workspace.json +27 -0
- package/example-flux-estate/control-plane/package.json +2 -2
- package/example-flux-estate/package-lock.json +133 -56
- package/example-k8s/README.md +20 -20
- package/example-k8s/chant.config.ts +1 -1
- package/example-k8s/ops/k3d-apply.op.ts +1 -1
- package/example-k8s/package-lock.json +129 -211
- package/example-k8s/package.json +2 -3
- package/example-terraform-estate/README.md +5 -5
- package/example-terraform-estate/chant.workspace.json +22 -0
- package/example-writes/README.md +5 -5
- package/example-writes/chant.config.ts +1 -1
- package/example-writes/ops/apply.op.ts +1 -1
- package/example-writes/ops/floci.op.ts +2 -2
- package/example-writes/ops/reconcile.op.ts +1 -1
- package/example-writes/package-lock.json +62 -175
- package/example-writes/package.json +2 -3
- package/package.json +6 -6
- package/web/app.js +431 -17
- package/web/behaviour-scale.js +32 -0
- package/web/behaviour-scale.test.js +39 -0
- package/web/embed.js +76 -0
- package/web/embed.test.js +45 -0
- package/web/index.html +20 -0
- package/web/operator.js +14 -1
- package/web/panel.js +11 -3
- package/web/run-playhead.js +13 -1
- package/web/view-url.js +138 -0
- package/web/view-url.test.js +84 -0
- package/web/workspace-gates.js +44 -0
- package/web/workspace-gates.test.js +34 -0
- package/example-choudoufu-estate/.behold.json +0 -8
package/dist/cli.js
CHANGED
|
@@ -118,17 +118,17 @@ var init_read_scheduler = __esm({
|
|
|
118
118
|
recordRefused();
|
|
119
119
|
return Promise.reject(new Error("Chant read queue is full; retry after current reads finish"));
|
|
120
120
|
}
|
|
121
|
-
let
|
|
121
|
+
let resolve19;
|
|
122
122
|
let reject;
|
|
123
123
|
const promise = new Promise((yes, no) => {
|
|
124
|
-
|
|
124
|
+
resolve19 = yes;
|
|
125
125
|
reject = no;
|
|
126
126
|
});
|
|
127
127
|
job = {
|
|
128
128
|
key,
|
|
129
129
|
run: run3,
|
|
130
130
|
promise,
|
|
131
|
-
resolve:
|
|
131
|
+
resolve: resolve19,
|
|
132
132
|
reject,
|
|
133
133
|
controller: new AbortController(),
|
|
134
134
|
users: 0,
|
|
@@ -142,7 +142,7 @@ var init_read_scheduler = __esm({
|
|
|
142
142
|
}
|
|
143
143
|
const shared2 = job;
|
|
144
144
|
shared2.users++;
|
|
145
|
-
const result = new Promise((
|
|
145
|
+
const result = new Promise((resolve19, reject) => {
|
|
146
146
|
let done = false;
|
|
147
147
|
const finish = (error, value, failed = false) => {
|
|
148
148
|
if (done) return;
|
|
@@ -150,7 +150,7 @@ var init_read_scheduler = __esm({
|
|
|
150
150
|
signal?.removeEventListener("abort", abort);
|
|
151
151
|
shared2.users--;
|
|
152
152
|
if (failed) reject(error);
|
|
153
|
-
else
|
|
153
|
+
else resolve19(value);
|
|
154
154
|
};
|
|
155
155
|
const abort = () => {
|
|
156
156
|
finish(signal?.reason, void 0, true);
|
|
@@ -1038,11 +1038,11 @@ async function createK8sClient(options = {}) {
|
|
|
1038
1038
|
}
|
|
1039
1039
|
return void 0;
|
|
1040
1040
|
}
|
|
1041
|
-
async function
|
|
1041
|
+
async function resolve19(selector, signal) {
|
|
1042
1042
|
return "apiVersion" in selector ? resolveByGvk(selector.apiVersion, selector.kind, signal) : resolveByResourceString(selector.resource, selector.group, signal);
|
|
1043
1043
|
}
|
|
1044
1044
|
async function resolveOrThrow(selector, signal) {
|
|
1045
|
-
const info = await
|
|
1045
|
+
const info = await resolve19(selector, signal);
|
|
1046
1046
|
if (!info) throw new UnknownResourceError(selectorText(selector));
|
|
1047
1047
|
return info;
|
|
1048
1048
|
}
|
|
@@ -1054,7 +1054,7 @@ async function createK8sClient(options = {}) {
|
|
|
1054
1054
|
return parts.join("/");
|
|
1055
1055
|
}
|
|
1056
1056
|
async function pathFor(ref, signal) {
|
|
1057
|
-
const info = await
|
|
1057
|
+
const info = await resolve19({ apiVersion: ref.apiVersion, kind: ref.kind }, signal);
|
|
1058
1058
|
if (!info) return void 0;
|
|
1059
1059
|
return objectPath(info, ref.name, ref.namespace);
|
|
1060
1060
|
}
|
|
@@ -1168,8 +1168,8 @@ async function createK8sClient(options = {}) {
|
|
|
1168
1168
|
const { lines } = await openStream(listPath, query, controller.signal, target);
|
|
1169
1169
|
let expired = false;
|
|
1170
1170
|
const iterator = lines[Symbol.asyncIterator]();
|
|
1171
|
-
const stopped = new Promise((
|
|
1172
|
-
const finish = () =>
|
|
1171
|
+
const stopped = new Promise((resolve20) => {
|
|
1172
|
+
const finish = () => resolve20({ done: true, value: void 0 });
|
|
1173
1173
|
if (controller.signal.aborted) finish();
|
|
1174
1174
|
else controller.signal.addEventListener("abort", finish, { once: true });
|
|
1175
1175
|
});
|
|
@@ -1196,11 +1196,11 @@ async function createK8sClient(options = {}) {
|
|
|
1196
1196
|
resourceVersion = await listResourceVersion();
|
|
1197
1197
|
continue;
|
|
1198
1198
|
}
|
|
1199
|
-
await new Promise((
|
|
1200
|
-
const timer = setTimeout(
|
|
1199
|
+
await new Promise((resolve20) => {
|
|
1200
|
+
const timer = setTimeout(resolve20, reopenDelayMs);
|
|
1201
1201
|
controller.signal.addEventListener("abort", () => {
|
|
1202
1202
|
clearTimeout(timer);
|
|
1203
|
-
|
|
1203
|
+
resolve20(void 0);
|
|
1204
1204
|
}, { once: true });
|
|
1205
1205
|
});
|
|
1206
1206
|
}
|
|
@@ -1318,7 +1318,7 @@ async function createK8sClient(options = {}) {
|
|
|
1318
1318
|
return {
|
|
1319
1319
|
provenance,
|
|
1320
1320
|
defaultNamespace,
|
|
1321
|
-
resolve:
|
|
1321
|
+
resolve: resolve19,
|
|
1322
1322
|
read,
|
|
1323
1323
|
pathFor,
|
|
1324
1324
|
readIfPresent,
|
|
@@ -1576,8 +1576,8 @@ function parseLiveCheckOutput(run3, dir) {
|
|
|
1576
1576
|
if (run3.code === 127) return refuse(`choudoufu is not on PATH (${stripAnsi(run3.stderr).trim() || "spawn failed"}).`, INSTALL);
|
|
1577
1577
|
const text = run3.stdout.trim();
|
|
1578
1578
|
if (!text) {
|
|
1579
|
-
const
|
|
1580
|
-
const why =
|
|
1579
|
+
const said4 = stripAnsiLines(run3.stderr);
|
|
1580
|
+
const why = said4.find((l) => /^Error:/.test(l)) ?? said4[0] ?? `exit ${run3.code}`;
|
|
1581
1581
|
return refuse(`choudoufu could not read ${dir}: ${why}`, "Fix the configuration until `choudoufu live-check` prints a document, then reload.");
|
|
1582
1582
|
}
|
|
1583
1583
|
let json;
|
|
@@ -1734,6 +1734,9 @@ function choudoufuVersion(bin = choudoufuBinary()) {
|
|
|
1734
1734
|
function setChoudoufuSpawnEnv(env) {
|
|
1735
1735
|
spawnEnvOverride = env && Object.keys(env).length ? { ...env } : void 0;
|
|
1736
1736
|
}
|
|
1737
|
+
function choudoufuSpawnOverride() {
|
|
1738
|
+
return spawnEnvOverride ? { ...spawnEnvOverride } : void 0;
|
|
1739
|
+
}
|
|
1737
1740
|
function choudoufuSpawnEnv(base = process.env) {
|
|
1738
1741
|
return { ...base, ...spawnEnvOverride ?? {}, NO_COLOR: "1" };
|
|
1739
1742
|
}
|
|
@@ -2312,9 +2315,9 @@ function beholdNodeModules(from = beholdDir()) {
|
|
|
2312
2315
|
dir = parent;
|
|
2313
2316
|
}
|
|
2314
2317
|
}
|
|
2315
|
-
function terraformScratchDir(estate, tmp = tmpdir()) {
|
|
2318
|
+
function terraformScratchDir(estate, tmp = tmpdir(), flavour = "tf") {
|
|
2316
2319
|
const target = resolve(estate);
|
|
2317
|
-
const name = `${SCRATCH_PREFIX}
|
|
2320
|
+
const name = `${SCRATCH_PREFIX}${flavour}-${createHash("sha1").update(target).digest("hex").slice(0, 12)}`;
|
|
2318
2321
|
assertScratch(name);
|
|
2319
2322
|
let root = resolve(tmp);
|
|
2320
2323
|
try {
|
|
@@ -2327,7 +2330,7 @@ function terraformScratchDir(estate, tmp = tmpdir()) {
|
|
|
2327
2330
|
}
|
|
2328
2331
|
return dir;
|
|
2329
2332
|
}
|
|
2330
|
-
function terraformScratchConfig(scan, estateLink) {
|
|
2333
|
+
function terraformScratchConfig(scan, estateLink, flavour = "tf") {
|
|
2331
2334
|
const roots = [...scan.roots].sort((a, b) => a.name < b.name ? -1 : 1);
|
|
2332
2335
|
const entries = roots.map((r) => ` ${JSON.stringify(r.name)}: { dir: ${JSON.stringify(r.dir === "." ? estateLink : `${estateLink}/${r.dir}`)} },`);
|
|
2333
2336
|
return [
|
|
@@ -2341,6 +2344,7 @@ function terraformScratchConfig(scan, estateLink) {
|
|
|
2341
2344
|
"export default {",
|
|
2342
2345
|
' lexicons: ["terraform"],',
|
|
2343
2346
|
" terraform: {",
|
|
2347
|
+
...flavour === "chdf" ? [' binary: "choudoufu",'] : [],
|
|
2344
2348
|
" roots: {",
|
|
2345
2349
|
...entries,
|
|
2346
2350
|
" },",
|
|
@@ -2357,14 +2361,14 @@ function relink(path, target) {
|
|
|
2357
2361
|
}
|
|
2358
2362
|
symlinkSync(target, path);
|
|
2359
2363
|
}
|
|
2360
|
-
function writeTerraformScratchProject(estate, scan, tmp = tmpdir()) {
|
|
2364
|
+
function writeTerraformScratchProject(estate, scan, tmp = tmpdir(), flavour = "tf") {
|
|
2361
2365
|
const target = resolve(estate);
|
|
2362
|
-
const dir = terraformScratchDir(target, tmp);
|
|
2366
|
+
const dir = terraformScratchDir(target, tmp, flavour);
|
|
2363
2367
|
mkdirSync2(dir, { recursive: true });
|
|
2364
2368
|
relink(join3(dir, ESTATE_LINK), target);
|
|
2365
2369
|
const modules = beholdNodeModules();
|
|
2366
2370
|
if (modules) relink(join3(dir, "node_modules"), modules);
|
|
2367
|
-
const config = terraformScratchConfig(scan, ESTATE_LINK);
|
|
2371
|
+
const config = terraformScratchConfig(scan, ESTATE_LINK, flavour);
|
|
2368
2372
|
const path = join3(dir, "chant.config.ts");
|
|
2369
2373
|
if (!existsSync3(path) || readFileSync3(path, "utf8") !== config) writeFileSync2(path, config);
|
|
2370
2374
|
return dir;
|
|
@@ -2398,7 +2402,7 @@ var init_terraform_member = __esm({
|
|
|
2398
2402
|
init_terraform_lens();
|
|
2399
2403
|
init_scratch();
|
|
2400
2404
|
TERRAFORM_LEXICON_PKG = "@intentius/chant-lexicon-terraform";
|
|
2401
|
-
HCL_PARSER_PKG = "@
|
|
2405
|
+
HCL_PARSER_PKG = "@cdktn/hcl2json";
|
|
2402
2406
|
ROOT_SCAN_DEPTH = 3;
|
|
2403
2407
|
SKIP_DIRS = /* @__PURE__ */ new Set([".terraform", "node_modules", ".git", "dist", ".chant", "cdk.out"]);
|
|
2404
2408
|
ROOT_BLOCK = /^(terraform\s*\{|provider\s+")/m;
|
|
@@ -2636,6 +2640,7 @@ function verdictFor(address, ls, plan) {
|
|
|
2636
2640
|
const gap = ls?.gaps?.find((g) => g.address === address);
|
|
2637
2641
|
const omission = plan?.omissions?.find((o) => o.addr === address);
|
|
2638
2642
|
const unowned = plan?.unowned?.find((u) => u.addr === address);
|
|
2643
|
+
const adoptable = plan?.adoptable?.find((a) => a.addr === address);
|
|
2639
2644
|
const listing = gap ? { listing: gap.detail } : {};
|
|
2640
2645
|
if (bound) {
|
|
2641
2646
|
const id = bound.identity || item?.id;
|
|
@@ -2649,6 +2654,26 @@ function verdictFor(address, ls, plan) {
|
|
|
2649
2654
|
if (omission) {
|
|
2650
2655
|
const tone = REASON_TONE[omission.reason] ?? "neutral";
|
|
2651
2656
|
const base = { omission: omission.reason, detail: omission.detail, ...listing };
|
|
2657
|
+
if (omission.reason === "NEEDS_DISCOVERY" && adoptable) {
|
|
2658
|
+
const adopt = adoptLine(adoptable);
|
|
2659
|
+
const matched = (adoptable.matched ?? []).filter((m) => m && m.attribute);
|
|
2660
|
+
return {
|
|
2661
|
+
_status: "warn",
|
|
2662
|
+
physicalId: adoptable.identity,
|
|
2663
|
+
ownership: "foreign",
|
|
2664
|
+
attrs: {
|
|
2665
|
+
// The write to run first, as #393 item 10 settled for the unowned
|
|
2666
|
+
// card. behold composes none of its own: `adoptLine` prints
|
|
2667
|
+
// choudoufu's `adopt_command` when there is one and the two marker
|
|
2668
|
+
// values when there is not.
|
|
2669
|
+
...adopt ? { adopt } : {},
|
|
2670
|
+
// Then why choudoufu believes it. Rendered as the estate's own
|
|
2671
|
+
// words rather than a shape behold invented.
|
|
2672
|
+
...matched.length ? { matchedOn: matched.map((m) => `${m.attribute}=${m.value}`).join(", ") } : {},
|
|
2673
|
+
...base
|
|
2674
|
+
}
|
|
2675
|
+
};
|
|
2676
|
+
}
|
|
2652
2677
|
if (omission.reason === "UNOWNED" && unowned) {
|
|
2653
2678
|
const adopt = adoptLine(unowned);
|
|
2654
2679
|
if (adopt) {
|
|
@@ -2918,6 +2943,12 @@ var init_member_kind = __esm({
|
|
|
2918
2943
|
import { readFileSync as readFileSync4, existsSync as existsSync4 } from "node:fs";
|
|
2919
2944
|
import { join as join4 } from "node:path";
|
|
2920
2945
|
import { pathToFileURL } from "node:url";
|
|
2946
|
+
function readBehaviour(raw) {
|
|
2947
|
+
const b = raw.behaviour;
|
|
2948
|
+
if (!b || typeof b !== "object" || Array.isArray(b)) return void 0;
|
|
2949
|
+
const traffic = b.traffic;
|
|
2950
|
+
return typeof traffic === "string" && traffic.trim() ? { traffic: traffic.trim() } : void 0;
|
|
2951
|
+
}
|
|
2921
2952
|
function readMember(v) {
|
|
2922
2953
|
if (typeof v === "string") return v ? { dir: v, kind: "chant" } : void 0;
|
|
2923
2954
|
if (!v || typeof v !== "object") return void 0;
|
|
@@ -3072,7 +3103,13 @@ function loadBeholdConfig(projectDir) {
|
|
|
3072
3103
|
const tiers = readTiers(raw);
|
|
3073
3104
|
const members = Array.isArray(raw.members) ? raw.members.map(readMember).filter((m) => !!m) : [];
|
|
3074
3105
|
const executor = readExecutor(raw);
|
|
3075
|
-
|
|
3106
|
+
const behaviour = readBehaviour(raw);
|
|
3107
|
+
return {
|
|
3108
|
+
...tiers ? { tiers } : {},
|
|
3109
|
+
...members.length ? { members } : {},
|
|
3110
|
+
...executor ? { executor } : {},
|
|
3111
|
+
...behaviour ? { behaviour } : {}
|
|
3112
|
+
};
|
|
3076
3113
|
} catch {
|
|
3077
3114
|
return {};
|
|
3078
3115
|
}
|
|
@@ -3142,8 +3179,12 @@ var init_project = __esm({
|
|
|
3142
3179
|
import { createHash as createHash2 } from "node:crypto";
|
|
3143
3180
|
import { readdirSync as readdirSync3, statSync } from "node:fs";
|
|
3144
3181
|
import { join as join5, relative as relative2, resolve as resolve2 } from "node:path";
|
|
3182
|
+
function setStampExclusions(next) {
|
|
3183
|
+
exclusions = new Map([...next].map(([k, v]) => [resolve2(k), new Set(v.map((d) => resolve2(d)))]));
|
|
3184
|
+
}
|
|
3145
3185
|
function memberSourceStamp(dir) {
|
|
3146
3186
|
const root = resolve2(dir);
|
|
3187
|
+
const skipDirs = exclusions.get(root);
|
|
3147
3188
|
const h = createHash2("sha1");
|
|
3148
3189
|
let any = false;
|
|
3149
3190
|
const walk = (at) => {
|
|
@@ -3152,7 +3193,7 @@ function memberSourceStamp(dir) {
|
|
|
3152
3193
|
if (SKIP.has(e.name)) continue;
|
|
3153
3194
|
const path = join5(at, e.name);
|
|
3154
3195
|
if (e.isDirectory()) {
|
|
3155
|
-
walk(path);
|
|
3196
|
+
if (!skipDirs?.has(path)) walk(path);
|
|
3156
3197
|
continue;
|
|
3157
3198
|
}
|
|
3158
3199
|
if (!e.isFile()) continue;
|
|
@@ -3169,11 +3210,12 @@ function memberSourceStamp(dir) {
|
|
|
3169
3210
|
}
|
|
3170
3211
|
return any ? h.digest("hex") : void 0;
|
|
3171
3212
|
}
|
|
3172
|
-
var SKIP;
|
|
3213
|
+
var SKIP, exclusions;
|
|
3173
3214
|
var init_member_source = __esm({
|
|
3174
3215
|
"src/member-source.ts"() {
|
|
3175
3216
|
"use strict";
|
|
3176
3217
|
SKIP = /* @__PURE__ */ new Set(["node_modules", "dist", ".git"]);
|
|
3218
|
+
exclusions = /* @__PURE__ */ new Map();
|
|
3177
3219
|
}
|
|
3178
3220
|
});
|
|
3179
3221
|
|
|
@@ -3887,6 +3929,7 @@ var chant_exports = {};
|
|
|
3887
3929
|
__export(chant_exports, {
|
|
3888
3930
|
CI_FORGES: () => CI_FORGES,
|
|
3889
3931
|
ChantCliError: () => ChantCliError,
|
|
3932
|
+
TRAFFIC_FLOOR: () => TRAFFIC_FLOOR,
|
|
3890
3933
|
applyArgs: () => applyArgs,
|
|
3891
3934
|
carveStatus: () => carveStatus,
|
|
3892
3935
|
chantBinFrom: () => chantBinFrom,
|
|
@@ -3920,6 +3963,7 @@ __export(chant_exports, {
|
|
|
3920
3963
|
lifecyclePlan: () => lifecyclePlan,
|
|
3921
3964
|
lifecyclePlanArgs: () => lifecyclePlanArgs,
|
|
3922
3965
|
meetsFloor: () => meetsFloor,
|
|
3966
|
+
memberTakesTraffic: () => memberTakesTraffic,
|
|
3923
3967
|
parseCiPipeline: () => parseCiPipeline,
|
|
3924
3968
|
pipelineEnvFromYaml: () => pipelineEnvFromYaml,
|
|
3925
3969
|
readsInFlight: () => readsInFlight,
|
|
@@ -3940,7 +3984,7 @@ import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
|
3940
3984
|
import { dirname as dirname4, join as join7, resolve as resolve5 } from "node:path";
|
|
3941
3985
|
import { createHash as createHash3 } from "node:crypto";
|
|
3942
3986
|
function envOverridesFor(opts) {
|
|
3943
|
-
const overrides = {};
|
|
3987
|
+
const overrides = { ...opts.spawnEnv ?? {} };
|
|
3944
3988
|
if (opts.tier && opts.tierEnvVar) overrides[opts.tierEnvVar] = opts.tier;
|
|
3945
3989
|
if (opts.substrateTargets?.length) {
|
|
3946
3990
|
Object.assign(overrides, targetEnvOverrides(opts.substrateTargets, opts.target));
|
|
@@ -3949,6 +3993,11 @@ function envOverridesFor(opts) {
|
|
|
3949
3993
|
}
|
|
3950
3994
|
return Object.keys(overrides).length ? overrides : void 0;
|
|
3951
3995
|
}
|
|
3996
|
+
function memberTakesTraffic(dir, kind) {
|
|
3997
|
+
if (kind === "terraform") return true;
|
|
3998
|
+
if (kind !== "chant") return false;
|
|
3999
|
+
return meetsFloor(resolveChant(dir).version, TRAFFIC_FLOOR);
|
|
4000
|
+
}
|
|
3952
4001
|
function graphFlags(opts) {
|
|
3953
4002
|
const flags = [];
|
|
3954
4003
|
if (opts.detail !== void 0) flags.push("--detail", String(opts.detail));
|
|
@@ -3959,6 +4008,7 @@ function graphFlags(opts) {
|
|
|
3959
4008
|
if (opts.live) flags.push("--live");
|
|
3960
4009
|
if (opts.overlay) flags.push("--overlay");
|
|
3961
4010
|
if (opts.namespace) flags.push("--namespace", opts.namespace);
|
|
4011
|
+
if (opts.traffic) flags.push("--traffic", opts.traffic);
|
|
3962
4012
|
return flags;
|
|
3963
4013
|
}
|
|
3964
4014
|
function packageManifestFrom(req, name) {
|
|
@@ -4304,7 +4354,7 @@ async function helmRenderDiffLive(projectDir, contentDigest, env, opts = {}) {
|
|
|
4304
4354
|
function applyArgs(target, env) {
|
|
4305
4355
|
return ["run", target, "--components", "--env", env, "--progress-json"];
|
|
4306
4356
|
}
|
|
4307
|
-
var reads, unstampableRead, ChantCliError, CI_FORGES;
|
|
4357
|
+
var TRAFFIC_FLOOR, reads, unstampableRead, ChantCliError, CI_FORGES;
|
|
4308
4358
|
var init_chant = __esm({
|
|
4309
4359
|
"src/chant.ts"() {
|
|
4310
4360
|
"use strict";
|
|
@@ -4318,6 +4368,7 @@ var init_chant = __esm({
|
|
|
4318
4368
|
init_yaml();
|
|
4319
4369
|
init_carve_manifest();
|
|
4320
4370
|
init_foreign();
|
|
4371
|
+
TRAFFIC_FLOOR = "0.75.0";
|
|
4321
4372
|
reads = new ReadScheduler();
|
|
4322
4373
|
unstampableRead = 0;
|
|
4323
4374
|
ChantCliError = class extends Error {
|
|
@@ -4333,8 +4384,8 @@ var init_chant = __esm({
|
|
|
4333
4384
|
});
|
|
4334
4385
|
|
|
4335
4386
|
// src/cli.ts
|
|
4336
|
-
import { resolve as
|
|
4337
|
-
import { realpathSync as realpathSync2, existsSync as
|
|
4387
|
+
import { resolve as resolve18, dirname as dirname10, join as join29 } from "node:path";
|
|
4388
|
+
import { realpathSync as realpathSync2, existsSync as existsSync21, readFileSync as readFileSync22, rmSync as rmSync4 } from "node:fs";
|
|
4338
4389
|
import { fileURLToPath as fileURLToPath6 } from "node:url";
|
|
4339
4390
|
import { spawn as spawn7, spawnSync as spawnSync3 } from "node:child_process";
|
|
4340
4391
|
|
|
@@ -4392,8 +4443,8 @@ import { serve } from "@hono/node-server";
|
|
|
4392
4443
|
import { fileURLToPath as fileURLToPath4 } from "node:url";
|
|
4393
4444
|
import { execFile as execFile3 } from "node:child_process";
|
|
4394
4445
|
import { promisify as promisify3 } from "node:util";
|
|
4395
|
-
import { existsSync as
|
|
4396
|
-
import { basename as
|
|
4446
|
+
import { existsSync as existsSync18, readFileSync as readFileSync19 } from "node:fs";
|
|
4447
|
+
import { basename as basename3, dirname as dirname7, join as join26, relative as relative8, resolve as resolve16, sep as sep7 } from "node:path";
|
|
4397
4448
|
|
|
4398
4449
|
// src/recents.ts
|
|
4399
4450
|
import { readFileSync, writeFileSync, mkdirSync, existsSync } from "node:fs";
|
|
@@ -4635,19 +4686,19 @@ function finishPipelineProgress(state, exitCode) {
|
|
|
4635
4686
|
}
|
|
4636
4687
|
|
|
4637
4688
|
// src/gh-run.ts
|
|
4638
|
-
var defaultGhExec = (args) => new Promise((
|
|
4689
|
+
var defaultGhExec = (args) => new Promise((resolve19) => {
|
|
4639
4690
|
let out = "";
|
|
4640
4691
|
let proc;
|
|
4641
4692
|
try {
|
|
4642
4693
|
proc = spawn3("gh", args, { stdio: ["ignore", "pipe", "pipe"] });
|
|
4643
4694
|
} catch {
|
|
4644
|
-
|
|
4695
|
+
resolve19({ code: 127, out: "" });
|
|
4645
4696
|
return;
|
|
4646
4697
|
}
|
|
4647
4698
|
proc.stdout.on("data", (d) => out += d);
|
|
4648
4699
|
proc.stderr.on("data", (d) => out += d);
|
|
4649
|
-
proc.on("error", () =>
|
|
4650
|
-
proc.on("close", (code) =>
|
|
4700
|
+
proc.on("error", () => resolve19({ code: 127, out }));
|
|
4701
|
+
proc.on("close", (code) => resolve19({ code: code ?? 1, out }));
|
|
4651
4702
|
});
|
|
4652
4703
|
async function ghReady(exec = defaultGhExec) {
|
|
4653
4704
|
const { code, out } = await exec(["auth", "status"]);
|
|
@@ -6143,7 +6194,7 @@ async function carveStatesUnder(dirs) {
|
|
|
6143
6194
|
init_read_scheduler();
|
|
6144
6195
|
init_chant();
|
|
6145
6196
|
import { statSync as statSync5 } from "node:fs";
|
|
6146
|
-
import { join as
|
|
6197
|
+
import { join as join13, resolve as resolve10, sep as sep4 } from "node:path";
|
|
6147
6198
|
import { composeStacks, shortStackNames } from "@intentius/pinhole";
|
|
6148
6199
|
|
|
6149
6200
|
// src/overlay-ir.ts
|
|
@@ -6271,6 +6322,241 @@ function tierMismatchNote(ir, tiers, currentTier) {
|
|
|
6271
6322
|
return void 0;
|
|
6272
6323
|
}
|
|
6273
6324
|
|
|
6325
|
+
// src/workspace.ts
|
|
6326
|
+
init_chant();
|
|
6327
|
+
init_floor();
|
|
6328
|
+
init_member_source();
|
|
6329
|
+
import { existsSync as existsSync8 } from "node:fs";
|
|
6330
|
+
import { join as join12, resolve as resolve9 } from "node:path";
|
|
6331
|
+
var WORKSPACE_CONTRACT = 1;
|
|
6332
|
+
var WORKSPACE_CHANT_FLOOR = "0.81.0";
|
|
6333
|
+
var DECLARATION_FILES = ["chant.workspace.json", "chant.workspace.jsonc"];
|
|
6334
|
+
function findDeclaration(dir) {
|
|
6335
|
+
const found = DECLARATION_FILES.filter((f) => existsSync8(join12(dir, f)));
|
|
6336
|
+
if (found.length > 1) return { ambiguous: true };
|
|
6337
|
+
return found.length === 1 ? { file: found[0] } : void 0;
|
|
6338
|
+
}
|
|
6339
|
+
function hasDeclaration(dir) {
|
|
6340
|
+
return findDeclaration(dir) !== void 0;
|
|
6341
|
+
}
|
|
6342
|
+
var refuse4 = (error, code, remedy) => ({ ok: false, refusal: { error, code, remedy } });
|
|
6343
|
+
var isRecord6 = (v) => v !== null && typeof v === "object" && !Array.isArray(v);
|
|
6344
|
+
function parseWorkspaceLs(text, root) {
|
|
6345
|
+
let doc;
|
|
6346
|
+
try {
|
|
6347
|
+
doc = JSON.parse(text);
|
|
6348
|
+
} catch {
|
|
6349
|
+
return refuse4(`chant workspace ls printed something that is not JSON for ${root}.`, "workspace-ls", "Run `chant workspace ls --json` in the workspace root to see what it says.");
|
|
6350
|
+
}
|
|
6351
|
+
if (!isRecord6(doc)) return refuse4("chant workspace ls printed JSON that is not an object.", "workspace-ls", "Run `chant workspace ls --json` in the workspace root.");
|
|
6352
|
+
const contract = contractRefusal(doc, "ls");
|
|
6353
|
+
if (contract) return { ok: false, refusal: contract };
|
|
6354
|
+
if (isRecord6(doc.error)) {
|
|
6355
|
+
const code = String(doc.error.code ?? "unknown");
|
|
6356
|
+
return refuse4(`chant could not read the workspace declaration in ${root}: ${code}: ${String(doc.error.message ?? "")}`, code, "`chant workspace check` names the problem and where it is.");
|
|
6357
|
+
}
|
|
6358
|
+
const ws = doc.workspace;
|
|
6359
|
+
if (!isRecord6(ws) || typeof ws.name !== "string" || !Array.isArray(doc.members)) {
|
|
6360
|
+
return refuse4("That is not a workspace ls document: it has no `workspace` and `members`.", "workspace-ls", "Run `chant workspace ls --json` in the workspace root.");
|
|
6361
|
+
}
|
|
6362
|
+
const members = [];
|
|
6363
|
+
for (const m of doc.members) {
|
|
6364
|
+
if (!isRecord6(m) || typeof m.name !== "string" || typeof m.dir !== "string" || typeof m.kind !== "string") {
|
|
6365
|
+
return refuse4("A member in the workspace ls document has no name, dir or kind.", "workspace-ls", "Run `chant workspace ls --json` in the workspace root.");
|
|
6366
|
+
}
|
|
6367
|
+
const reason = isRecord6(m.reason) ? { code: String(m.reason.code), message: String(m.reason.message ?? "") } : null;
|
|
6368
|
+
members.push({
|
|
6369
|
+
name: m.name,
|
|
6370
|
+
dir: m.dir,
|
|
6371
|
+
kind: m.kind,
|
|
6372
|
+
abs: m.dir === "." ? resolve9(root) : resolve9(root, m.dir),
|
|
6373
|
+
reason,
|
|
6374
|
+
because: typeof m.because === "string" ? m.because : null
|
|
6375
|
+
});
|
|
6376
|
+
}
|
|
6377
|
+
return {
|
|
6378
|
+
ok: true,
|
|
6379
|
+
workspace: { name: ws.name, root: resolve9(root), file: typeof ws.file === "string" ? ws.file : "chant.workspace.json", chant: String(doc.chant ?? ""), members }
|
|
6380
|
+
};
|
|
6381
|
+
}
|
|
6382
|
+
function contractRefusal(doc, command) {
|
|
6383
|
+
if (doc.contract === WORKSPACE_CONTRACT) return void 0;
|
|
6384
|
+
const said4 = doc.contract === void 0 ? "no contract version" : `contract ${JSON.stringify(doc.contract)}`;
|
|
6385
|
+
return {
|
|
6386
|
+
error: `chant workspace ${command} wrote a document with ${said4}; behold reads contract ${WORKSPACE_CONTRACT}.`,
|
|
6387
|
+
code: "workspace-contract",
|
|
6388
|
+
remedy: doc.contract === void 0 ? `Upgrade the workspace's chant to ${WORKSPACE_CHANT_FLOOR} or newer.` : "Upgrade behold: this chant writes a newer workspace contract than it knows."
|
|
6389
|
+
};
|
|
6390
|
+
}
|
|
6391
|
+
async function readWorkspace(root) {
|
|
6392
|
+
const dir = resolve9(root);
|
|
6393
|
+
const found = findDeclaration(dir);
|
|
6394
|
+
if (!found) return refuse4(`${dir} has no chant.workspace.json.`, "declaration-missing", "`chant workspace init` proposes one.");
|
|
6395
|
+
if ("ambiguous" in found) {
|
|
6396
|
+
return refuse4(`${dir} has both chant.workspace.json and chant.workspace.jsonc.`, "declaration-ambiguous", "Keep one of the two files.");
|
|
6397
|
+
}
|
|
6398
|
+
const chant = resolveChant(dir);
|
|
6399
|
+
if (chant.version && !meetsFloor(chant.version, WORKSPACE_CHANT_FLOOR)) {
|
|
6400
|
+
return refuse4(
|
|
6401
|
+
`${dir} declares a chant workspace, and the chant behold would read it with is ${chant.version}; workspaces need ${WORKSPACE_CHANT_FLOOR} or newer.`,
|
|
6402
|
+
"workspace-chant-too-old",
|
|
6403
|
+
`Install @intentius/chant ${WORKSPACE_CHANT_FLOOR} or newer in the workspace root.`
|
|
6404
|
+
);
|
|
6405
|
+
}
|
|
6406
|
+
const run3 = await runChantRaw(["workspace", "ls", dir, "--json"], dir);
|
|
6407
|
+
if (!run3.stdout.trim()) {
|
|
6408
|
+
return refuse4(`chant workspace ls failed in ${dir} (exit ${run3.code}): ${run3.stderr.trim().split("\n").slice(-3).join(" ")}`, "workspace-ls", "Run `chant workspace ls` in the workspace root.");
|
|
6409
|
+
}
|
|
6410
|
+
return parseWorkspaceLs(run3.stdout, dir);
|
|
6411
|
+
}
|
|
6412
|
+
var served;
|
|
6413
|
+
var byDir = /* @__PURE__ */ new Map();
|
|
6414
|
+
function setServedWorkspace(ws) {
|
|
6415
|
+
served = ws;
|
|
6416
|
+
byDir.clear();
|
|
6417
|
+
for (const m of ws?.members ?? []) byDir.set(m.abs, m);
|
|
6418
|
+
const root = ws?.members.find((m) => m.dir === ".");
|
|
6419
|
+
setStampExclusions(root && ws ? /* @__PURE__ */ new Map([[root.abs, ws.members.filter((m) => m !== root).map((m) => m.abs)]]) : /* @__PURE__ */ new Map());
|
|
6420
|
+
}
|
|
6421
|
+
function servedWorkspace() {
|
|
6422
|
+
return served;
|
|
6423
|
+
}
|
|
6424
|
+
function workspaceMemberOf(dir) {
|
|
6425
|
+
return byDir.get(resolve9(dir));
|
|
6426
|
+
}
|
|
6427
|
+
var UNDRAWN_KINDS = /* @__PURE__ */ new Set(["other", "workspace"]);
|
|
6428
|
+
function drawnMembers(ws) {
|
|
6429
|
+
return ws.members.filter((m) => m.reason === null && !UNDRAWN_KINDS.has(m.kind));
|
|
6430
|
+
}
|
|
6431
|
+
function unreadableMembers(ws) {
|
|
6432
|
+
return ws.members.filter((m) => m.reason !== null && !UNDRAWN_KINDS.has(m.kind));
|
|
6433
|
+
}
|
|
6434
|
+
function undrawnMembers(ws) {
|
|
6435
|
+
return ws.members.filter((m) => UNDRAWN_KINDS.has(m.kind));
|
|
6436
|
+
}
|
|
6437
|
+
function unreadableMemberIr(m) {
|
|
6438
|
+
return {
|
|
6439
|
+
nodes: [
|
|
6440
|
+
{
|
|
6441
|
+
id: "unreadable",
|
|
6442
|
+
kind: "UnreadableMember",
|
|
6443
|
+
lexicon: "workspace",
|
|
6444
|
+
attrs: {
|
|
6445
|
+
_status: "neutral",
|
|
6446
|
+
_unreadable: { code: m.reason?.code ?? "unknown", message: m.reason?.message ?? "" },
|
|
6447
|
+
dir: m.dir,
|
|
6448
|
+
kind: m.kind
|
|
6449
|
+
}
|
|
6450
|
+
}
|
|
6451
|
+
],
|
|
6452
|
+
edges: [],
|
|
6453
|
+
groups: {}
|
|
6454
|
+
};
|
|
6455
|
+
}
|
|
6456
|
+
function workspaceGraphTakes(opts) {
|
|
6457
|
+
return opts.detail === void 0 && !opts.lens && !opts.up && !opts.down && !opts.namespace;
|
|
6458
|
+
}
|
|
6459
|
+
var WorkspaceMemberError = class extends Error {
|
|
6460
|
+
constructor(member, reason) {
|
|
6461
|
+
super(`chant workspace graph could not read ${member}: ${reason.code}: ${reason.message}`);
|
|
6462
|
+
this.member = member;
|
|
6463
|
+
this.reason = reason;
|
|
6464
|
+
}
|
|
6465
|
+
member;
|
|
6466
|
+
reason;
|
|
6467
|
+
};
|
|
6468
|
+
function stripRefs(value, prefix) {
|
|
6469
|
+
if (Array.isArray(value)) return value.map((v) => stripRefs(v, prefix));
|
|
6470
|
+
if (!isRecord6(value)) return value;
|
|
6471
|
+
const out = {};
|
|
6472
|
+
for (const [k, v] of Object.entries(value)) {
|
|
6473
|
+
out[k] = k === "$ref" && typeof v === "string" && v.startsWith(prefix) ? v.slice(prefix.length) : stripRefs(v, prefix);
|
|
6474
|
+
}
|
|
6475
|
+
return out;
|
|
6476
|
+
}
|
|
6477
|
+
function memberIrFromWorkspaceGraph(text, member) {
|
|
6478
|
+
let doc;
|
|
6479
|
+
try {
|
|
6480
|
+
doc = JSON.parse(text);
|
|
6481
|
+
} catch {
|
|
6482
|
+
throw new Error("chant workspace graph printed something that is not JSON");
|
|
6483
|
+
}
|
|
6484
|
+
if (!isRecord6(doc)) throw new Error("chant workspace graph printed JSON that is not an object");
|
|
6485
|
+
const contract = contractRefusal(doc, "graph");
|
|
6486
|
+
if (contract) throw new Error(`${contract.error} ${contract.remedy}`);
|
|
6487
|
+
if (isRecord6(doc.error)) throw new Error(`chant workspace graph: ${String(doc.error.code)}: ${String(doc.error.message ?? "")}`);
|
|
6488
|
+
const entry = Array.isArray(doc.members) ? doc.members.find((m) => isRecord6(m) && m.name === member) : void 0;
|
|
6489
|
+
if (!isRecord6(entry)) throw new Error(`chant workspace graph did not list member ${member}`);
|
|
6490
|
+
if (entry.status !== "composed") {
|
|
6491
|
+
const r = isRecord6(entry.reason) ? entry.reason : {};
|
|
6492
|
+
throw new WorkspaceMemberError(member, { code: String(r.code ?? entry.status), message: String(r.message ?? "") });
|
|
6493
|
+
}
|
|
6494
|
+
const p = `${member}/`;
|
|
6495
|
+
const un = (id) => typeof id === "string" && id.startsWith(p) ? id.slice(p.length) : String(id);
|
|
6496
|
+
const mine = (x) => isRecord6(x) && x.member === member;
|
|
6497
|
+
const nodes = (Array.isArray(doc.nodes) ? doc.nodes : []).filter(mine).map((n) => {
|
|
6498
|
+
const { member: _m, ...rest } = n;
|
|
6499
|
+
const out = { ...rest, id: un(n.id), attrs: stripRefs(n.attrs ?? {}, p) };
|
|
6500
|
+
if (typeof n.compositeInstance === "string") out.compositeInstance = un(n.compositeInstance);
|
|
6501
|
+
if (typeof n.runtimeOwner === "string") out.runtimeOwner = un(n.runtimeOwner);
|
|
6502
|
+
return out;
|
|
6503
|
+
});
|
|
6504
|
+
const edges = (Array.isArray(doc.edges) ? doc.edges : []).filter(mine).map((e) => {
|
|
6505
|
+
const { member: _m, ...rest } = e;
|
|
6506
|
+
return { ...rest, from: un(e.from), to: un(e.to) };
|
|
6507
|
+
});
|
|
6508
|
+
const own = new Set(nodes.map((n) => n.id));
|
|
6509
|
+
const groups = {};
|
|
6510
|
+
if (isRecord6(doc.groups)) {
|
|
6511
|
+
for (const [key, table] of Object.entries(doc.groups)) {
|
|
6512
|
+
if (key === "byMember" || !isRecord6(table)) continue;
|
|
6513
|
+
const scoped = key === "byStack" || key === "byContainer" || key === "byWave";
|
|
6514
|
+
for (const [k, ids] of Object.entries(table)) {
|
|
6515
|
+
if (scoped && !k.startsWith(p)) continue;
|
|
6516
|
+
const members = (Array.isArray(ids) ? ids : []).map(un).filter((id) => own.has(id));
|
|
6517
|
+
if (members.length) (groups[key] ??= {})[scoped ? k.slice(p.length) : k] = members;
|
|
6518
|
+
}
|
|
6519
|
+
}
|
|
6520
|
+
}
|
|
6521
|
+
const exports = (Array.isArray(doc.exports) ? doc.exports : []).filter(mine).map((e) => {
|
|
6522
|
+
const { member: _m, ...rest } = e;
|
|
6523
|
+
return typeof e.node === "string" ? { ...rest, node: un(e.node) } : rest;
|
|
6524
|
+
});
|
|
6525
|
+
const imports = (Array.isArray(doc.imports) ? doc.imports : []).filter(mine).map((i) => {
|
|
6526
|
+
const { member: _m, ...rest } = i;
|
|
6527
|
+
return { ...rest, node: un(i.node) };
|
|
6528
|
+
});
|
|
6529
|
+
const ir = { version: 1, nodes, edges, groups, exports, imports };
|
|
6530
|
+
if (isRecord6(entry.meta)) ir.meta = entry.meta;
|
|
6531
|
+
if (entry.pipeline !== void 0) ir.pipeline = entry.pipeline;
|
|
6532
|
+
if (isRecord6(doc.derivedAttrs)) ir.derivedAttrs = doc.derivedAttrs;
|
|
6533
|
+
return ir;
|
|
6534
|
+
}
|
|
6535
|
+
function workspaceGraphArgs(root, member, opts) {
|
|
6536
|
+
const args = ["workspace", "graph", root, "--member", member];
|
|
6537
|
+
if (opts.env) args.push("--env", opts.env);
|
|
6538
|
+
if (opts.live) args.push("--live");
|
|
6539
|
+
if (opts.overlay) args.push("--overlay");
|
|
6540
|
+
if (opts.traffic) args.push("--traffic", opts.traffic);
|
|
6541
|
+
return args;
|
|
6542
|
+
}
|
|
6543
|
+
var CONTRACT_READ_KINDS = /* @__PURE__ */ new Set(["chant", "terraform"]);
|
|
6544
|
+
function workspaceVia(member, root, fallback) {
|
|
6545
|
+
return {
|
|
6546
|
+
tool: (dir) => {
|
|
6547
|
+
const chant = resolveChant(root);
|
|
6548
|
+
return `workspace\0${chant.bin}\0${chant.version ?? ""}\0${fallback.tool(dir)}`;
|
|
6549
|
+
},
|
|
6550
|
+
read: async (dir, opts) => {
|
|
6551
|
+
if (!workspaceGraphTakes(opts)) return fallback.read(dir, opts);
|
|
6552
|
+
const args = workspaceGraphArgs(root, member.name, opts);
|
|
6553
|
+
const run3 = await runChantRaw(args, root, envOverridesFor(opts));
|
|
6554
|
+
if (!run3.stdout.trim()) throw new Error(`chant ${args.join(" ")} exited ${run3.code}: ${run3.stderr.trim()}`);
|
|
6555
|
+
return memberIrFromWorkspaceGraph(run3.stdout, member.name);
|
|
6556
|
+
}
|
|
6557
|
+
};
|
|
6558
|
+
}
|
|
6559
|
+
|
|
6274
6560
|
// src/estate.ts
|
|
6275
6561
|
function estateReadPool(members, env = process.env) {
|
|
6276
6562
|
return Math.max(1, Math.min(readConcurrency(env), members));
|
|
@@ -6287,14 +6573,35 @@ async function mapPool(items, width, fn) {
|
|
|
6287
6573
|
return results;
|
|
6288
6574
|
}
|
|
6289
6575
|
function estateMembers(projectDirs) {
|
|
6290
|
-
const names =
|
|
6291
|
-
|
|
6576
|
+
const names = estateMemberNames(projectDirs);
|
|
6577
|
+
const kindOf2 = (dir) => workspaceMemberOf(dir)?.kind ?? memberKindOf(dir) ?? "chant";
|
|
6578
|
+
return projectDirs.map((dir, i) => ({ name: names[i], dir, kind: kindOf2(dir) }));
|
|
6579
|
+
}
|
|
6580
|
+
function estateMemberNames(projectDirs) {
|
|
6581
|
+
const declared = projectDirs.map((d) => workspaceMemberOf(d)?.name);
|
|
6582
|
+
if (declared.every((n) => n !== void 0)) return declared;
|
|
6583
|
+
return shortStackNames([...projectDirs]);
|
|
6584
|
+
}
|
|
6585
|
+
function unreadableStacks(projectDirs) {
|
|
6586
|
+
const ws = servedWorkspace();
|
|
6587
|
+
if (!ws || !projectDirs.every((d) => workspaceMemberOf(d))) return [];
|
|
6588
|
+
return unreadableMembers(ws).map((m) => ({ name: m.name, ir: unreadableMemberIr(m) }));
|
|
6589
|
+
}
|
|
6590
|
+
var memberViaFor = (dir) => {
|
|
6591
|
+
const own = memberKindSpec(memberKindOf(dir) ?? "chant")?.via ?? chantVia;
|
|
6592
|
+
const declared = workspaceMemberOf(dir);
|
|
6593
|
+
const ws = servedWorkspace();
|
|
6594
|
+
return declared && ws && CONTRACT_READ_KINDS.has(declared.kind) ? workspaceVia(declared, ws.root, own) : own;
|
|
6595
|
+
};
|
|
6596
|
+
function forMember(dir, opts) {
|
|
6597
|
+
if (!opts.traffic || memberTakesTraffic(dir, memberKindOf(dir) ?? "chant")) return opts;
|
|
6598
|
+
const { traffic: _traffic, ...rest } = opts;
|
|
6599
|
+
return rest;
|
|
6292
6600
|
}
|
|
6293
|
-
var
|
|
6294
|
-
var
|
|
6295
|
-
var memberLive = (dir, opts, fresh) => overlayIr(dir, opts, memberViaFor(dir), fresh);
|
|
6601
|
+
var memberSource = (dir, opts) => memberIr(dir, forMember(dir, opts), memberViaFor(dir));
|
|
6602
|
+
var memberLive = (dir, opts, fresh) => overlayIr(dir, forMember(dir, opts), memberViaFor(dir), fresh);
|
|
6296
6603
|
async function composeEstate(projectDirs, opts = {}) {
|
|
6297
|
-
const names =
|
|
6604
|
+
const names = estateMemberNames(projectDirs);
|
|
6298
6605
|
const stacks = await mapPool(projectDirs, estateReadPool(projectDirs.length), async (dir, i) => ({
|
|
6299
6606
|
name: names[i],
|
|
6300
6607
|
// chant#2040: the manifests a member holds name the files its carved
|
|
@@ -6302,17 +6609,17 @@ async function composeEstate(projectDirs, opts = {}) {
|
|
|
6302
6609
|
// known, onto a COPY (the member IR is the #312 cache's own object).
|
|
6303
6610
|
ir: joinCarvedSources(await memberSource(dir, opts), (await carveStatesFor(dir)).values(), dir).ir
|
|
6304
6611
|
}));
|
|
6305
|
-
return composeStacks(stacks);
|
|
6612
|
+
return composeStacks([...stacks, ...unreadableStacks(projectDirs)]);
|
|
6306
6613
|
}
|
|
6307
6614
|
async function composeEstatePending(projectDirs, opts = {}) {
|
|
6308
|
-
const names =
|
|
6615
|
+
const names = estateMemberNames(projectDirs);
|
|
6309
6616
|
const { env: _env, live: _live, overlay: _overlay, ...srcOpts } = opts;
|
|
6310
6617
|
const stacks = await mapPool(projectDirs, estateReadPool(projectDirs.length), async (dir, i) => {
|
|
6311
6618
|
const ir = joinCarvedSources(await memberSource(dir, srcOpts), (await carveStatesFor(dir)).values(), dir).ir;
|
|
6312
6619
|
for (const n of ir.nodes) n.attrs = { ...n.attrs, _pendingRead: true };
|
|
6313
6620
|
return { name: names[i], ir };
|
|
6314
6621
|
});
|
|
6315
|
-
return composeStacks(stacks);
|
|
6622
|
+
return composeStacks([...stacks, ...unreadableStacks(projectDirs)]);
|
|
6316
6623
|
}
|
|
6317
6624
|
var firstLine2 = (e) => {
|
|
6318
6625
|
const msg = e instanceof Error ? e.message : String(e);
|
|
@@ -6361,11 +6668,11 @@ function declaredSegments(path) {
|
|
|
6361
6668
|
function pathAlignment(declared, dir, isDir = realIsDir) {
|
|
6362
6669
|
const want = declaredSegments(declared);
|
|
6363
6670
|
if (!want) return 0;
|
|
6364
|
-
const root =
|
|
6671
|
+
const root = resolve10(dir);
|
|
6365
6672
|
const have = root.split(sep4).filter(Boolean);
|
|
6366
6673
|
for (let k = Math.min(want.length, have.length); k >= 1; k--) {
|
|
6367
6674
|
if (!have.slice(-k).every((s, i) => s === want[i])) continue;
|
|
6368
|
-
if (!isDir(
|
|
6675
|
+
if (!isDir(join13(root, ...want.slice(k)))) continue;
|
|
6369
6676
|
return k;
|
|
6370
6677
|
}
|
|
6371
6678
|
return 0;
|
|
@@ -6395,10 +6702,10 @@ function joinNamespaceBindings(members, isDir = realIsDir) {
|
|
|
6395
6702
|
if (!winner || tied) continue;
|
|
6396
6703
|
const prev = best.get(winner.dir);
|
|
6397
6704
|
if (prev === "ambiguous") continue;
|
|
6398
|
-
const
|
|
6399
|
-
if (!prev) best.set(winner.dir, { score: winner.score, join:
|
|
6705
|
+
const join30 = { dir: winner.dir, namespace: b.namespace, path: b.path, declaredBy: b.declaredBy };
|
|
6706
|
+
if (!prev) best.set(winner.dir, { score: winner.score, join: join30 });
|
|
6400
6707
|
else if (prev.join.namespace !== b.namespace) best.set(winner.dir, "ambiguous");
|
|
6401
|
-
else if (winner.score > prev.score) best.set(winner.dir, { score: winner.score, join:
|
|
6708
|
+
else if (winner.score > prev.score) best.set(winner.dir, { score: winner.score, join: join30 });
|
|
6402
6709
|
}
|
|
6403
6710
|
return [...best.values()].filter((v) => v !== "ambiguous").map((v) => v.join);
|
|
6404
6711
|
}
|
|
@@ -6422,7 +6729,7 @@ function withoutJoinedMembers(nodes, joined) {
|
|
|
6422
6729
|
return nodes.filter((n) => !joined.some((j) => n.id.startsWith(`${j.name}/`)));
|
|
6423
6730
|
}
|
|
6424
6731
|
async function composeEstateOverlay(projectDirs, opts, classify, observe = {}) {
|
|
6425
|
-
const names =
|
|
6732
|
+
const names = estateMemberNames(projectDirs);
|
|
6426
6733
|
const unobserved = [];
|
|
6427
6734
|
const dropped = [];
|
|
6428
6735
|
const joined = [];
|
|
@@ -6456,7 +6763,7 @@ async function composeEstateOverlay(projectDirs, opts, classify, observe = {}) {
|
|
|
6456
6763
|
});
|
|
6457
6764
|
const present = stacks.filter((s) => !!s);
|
|
6458
6765
|
return {
|
|
6459
|
-
ir: composeStacks(present),
|
|
6766
|
+
ir: composeStacks([...present, ...unreadableStacks(projectDirs)]),
|
|
6460
6767
|
observed: present.length - unobserved.length,
|
|
6461
6768
|
total: projectDirs.length,
|
|
6462
6769
|
unobserved,
|
|
@@ -7123,8 +7430,8 @@ function projectHelmLogical(ir) {
|
|
|
7123
7430
|
}
|
|
7124
7431
|
|
|
7125
7432
|
// src/logical-kustomize.ts
|
|
7126
|
-
import { existsSync as
|
|
7127
|
-
import { join as
|
|
7433
|
+
import { existsSync as existsSync9 } from "node:fs";
|
|
7434
|
+
import { join as join14 } from "node:path";
|
|
7128
7435
|
function overlayBoxTitle(name) {
|
|
7129
7436
|
return `overlay ${name}`;
|
|
7130
7437
|
}
|
|
@@ -7144,7 +7451,7 @@ function kustomizationRoot(bases, dir, exists) {
|
|
|
7144
7451
|
let current = dir;
|
|
7145
7452
|
for (let i = 0; i < 4 && current; i++) {
|
|
7146
7453
|
for (const base of bases) {
|
|
7147
|
-
if (exists(
|
|
7454
|
+
if (exists(join14(base, current, "kustomization.yaml")) || exists(join14(base, current, "kustomization.yml"))) {
|
|
7148
7455
|
return current;
|
|
7149
7456
|
}
|
|
7150
7457
|
}
|
|
@@ -7153,7 +7460,7 @@ function kustomizationRoot(bases, dir, exists) {
|
|
|
7153
7460
|
}
|
|
7154
7461
|
return void 0;
|
|
7155
7462
|
}
|
|
7156
|
-
function projectKustomizeLogical(ir, sourceRoots, exists =
|
|
7463
|
+
function projectKustomizeLogical(ir, sourceRoots, exists = existsSync9) {
|
|
7157
7464
|
const bases = Array.isArray(sourceRoots) ? sourceRoots : [sourceRoots];
|
|
7158
7465
|
const k8s = ir.nodes.filter((n) => n.lexicon === "k8s");
|
|
7159
7466
|
if (k8s.length === 0) return { ir: { nodes: [], edges: [], groups: {} }, byContainer: {} };
|
|
@@ -7934,9 +8241,9 @@ import {
|
|
|
7934
8241
|
|
|
7935
8242
|
// src/icon-packs.ts
|
|
7936
8243
|
import { readFileSync as readFileSync10 } from "node:fs";
|
|
7937
|
-
import { dirname as dirname5, join as
|
|
8244
|
+
import { dirname as dirname5, join as join15 } from "node:path";
|
|
7938
8245
|
import { fileURLToPath as fileURLToPath3 } from "node:url";
|
|
7939
|
-
var ICON_ROOT =
|
|
8246
|
+
var ICON_ROOT = join15(dirname5(fileURLToPath3(import.meta.url)), "..", "web", "icons");
|
|
7940
8247
|
var cache4 = /* @__PURE__ */ new Map();
|
|
7941
8248
|
function unprefix(body) {
|
|
7942
8249
|
return body.replace(/\s(?:inkscape|sodipodi):[\w-]+\s*=\s*(["'])[\s\S]*?\1/g, "").replace(/\sxlink:href\s*=/g, " href=");
|
|
@@ -7958,7 +8265,7 @@ function plate(glyph) {
|
|
|
7958
8265
|
function loadIcon(rel) {
|
|
7959
8266
|
const hit = cache4.get(rel);
|
|
7960
8267
|
if (hit) return hit;
|
|
7961
|
-
const raw = readFileSync10(
|
|
8268
|
+
const raw = readFileSync10(join15(ICON_ROOT, `${rel}.svg`), "utf8");
|
|
7962
8269
|
const open = raw.indexOf("<svg");
|
|
7963
8270
|
const openEnd = raw.indexOf(">", open);
|
|
7964
8271
|
const close = raw.lastIndexOf("</svg");
|
|
@@ -8119,22 +8426,22 @@ init_carve_manifest();
|
|
|
8119
8426
|
|
|
8120
8427
|
// src/ops-lens.ts
|
|
8121
8428
|
var SUPPORTED_MAJOR2 = 1;
|
|
8122
|
-
var
|
|
8429
|
+
var refuse5 = (error, remedy) => ({ ok: false, refusal: { error, code: "op-ir", remedy } });
|
|
8123
8430
|
var HOW_TO_EMIT = "Build the project with a chant >= 0.50 (`chant build --lexicon temporal`), which writes `dist/ops/<name>/op.json` beside each Op's generated workflow.";
|
|
8124
|
-
var
|
|
8431
|
+
var isRecord7 = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
8125
8432
|
function parseOpIr(value) {
|
|
8126
|
-
if (!
|
|
8127
|
-
return
|
|
8433
|
+
if (!isRecord7(value)) {
|
|
8434
|
+
return refuse5("That file isn't an op.json \u2014 its top level isn't a JSON object.", HOW_TO_EMIT);
|
|
8128
8435
|
}
|
|
8129
8436
|
if (typeof value.name !== "string" || !Array.isArray(value.phases)) {
|
|
8130
|
-
return
|
|
8437
|
+
return refuse5(
|
|
8131
8438
|
"That JSON doesn't look like an op.json \u2014 it needs a string `name` and a `phases` array.",
|
|
8132
8439
|
HOW_TO_EMIT
|
|
8133
8440
|
);
|
|
8134
8441
|
}
|
|
8135
|
-
const bad2 = value.phases.findIndex((p) => !
|
|
8442
|
+
const bad2 = value.phases.findIndex((p) => !isRecord7(p) || typeof p.name !== "string" || !Array.isArray(p.steps));
|
|
8136
8443
|
if (bad2 >= 0) {
|
|
8137
|
-
return
|
|
8444
|
+
return refuse5(
|
|
8138
8445
|
`That JSON has a \`phases\` array, but entry ${bad2} isn't a phase (needs a string \`name\` and a \`steps\` array).`,
|
|
8139
8446
|
HOW_TO_EMIT
|
|
8140
8447
|
);
|
|
@@ -8143,7 +8450,7 @@ function parseOpIr(value) {
|
|
|
8143
8450
|
if (version !== void 0) {
|
|
8144
8451
|
const major = Number(String(version).split(".")[0]);
|
|
8145
8452
|
if (!Number.isFinite(major) || major !== SUPPORTED_MAJOR2) {
|
|
8146
|
-
return
|
|
8453
|
+
return refuse5(
|
|
8147
8454
|
`This op.json declares format version ${String(version)}; behold reads version ${SUPPORTED_MAJOR2}.`,
|
|
8148
8455
|
"Upgrade behold, or render the Op with a behold that speaks its version."
|
|
8149
8456
|
);
|
|
@@ -8156,13 +8463,13 @@ function readOpIr(path, readFile) {
|
|
|
8156
8463
|
try {
|
|
8157
8464
|
text = readFile(path);
|
|
8158
8465
|
} catch (err) {
|
|
8159
|
-
return
|
|
8466
|
+
return refuse5(`Couldn't read ${path}: ${err instanceof Error ? err.message : String(err)}`, HOW_TO_EMIT);
|
|
8160
8467
|
}
|
|
8161
8468
|
let json;
|
|
8162
8469
|
try {
|
|
8163
8470
|
json = JSON.parse(text);
|
|
8164
8471
|
} catch (err) {
|
|
8165
|
-
return
|
|
8472
|
+
return refuse5(`${path} isn't valid JSON: ${err instanceof Error ? err.message : String(err)}`, HOW_TO_EMIT);
|
|
8166
8473
|
}
|
|
8167
8474
|
return parseOpIr(json);
|
|
8168
8475
|
}
|
|
@@ -8229,14 +8536,14 @@ function opPlacedSteps(op) {
|
|
|
8229
8536
|
}
|
|
8230
8537
|
var RULE_BOX = (opName) => `${opName} \xB7 rule table`;
|
|
8231
8538
|
var ruleId = (opName, rule) => `${opName}/rules/${rule.id}`;
|
|
8232
|
-
var isPredicate = (v) =>
|
|
8539
|
+
var isPredicate = (v) => isRecord7(v) && typeof v.kind === "string";
|
|
8233
8540
|
function isConvergeRule(v) {
|
|
8234
|
-
if (!
|
|
8541
|
+
if (!isRecord7(v)) return false;
|
|
8235
8542
|
if (typeof v.id !== "string" || !v.id) return false;
|
|
8236
8543
|
if (typeof v.why !== "string" || !v.why) return false;
|
|
8237
8544
|
if (!isPredicate(v.when)) return false;
|
|
8238
8545
|
const then = v.then;
|
|
8239
|
-
if (!
|
|
8546
|
+
if (!isRecord7(then)) return false;
|
|
8240
8547
|
return then.kind === "run" && typeof then.op === "string" || then.kind === "report" && typeof then.reason === "string";
|
|
8241
8548
|
}
|
|
8242
8549
|
function convergeTable(op) {
|
|
@@ -8577,15 +8884,15 @@ function entityCountClause(ir) {
|
|
|
8577
8884
|
var LAYOUT_ROW_VIA = "_layout-row";
|
|
8578
8885
|
var ROW_ASPECT = 2.4;
|
|
8579
8886
|
var ROW_MIN_CARDS = 6;
|
|
8580
|
-
function rowColumns(n, cell,
|
|
8887
|
+
function rowColumns(n, cell, sep9, aspect = ROW_ASPECT) {
|
|
8581
8888
|
if (n < 2) return Math.max(1, n);
|
|
8582
|
-
const cols = Math.round(Math.sqrt(n * (cell.h +
|
|
8889
|
+
const cols = Math.round(Math.sqrt(n * (cell.h + sep9.y) * aspect / (cell.w + sep9.x)));
|
|
8583
8890
|
return Math.max(1, Math.min(n, cols));
|
|
8584
8891
|
}
|
|
8585
8892
|
var FALLBACK = { w: 175, h: 104 };
|
|
8586
8893
|
function withRowChains(ir, groups, opts) {
|
|
8587
8894
|
const min = opts.min ?? ROW_MIN_CARDS;
|
|
8588
|
-
const
|
|
8895
|
+
const sep9 = { x: opts.nodesep ?? 64, y: opts.ranksep ?? 72 };
|
|
8589
8896
|
const known = new Set(ir.nodes.map((n) => n.id));
|
|
8590
8897
|
const boxOf2 = /* @__PURE__ */ new Map();
|
|
8591
8898
|
for (const [key, ids] of Object.entries(groups)) for (const id of ids) if (known.has(id)) boxOf2.set(id, key);
|
|
@@ -8606,7 +8913,7 @@ function withRowChains(ir, groups, opts) {
|
|
|
8606
8913
|
},
|
|
8607
8914
|
{ w: 0, h: 0 }
|
|
8608
8915
|
);
|
|
8609
|
-
const cols = rowColumns(ids.length, cell.w ? cell : FALLBACK,
|
|
8916
|
+
const cols = rowColumns(ids.length, cell.w ? cell : FALLBACK, sep9, opts.aspect);
|
|
8610
8917
|
if (cols >= ids.length) continue;
|
|
8611
8918
|
const bands = bandsOf(ids, opts.bandOf);
|
|
8612
8919
|
let rows = 0;
|
|
@@ -9364,17 +9671,17 @@ function gridComponent(one, sizes) {
|
|
|
9364
9671
|
},
|
|
9365
9672
|
{ w: NODE_W, h: NODE_H }
|
|
9366
9673
|
);
|
|
9367
|
-
const
|
|
9368
|
-
const cols = rowColumns(order.length, cell,
|
|
9674
|
+
const sep9 = { x: 48, y: 60 };
|
|
9675
|
+
const cols = rowColumns(order.length, cell, sep9);
|
|
9369
9676
|
const local = /* @__PURE__ */ new Map();
|
|
9370
9677
|
order.forEach((n, i) => {
|
|
9371
9678
|
local.set(n.id, {
|
|
9372
|
-
x: i % cols * (cell.w +
|
|
9373
|
-
y: Math.floor(i / cols) * (cell.h +
|
|
9679
|
+
x: i % cols * (cell.w + sep9.x) + cell.w / 2,
|
|
9680
|
+
y: Math.floor(i / cols) * (cell.h + sep9.y) + cell.h / 2
|
|
9374
9681
|
});
|
|
9375
9682
|
});
|
|
9376
9683
|
const rows = Math.ceil(order.length / cols);
|
|
9377
|
-
return { local, w: Math.min(cols, order.length) * (cell.w +
|
|
9684
|
+
return { local, w: Math.min(cols, order.length) * (cell.w + sep9.x) - sep9.x, h: rows * (cell.h + sep9.y) - sep9.y };
|
|
9378
9685
|
}
|
|
9379
9686
|
function packBoxComponents(layout, ir, boxes, bandOf) {
|
|
9380
9687
|
const grids = /* @__PURE__ */ new Map();
|
|
@@ -9618,12 +9925,12 @@ init_carve_manifest();
|
|
|
9618
9925
|
|
|
9619
9926
|
// src/carve-actions.ts
|
|
9620
9927
|
init_chant();
|
|
9621
|
-
import { existsSync as
|
|
9622
|
-
import { isAbsolute as isAbsolute3, join as
|
|
9928
|
+
import { existsSync as existsSync12, mkdirSync as mkdirSync5, readFileSync as readFileSync13, readdirSync as readdirSync8, statSync as statSync6 } from "node:fs";
|
|
9929
|
+
import { isAbsolute as isAbsolute3, join as join18, relative as relative4, resolve as resolve11, sep as sep5 } from "node:path";
|
|
9623
9930
|
|
|
9624
9931
|
// src/layout.ts
|
|
9625
|
-
import { existsSync as
|
|
9626
|
-
import { join as
|
|
9932
|
+
import { existsSync as existsSync10, mkdirSync as mkdirSync4, readFileSync as readFileSync11, renameSync, rmSync as rmSync2, writeFileSync as writeFileSync4, accessSync, constants } from "node:fs";
|
|
9933
|
+
import { join as join16 } from "node:path";
|
|
9627
9934
|
var LAYOUT_DIR = ".behold";
|
|
9628
9935
|
var LAYOUT_FILE = "layout.json";
|
|
9629
9936
|
var MAX_BODY_BYTES = 64 * 1024;
|
|
@@ -9664,10 +9971,10 @@ function lensFromQuery(params) {
|
|
|
9664
9971
|
return [slug2(zoom), params.get("radial") === "1" ? "radial" : null, stack ? slug2(`stack-${stack}`) : null].filter(Boolean).join("+");
|
|
9665
9972
|
}
|
|
9666
9973
|
function layoutPath(projectDir) {
|
|
9667
|
-
return
|
|
9974
|
+
return join16(projectDir, LAYOUT_DIR, LAYOUT_FILE);
|
|
9668
9975
|
}
|
|
9669
9976
|
function unwritableReason(projectDir) {
|
|
9670
|
-
if (!projectDir || !
|
|
9977
|
+
if (!projectDir || !existsSync10(projectDir)) return `no such project directory: ${projectDir || "(none)"}`;
|
|
9671
9978
|
try {
|
|
9672
9979
|
accessSync(projectDir, constants.W_OK);
|
|
9673
9980
|
} catch {
|
|
@@ -9679,7 +9986,7 @@ function readLayoutFile(projectDir) {
|
|
|
9679
9986
|
const empty = { version: 1, lenses: {} };
|
|
9680
9987
|
const file = layoutPath(projectDir);
|
|
9681
9988
|
try {
|
|
9682
|
-
if (!
|
|
9989
|
+
if (!existsSync10(file)) return empty;
|
|
9683
9990
|
const raw = readFileSync11(file, "utf8");
|
|
9684
9991
|
if (raw.length > MAX_FILE_BYTES) return empty;
|
|
9685
9992
|
const parsed = JSON.parse(raw);
|
|
@@ -9720,7 +10027,7 @@ function writeLens(projectDir, lens, deltas) {
|
|
|
9720
10027
|
}
|
|
9721
10028
|
const body = JSON.stringify({ version: 1, lenses }, null, 2) + "\n";
|
|
9722
10029
|
if (body.length > MAX_FILE_BYTES) throw new LayoutTooLarge(`a layout sidecar is capped at ${MAX_FILE_BYTES} bytes`);
|
|
9723
|
-
mkdirSync4(
|
|
10030
|
+
mkdirSync4(join16(projectDir, LAYOUT_DIR), { recursive: true });
|
|
9724
10031
|
const tmp = `${file}.tmp`;
|
|
9725
10032
|
writeFileSync4(tmp, body, "utf8");
|
|
9726
10033
|
renameSync(tmp, file);
|
|
@@ -9806,8 +10113,8 @@ function reanchorEdges(svg, moved) {
|
|
|
9806
10113
|
// src/carve-live.ts
|
|
9807
10114
|
init_scratch();
|
|
9808
10115
|
import { spawn as spawn4 } from "node:child_process";
|
|
9809
|
-
import { readFileSync as readFileSync12, writeFileSync as writeFileSync5, existsSync as
|
|
9810
|
-
import { join as
|
|
10116
|
+
import { readFileSync as readFileSync12, writeFileSync as writeFileSync5, existsSync as existsSync11 } from "node:fs";
|
|
10117
|
+
import { join as join17 } from "node:path";
|
|
9811
10118
|
var LIVE_CONTAINER = "behold-carve-floci";
|
|
9812
10119
|
var LIVE_PORT = 4602;
|
|
9813
10120
|
var LIVE_IMAGE = "floci/floci:1.5.34";
|
|
@@ -9867,9 +10174,9 @@ async function teardownScratchFloci() {
|
|
|
9867
10174
|
await capture("docker", ["rm", "-f", LIVE_CONTAINER], process.cwd()).catch(() => void 0);
|
|
9868
10175
|
}
|
|
9869
10176
|
async function applyIntoFloci(fromDir, step) {
|
|
9870
|
-
const template =
|
|
9871
|
-
if (!
|
|
9872
|
-
writeFileSync5(
|
|
10177
|
+
const template = join17(fromDir, "floci-override.tf.disabled");
|
|
10178
|
+
if (!existsSync11(template)) return `no floci-override.tf.disabled in ${fromDir} \u2014 this estate wasn't authored for the live tier`;
|
|
10179
|
+
writeFileSync5(join17(fromDir, "floci_override.tf"), armOverride(readFileSync12(template, "utf8"), LIVE_PORT));
|
|
9873
10180
|
const init = await step("terraform", ["init", "-input=false", "-no-color"], fromDir);
|
|
9874
10181
|
if (init !== 0) return `terraform init exited ${init} (provider downloads need network)`;
|
|
9875
10182
|
const apply = await step(
|
|
@@ -9893,7 +10200,7 @@ ${tail}`, exitCode: r.code };
|
|
|
9893
10200
|
// src/carve-actions.ts
|
|
9894
10201
|
async function runCarvePlan(demo) {
|
|
9895
10202
|
if (!demo?.live) {
|
|
9896
|
-
return
|
|
10203
|
+
return refuse6(
|
|
9897
10204
|
"carve-action",
|
|
9898
10205
|
"terraform plan is the live tier's beat, and this server isn't serving one",
|
|
9899
10206
|
"run `behold demo carve --live` (needs docker + terraform)"
|
|
@@ -9901,7 +10208,7 @@ async function runCarvePlan(demo) {
|
|
|
9901
10208
|
}
|
|
9902
10209
|
const r = await runLivePlan(demo.from);
|
|
9903
10210
|
if ("error" in r) {
|
|
9904
|
-
return
|
|
10211
|
+
return refuse6("carve-action", r.error, "Is the scratch Floci still up? `docker ps` should list " + demo.live.container + ".");
|
|
9905
10212
|
}
|
|
9906
10213
|
return {
|
|
9907
10214
|
ok: true,
|
|
@@ -9913,14 +10220,14 @@ async function runCarvePlan(demo) {
|
|
|
9913
10220
|
}
|
|
9914
10221
|
async function runCarveObserve(demo, select) {
|
|
9915
10222
|
if (!demo.live) {
|
|
9916
|
-
return
|
|
10223
|
+
return refuse6(
|
|
9917
10224
|
"carve-action",
|
|
9918
10225
|
"the observe beat reads the scratch Floci, and this server isn't serving one",
|
|
9919
10226
|
"run `behold demo carve --live` (needs docker + terraform)"
|
|
9920
10227
|
);
|
|
9921
10228
|
}
|
|
9922
|
-
if (!
|
|
9923
|
-
return
|
|
10229
|
+
if (!existsSync12(join18(demo.out, "src"))) {
|
|
10230
|
+
return refuse6("carve-action", "nothing has been emitted yet", "run Emit first \u2014 observe reads the carveout emit writes");
|
|
9924
10231
|
}
|
|
9925
10232
|
const args = ["lifecycle", "diff", "prod", "--live", "--json"];
|
|
9926
10233
|
const run3 = await runChantRaw(args, demo.out, { AWS_ENDPOINT_URL: demo.live.endpoint }).catch((err) => ({
|
|
@@ -9929,7 +10236,7 @@ async function runCarveObserve(demo, select) {
|
|
|
9929
10236
|
stderr: err instanceof Error ? err.message : String(err)
|
|
9930
10237
|
}));
|
|
9931
10238
|
if (run3.code !== 0) {
|
|
9932
|
-
return
|
|
10239
|
+
return refuse6(
|
|
9933
10240
|
"carve-action",
|
|
9934
10241
|
`chant lifecycle diff exited ${run3.code}: ${merge(run3, demo.root) || "(no output)"}`,
|
|
9935
10242
|
"Is the scratch Floci still up? `docker ps` should list " + demo.live.container + "."
|
|
@@ -9939,10 +10246,10 @@ async function runCarveObserve(demo, select) {
|
|
|
9939
10246
|
try {
|
|
9940
10247
|
parsed = JSON.parse(run3.stdout);
|
|
9941
10248
|
} catch {
|
|
9942
|
-
return
|
|
10249
|
+
return refuse6("carve-action", "chant lifecycle diff answered something that isn't JSON", "re-run \u2014 a partial read is not a verdict");
|
|
9943
10250
|
}
|
|
9944
10251
|
if (parsed.stacks && !parsed.lexicons) {
|
|
9945
|
-
return
|
|
10252
|
+
return refuse6(
|
|
9946
10253
|
"carve-action",
|
|
9947
10254
|
"chant lifecycle diff answered a stacked shape ({stacks}) \u2014 observe only reads the stack-less {lexicons} shape",
|
|
9948
10255
|
"point the target at a stack-less carveout, or file a shape-read gap against observe for the stacked case"
|
|
@@ -9983,13 +10290,13 @@ async function runCarveObserve(demo, select) {
|
|
|
9983
10290
|
var MAX_ARTIFACT_BYTES = 64 * 1024;
|
|
9984
10291
|
var MAX_ARTIFACTS = 24;
|
|
9985
10292
|
var MAX_OUTPUT_BYTES = 32 * 1024;
|
|
9986
|
-
var
|
|
10293
|
+
var refuse6 = (code, error, remedy) => ({
|
|
9987
10294
|
ok: false,
|
|
9988
10295
|
refusal: { error, code, remedy }
|
|
9989
10296
|
});
|
|
9990
10297
|
var BUILD_CAVEAT = "The gate shown is `chant lint`, not `chant build`. chant#1637's fold is applied now: the bucket's versioning and public-access-block sub-resources come out as native props, not just folded-in mentions. `chant build` still fails, but on one rule only \u2014 WAW042, a TLS-deny bucket policy the source Terraform never declared. That's real drift the estate inherited, not a gap in the carve. example-carve/README.md, \"Why build still fails\", has the full statement.";
|
|
9991
10298
|
function insideDemo(root, p) {
|
|
9992
|
-
const rel = relative4(
|
|
10299
|
+
const rel = relative4(resolve11(root), resolve11(p));
|
|
9993
10300
|
return rel !== "" && !rel.startsWith("..") && !isAbsolute3(rel);
|
|
9994
10301
|
}
|
|
9995
10302
|
function selectFromReport(report, raw) {
|
|
@@ -10000,7 +10307,7 @@ function carveWriteBlock(demo) {
|
|
|
10000
10307
|
if (!demo) {
|
|
10001
10308
|
return "this server isn't running a carve demo \u2014 the carve actions only exist inside a `behold demo carve` copy";
|
|
10002
10309
|
}
|
|
10003
|
-
if (!
|
|
10310
|
+
if (!existsSync12(demo.from)) return `the demo copy has no Terraform estate at ${demo.from}`;
|
|
10004
10311
|
if (!insideDemo(demo.root, demo.out)) return "the carve output directory is outside the demo copy";
|
|
10005
10312
|
return unwritableReason(demo.root);
|
|
10006
10313
|
}
|
|
@@ -10017,7 +10324,7 @@ function readArtifacts(demo, dir, filter) {
|
|
|
10017
10324
|
for (const name of entries) {
|
|
10018
10325
|
if (out.length >= MAX_ARTIFACTS) return;
|
|
10019
10326
|
if (name === "node_modules" || name.startsWith(".")) continue;
|
|
10020
|
-
const full =
|
|
10327
|
+
const full = join18(d, name);
|
|
10021
10328
|
let st;
|
|
10022
10329
|
try {
|
|
10023
10330
|
st = statSync6(full);
|
|
@@ -10062,9 +10369,9 @@ function merge(r, root) {
|
|
|
10062
10369
|
}
|
|
10063
10370
|
async function runCarveEmit(demo, select) {
|
|
10064
10371
|
const block = carveWriteBlock(demo);
|
|
10065
|
-
if (block) return
|
|
10372
|
+
if (block) return refuse6("read-only", block, "Start the walkthrough with `behold demo carve`.");
|
|
10066
10373
|
mkdirSync5(demo.out, { recursive: true });
|
|
10067
|
-
const reportFile =
|
|
10374
|
+
const reportFile = join18(demo.out, `${carveSlug(select)}-boundary.json`);
|
|
10068
10375
|
const args = [
|
|
10069
10376
|
"carve",
|
|
10070
10377
|
"emit",
|
|
@@ -10085,10 +10392,10 @@ async function runCarveEmit(demo, select) {
|
|
|
10085
10392
|
}));
|
|
10086
10393
|
const output = merge(run3, demo.root);
|
|
10087
10394
|
if (run3.code !== 0) {
|
|
10088
|
-
return
|
|
10395
|
+
return refuse6(
|
|
10089
10396
|
"carve-action",
|
|
10090
10397
|
`chant carve emit exited ${run3.code}: ${output || "(no output)"}`,
|
|
10091
|
-
"The offline emit needs `@
|
|
10398
|
+
"The offline emit needs `@cdktn/hcl2json` in the demo copy and the demo's own chant install \u2014 `npm install` in the copy's `app/` and re-run `behold demo carve`."
|
|
10092
10399
|
);
|
|
10093
10400
|
}
|
|
10094
10401
|
let boundary = null;
|
|
@@ -10096,7 +10403,7 @@ async function runCarveEmit(demo, select) {
|
|
|
10096
10403
|
boundary = JSON.parse(readFileSync13(reportFile, "utf8"));
|
|
10097
10404
|
} catch {
|
|
10098
10405
|
}
|
|
10099
|
-
const lintPath = relative4(demo.project,
|
|
10406
|
+
const lintPath = relative4(demo.project, join18(demo.out, "src")).split(sep5).join("/");
|
|
10100
10407
|
const lintArgs = ["lint", lintPath];
|
|
10101
10408
|
const lint = await runChantRaw(lintArgs, demo.project).catch((err) => ({
|
|
10102
10409
|
code: 127,
|
|
@@ -10120,7 +10427,7 @@ async function runCarveEmit(demo, select) {
|
|
|
10120
10427
|
}
|
|
10121
10428
|
async function runCarveBridge(demo, select) {
|
|
10122
10429
|
const block = carveWriteBlock(demo);
|
|
10123
|
-
if (block) return
|
|
10430
|
+
if (block) return refuse6("read-only", block, "Start the walkthrough with `behold demo carve`.");
|
|
10124
10431
|
mkdirSync5(demo.out, { recursive: true });
|
|
10125
10432
|
const args = [
|
|
10126
10433
|
"carve",
|
|
@@ -10140,7 +10447,7 @@ async function runCarveBridge(demo, select) {
|
|
|
10140
10447
|
}));
|
|
10141
10448
|
const output = merge(run3, demo.root);
|
|
10142
10449
|
if (run3.code !== 0) {
|
|
10143
|
-
return
|
|
10450
|
+
return refuse6(
|
|
10144
10451
|
"carve-action",
|
|
10145
10452
|
`chant carve bridge exited ${run3.code}: ${output || "(no output)"}`,
|
|
10146
10453
|
"Run the Emit step first \u2014 bridge reads the carve manifest emit leaves in the output directory."
|
|
@@ -10159,8 +10466,8 @@ async function runCarveBridge(demo, select) {
|
|
|
10159
10466
|
}
|
|
10160
10467
|
|
|
10161
10468
|
// src/ops.ts
|
|
10162
|
-
import { readdirSync as readdirSync9, readFileSync as readFileSync14, existsSync as
|
|
10163
|
-
import { join as
|
|
10469
|
+
import { readdirSync as readdirSync9, readFileSync as readFileSync14, existsSync as existsSync13 } from "node:fs";
|
|
10470
|
+
import { join as join19 } from "node:path";
|
|
10164
10471
|
var APPLY_TARGET_LEXICON = {
|
|
10165
10472
|
cloudformation: "aws",
|
|
10166
10473
|
kubectl: "k8s",
|
|
@@ -10182,11 +10489,11 @@ function discoverOps(projectDir) {
|
|
|
10182
10489
|
const seen = /* @__PURE__ */ new Set();
|
|
10183
10490
|
const out = [];
|
|
10184
10491
|
for (const sub of ["ops", "src", "."]) {
|
|
10185
|
-
const dir =
|
|
10186
|
-
if (!
|
|
10492
|
+
const dir = join19(projectDir, sub);
|
|
10493
|
+
if (!existsSync13(dir)) continue;
|
|
10187
10494
|
for (const f of readdirSync9(dir)) {
|
|
10188
10495
|
if (!f.endsWith(".op.ts")) continue;
|
|
10189
|
-
const content = readFileSync14(
|
|
10496
|
+
const content = readFileSync14(join19(dir, f), "utf8");
|
|
10190
10497
|
const name = content.match(/name:\s*["'`]([^"'`]+)["'`]/)?.[1];
|
|
10191
10498
|
if (!name || seen.has(name)) continue;
|
|
10192
10499
|
seen.add(name);
|
|
@@ -10202,25 +10509,25 @@ function discoverOps(projectDir) {
|
|
|
10202
10509
|
...gate ? { gate } : {},
|
|
10203
10510
|
...env ? { env } : {},
|
|
10204
10511
|
...substrate ? { substrate } : {},
|
|
10205
|
-
...
|
|
10512
|
+
...existsSync13(irPath) ? { irPath } : {}
|
|
10206
10513
|
});
|
|
10207
10514
|
}
|
|
10208
10515
|
}
|
|
10209
10516
|
return out.sort((a, b) => a.name.localeCompare(b.name));
|
|
10210
10517
|
}
|
|
10211
10518
|
function opIrPath(projectDir, name) {
|
|
10212
|
-
return
|
|
10519
|
+
return join19(projectDir, "dist", "ops", name, "op.json");
|
|
10213
10520
|
}
|
|
10214
10521
|
function discoverOpIrs(projectDirs) {
|
|
10215
10522
|
const seen = /* @__PURE__ */ new Set();
|
|
10216
10523
|
const out = [];
|
|
10217
10524
|
for (const dir of projectDirs) {
|
|
10218
|
-
const root =
|
|
10219
|
-
if (!
|
|
10525
|
+
const root = join19(dir, "dist", "ops");
|
|
10526
|
+
if (!existsSync13(root)) continue;
|
|
10220
10527
|
for (const name of readdirSync9(root)) {
|
|
10221
10528
|
if (seen.has(name)) continue;
|
|
10222
10529
|
const path = opIrPath(dir, name);
|
|
10223
|
-
if (!
|
|
10530
|
+
if (!existsSync13(path)) continue;
|
|
10224
10531
|
seen.add(name);
|
|
10225
10532
|
out.push({ name, dir, path });
|
|
10226
10533
|
}
|
|
@@ -10244,9 +10551,9 @@ function discoverEstateOps(projectDirs) {
|
|
|
10244
10551
|
var RECEIPT_READ_FN = "receiptRead";
|
|
10245
10552
|
var RECEIPT_WRITE_FN = "receiptWrite";
|
|
10246
10553
|
var STEP_STATUSES = /* @__PURE__ */ new Set(["ok", "fail", "skipped"]);
|
|
10247
|
-
var
|
|
10554
|
+
var isRecord8 = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
10248
10555
|
function asStepRecord(value) {
|
|
10249
|
-
if (!
|
|
10556
|
+
if (!isRecord8(value)) return null;
|
|
10250
10557
|
if (typeof value.phase !== "string" || typeof value.fn !== "string") return null;
|
|
10251
10558
|
if (typeof value.status !== "string" || !STEP_STATUSES.has(value.status)) return null;
|
|
10252
10559
|
if (typeof value.durationMs !== "number") return null;
|
|
@@ -10261,7 +10568,7 @@ function parseRunProgressLine(line) {
|
|
|
10261
10568
|
}
|
|
10262
10569
|
const one = asStepRecord(parsed);
|
|
10263
10570
|
if (one) return [one];
|
|
10264
|
-
if (
|
|
10571
|
+
if (isRecord8(parsed) && typeof parsed.op === "string" && Array.isArray(parsed.records)) {
|
|
10265
10572
|
const records = parsed.records.map(asStepRecord).filter((r) => r !== null);
|
|
10266
10573
|
return records.length ? records : null;
|
|
10267
10574
|
}
|
|
@@ -10455,11 +10762,11 @@ var RAN = { ok: "ok", fail: "failed", skipped: "skipped" };
|
|
|
10455
10762
|
function tookLabel(ms) {
|
|
10456
10763
|
return ms < 1e3 ? `${ms}ms` : `${(ms / 1e3).toFixed(1)}s`;
|
|
10457
10764
|
}
|
|
10458
|
-
function paintPlayhead(ir,
|
|
10459
|
-
if (!
|
|
10765
|
+
function paintPlayhead(ir, join30, gate) {
|
|
10766
|
+
if (!join30.byNode.size && !join30.at) return ir;
|
|
10460
10767
|
const nodes = ir.nodes.map((node) => {
|
|
10461
|
-
const run3 =
|
|
10462
|
-
const here =
|
|
10768
|
+
const run3 = join30.byNode.get(node.id);
|
|
10769
|
+
const here = join30.at === node.id;
|
|
10463
10770
|
if (!run3 && !here) return node;
|
|
10464
10771
|
const isGate = node.attrs?._step === "gate";
|
|
10465
10772
|
const attrs = { ...node.attrs };
|
|
@@ -10474,7 +10781,7 @@ function paintPlayhead(ir, join28, gate) {
|
|
|
10474
10781
|
if (here) {
|
|
10475
10782
|
attrs._playhead = true;
|
|
10476
10783
|
attrs._status = isGate && gate ? "warn" : "accent";
|
|
10477
|
-
attrs.run = isGate && gate ? `waiting for approval since ${gate.since}` :
|
|
10784
|
+
attrs.run = isGate && gate ? `waiting for approval since ${gate.since}` : join30.atReason ?? "here";
|
|
10478
10785
|
if (isGate && gate?.description) attrs.gateDescription = gate.description;
|
|
10479
10786
|
}
|
|
10480
10787
|
return { ...node, attrs };
|
|
@@ -10496,11 +10803,11 @@ function pendingGateCard(op, gate, ir) {
|
|
|
10496
10803
|
}
|
|
10497
10804
|
};
|
|
10498
10805
|
}
|
|
10499
|
-
function playheadNote(state,
|
|
10806
|
+
function playheadNote(state, join30) {
|
|
10500
10807
|
if (!state.op) {
|
|
10501
10808
|
return "No run state: nothing has been run from this behold session, so every step paints as declared.";
|
|
10502
10809
|
}
|
|
10503
|
-
const settled =
|
|
10810
|
+
const settled = join30 ? join30.byNode.size : state.records.length;
|
|
10504
10811
|
const bits = [];
|
|
10505
10812
|
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`;
|
|
10506
10813
|
if (state.status === "running") {
|
|
@@ -10516,9 +10823,9 @@ function playheadNote(state, join28) {
|
|
|
10516
10823
|
} else if (state.gateNote) {
|
|
10517
10824
|
bits.push(`gate state unavailable \u2014 ${state.gateNote}`);
|
|
10518
10825
|
}
|
|
10519
|
-
if (
|
|
10826
|
+
if (join30?.unplaced.length) {
|
|
10520
10827
|
bits.push(
|
|
10521
|
-
`${
|
|
10828
|
+
`${join30.unplaced.length} record${join30.unplaced.length === 1 ? "" : "s"} matched no declared step and ${join30.unplaced.length === 1 ? "is" : "are"} not painted`
|
|
10522
10829
|
);
|
|
10523
10830
|
}
|
|
10524
10831
|
if (!bits.length) return `${head}.`;
|
|
@@ -11190,24 +11497,24 @@ var OpRunner = class {
|
|
|
11190
11497
|
|
|
11191
11498
|
// src/substrates.ts
|
|
11192
11499
|
import { spawn as spawn5 } from "node:child_process";
|
|
11193
|
-
import { existsSync as
|
|
11194
|
-
import { join as
|
|
11500
|
+
import { existsSync as existsSync14, readFileSync as readFileSync15 } from "node:fs";
|
|
11501
|
+
import { join as join20 } from "node:path";
|
|
11195
11502
|
import { platform } from "node:os";
|
|
11196
11503
|
var DOCKER_DOWN_DETAIL = "docker is down";
|
|
11197
11504
|
function probe(cmd, args) {
|
|
11198
|
-
return new Promise((
|
|
11505
|
+
return new Promise((resolve19) => {
|
|
11199
11506
|
let out = "";
|
|
11200
11507
|
let proc;
|
|
11201
11508
|
try {
|
|
11202
11509
|
proc = spawn5(cmd, args, { stdio: ["ignore", "pipe", "pipe"] });
|
|
11203
11510
|
} catch {
|
|
11204
|
-
|
|
11511
|
+
resolve19({ code: 127, out: "" });
|
|
11205
11512
|
return;
|
|
11206
11513
|
}
|
|
11207
11514
|
proc.stdout.on("data", (d) => out += d);
|
|
11208
11515
|
proc.stderr.on("data", (d) => out += d);
|
|
11209
|
-
proc.on("error", () =>
|
|
11210
|
-
proc.on("close", (code) =>
|
|
11516
|
+
proc.on("error", () => resolve19({ code: 127, out }));
|
|
11517
|
+
proc.on("close", (code) => resolve19({ code: code ?? 1, out }));
|
|
11211
11518
|
});
|
|
11212
11519
|
}
|
|
11213
11520
|
async function dockerAvailable(run3) {
|
|
@@ -11220,11 +11527,11 @@ async function dockerRunning(run3, nameFilter) {
|
|
|
11220
11527
|
return out.split(/\r?\n/).map((s) => s.trim()).filter(Boolean);
|
|
11221
11528
|
}
|
|
11222
11529
|
function scriptBringUp(projectDir, relPath, label) {
|
|
11223
|
-
return
|
|
11530
|
+
return existsSync14(join20(projectDir, relPath)) ? { label, cmd: "bash", args: [relPath] } : void 0;
|
|
11224
11531
|
}
|
|
11225
11532
|
function projectLexicons(projectDir) {
|
|
11226
11533
|
try {
|
|
11227
|
-
const src = readFileSync15(
|
|
11534
|
+
const src = readFileSync15(join20(projectDir, "chant.config.ts"), "utf-8");
|
|
11228
11535
|
const m = src.match(/lexicons\s*:\s*\[([^\]]*)\]/);
|
|
11229
11536
|
if (!m) return [];
|
|
11230
11537
|
return [...m[1].matchAll(/["']([^"']+)["']/g)].map((x) => x[1]);
|
|
@@ -11276,7 +11583,7 @@ async function detectSubstrates(projectDir, preview = false, boundContext, probe
|
|
|
11276
11583
|
["forgejo", "Forgejo", ".forgejo", "test/forgejo-runtime-e2e.sh"]
|
|
11277
11584
|
];
|
|
11278
11585
|
for (const [name, label, marker, script] of forges) {
|
|
11279
|
-
if (!
|
|
11586
|
+
if (!existsSync14(join20(projectDir, marker))) continue;
|
|
11280
11587
|
const c = docker ? await dockerRunning(run3, name) : [];
|
|
11281
11588
|
const d = dep(c.length > 0, "container up", "on-demand (pipeline run)");
|
|
11282
11589
|
subs.push({
|
|
@@ -11310,7 +11617,7 @@ async function detectSubstrates(projectDir, preview = false, boundContext, probe
|
|
|
11310
11617
|
detail: endpoint ? `targeting ${endpoint}` : "real Fly (FLY_FLAPS_BASE_URL unset)"
|
|
11311
11618
|
});
|
|
11312
11619
|
}
|
|
11313
|
-
if (
|
|
11620
|
+
if (existsSync14(join20(projectDir, ".github", "workflows"))) {
|
|
11314
11621
|
const gh = await run3("gh", ["auth", "status"]);
|
|
11315
11622
|
const ready = gh.code === 0;
|
|
11316
11623
|
subs.push({
|
|
@@ -11323,7 +11630,7 @@ async function detectSubstrates(projectDir, preview = false, boundContext, probe
|
|
|
11323
11630
|
if (lexicons.includes("temporal")) {
|
|
11324
11631
|
let hasProfiles = false;
|
|
11325
11632
|
try {
|
|
11326
|
-
hasProfiles = /temporal\s*:\s*\{[\s\S]{0,400}?profiles\s*:/.test(readFileSync15(
|
|
11633
|
+
hasProfiles = /temporal\s*:\s*\{[\s\S]{0,400}?profiles\s*:/.test(readFileSync15(join20(projectDir, "chant.config.ts"), "utf-8"));
|
|
11327
11634
|
} catch {
|
|
11328
11635
|
}
|
|
11329
11636
|
subs.push({
|
|
@@ -11467,7 +11774,7 @@ function statusVocabulary(kinds) {
|
|
|
11467
11774
|
|
|
11468
11775
|
// src/behaviour.ts
|
|
11469
11776
|
import { readFileSync as readFileSync16 } from "node:fs";
|
|
11470
|
-
import { join as
|
|
11777
|
+
import { join as join21 } from "node:path";
|
|
11471
11778
|
var rec5 = (v) => v && typeof v === "object" && !Array.isArray(v) ? v : void 0;
|
|
11472
11779
|
var str5 = (v) => typeof v === "string" && v.trim().length > 0 ? v : void 0;
|
|
11473
11780
|
var num = (v) => typeof v === "number" && Number.isFinite(v) ? v : void 0;
|
|
@@ -11570,6 +11877,7 @@ function validateBehaviourReport(v) {
|
|
|
11570
11877
|
if (d.entities !== void 0 && !rec5(d.entities)) return bad("entities is not an object");
|
|
11571
11878
|
return { ok: true, value: { meta: meta.value, entities } };
|
|
11572
11879
|
}
|
|
11880
|
+
var FIRST_TOTAL_SHOWN = "two members stated an estate total; the first is shown";
|
|
11573
11881
|
var behaviourFileName = (env) => `behaviour.${env}.json`;
|
|
11574
11882
|
var realRead = (path) => {
|
|
11575
11883
|
try {
|
|
@@ -11612,7 +11920,7 @@ function attachBehaviour(ir, members, env, readFile = realRead) {
|
|
|
11612
11920
|
const label = m.name ?? "the project";
|
|
11613
11921
|
const slice = sliceOf(ir.nodes, m.name);
|
|
11614
11922
|
const inline = slice.filter((n) => n.attrs?.[BEHAVIOUR_ATTR] !== void 0);
|
|
11615
|
-
if (inline.length > 0) {
|
|
11923
|
+
if (inline.length > 0 || m.meta !== void 0) {
|
|
11616
11924
|
sources.set(label, "attrs");
|
|
11617
11925
|
for (const n of inline) {
|
|
11618
11926
|
const v = validateBehaviourBlock(n.attrs[BEHAVIOUR_ATTR]);
|
|
@@ -11631,7 +11939,7 @@ function attachBehaviour(ir, members, env, readFile = realRead) {
|
|
|
11631
11939
|
}
|
|
11632
11940
|
const file = behaviourFileName(env);
|
|
11633
11941
|
looked.push(`${label}/${file}`);
|
|
11634
|
-
const raw = readFile(
|
|
11942
|
+
const raw = readFile(join21(m.dir, file));
|
|
11635
11943
|
if (raw === void 0) {
|
|
11636
11944
|
sources.set(label, "none");
|
|
11637
11945
|
continue;
|
|
@@ -11698,7 +12006,7 @@ function attachBehaviour(ir, members, env, readFile = realRead) {
|
|
|
11698
12006
|
const engineTotal = metas.map((m) => m.meta.total).find((t) => !!t);
|
|
11699
12007
|
if (engineTotal) {
|
|
11700
12008
|
meta.total = engineTotal;
|
|
11701
|
-
if (metas.filter((m) => m.meta.total).length > 1) diagnostics.push(
|
|
12009
|
+
if (metas.filter((m) => m.meta.total).length > 1) diagnostics.push(FIRST_TOTAL_SHOWN);
|
|
11702
12010
|
} else {
|
|
11703
12011
|
const sum = sumOver(ir.nodes, (cs) => diagnostics.push(`no estate sum: the figures are in ${cs.join(" and ")} and behold converts no currency`));
|
|
11704
12012
|
if (sum) meta.sum = sum;
|
|
@@ -11722,6 +12030,239 @@ function attachBehaviour(ir, members, env, readFile = realRead) {
|
|
|
11722
12030
|
return meta;
|
|
11723
12031
|
}
|
|
11724
12032
|
|
|
12033
|
+
// src/behaviour-delta.ts
|
|
12034
|
+
init_chant();
|
|
12035
|
+
import { basename as basename2, resolve as resolve12 } from "node:path";
|
|
12036
|
+
init_choudoufu_member();
|
|
12037
|
+
init_terraform_member();
|
|
12038
|
+
var DECLARED_ATTR = "_behaviourDeclared";
|
|
12039
|
+
function trafficFor(query, config) {
|
|
12040
|
+
const asked = query.get("traffic")?.trim();
|
|
12041
|
+
return asked || config.behaviour?.traffic || void 0;
|
|
12042
|
+
}
|
|
12043
|
+
function reportFromIr(ir, strip3 = "") {
|
|
12044
|
+
const meta = ir.meta?._behaviour;
|
|
12045
|
+
const entities = {};
|
|
12046
|
+
for (const n of ir.nodes) {
|
|
12047
|
+
const block = n.attrs?._behaviour;
|
|
12048
|
+
if (block === void 0) continue;
|
|
12049
|
+
entities[strip3 && n.id.startsWith(strip3) ? n.id.slice(strip3.length) : n.id] = block;
|
|
12050
|
+
}
|
|
12051
|
+
if (meta === void 0 && Object.keys(entities).length === 0) return void 0;
|
|
12052
|
+
return { meta, entities };
|
|
12053
|
+
}
|
|
12054
|
+
function choudoufuPredictVia(read, readerState = terraformReaderState) {
|
|
12055
|
+
return {
|
|
12056
|
+
tool: () => `${TERRAFORM_LEXICON_PKG}\0predict\0${readerState().lexicon.version ?? "absent"}`,
|
|
12057
|
+
read: async (dir, opts) => {
|
|
12058
|
+
const state = readerState();
|
|
12059
|
+
if (state.refusal) throw new TerraformReadError(state.refusal, dir);
|
|
12060
|
+
const own = [{ name: basename2(dir), dir: "." }];
|
|
12061
|
+
const project = writeTerraformScratchProject(dir, { roots: own, skipped: [] }, void 0, "chdf");
|
|
12062
|
+
const spawnEnv = choudoufuSpawnOverride();
|
|
12063
|
+
const ir = await read(project, {
|
|
12064
|
+
traffic: opts.traffic,
|
|
12065
|
+
...opts.live ? { live: true, env: opts.env } : {},
|
|
12066
|
+
...spawnEnv ? { spawnEnv } : {}
|
|
12067
|
+
});
|
|
12068
|
+
return Object.assign(ir, { behaviourRoot: own[0].name });
|
|
12069
|
+
}
|
|
12070
|
+
};
|
|
12071
|
+
}
|
|
12072
|
+
var rootPrefix = (ir) => {
|
|
12073
|
+
const root = ir.behaviourRoot;
|
|
12074
|
+
return root ? `${root}/` : "";
|
|
12075
|
+
};
|
|
12076
|
+
var said2 = (err) => (err instanceof Error ? err.message : String(err)).split("\n")[0];
|
|
12077
|
+
async function predictMember(member, env, traffic, deps, fresh = false) {
|
|
12078
|
+
const dir = resolve12(member.dir);
|
|
12079
|
+
if (member.kind === "terraform") {
|
|
12080
|
+
try {
|
|
12081
|
+
const declared = reportFromIr(await memberIr(dir, { traffic }, terraformVia));
|
|
12082
|
+
return declared ? { declared } : {};
|
|
12083
|
+
} catch (err) {
|
|
12084
|
+
return { note: `declared prediction unavailable: ${said2(err)}` };
|
|
12085
|
+
}
|
|
12086
|
+
}
|
|
12087
|
+
if (member.kind === "choudoufu") {
|
|
12088
|
+
const via = choudoufuPredictVia(deps.read, deps.readerState);
|
|
12089
|
+
const notes = [];
|
|
12090
|
+
const side = async (from) => {
|
|
12091
|
+
try {
|
|
12092
|
+
const ir = from === "live" ? await overlayIr(dir, { traffic, live: true, env }, via, fresh) : await memberIr(dir, { traffic }, via);
|
|
12093
|
+
return reportFromIr(ir, rootPrefix(ir));
|
|
12094
|
+
} catch (err) {
|
|
12095
|
+
notes.push(`${from} prediction unavailable: ${said2(err)}`);
|
|
12096
|
+
return void 0;
|
|
12097
|
+
}
|
|
12098
|
+
};
|
|
12099
|
+
const [live2, declared] = await Promise.all([env === void 0 ? void 0 : side("live"), side("declared")]);
|
|
12100
|
+
return { ...live2 ? { live: live2 } : {}, ...declared ? { declared } : {}, ...notes.length ? { note: notes.join("; ") } : {} };
|
|
12101
|
+
}
|
|
12102
|
+
if (member.kind === "chant" && memberTakesTraffic(dir, "chant")) {
|
|
12103
|
+
try {
|
|
12104
|
+
const declared = reportFromIr(await memberIr(dir, { traffic }));
|
|
12105
|
+
return declared ? { declared } : {};
|
|
12106
|
+
} catch (err) {
|
|
12107
|
+
return { note: `declared prediction unavailable: ${said2(err)}` };
|
|
12108
|
+
}
|
|
12109
|
+
}
|
|
12110
|
+
if (member.kind === "chant") {
|
|
12111
|
+
return { note: `this member's chant predates \`graph --traffic\` (${TRAFFIC_FLOOR}), so nothing was predicted for it` };
|
|
12112
|
+
}
|
|
12113
|
+
return {};
|
|
12114
|
+
}
|
|
12115
|
+
var idFor = (name, address) => name === void 0 ? address : `${name}/${address}`;
|
|
12116
|
+
function paintLive(ir, name, report, diagnostics) {
|
|
12117
|
+
const byId = new Map(ir.nodes.map((n) => [n.id, n]));
|
|
12118
|
+
for (const [address, block] of Object.entries(report.entities)) {
|
|
12119
|
+
const node = byId.get(idFor(name, address));
|
|
12120
|
+
if (!node) {
|
|
12121
|
+
diagnostics.push(`${name ?? "the project"}: the live prediction prices ${address}, which this overlay has no entity for`);
|
|
12122
|
+
continue;
|
|
12123
|
+
}
|
|
12124
|
+
node.attrs = { ...node.attrs, _behaviour: block };
|
|
12125
|
+
}
|
|
12126
|
+
return report.meta;
|
|
12127
|
+
}
|
|
12128
|
+
var round62 = (n) => Math.round(n * 1e6) / 1e6;
|
|
12129
|
+
function figureOf(meta, blocks, onMixed) {
|
|
12130
|
+
if (meta?.total) return { perHour: meta.total.perHour, currency: meta.total.currency, basis: "total" };
|
|
12131
|
+
if (!blocks.length) return void 0;
|
|
12132
|
+
const currencies = [...new Set(blocks.map((b) => b.cost.currency))].sort();
|
|
12133
|
+
if (currencies.length > 1) {
|
|
12134
|
+
onMixed(currencies);
|
|
12135
|
+
return void 0;
|
|
12136
|
+
}
|
|
12137
|
+
return { perHour: round62(blocks.reduce((sum, b) => sum + b.cost.perHour, 0)), currency: currencies[0], basis: "sum" };
|
|
12138
|
+
}
|
|
12139
|
+
function deltaOf(live2, declared, label, diagnostics) {
|
|
12140
|
+
if (!live2 || !declared) return void 0;
|
|
12141
|
+
if (live2.currency !== declared.currency) {
|
|
12142
|
+
diagnostics.push(`no delta for ${label}: the live figure is in ${live2.currency} and the declared one in ${declared.currency}`);
|
|
12143
|
+
return void 0;
|
|
12144
|
+
}
|
|
12145
|
+
if (live2.basis !== declared.basis) {
|
|
12146
|
+
diagnostics.push(`no delta for ${label}: one side is the engine's own total and the other is behold's sum, and those are not the same kind of figure`);
|
|
12147
|
+
return void 0;
|
|
12148
|
+
}
|
|
12149
|
+
return { perHour: round62(live2.perHour - declared.perHour), currency: live2.currency };
|
|
12150
|
+
}
|
|
12151
|
+
function attachBehaviourDelta(ir, members, behaviour) {
|
|
12152
|
+
const diagnostics = [...behaviour.diagnostics ?? []];
|
|
12153
|
+
const perMember = {};
|
|
12154
|
+
const byId = new Map(ir.nodes.map((n) => [n.id, n]));
|
|
12155
|
+
for (const m of members) {
|
|
12156
|
+
const label = m.name ?? "the project";
|
|
12157
|
+
if (m.prediction.note) diagnostics.push(`${label}: ${m.prediction.note}`);
|
|
12158
|
+
const slice = m.name === void 0 ? ir.nodes : ir.nodes.filter((n) => n.id.startsWith(`${m.name}/`));
|
|
12159
|
+
const liveMeta = m.liveMeta === void 0 ? void 0 : validateBehaviourMeta(m.liveMeta);
|
|
12160
|
+
const liveBlocks = slice.map((n) => n.attrs?._behaviour).filter((b) => !!b);
|
|
12161
|
+
const live3 = behaviour.refusal ? void 0 : figureOf(liveMeta?.ok ? liveMeta.value : void 0, liveBlocks, (cs) => diagnostics.push(`no live figure for ${label}: the figures are in ${cs.join(" and ")}`));
|
|
12162
|
+
let declared2;
|
|
12163
|
+
const report = m.prediction.declared;
|
|
12164
|
+
if (report) {
|
|
12165
|
+
const meta = validateBehaviourMeta(report.meta);
|
|
12166
|
+
if (!meta.ok) diagnostics.push(`${label}: dropped the declared prediction's meta \u2014 ${meta.reason}`);
|
|
12167
|
+
else if (meta.value.refusal) diagnostics.push(`${label}: the declared prediction was refused \u2014 ${meta.value.refusal.reason}`);
|
|
12168
|
+
else {
|
|
12169
|
+
const blocks = [];
|
|
12170
|
+
for (const [address, raw] of Object.entries(report.entities)) {
|
|
12171
|
+
const v = validateBehaviourBlock(raw);
|
|
12172
|
+
if (!v.ok) {
|
|
12173
|
+
diagnostics.push(`dropped the declared behaviour block on ${idFor(m.name, address)}: ${v.reason}`);
|
|
12174
|
+
continue;
|
|
12175
|
+
}
|
|
12176
|
+
blocks.push(v.value);
|
|
12177
|
+
const node = byId.get(idFor(m.name, address));
|
|
12178
|
+
if (node) node.attrs = { ...node.attrs, [DECLARED_ATTR]: v.value };
|
|
12179
|
+
}
|
|
12180
|
+
declared2 = figureOf(meta.value, blocks, (cs) => diagnostics.push(`no declared figure for ${label}: the figures are in ${cs.join(" and ")}`));
|
|
12181
|
+
}
|
|
12182
|
+
}
|
|
12183
|
+
const delta2 = deltaOf(live3, declared2, label, diagnostics);
|
|
12184
|
+
if (live3 || declared2) perMember[label] = { ...live3 ? { live: live3 } : {}, ...declared2 ? { declared: declared2 } : {}, ...delta2 ? { delta: delta2 } : {} };
|
|
12185
|
+
}
|
|
12186
|
+
const rows = Object.values(perMember);
|
|
12187
|
+
const addUp = (side) => {
|
|
12188
|
+
const figures = rows.map((r) => r[side]).filter((f) => !!f);
|
|
12189
|
+
if (!figures.length || figures.length !== rows.length) return void 0;
|
|
12190
|
+
if (new Set(figures.map((f) => f.currency)).size > 1) return void 0;
|
|
12191
|
+
const basis = figures.every((f) => f.basis === "total") && figures.length === 1 ? "total" : "sum";
|
|
12192
|
+
return { perHour: round62(figures.reduce((sum, f) => sum + f.perHour, 0)), currency: figures[0].currency, basis };
|
|
12193
|
+
};
|
|
12194
|
+
const live2 = addUp("live");
|
|
12195
|
+
const declared = addUp("declared");
|
|
12196
|
+
const allDeltas = rows.length > 0 && rows.every((r) => r.delta);
|
|
12197
|
+
const delta = live2 && declared && allDeltas ? { perHour: round62(live2.perHour - declared.perHour), currency: live2.currency } : void 0;
|
|
12198
|
+
const out = { ...behaviour };
|
|
12199
|
+
const stated = rows.filter((r) => r.live?.basis === "total").length;
|
|
12200
|
+
if (live2 && stated > 1) {
|
|
12201
|
+
delete out.total;
|
|
12202
|
+
const at = diagnostics.indexOf(FIRST_TOTAL_SHOWN);
|
|
12203
|
+
if (at >= 0) diagnostics.splice(at, 1);
|
|
12204
|
+
}
|
|
12205
|
+
if (live2) out.live = live2;
|
|
12206
|
+
if (declared) out.declared = declared;
|
|
12207
|
+
if (delta) out.delta = delta;
|
|
12208
|
+
if (rows.length && members.some((m) => m.name !== void 0)) out.members = perMember;
|
|
12209
|
+
if (diagnostics.length) out.diagnostics = diagnostics;
|
|
12210
|
+
else delete out.diagnostics;
|
|
12211
|
+
return out;
|
|
12212
|
+
}
|
|
12213
|
+
function attachDeclaredBehaviour(ir, members) {
|
|
12214
|
+
const diagnostics = [];
|
|
12215
|
+
const perMember = {};
|
|
12216
|
+
const byId = new Map(ir.nodes.map((n) => [n.id, n]));
|
|
12217
|
+
let first;
|
|
12218
|
+
for (const m of members) {
|
|
12219
|
+
const label = m.name ?? "the project";
|
|
12220
|
+
if (m.prediction.note) diagnostics.push(`${label}: ${m.prediction.note}`);
|
|
12221
|
+
const report = m.prediction.declared;
|
|
12222
|
+
if (!report) continue;
|
|
12223
|
+
const meta = validateBehaviourMeta(report.meta);
|
|
12224
|
+
if (!meta.ok) {
|
|
12225
|
+
diagnostics.push(`${label}: dropped the declared prediction's meta \u2014 ${meta.reason}`);
|
|
12226
|
+
continue;
|
|
12227
|
+
}
|
|
12228
|
+
if (meta.value.refusal) {
|
|
12229
|
+
for (const n of ir.nodes) if (n.attrs?._behaviour !== void 0) delete n.attrs._behaviour;
|
|
12230
|
+
return { refusal: meta.value.refusal };
|
|
12231
|
+
}
|
|
12232
|
+
first ??= meta.value;
|
|
12233
|
+
const blocks = [];
|
|
12234
|
+
for (const [address, raw] of Object.entries(report.entities)) {
|
|
12235
|
+
const v = validateBehaviourBlock(raw);
|
|
12236
|
+
if (!v.ok) {
|
|
12237
|
+
diagnostics.push(`dropped the behaviour block on ${idFor(m.name, address)}: ${v.reason}`);
|
|
12238
|
+
continue;
|
|
12239
|
+
}
|
|
12240
|
+
blocks.push(v.value);
|
|
12241
|
+
const node = byId.get(idFor(m.name, address));
|
|
12242
|
+
if (node) node.attrs = { ...node.attrs, _behaviour: v.value };
|
|
12243
|
+
}
|
|
12244
|
+
const declared2 = figureOf(meta.value, blocks, (cs) => diagnostics.push(`no declared figure for ${label}: the figures are in ${cs.join(" and ")}`));
|
|
12245
|
+
if (declared2) perMember[label] = { declared: declared2 };
|
|
12246
|
+
}
|
|
12247
|
+
if (!first) {
|
|
12248
|
+
return {
|
|
12249
|
+
absent: "no behaviour block: a traffic level was given and no member's declared prediction came back",
|
|
12250
|
+
...diagnostics.length ? { diagnostics } : {}
|
|
12251
|
+
};
|
|
12252
|
+
}
|
|
12253
|
+
const figures = Object.values(perMember).map((r) => r.declared);
|
|
12254
|
+
const oneCurrency = new Set(figures.map((f) => f.currency)).size === 1;
|
|
12255
|
+
const declared = figures.length && oneCurrency ? { perHour: round62(figures.reduce((sum, f) => sum + f.perHour, 0)), currency: figures[0].currency, basis: figures.length === 1 ? figures[0].basis : "sum" } : void 0;
|
|
12256
|
+
return {
|
|
12257
|
+
...first.engine ? { engine: first.engine } : {},
|
|
12258
|
+
...first.version ? { version: first.version } : {},
|
|
12259
|
+
...first.at ? { at: first.at } : {},
|
|
12260
|
+
...declared ? { declared } : {},
|
|
12261
|
+
...members.some((m) => m.name !== void 0) && figures.length ? { members: perMember } : {},
|
|
12262
|
+
...diagnostics.length ? { diagnostics } : {}
|
|
12263
|
+
};
|
|
12264
|
+
}
|
|
12265
|
+
|
|
11725
12266
|
// src/estate-edges.ts
|
|
11726
12267
|
import { statSync as statSync7 } from "node:fs";
|
|
11727
12268
|
var rec6 = (v) => v && typeof v === "object" && !Array.isArray(v) ? v : void 0;
|
|
@@ -11834,29 +12375,29 @@ init_choudoufu_live();
|
|
|
11834
12375
|
init_choudoufu_member();
|
|
11835
12376
|
import { mkdtempSync, rmSync as rmSync3 } from "node:fs";
|
|
11836
12377
|
import { tmpdir as tmpdir2 } from "node:os";
|
|
11837
|
-
import { join as
|
|
11838
|
-
var
|
|
11839
|
-
var
|
|
12378
|
+
import { join as join22 } from "node:path";
|
|
12379
|
+
var isRecord9 = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
12380
|
+
var refuse7 = (error, remedy) => ({ ok: false, refusal: { error, code: "choudoufu-live-check", remedy } });
|
|
11840
12381
|
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.";
|
|
11841
|
-
function
|
|
12382
|
+
function said3(run3, fallback) {
|
|
11842
12383
|
const lines = stripAnsiLines(run3.stderr);
|
|
11843
12384
|
return lines.find((l) => /^Error:/.test(l)) ?? lines[0] ?? fallback;
|
|
11844
12385
|
}
|
|
11845
12386
|
function parsePlanShow(run3, dir) {
|
|
11846
|
-
if (run3.code === 127) return
|
|
12387
|
+
if (run3.code === 127) return refuse7("choudoufu is not on PATH.", "Install choudoufu and put it on PATH.");
|
|
11847
12388
|
const text = run3.stdout.trim();
|
|
11848
|
-
if (!text) return
|
|
12389
|
+
if (!text) return refuse7(`choudoufu show could not read the plan for ${dir}: ${said3(run3, `exit ${run3.code}`)}`, CREDS2);
|
|
11849
12390
|
let json;
|
|
11850
12391
|
try {
|
|
11851
12392
|
json = JSON.parse(text);
|
|
11852
12393
|
} catch {
|
|
11853
|
-
return
|
|
12394
|
+
return refuse7(`choudoufu show printed something that is not JSON for ${dir}: ${said3(run3, "unparseable stdout")}`, CREDS2);
|
|
11854
12395
|
}
|
|
11855
|
-
if (!
|
|
11856
|
-
return
|
|
12396
|
+
if (!isRecord9(json) || typeof json.format_version !== "string") {
|
|
12397
|
+
return refuse7("That is not a plan document \u2014 no `format_version`.", CREDS2);
|
|
11857
12398
|
}
|
|
11858
12399
|
if (json.resource_changes !== void 0 && json.resource_changes !== null && !Array.isArray(json.resource_changes)) {
|
|
11859
|
-
return
|
|
12400
|
+
return refuse7("That plan document's `resource_changes` is not a list.", CREDS2);
|
|
11860
12401
|
}
|
|
11861
12402
|
return { ok: true, doc: json };
|
|
11862
12403
|
}
|
|
@@ -11864,9 +12405,9 @@ var KNOWN_AFTER_APPLY = "(known after apply)";
|
|
|
11864
12405
|
var SENSITIVE = "(sensitive)";
|
|
11865
12406
|
var flagged = (tree, key) => {
|
|
11866
12407
|
if (tree === true) return true;
|
|
11867
|
-
if (!
|
|
12408
|
+
if (!isRecord9(tree)) return false;
|
|
11868
12409
|
const v = tree[key];
|
|
11869
|
-
return v === true ||
|
|
12410
|
+
return v === true || isRecord9(v) && Object.values(v).some((x) => x === true) || Array.isArray(v) && v.some((x) => x === true);
|
|
11870
12411
|
};
|
|
11871
12412
|
function same(a, b) {
|
|
11872
12413
|
if (a === b) return true;
|
|
@@ -11874,7 +12415,7 @@ function same(a, b) {
|
|
|
11874
12415
|
if (!Array.isArray(a) || !Array.isArray(b) || a.length !== b.length) return false;
|
|
11875
12416
|
return a.every((x, i) => same(x, b[i]));
|
|
11876
12417
|
}
|
|
11877
|
-
if (
|
|
12418
|
+
if (isRecord9(a) && isRecord9(b)) {
|
|
11878
12419
|
const ka = Object.keys(a);
|
|
11879
12420
|
const kb = Object.keys(b);
|
|
11880
12421
|
if (ka.length !== kb.length) return false;
|
|
@@ -11885,10 +12426,10 @@ function same(a, b) {
|
|
|
11885
12426
|
function planDrift(doc) {
|
|
11886
12427
|
const out = /* @__PURE__ */ new Map();
|
|
11887
12428
|
for (const rc of doc.resource_changes ?? []) {
|
|
11888
|
-
if (!rc || typeof rc.address !== "string" || !
|
|
12429
|
+
if (!rc || typeof rc.address !== "string" || !isRecord9(rc.change)) continue;
|
|
11889
12430
|
const { actions, before, after } = rc.change;
|
|
11890
12431
|
if (!Array.isArray(actions) || actions.every((a) => a === "no-op")) continue;
|
|
11891
|
-
if (!
|
|
12432
|
+
if (!isRecord9(before) || !isRecord9(after)) continue;
|
|
11892
12433
|
const changes = [];
|
|
11893
12434
|
for (const key of [.../* @__PURE__ */ new Set([...Object.keys(before), ...Object.keys(after)])].sort()) {
|
|
11894
12435
|
const unknownAfter = flagged(rc.change.after_unknown, key);
|
|
@@ -11908,14 +12449,14 @@ var planArgv = (out) => ({
|
|
|
11908
12449
|
show: ["show", "-json", out]
|
|
11909
12450
|
});
|
|
11910
12451
|
async function readChoudoufuPlan(dir, run3 = captureChoudoufu) {
|
|
11911
|
-
const scratch = mkdtempSync(
|
|
11912
|
-
const file =
|
|
12452
|
+
const scratch = mkdtempSync(join22(tmpdir2(), "behold-choudoufu-plan-"));
|
|
12453
|
+
const file = join22(scratch, "plan.tfplan");
|
|
11913
12454
|
const argv = planArgv(file);
|
|
11914
12455
|
try {
|
|
11915
12456
|
const planRun = await run3(argv.plan, dir);
|
|
11916
12457
|
if (planRun.code === 127) throw new ChoudoufuReadError({ error: "choudoufu is not on PATH.", code: "choudoufu-live-check", remedy: "Install choudoufu and put it on PATH." }, dir);
|
|
11917
12458
|
if (planRun.code !== 0) {
|
|
11918
|
-
throw new ChoudoufuReadError({ error: `choudoufu plan could not plan ${dir}: ${
|
|
12459
|
+
throw new ChoudoufuReadError({ error: `choudoufu plan could not plan ${dir}: ${said3(planRun, `exit ${planRun.code}`)}`, code: "choudoufu-live-check", remedy: CREDS2 }, dir);
|
|
11919
12460
|
}
|
|
11920
12461
|
const parsed = parsePlanShow(await run3(argv.show, dir), dir);
|
|
11921
12462
|
if (!parsed.ok) throw new ChoudoufuReadError(parsed.refusal, dir);
|
|
@@ -11941,16 +12482,16 @@ init_choudoufu_refs();
|
|
|
11941
12482
|
// src/choudoufu-moves.ts
|
|
11942
12483
|
init_choudoufu_member();
|
|
11943
12484
|
init_choudoufu_live();
|
|
11944
|
-
import { existsSync as
|
|
11945
|
-
import { join as
|
|
11946
|
-
var
|
|
11947
|
-
var
|
|
12485
|
+
import { existsSync as existsSync15, readFileSync as readFileSync17 } from "node:fs";
|
|
12486
|
+
import { join as join23, relative as relative5 } from "node:path";
|
|
12487
|
+
var isRecord10 = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
12488
|
+
var refuse8 = (error, remedy) => ({ ok: false, refusal: { error, code: "choudoufu-live-check", remedy } });
|
|
11948
12489
|
var HOW_TO_GET_ONE2 = "A plan is the live-mv workbench's carve.json: `{from, estates[], moves[{address, from, to}]}` \u2014 the workbench's plan phase writes one, or write it by hand.";
|
|
11949
12490
|
function parseCarvePlan(value) {
|
|
11950
|
-
if (!
|
|
11951
|
-
if (!Array.isArray(value.moves)) return
|
|
11952
|
-
const bad2 = value.moves.findIndex((m) => !
|
|
11953
|
-
if (bad2 >= 0) return
|
|
12491
|
+
if (!isRecord10(value)) return refuse8("That file is not a move plan \u2014 its top level is not a JSON object.", HOW_TO_GET_ONE2);
|
|
12492
|
+
if (!Array.isArray(value.moves)) return refuse8("That JSON is not a move plan \u2014 it has no `moves` array.", HOW_TO_GET_ONE2);
|
|
12493
|
+
const bad2 = value.moves.findIndex((m) => !isRecord10(m) || typeof m.address !== "string" || typeof m.from !== "string" || typeof m.to !== "string");
|
|
12494
|
+
if (bad2 >= 0) return refuse8(`moves[${bad2}] is not a move (needs string \`address\`, \`from\` and \`to\`).`, HOW_TO_GET_ONE2);
|
|
11954
12495
|
const moves = value.moves;
|
|
11955
12496
|
return {
|
|
11956
12497
|
ok: true,
|
|
@@ -11967,34 +12508,34 @@ function readCarvePlan(path, readFile = (p) => readFileSync17(p, "utf8")) {
|
|
|
11967
12508
|
try {
|
|
11968
12509
|
text = readFile(path);
|
|
11969
12510
|
} catch (err) {
|
|
11970
|
-
return
|
|
12511
|
+
return refuse8(`Couldn't read ${path}: ${err instanceof Error ? err.message : String(err)}`, HOW_TO_GET_ONE2);
|
|
11971
12512
|
}
|
|
11972
12513
|
try {
|
|
11973
12514
|
return parseCarvePlan(JSON.parse(text));
|
|
11974
12515
|
} catch (err) {
|
|
11975
|
-
return
|
|
12516
|
+
return refuse8(`${path} is not JSON: ${err instanceof Error ? err.message : String(err)}`, HOW_TO_GET_ONE2);
|
|
11976
12517
|
}
|
|
11977
12518
|
}
|
|
11978
12519
|
function discoverCarvePlans(memberDirs, base) {
|
|
11979
12520
|
const out = [];
|
|
11980
12521
|
for (const dir of memberDirs) {
|
|
11981
|
-
const p =
|
|
11982
|
-
if (
|
|
12522
|
+
const p = join23(dir, "carve.json");
|
|
12523
|
+
if (existsSync15(p)) out.push(relative5(base, p));
|
|
11983
12524
|
}
|
|
11984
12525
|
return out;
|
|
11985
12526
|
}
|
|
11986
12527
|
function parseLiveMv(run3, what) {
|
|
11987
|
-
if (run3.code === 127) return
|
|
12528
|
+
if (run3.code === 127) return refuse8("choudoufu is not on PATH.", "Install choudoufu and put it on PATH.");
|
|
11988
12529
|
const text = run3.stdout.trim();
|
|
11989
|
-
if (!text) return
|
|
12530
|
+
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.");
|
|
11990
12531
|
let json;
|
|
11991
12532
|
try {
|
|
11992
12533
|
json = JSON.parse(text);
|
|
11993
12534
|
} catch {
|
|
11994
|
-
return
|
|
12535
|
+
return refuse8(`choudoufu live-mv printed something that is not JSON for ${what}.`, "Run the line by hand to see choudoufu's own words.");
|
|
11995
12536
|
}
|
|
11996
|
-
if (!
|
|
11997
|
-
return
|
|
12537
|
+
if (!isRecord10(json) || !isRecord10(json.from) || !isRecord10(json.to) || typeof json.dry_run !== "boolean") {
|
|
12538
|
+
return refuse8(`That is not a live-mv document (${what}).`, "Upgrade choudoufu to 0.16.0 or newer.");
|
|
11998
12539
|
}
|
|
11999
12540
|
return { ok: true, doc: json };
|
|
12000
12541
|
}
|
|
@@ -12070,6 +12611,142 @@ async function moveReceipt(plan, members, run3 = captureChoudoufu) {
|
|
|
12070
12611
|
};
|
|
12071
12612
|
}
|
|
12072
12613
|
|
|
12614
|
+
// src/workspace-gates.ts
|
|
12615
|
+
init_chant();
|
|
12616
|
+
import { resolve as resolve13 } from "node:path";
|
|
12617
|
+
var isRecord11 = (v) => v !== null && typeof v === "object" && !Array.isArray(v);
|
|
12618
|
+
var str7 = (v) => typeof v === "string" && v ? v : null;
|
|
12619
|
+
function gateKey(member, op, gate) {
|
|
12620
|
+
return `${member}/${op}/${gate}`;
|
|
12621
|
+
}
|
|
12622
|
+
function approveArgs(g) {
|
|
12623
|
+
return ["approve", g.op, g.gate, ...g.env ? ["--env", g.env] : [], ...g.planDigest ? ["--plan", g.planDigest] : []];
|
|
12624
|
+
}
|
|
12625
|
+
function parseWorkspaceStatus(text, root) {
|
|
12626
|
+
let doc;
|
|
12627
|
+
try {
|
|
12628
|
+
doc = JSON.parse(text);
|
|
12629
|
+
} catch {
|
|
12630
|
+
return { ok: false, refusal: { error: "chant workspace status printed something that is not JSON.", code: "workspace-status", remedy: "Run `chant workspace status <env> --json` in the workspace root to see what it says." } };
|
|
12631
|
+
}
|
|
12632
|
+
if (!isRecord11(doc)) return { ok: false, refusal: { error: "chant workspace status printed JSON that is not an object.", code: "workspace-status", remedy: "Run `chant workspace status <env> --json` in the workspace root." } };
|
|
12633
|
+
const contract = contractRefusal(doc, "status");
|
|
12634
|
+
if (contract) return { ok: false, refusal: contract };
|
|
12635
|
+
if (isRecord11(doc.error)) {
|
|
12636
|
+
const code = String(doc.error.code ?? "unknown");
|
|
12637
|
+
return { ok: false, refusal: { error: `chant could not read the workspace's status: ${code}: ${String(doc.error.message ?? "")}`, code, remedy: "`chant workspace check` names the problem and where it is." } };
|
|
12638
|
+
}
|
|
12639
|
+
const gates = [];
|
|
12640
|
+
for (const m of Array.isArray(doc.members) ? doc.members : []) {
|
|
12641
|
+
if (!isRecord11(m) || typeof m.name !== "string") continue;
|
|
12642
|
+
const dir = typeof m.dir === "string" ? m.dir === "." ? resolve13(root) : resolve13(root, m.dir) : resolve13(root);
|
|
12643
|
+
for (const g of Array.isArray(m.gates) ? m.gates : []) {
|
|
12644
|
+
if (!isRecord11(g)) continue;
|
|
12645
|
+
if (g.state !== void 0 && g.state !== "pending") continue;
|
|
12646
|
+
const op = str7(g.component);
|
|
12647
|
+
const gate = str7(g.name);
|
|
12648
|
+
if (!op || !gate) continue;
|
|
12649
|
+
const env = str7(g.env);
|
|
12650
|
+
const planDigest = str7(g.planDigest);
|
|
12651
|
+
const approvals = (Array.isArray(g.approvals) ? g.approvals : []).filter(isRecord11).map((a) => ({
|
|
12652
|
+
principal: String(a.principal ?? "unknown"),
|
|
12653
|
+
at: str7(a.at),
|
|
12654
|
+
channel: str7(a.channel)
|
|
12655
|
+
}));
|
|
12656
|
+
gates.push({
|
|
12657
|
+
key: gateKey(m.name, op, gate),
|
|
12658
|
+
member: m.name,
|
|
12659
|
+
dir,
|
|
12660
|
+
op,
|
|
12661
|
+
gate,
|
|
12662
|
+
env,
|
|
12663
|
+
planDigest,
|
|
12664
|
+
approvals,
|
|
12665
|
+
needed: typeof g.needed === "number" ? g.needed : 1,
|
|
12666
|
+
recordedAt: str7(g.recordedAt),
|
|
12667
|
+
expiresAt: str7(g.expiresAt),
|
|
12668
|
+
signed: isRecord11(g.signed),
|
|
12669
|
+
// status's line already carries --sign for a signed gate; only the
|
|
12670
|
+
// line built here needs it added.
|
|
12671
|
+
approve: str7(g.approve) ?? ["chant", ...approveArgs({ op, gate, env, planDigest }), ...isRecord11(g.signed) ? ["--sign"] : []].join(" ")
|
|
12672
|
+
});
|
|
12673
|
+
}
|
|
12674
|
+
}
|
|
12675
|
+
return { ok: true, env: String(doc.env ?? ""), gates };
|
|
12676
|
+
}
|
|
12677
|
+
async function readWorkspaceGates(ws, env) {
|
|
12678
|
+
const run3 = await runChantRaw(["workspace", "status", env, "--json"], ws.root);
|
|
12679
|
+
if (!run3.stdout.trim()) {
|
|
12680
|
+
return {
|
|
12681
|
+
ok: false,
|
|
12682
|
+
refusal: {
|
|
12683
|
+
error: `chant workspace status ${env} failed in ${ws.root} (exit ${run3.code}): ${run3.stderr.trim().split("\n").slice(-3).join(" ")}`,
|
|
12684
|
+
code: "workspace-status",
|
|
12685
|
+
remedy: "Run `chant workspace status <env>` in the workspace root."
|
|
12686
|
+
}
|
|
12687
|
+
};
|
|
12688
|
+
}
|
|
12689
|
+
return parseWorkspaceStatus(run3.stdout, ws.root);
|
|
12690
|
+
}
|
|
12691
|
+
function isEnvName(env) {
|
|
12692
|
+
return /^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(env);
|
|
12693
|
+
}
|
|
12694
|
+
function localApprover(env = process.env) {
|
|
12695
|
+
return env.GITHUB_ACTOR ?? env.GITLAB_USER_LOGIN ?? env.USER ?? "unknown";
|
|
12696
|
+
}
|
|
12697
|
+
|
|
12698
|
+
// src/request-guard.ts
|
|
12699
|
+
var LOOPBACK = /* @__PURE__ */ new Set(["localhost", "127.0.0.1", "::1", "[::1]"]);
|
|
12700
|
+
var SAFE = /* @__PURE__ */ new Set(["GET", "HEAD", "OPTIONS"]);
|
|
12701
|
+
function hostName(hostport) {
|
|
12702
|
+
const h = hostport.trim().toLowerCase();
|
|
12703
|
+
if (h.startsWith("[")) {
|
|
12704
|
+
const end = h.indexOf("]");
|
|
12705
|
+
return end >= 0 ? h.slice(0, end + 1) : h;
|
|
12706
|
+
}
|
|
12707
|
+
const colon = h.lastIndexOf(":");
|
|
12708
|
+
return colon >= 0 && h.indexOf(":") === colon ? h.slice(0, colon) : h;
|
|
12709
|
+
}
|
|
12710
|
+
function isLoopbackName(name) {
|
|
12711
|
+
return LOOPBACK.has(name) || name.endsWith(".localhost");
|
|
12712
|
+
}
|
|
12713
|
+
function allowedName(name, cfg) {
|
|
12714
|
+
if (isLoopbackName(name)) return true;
|
|
12715
|
+
if (cfg.boundHost && !isWildcard(cfg.boundHost) && hostName(cfg.boundHost) === name) return true;
|
|
12716
|
+
return cfg.allowedHosts.some((h) => hostName(h) === name);
|
|
12717
|
+
}
|
|
12718
|
+
function isWildcard(h) {
|
|
12719
|
+
return h === "0.0.0.0" || h === "::" || h === "[::]";
|
|
12720
|
+
}
|
|
12721
|
+
function guardRequest(req, cfg) {
|
|
12722
|
+
const name = hostName(req.host);
|
|
12723
|
+
if (!allowedName(name, cfg)) {
|
|
12724
|
+
return { ok: false, status: 421, error: `behold doesn't answer to host ${name}. Start it with --allow-host ${name} if a proxy of yours sends that name.` };
|
|
12725
|
+
}
|
|
12726
|
+
if (SAFE.has(req.method.toUpperCase())) return { ok: true };
|
|
12727
|
+
if ((req.secFetchSite || "").toLowerCase() === "cross-site") {
|
|
12728
|
+
return { ok: false, status: 403, error: "a cross-site page can't write to behold" };
|
|
12729
|
+
}
|
|
12730
|
+
if (req.origin && req.origin !== "null") {
|
|
12731
|
+
let originHost;
|
|
12732
|
+
try {
|
|
12733
|
+
originHost = new URL(req.origin).host.toLowerCase();
|
|
12734
|
+
} catch {
|
|
12735
|
+
return { ok: false, status: 403, error: "a write with an unreadable Origin is refused" };
|
|
12736
|
+
}
|
|
12737
|
+
const same2 = originHost === req.host.trim().toLowerCase();
|
|
12738
|
+
if (!same2 && !cfg.allowedHosts.some((h) => hostName(h) === hostName(originHost))) {
|
|
12739
|
+
return { ok: false, status: 403, error: `a page from ${originHost} can't write to behold` };
|
|
12740
|
+
}
|
|
12741
|
+
} else if (req.origin === "null") {
|
|
12742
|
+
return { ok: false, status: 403, error: "a write from an opaque origin is refused" };
|
|
12743
|
+
}
|
|
12744
|
+
return { ok: true };
|
|
12745
|
+
}
|
|
12746
|
+
function allowedHostsFrom(flag = [], env = process.env.BEHOLD_ALLOWED_HOSTS) {
|
|
12747
|
+
return [...flag, ...(env || "").split(",")].map((s) => s.trim()).filter(Boolean);
|
|
12748
|
+
}
|
|
12749
|
+
|
|
12073
12750
|
// src/server.ts
|
|
12074
12751
|
init_member_kind();
|
|
12075
12752
|
init_terraform_member();
|
|
@@ -12079,8 +12756,8 @@ init_chant();
|
|
|
12079
12756
|
init_member_kind();
|
|
12080
12757
|
init_choudoufu_member();
|
|
12081
12758
|
init_project();
|
|
12759
|
+
import { relative as relative6, resolve as resolve14 } from "node:path";
|
|
12082
12760
|
init_terraform_member();
|
|
12083
|
-
import { relative as relative6, resolve as resolve11 } from "node:path";
|
|
12084
12761
|
init_targets();
|
|
12085
12762
|
var list2 = (xs) => xs.join(", ");
|
|
12086
12763
|
var EMPTY_KUBECONFIG = { contexts: /* @__PURE__ */ new Map(), servers: /* @__PURE__ */ new Map() };
|
|
@@ -12262,11 +12939,11 @@ async function choudoufuCheck(root, members, probe2) {
|
|
|
12262
12939
|
};
|
|
12263
12940
|
}
|
|
12264
12941
|
if (!choudoufuMeetsFloor(v)) {
|
|
12265
|
-
const
|
|
12942
|
+
const said4 = v.forkField ? `choudoufu ${v.version}` : `a choudoufu whose \`version -json\` carries no \`choudoufu_version\` (v0.15.0 or older)`;
|
|
12266
12943
|
return {
|
|
12267
12944
|
name: "choudoufu",
|
|
12268
12945
|
status: "fail",
|
|
12269
|
-
detail: `${
|
|
12946
|
+
detail: `${said4} at ${v.bin} \u2014 below behold's floor ${CHOUDOUFU_FLOOR}, whose \`-json\` documents carry the fields behold reads (schemas, identity, choudoufu_version)`,
|
|
12270
12947
|
fix: `Upgrade choudoufu to ${CHOUDOUFU_FLOOR} or newer (a build from main after 2026-09-08 also carries them).`
|
|
12271
12948
|
};
|
|
12272
12949
|
}
|
|
@@ -12308,10 +12985,41 @@ function terraformCheck(members, probe2) {
|
|
|
12308
12985
|
const reader = `${TERRAFORM_LEXICON_PKG} ${state.lexicon.version || "dev"} (${HCL_PARSER_PKG} ${state.parser.version || "dev"})`;
|
|
12309
12986
|
return { name: "terraform", status: "pass", detail: `${reader}; ${perMember}` };
|
|
12310
12987
|
}
|
|
12988
|
+
function workspaceShape(ws) {
|
|
12989
|
+
return { kind: "estate", members: drawnMembers(ws).map((m) => ({ dir: m.dir, kind: m.kind })) };
|
|
12990
|
+
}
|
|
12991
|
+
function workspaceCheck(ws, beholdMembers) {
|
|
12992
|
+
const drawn = drawnMembers(ws);
|
|
12993
|
+
const unreadable = unreadableMembers(ws);
|
|
12994
|
+
const undrawn = undrawnMembers(ws);
|
|
12995
|
+
const parts = [
|
|
12996
|
+
`chant workspace ${ws.name} (${ws.file}, listed by chant ${ws.chant}): ${drawn.length} of ${ws.members.length} members drawn: ${list2(drawn.map((m) => `${m.name} (${m.kind})`))}`,
|
|
12997
|
+
...unreadable.length ? [`unreadable: ${list2(unreadable.map((m) => `${m.name} ${m.reason.code}: ${m.reason.message}`))}`] : [],
|
|
12998
|
+
...undrawn.length ? [`listed, not drawn: ${list2(undrawn.map((m) => `${m.name} (${m.kind})`))}`] : [],
|
|
12999
|
+
...beholdMembers ? [".behold.json also lists members, which are ignored beside the declaration"] : []
|
|
13000
|
+
];
|
|
13001
|
+
const warn = unreadable.length > 0 || beholdMembers;
|
|
13002
|
+
const fix = [
|
|
13003
|
+
...unreadable.length ? ["`chant workspace check` says what each unreadable member needs."] : [],
|
|
13004
|
+
...beholdMembers ? ["Delete `members` from .behold.json; chant.workspace.json lists them now."] : []
|
|
13005
|
+
].join(" ");
|
|
13006
|
+
return { name: "project", status: warn ? "warn" : "pass", detail: parts.join("; "), ...warn ? { fix } : {} };
|
|
13007
|
+
}
|
|
12311
13008
|
async function diagnose(dir, probes = {}) {
|
|
12312
|
-
const root =
|
|
12313
|
-
const shape = detectProjectShape(root);
|
|
13009
|
+
const root = resolve14(dir);
|
|
12314
13010
|
const behold = beholdVersion();
|
|
13011
|
+
const listing = hasDeclaration(root) ? await (probes.readWorkspace ?? readWorkspace)(root) : void 0;
|
|
13012
|
+
if (listing && !listing.ok) {
|
|
13013
|
+
return {
|
|
13014
|
+
behold,
|
|
13015
|
+
dir: root,
|
|
13016
|
+
kind: "estate",
|
|
13017
|
+
ok: false,
|
|
13018
|
+
checks: [{ name: "project", status: "fail", detail: listing.refusal.error, fix: listing.refusal.remedy }]
|
|
13019
|
+
};
|
|
13020
|
+
}
|
|
13021
|
+
const workspace = listing?.ok ? listing.workspace : void 0;
|
|
13022
|
+
const shape = workspace ? workspaceShape(workspace) : detectProjectShape(root);
|
|
12315
13023
|
const invalid = shape.invalidMembers ?? [];
|
|
12316
13024
|
const invalidDetail = invalid.map((m) => `${m.dir} \u2014 ${m.invalid}`);
|
|
12317
13025
|
const kindsFix = `Member kinds this behold reads: ${registeredMemberKinds().join(", ")}. Fix or remove the entry in .behold.json \`members\`.`;
|
|
@@ -12337,14 +13045,14 @@ async function diagnose(dir, probes = {}) {
|
|
|
12337
13045
|
};
|
|
12338
13046
|
}
|
|
12339
13047
|
const estate = shape.kind === "estate";
|
|
12340
|
-
const members = (shape.members ?? []).map((m) => ({ ...m, abs:
|
|
13048
|
+
const members = (shape.members ?? []).map((m) => ({ ...m, abs: resolve14(root, m.dir) }));
|
|
12341
13049
|
const targets = estate ? members.map((m) => m.abs) : [root];
|
|
12342
13050
|
const primary = targets[0];
|
|
12343
13051
|
const chantTargets = estate ? members.filter((m) => m.kind === "chant").map((m) => m.abs) : [root];
|
|
12344
13052
|
const memberList = list2(members.map((m) => `${m.dir} (${m.kind})`));
|
|
12345
13053
|
const membersFrom = shape.membersFrom === "behold-config" ? ".behold.json members" : "npm workspaces";
|
|
12346
13054
|
const estateDetail = shape.membersFrom === "itself" ? `a ${members[0].kind} member \u2014 the directory itself, no member list` : `estate of ${members.length} members (${membersFrom}): ${memberList}`;
|
|
12347
|
-
const projectCheck = estate ? invalid.length ? {
|
|
13055
|
+
const projectCheck = workspace ? workspaceCheck(workspace, !!loadBeholdConfig(root).members) : estate ? invalid.length ? {
|
|
12348
13056
|
name: "project",
|
|
12349
13057
|
status: "fail",
|
|
12350
13058
|
detail: `${estateDetail}; invalid: ${list2(invalidDetail)}`,
|
|
@@ -12397,12 +13105,11 @@ function formatReport(report) {
|
|
|
12397
13105
|
|
|
12398
13106
|
// src/server.ts
|
|
12399
13107
|
init_read_stats();
|
|
12400
|
-
import { shortStackNames as shortStackNames2 } from "@intentius/pinhole";
|
|
12401
13108
|
init_foreign();
|
|
12402
13109
|
|
|
12403
13110
|
// src/events.ts
|
|
12404
|
-
import { watch, existsSync as
|
|
12405
|
-
import { join as
|
|
13111
|
+
import { watch, existsSync as existsSync16 } from "node:fs";
|
|
13112
|
+
import { join as join24 } from "node:path";
|
|
12406
13113
|
var Broadcaster = class {
|
|
12407
13114
|
listeners = /* @__PURE__ */ new Set();
|
|
12408
13115
|
subscribe(fn) {
|
|
@@ -12420,7 +13127,7 @@ var Broadcaster = class {
|
|
|
12420
13127
|
};
|
|
12421
13128
|
var IGNORE = /(^|[\\/])(node_modules|dist|\.git)([\\/]|$)/;
|
|
12422
13129
|
function watchSource(projectDir, onChange, debounceMs = 200) {
|
|
12423
|
-
const dir =
|
|
13130
|
+
const dir = existsSync16(join24(projectDir, "src")) ? join24(projectDir, "src") : projectDir;
|
|
12424
13131
|
let timer;
|
|
12425
13132
|
const watcher = watch(dir, { recursive: true }, (_event, file) => {
|
|
12426
13133
|
const name = typeof file === "string" ? file : "";
|
|
@@ -12722,8 +13429,8 @@ async function emulatorDown(projectDir) {
|
|
|
12722
13429
|
}
|
|
12723
13430
|
|
|
12724
13431
|
// src/demos.ts
|
|
12725
|
-
import { readFileSync as readFileSync18, existsSync as
|
|
12726
|
-
import { dirname as dirname6, isAbsolute as isAbsolute4, join as
|
|
13432
|
+
import { readFileSync as readFileSync18, existsSync as existsSync17, cpSync, mkdirSync as mkdirSync6 } from "node:fs";
|
|
13433
|
+
import { dirname as dirname6, isAbsolute as isAbsolute4, join as join25, relative as relative7, resolve as resolve15, sep as sep6 } from "node:path";
|
|
12727
13434
|
import { spawn as spawn6, spawnSync as spawnSync2 } from "node:child_process";
|
|
12728
13435
|
function readCatalog(file, catalog) {
|
|
12729
13436
|
let raw;
|
|
@@ -12734,7 +13441,7 @@ function readCatalog(file, catalog) {
|
|
|
12734
13441
|
}
|
|
12735
13442
|
const list3 = raw?.demos;
|
|
12736
13443
|
if (!Array.isArray(list3)) return [];
|
|
12737
|
-
const catalogDir = dirname6(
|
|
13444
|
+
const catalogDir = dirname6(resolve15(file));
|
|
12738
13445
|
return list3.filter((e) => validEntry(e)).map((e) => ({ ...e, catalog, catalogDir }));
|
|
12739
13446
|
}
|
|
12740
13447
|
function validEntry(e) {
|
|
@@ -12768,10 +13475,10 @@ function validEntry(e) {
|
|
|
12768
13475
|
return true;
|
|
12769
13476
|
}
|
|
12770
13477
|
function loadDemoRegistry(pkgRoot3) {
|
|
12771
|
-
const merged = readCatalog(
|
|
13478
|
+
const merged = readCatalog(join25(pkgRoot3, "demos.json"), "demos");
|
|
12772
13479
|
const seen = new Set(merged.map((e) => e.name));
|
|
12773
13480
|
const named = process.env.BEHOLD_WORKBENCH;
|
|
12774
|
-
for (const e of [...readCatalog(
|
|
13481
|
+
for (const e of [...readCatalog(join25(pkgRoot3, "workbench.json"), "workbench"), ...named ? readCatalog(resolve15(named), "workbench") : []]) {
|
|
12775
13482
|
if (seen.has(e.name)) {
|
|
12776
13483
|
process.stderr.write(`behold: workbench entry "${e.name}" collides with a demo already in the catalog \u2014 dropped
|
|
12777
13484
|
`);
|
|
@@ -12784,17 +13491,17 @@ function loadDemoRegistry(pkgRoot3) {
|
|
|
12784
13491
|
}
|
|
12785
13492
|
function demoLocalPath(entry) {
|
|
12786
13493
|
if (entry.source !== "local" || !entry.path) return void 0;
|
|
12787
|
-
return isAbsolute4(entry.path) ? entry.path :
|
|
13494
|
+
return isAbsolute4(entry.path) ? entry.path : resolve15(entry.catalogDir ?? process.cwd(), entry.path);
|
|
12788
13495
|
}
|
|
12789
13496
|
function missingRequirements(entry) {
|
|
12790
13497
|
const bins = entry.source === "git" && !entry.requires.includes("git") ? [...entry.requires, "git"] : entry.requires;
|
|
12791
13498
|
const finder = process.platform === "win32" ? "where" : "which";
|
|
12792
13499
|
const missing = bins.filter((bin) => {
|
|
12793
|
-
if (bin === "choudoufu" && process.env.CHOUDOUFU_BIN &&
|
|
13500
|
+
if (bin === "choudoufu" && process.env.CHOUDOUFU_BIN && existsSync17(process.env.CHOUDOUFU_BIN)) return false;
|
|
12794
13501
|
return spawnSync2(finder, [bin], { stdio: "ignore" }).status !== 0;
|
|
12795
13502
|
});
|
|
12796
13503
|
const local = demoLocalPath(entry);
|
|
12797
|
-
if (local && !
|
|
13504
|
+
if (local && !existsSync17(local)) missing.push(`${entry.path} (not checked out)`);
|
|
12798
13505
|
return missing;
|
|
12799
13506
|
}
|
|
12800
13507
|
function fetchesFromNetwork(entry) {
|
|
@@ -12803,9 +13510,9 @@ function fetchesFromNetwork(entry) {
|
|
|
12803
13510
|
function demoTargetDir(entry, cwd = process.cwd()) {
|
|
12804
13511
|
const inPlace = entry.inPlace ? demoLocalPath(entry) : void 0;
|
|
12805
13512
|
if (inPlace) return inPlace;
|
|
12806
|
-
const legacy =
|
|
12807
|
-
if (entry.name === "writes" &&
|
|
12808
|
-
return
|
|
13513
|
+
const legacy = resolve15(cwd, "behold-demo");
|
|
13514
|
+
if (entry.name === "writes" && existsSync17(legacy)) return legacy;
|
|
13515
|
+
return resolve15(cwd, "behold-demos", entry.name);
|
|
12809
13516
|
}
|
|
12810
13517
|
async function loadDemo(entry, opts) {
|
|
12811
13518
|
const { pkgRoot: pkgRoot3, target } = opts;
|
|
@@ -12814,12 +13521,12 @@ async function loadDemo(entry, opts) {
|
|
|
12814
13521
|
const inPlace = entry.source === "local" && entry.inPlace;
|
|
12815
13522
|
const work = inPlace ? local : target;
|
|
12816
13523
|
if (inPlace) {
|
|
12817
|
-
if (!
|
|
13524
|
+
if (!existsSync17(work)) return { ok: false, error: `${entry.path} is not checked out (looked in ${work})` };
|
|
12818
13525
|
say(`serving ${work} where it is \u2014 nothing copied`);
|
|
12819
|
-
} else if (!
|
|
13526
|
+
} else if (!existsSync17(target)) {
|
|
12820
13527
|
if (entry.source === "bundled" || entry.source === "local" && local) {
|
|
12821
|
-
const from = entry.source === "bundled" ?
|
|
12822
|
-
if (!
|
|
13528
|
+
const from = entry.source === "bundled" ? join25(pkgRoot3, entry.dir) : local;
|
|
13529
|
+
if (!existsSync17(from)) return { ok: false, error: entry.source === "bundled" ? `this install has no bundled ${entry.dir}` : `${entry.path} is not checked out (looked in ${from})` };
|
|
12823
13530
|
say(`copying to ${target} (it's yours \u2014 edit it)`);
|
|
12824
13531
|
try {
|
|
12825
13532
|
cpSync(from, target, {
|
|
@@ -12845,7 +13552,7 @@ async function loadDemo(entry, opts) {
|
|
|
12845
13552
|
} else {
|
|
12846
13553
|
say(`reusing ${target}`);
|
|
12847
13554
|
}
|
|
12848
|
-
if (!inPlace &&
|
|
13555
|
+
if (!inPlace && existsSync17(join25(work, "package.json")) && !existsSync17(join25(work, "node_modules"))) {
|
|
12849
13556
|
say("npm install\u2026");
|
|
12850
13557
|
if (await runStep("npm", ["install"], { cwd: work, shell: process.platform === "win32" }) !== 0) {
|
|
12851
13558
|
return { ok: false, error: `npm install failed in ${work}` };
|
|
@@ -12862,7 +13569,7 @@ async function loadDemo(entry, opts) {
|
|
|
12862
13569
|
return { ok: false, error: `setup failed (${entry.setup})` };
|
|
12863
13570
|
}
|
|
12864
13571
|
}
|
|
12865
|
-
return { ok: true, serveDirs: entry.serve.dirs?.length ? entry.serve.dirs.map((d) =>
|
|
13572
|
+
return { ok: true, serveDirs: entry.serve.dirs?.length ? entry.serve.dirs.map((d) => join25(work, d)) : [work] };
|
|
12866
13573
|
}
|
|
12867
13574
|
function runStep(cmd, args, opts = {}) {
|
|
12868
13575
|
return new Promise((res) => {
|
|
@@ -12873,8 +13580,8 @@ function runStep(cmd, args, opts = {}) {
|
|
|
12873
13580
|
}
|
|
12874
13581
|
|
|
12875
13582
|
// src/server.ts
|
|
12876
|
-
var pkgRoot =
|
|
12877
|
-
var webRoot =
|
|
13583
|
+
var pkgRoot = join26(dirname7(fileURLToPath4(import.meta.url)), "..");
|
|
13584
|
+
var webRoot = join26(pkgRoot, "web");
|
|
12878
13585
|
var execFileP = async (cmd, args) => (await promisify3(execFile3)(cmd, args, { encoding: "utf8", timeout: 1e4 })).stdout;
|
|
12879
13586
|
function optsFromQuery(url, tierEnvVar, projectDir) {
|
|
12880
13587
|
const q = url.searchParams;
|
|
@@ -12925,7 +13632,7 @@ function tierFailure(tier, message) {
|
|
|
12925
13632
|
function beholdVersion() {
|
|
12926
13633
|
try {
|
|
12927
13634
|
const here = dirname7(fileURLToPath4(import.meta.url));
|
|
12928
|
-
return JSON.parse(readFileSync19(
|
|
13635
|
+
return JSON.parse(readFileSync19(join26(here, "..", "package.json"), "utf8")).version ?? "unknown";
|
|
12929
13636
|
} catch {
|
|
12930
13637
|
return "unknown";
|
|
12931
13638
|
}
|
|
@@ -13214,13 +13921,29 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
|
|
|
13214
13921
|
onDone: (opEnv) => captureFrame(cfg.projectDir, opEnv ?? cfg.env, frames, broadcaster)
|
|
13215
13922
|
})) {
|
|
13216
13923
|
const app = new Hono();
|
|
13924
|
+
const guard = { allowedHosts: allowedHostsFrom(cfg.allowedHosts ?? []), ...cfg.host ? { boundHost: cfg.host } : {} };
|
|
13925
|
+
app.use("*", async (c, next) => {
|
|
13926
|
+
const verdict = guardRequest(
|
|
13927
|
+
{
|
|
13928
|
+
method: c.req.method,
|
|
13929
|
+
host: c.req.header("host") ?? new URL(c.req.url).host,
|
|
13930
|
+
origin: c.req.header("origin") ?? null,
|
|
13931
|
+
secFetchSite: c.req.header("sec-fetch-site") ?? null
|
|
13932
|
+
},
|
|
13933
|
+
guard
|
|
13934
|
+
);
|
|
13935
|
+
if (!verdict.ok) return c.json({ error: verdict.error, code: "request-refused" }, verdict.status);
|
|
13936
|
+
await next();
|
|
13937
|
+
});
|
|
13217
13938
|
setChoudoufuRunner(cfg.choudoufu?.run);
|
|
13218
13939
|
setEstateLexiconRead(cfg.choudoufu ? cfg.choudoufu.lexicon ?? null : void 0);
|
|
13940
|
+
setServedWorkspace(cfg.workspace);
|
|
13219
13941
|
app.use("/api/*", async (c, next) => {
|
|
13220
13942
|
if (c.req.method === "GET") return withReadSignal(c.req.raw.signal, next);
|
|
13221
13943
|
await next();
|
|
13222
13944
|
});
|
|
13223
13945
|
if (cfg.carveReport) carveRoutes(app, cfg.carveReport, cfg.carveDemo);
|
|
13946
|
+
const predictDeps = cfg.behaviour ?? { read: graphIr };
|
|
13224
13947
|
let beholdConfig = loadBeholdConfig(cfg.projectDir);
|
|
13225
13948
|
let tierEnvVar = beholdConfig.tiers?.envVar;
|
|
13226
13949
|
const executorFor = (env) => env ? beholdConfig.executor?.[env] : void 0;
|
|
@@ -13331,9 +14054,9 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
|
|
|
13331
14054
|
return c.json({ started: true, name, ran: label });
|
|
13332
14055
|
});
|
|
13333
14056
|
app.post("/api/local/reset", (c) => {
|
|
13334
|
-
const down =
|
|
13335
|
-
const up =
|
|
13336
|
-
if (!
|
|
14057
|
+
const down = join26(cfg.projectDir, "scripts/local/local-down.sh");
|
|
14058
|
+
const up = join26(cfg.projectDir, "scripts/local/local-up.sh");
|
|
14059
|
+
if (!existsSync18(down) || !existsSync18(up)) {
|
|
13337
14060
|
return c.json({ error: "no local-down.sh / local-up.sh in scripts/local \u2014 reset is only for local emulator projects" }, 400);
|
|
13338
14061
|
}
|
|
13339
14062
|
if (!runner.bringUp("reset local emulator", "bash", ["-c", "bash scripts/local/local-down.sh && bash scripts/local/local-up.sh"], cfg.projectDir)) {
|
|
@@ -13433,6 +14156,7 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
|
|
|
13433
14156
|
return c.json({ history });
|
|
13434
14157
|
});
|
|
13435
14158
|
app.post("/api/operator/approve/:op/:gate", async (c) => {
|
|
14159
|
+
if (cfg.previewMode) return c.json({ error: "approving is disabled in preview mode", code: "preview" }, 403);
|
|
13436
14160
|
const { op, gate } = c.req.param();
|
|
13437
14161
|
const info = estateOps().find((o) => o.name === op);
|
|
13438
14162
|
broadcaster.emit("op", `\u270E approve ${op} ${gate} \u2014 records a fact; the next tick acts on it`);
|
|
@@ -13443,6 +14167,7 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
|
|
|
13443
14167
|
return c.json({ recorded: true, op, gate, semantics: APPROVE_SEMANTICS });
|
|
13444
14168
|
});
|
|
13445
14169
|
app.post("/api/ops/:name/signal/:gate", async (c) => {
|
|
14170
|
+
if (cfg.previewMode) return c.json({ error: "approving is disabled in preview mode", code: "preview" }, 403);
|
|
13446
14171
|
const { name, gate } = c.req.param();
|
|
13447
14172
|
const info = estateOps().find((o) => o.name === name);
|
|
13448
14173
|
broadcaster.emit("op", `\u270E signal ${name} ${gate}`);
|
|
@@ -13450,6 +14175,38 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
|
|
|
13450
14175
|
if (code !== 0) return c.json({ error: stderr.trim() || `signal exited ${code}` }, 500);
|
|
13451
14176
|
return c.json({ signalled: true });
|
|
13452
14177
|
});
|
|
14178
|
+
app.get("/api/workspace/gates", async (c) => {
|
|
14179
|
+
const ws = servedWorkspace();
|
|
14180
|
+
const env = c.req.query("env") || cfg.env || "local";
|
|
14181
|
+
if (!isEnvName(env)) return c.json({ error: `${JSON.stringify(env)} isn't an env name`, code: "env" }, 400);
|
|
14182
|
+
if (!ws) return c.json({ workspace: false, env, gates: [], approver: localApprover() });
|
|
14183
|
+
const read = await readWorkspaceGates(ws, env);
|
|
14184
|
+
if (!read.ok) return c.json({ workspace: true, env, gates: [], refusal: read.refusal, approver: localApprover() });
|
|
14185
|
+
return c.json({ workspace: true, env, gates: read.gates.map(({ dir: _dir, ...g }) => g), approver: localApprover() });
|
|
14186
|
+
});
|
|
14187
|
+
app.post("/api/workspace/gates/approve", async (c) => {
|
|
14188
|
+
if (cfg.previewMode) return c.json({ error: "approving is disabled in preview mode", code: "preview" }, 403);
|
|
14189
|
+
if (!(c.req.header("content-type") ?? "").toLowerCase().startsWith("application/json")) {
|
|
14190
|
+
return c.json({ error: "send the body as application/json", code: "content-type" }, 415);
|
|
14191
|
+
}
|
|
14192
|
+
const ws = servedWorkspace();
|
|
14193
|
+
if (!ws) return c.json({ error: "behold isn't serving a declared workspace", code: "no-workspace" }, 404);
|
|
14194
|
+
const body = await c.req.json().catch(() => ({}));
|
|
14195
|
+
const key = typeof body?.key === "string" ? body.key : "";
|
|
14196
|
+
const env = typeof body?.env === "string" && body.env ? body.env : cfg.env || "local";
|
|
14197
|
+
if (!isEnvName(env)) return c.json({ error: `${JSON.stringify(env)} isn't an env name`, code: "env" }, 400);
|
|
14198
|
+
const read = await readWorkspaceGates(ws, env);
|
|
14199
|
+
if (!read.ok) return c.json({ error: read.refusal.error, code: read.refusal.code }, 422);
|
|
14200
|
+
const g = read.gates.find((x) => x.key === key);
|
|
14201
|
+
if (!g) return c.json({ error: `no gate ${key} is waiting in ${env}`, code: "no-gate" }, 404);
|
|
14202
|
+
if (g.signed) return c.json({ error: `${key} wants a signed approval, which behold can't make. Run: ${g.approve}`, code: "signed" }, 422);
|
|
14203
|
+
const approver = localApprover();
|
|
14204
|
+
broadcaster.emit("op", `\u270E approve ${g.key} as ${approver}`);
|
|
14205
|
+
const { code, stderr, stdout } = await runChantRaw(approveArgs(g), g.dir);
|
|
14206
|
+
if (code !== 0) return c.json({ error: stderr.trim() || stdout.trim() || `approve exited ${code}`, code: "approve" }, 500);
|
|
14207
|
+
broadcaster.emit("changed", "");
|
|
14208
|
+
return c.json({ recorded: true, key: g.key, approver });
|
|
14209
|
+
});
|
|
13453
14210
|
app.get(
|
|
13454
14211
|
"/api/events",
|
|
13455
14212
|
(c) => streamSSE(c, async (stream) => {
|
|
@@ -13516,6 +14273,9 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
|
|
|
13516
14273
|
currentEnv: cfg.env ?? null,
|
|
13517
14274
|
// v0.1.0 preview: the SPA hides git/PR ops + arbitrary-project affordances.
|
|
13518
14275
|
...cfg.previewMode ? { previewMode: true } : {},
|
|
14276
|
+
// #477: who `chant approve` records when behold runs it. The approve
|
|
14277
|
+
// buttons say it before the click.
|
|
14278
|
+
approver: localApprover(),
|
|
13519
14279
|
// The tier picker's options (M2 #54, sourced #70): gated on the served
|
|
13520
14280
|
// project's `.behold.json` declaring a `tiers` block at all — NOT on
|
|
13521
14281
|
// whether its env var happens to be set in behold's own launch env
|
|
@@ -13566,9 +14326,9 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
|
|
|
13566
14326
|
app.post("/api/project/open", async (c) => {
|
|
13567
14327
|
if (cfg.previewMode) return c.json({ error: "switching projects is locked in preview mode" }, 403);
|
|
13568
14328
|
const body = await c.req.json().catch(() => ({}));
|
|
13569
|
-
const dir = typeof body.dir === "string" && body.dir.trim() ?
|
|
13570
|
-
if (!dir || !
|
|
13571
|
-
if (!
|
|
14329
|
+
const dir = typeof body.dir === "string" && body.dir.trim() ? resolve16(body.dir.trim()) : "";
|
|
14330
|
+
if (!dir || !existsSync18(dir)) return c.json({ error: `no such directory: ${dir || "(no dir given)"}` }, 400);
|
|
14331
|
+
if (!existsSync18(join26(dir, "chant.config.ts"))) {
|
|
13572
14332
|
return c.json({ error: `${dir} doesn't look like a chant project \u2014 no chant.config.ts` }, 400);
|
|
13573
14333
|
}
|
|
13574
14334
|
switchServedProject([dir]);
|
|
@@ -13577,6 +14337,8 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
|
|
|
13577
14337
|
const switchServedProject = (dirs, env, spawnEnv) => {
|
|
13578
14338
|
addRecent(cfg.projectDir);
|
|
13579
14339
|
cfg.projectDir = dirs[0];
|
|
14340
|
+
cfg.workspace = void 0;
|
|
14341
|
+
setServedWorkspace(void 0);
|
|
13580
14342
|
cfg.projectDirs = servesAsEstate(dirs) ? dirs : void 0;
|
|
13581
14343
|
setChoudoufuSpawnEnv(spawnEnv);
|
|
13582
14344
|
cfg.env = env;
|
|
@@ -13589,7 +14351,7 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
|
|
|
13589
14351
|
};
|
|
13590
14352
|
app.post("/api/project/reveal", async (c) => {
|
|
13591
14353
|
const body = await c.req.json().catch(() => ({}));
|
|
13592
|
-
const dir = typeof body.dir === "string" && body.dir.trim() ?
|
|
14354
|
+
const dir = typeof body.dir === "string" && body.dir.trim() ? resolve16(body.dir.trim()) : cfg.projectDir;
|
|
13593
14355
|
const known = /* @__PURE__ */ new Set([cfg.projectDir, ...cfg.projectDirs ?? [], ...listRecents().map((r) => r.dir)]);
|
|
13594
14356
|
if (!known.has(dir)) return c.json({ error: "not a served or recent project directory" }, 400);
|
|
13595
14357
|
const opener = process.platform === "darwin" ? "open" : process.platform === "win32" ? "explorer" : "xdg-open";
|
|
@@ -13617,7 +14379,7 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
|
|
|
13617
14379
|
fetches: fetchesFromNetwork(e),
|
|
13618
14380
|
...e.repo ? { repo: e.repo } : {},
|
|
13619
14381
|
target,
|
|
13620
|
-
loaded:
|
|
14382
|
+
loaded: existsSync18(target),
|
|
13621
14383
|
satisfiable: missing.length === 0,
|
|
13622
14384
|
// #254: the carve walkthrough runs fine here — it just isn't a project
|
|
13623
14385
|
// to switch INTO. Carve mode claims `/api/graph` and `/api/project` at
|
|
@@ -13676,20 +14438,21 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
|
|
|
13676
14438
|
loadingDemo = null;
|
|
13677
14439
|
}
|
|
13678
14440
|
});
|
|
14441
|
+
const layoutDir = () => cfg.workspace?.root ?? cfg.projectDir;
|
|
13679
14442
|
const layoutWriteBlock = () => {
|
|
13680
14443
|
if (cfg.previewMode) return "the layout sidecar is read-only in preview mode";
|
|
13681
14444
|
if (cfg.carveReport) return "a carve report isn't a project \u2014 there's nowhere to keep a hand layout";
|
|
13682
14445
|
if (cfg.layoutWrites === false) return "a static export captures a snapshot \u2014 it doesn't write to the project";
|
|
13683
|
-
return unwritableReason(
|
|
14446
|
+
return unwritableReason(layoutDir());
|
|
13684
14447
|
};
|
|
13685
14448
|
app.get("/api/layout", (c) => {
|
|
13686
14449
|
const block = layoutWriteBlock();
|
|
13687
|
-
const shared2 = { path: layoutPath(
|
|
14450
|
+
const shared2 = { path: layoutPath(layoutDir()), writable: !block, ...block ? { reason: block } : {} };
|
|
13688
14451
|
const raw = new URL(c.req.url).searchParams.get("lens");
|
|
13689
|
-
if (raw === null) return c.json({ ...shared2, lenses: readLayoutFile(
|
|
14452
|
+
if (raw === null) return c.json({ ...shared2, lenses: readLayoutFile(layoutDir()).lenses });
|
|
13690
14453
|
const lens = normalizeLens(raw);
|
|
13691
14454
|
if (!lens) return c.json({ error: `not a lens key: ${raw}`, code: "bad-layout" }, 400);
|
|
13692
|
-
return c.json({ ...shared2, lens, deltas: readLens(
|
|
14455
|
+
return c.json({ ...shared2, lens, deltas: readLens(layoutDir(), lens) });
|
|
13693
14456
|
});
|
|
13694
14457
|
app.post("/api/layout", async (c) => {
|
|
13695
14458
|
const block = layoutWriteBlock();
|
|
@@ -13713,8 +14476,8 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
|
|
|
13713
14476
|
return c.json({ error: "body needs `deltas`: {<node id>: {dx,dy,dw,dh}}", code: "bad-layout" }, 400);
|
|
13714
14477
|
}
|
|
13715
14478
|
try {
|
|
13716
|
-
const stored = writeLens(
|
|
13717
|
-
return c.json({ ok: true, lens: stored.lens, deltas: stored.deltas, count: Object.keys(stored.deltas).length, path: layoutPath(
|
|
14479
|
+
const stored = writeLens(layoutDir(), lens, body.deltas);
|
|
14480
|
+
return c.json({ ok: true, lens: stored.lens, deltas: stored.deltas, count: Object.keys(stored.deltas).length, path: layoutPath(layoutDir()) });
|
|
13718
14481
|
} catch (err) {
|
|
13719
14482
|
if (err instanceof LayoutTooLarge) return c.json({ error: err.message, code: "too-large" }, 413);
|
|
13720
14483
|
return c.json({ error: err instanceof Error ? err.message : String(err) }, 500);
|
|
@@ -13727,7 +14490,7 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
|
|
|
13727
14490
|
const res = c.res;
|
|
13728
14491
|
if (!res || res.status !== 200 || !(res.headers.get("content-type") ?? "").includes("json")) return;
|
|
13729
14492
|
const lens = lensFromQuery(url.searchParams);
|
|
13730
|
-
const deltas = readLens(
|
|
14493
|
+
const deltas = readLens(layoutDir(), lens);
|
|
13731
14494
|
if (!Object.keys(deltas).length) return;
|
|
13732
14495
|
let body;
|
|
13733
14496
|
try {
|
|
@@ -13776,7 +14539,7 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
|
|
|
13776
14539
|
{
|
|
13777
14540
|
method: "GET",
|
|
13778
14541
|
path: "/api/overlay",
|
|
13779
|
-
desc: "live drift overlay for ?env= \u2014 same shape/params as /api/graph, plus runtime=1; each priced entity carries attrs._behaviour (cost/headroom/errorRate/resilience/rightSize/provenance, #398) and meta.behaviour holds the engine, its sums, a refusal or an absent line"
|
|
14542
|
+
desc: "live drift overlay for ?env= \u2014 same shape/params as /api/graph, plus runtime=1; each priced entity carries attrs._behaviour (cost/headroom/errorRate/resilience/rightSize/provenance, #398) and meta.behaviour holds the engine, its sums, a refusal or an absent line; with ?traffic= (or .behold.json behaviour.traffic) each member is predicted live and declared, and meta.behaviour adds live, declared and delta with attrs._behaviourDeclared per entity (#402)"
|
|
13780
14543
|
},
|
|
13781
14544
|
{ method: "GET", path: "/api/layout", desc: "hand-layout sidecar (.behold/layout.json): ?lens=<key> \u2192 {lens, deltas, writable}; no lens \u2192 every lens" },
|
|
13782
14545
|
{ method: "POST", path: "/api/layout", desc: "store one lens's deltas: JSON body {lens, deltas: {<node id>: {dx,dy,dw,dh}}} (the only file behold writes in your project)" },
|
|
@@ -13799,10 +14562,12 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
|
|
|
13799
14562
|
{ method: "GET", path: "/api/history", desc: "recent source commits (rollback targets)" },
|
|
13800
14563
|
{ method: "GET", path: "/api/frames", desc: "captured lanes frames" },
|
|
13801
14564
|
{ method: "GET", path: "/api/events", desc: "SSE: changed / op / apply / run / pr" },
|
|
13802
|
-
{ method: "POST", path: "/api/refresh", desc: "re-observe live now (?env=) \u2014 returns the fresh graph" },
|
|
14565
|
+
{ method: "POST", path: "/api/refresh", desc: "re-observe live now (?env=) \u2014 returns the fresh graph. With ?notify=1 it is a host's notice that the workspace changed: cached reads are dropped and every open page re-pulls (#476)" },
|
|
13803
14566
|
{ method: "POST", path: "/api/apply", desc: "delegated apply: ?env=&component=<name|all> (guarded, preview-locked)" },
|
|
13804
14567
|
{ method: "POST", path: "/api/ops/:name/run", desc: "run a committed Op (delegated write)" },
|
|
13805
14568
|
{ method: "POST", path: "/api/ops/:name/signal/:gate", desc: "approve an Op's gate (releases the waiting run)" },
|
|
14569
|
+
{ method: "GET", path: "/api/workspace/gates", desc: "a declared workspace's waiting gates from chant workspace status <env> --json (?env=), keyed member/op/gate, with the approver behold would record (#477)" },
|
|
14570
|
+
{ method: "POST", path: "/api/workspace/gates/approve", desc: "approve one workspace gate: JSON body {key, env}; env and plan come from a fresh status read; recorded as the user running behold; preview-locked (#477)" },
|
|
13806
14571
|
{ method: "POST", path: "/api/operator/approve/:op/:gate", desc: "record a converge gate's resolution (chant approve \u2014 a fact for the next tick, not an unblock)" },
|
|
13807
14572
|
{ method: "POST", path: "/api/rollback", desc: "open a rollback PR: ?to=<sha>" },
|
|
13808
14573
|
{ method: "POST", path: "/api/substrates/:name/up", desc: "bring a substrate up" },
|
|
@@ -13878,8 +14643,8 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
|
|
|
13878
14643
|
}
|
|
13879
14644
|
const runState = runner.runState;
|
|
13880
14645
|
const ranOp = runState.op ? parsedOps.find((o) => o.name === runState.op) : void 0;
|
|
13881
|
-
const
|
|
13882
|
-
const opsIr =
|
|
14646
|
+
const join30 = ranOp ? joinRun(ranOp, runState.records, runState.gate) : null;
|
|
14647
|
+
const opsIr = join30 ? paintPlayhead(declaredIr, join30, runState.gate) : declaredIr;
|
|
13883
14648
|
const { svg: svg2 } = renderGraph(opsIr, { boxes: "byStack" });
|
|
13884
14649
|
const operator = runner.noteDeclaredConvergeOps(declaredConvergeOps(parsedOps));
|
|
13885
14650
|
return c.json({
|
|
@@ -13892,7 +14657,7 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
|
|
|
13892
14657
|
target: null,
|
|
13893
14658
|
mode: "ops",
|
|
13894
14659
|
ops: parsedOps.length,
|
|
13895
|
-
note: `${opsNote(parsedOps, opsIr)} ${playheadNote(runState,
|
|
14660
|
+
note: `${opsNote(parsedOps, opsIr)} ${playheadNote(runState, join30)} ${operatorNote(operator)}`,
|
|
13896
14661
|
// The operator strip's own model: the ConvergeOps this project
|
|
13897
14662
|
// declares, plus whatever the last status read said. `declared`
|
|
13898
14663
|
// without a `strip` is the honest starting state — a loop exists and
|
|
@@ -13999,12 +14764,21 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
|
|
|
13999
14764
|
srcCompositeEdgesAttached = 0;
|
|
14000
14765
|
}
|
|
14001
14766
|
}
|
|
14002
|
-
if (!multi && !components && !logical && !opts.lens && !ownKindVia && ir.nodes.length === 0 && !
|
|
14767
|
+
if (!multi && !components && !logical && !opts.lens && !ownKindVia && ir.nodes.length === 0 && !existsSync18(join26(cfg.projectDir, "chant.config.ts"))) {
|
|
14003
14768
|
return c.json(noProjectError(cfg.projectDir), 404);
|
|
14004
14769
|
}
|
|
14005
14770
|
const radial = new URL(c.req.url).searchParams.get("radial") === "1";
|
|
14006
14771
|
const collapse = new URL(c.req.url).searchParams.get("collapse") === "1";
|
|
14007
14772
|
let collapsedNote;
|
|
14773
|
+
const srcTraffic = components ? void 0 : trafficFor(new URL(c.req.url).searchParams, beholdConfig);
|
|
14774
|
+
const srcBehaviour = srcTraffic ? attachDeclaredBehaviour(
|
|
14775
|
+
ir,
|
|
14776
|
+
await Promise.all(
|
|
14777
|
+
(multi ? estateMembers(cfg.projectDirs) : [{ name: void 0, dir: cfg.projectDir, kind: memberKindOf(cfg.projectDir) ?? "chant" }]).map(
|
|
14778
|
+
async (m) => ({ name: m.name, prediction: await predictMember(m, void 0, srcTraffic, predictDeps) })
|
|
14779
|
+
)
|
|
14780
|
+
)
|
|
14781
|
+
) : void 0;
|
|
14008
14782
|
const expanded = ir;
|
|
14009
14783
|
if (multi && collapse) {
|
|
14010
14784
|
const shut = collapseBoxes(ir);
|
|
@@ -14029,6 +14803,7 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
|
|
|
14029
14803
|
meta: {
|
|
14030
14804
|
projectDir: cfg.projectDir,
|
|
14031
14805
|
env: metaEnv,
|
|
14806
|
+
...srcBehaviour ? { behaviour: srcBehaviour } : {},
|
|
14032
14807
|
...srcNote ? { note: srcNote } : {},
|
|
14033
14808
|
// #393 item 7: the strip's form of the same note, when there is a
|
|
14034
14809
|
// shorter true one. Absent means "the note fits" — the SPA shows
|
|
@@ -14162,7 +14937,7 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
|
|
|
14162
14937
|
});
|
|
14163
14938
|
const choudoufuPlanDrift = async (dirs, wanted) => {
|
|
14164
14939
|
const members = estateMembers(dirs).filter((m) => m.kind === "choudoufu");
|
|
14165
|
-
const
|
|
14940
|
+
const byDir2 = /* @__PURE__ */ new Map();
|
|
14166
14941
|
let read = false;
|
|
14167
14942
|
for (const m of members) {
|
|
14168
14943
|
const stamp = memberSourceStamp(m.dir);
|
|
@@ -14170,7 +14945,7 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
|
|
|
14170
14945
|
const hit = cachedChoudoufuPlan(m.dir, stamp);
|
|
14171
14946
|
if (hit) {
|
|
14172
14947
|
read = true;
|
|
14173
|
-
|
|
14948
|
+
byDir2.set(m.dir, { name: m.name, drift: hit });
|
|
14174
14949
|
}
|
|
14175
14950
|
continue;
|
|
14176
14951
|
}
|
|
@@ -14178,11 +14953,11 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
|
|
|
14178
14953
|
const drift = await readChoudoufuPlan(m.dir, cfg.choudoufu?.run);
|
|
14179
14954
|
cacheChoudoufuPlan(m.dir, stamp, drift);
|
|
14180
14955
|
read = true;
|
|
14181
|
-
|
|
14956
|
+
byDir2.set(m.dir, { name: m.name, drift });
|
|
14182
14957
|
} catch {
|
|
14183
14958
|
}
|
|
14184
14959
|
}
|
|
14185
|
-
return { read, byDir };
|
|
14960
|
+
return { read, byDir: byDir2 };
|
|
14186
14961
|
};
|
|
14187
14962
|
app.get("/api/overlay", async (c) => {
|
|
14188
14963
|
const query = optsFromQuery(new URL(c.req.url), tierEnvVar, cfg.projectDir);
|
|
@@ -14224,11 +14999,12 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
|
|
|
14224
14999
|
mode: "progressive",
|
|
14225
15000
|
// Which members the picture is still waiting on, so a client can
|
|
14226
15001
|
// say so without inferring it from the attrs.
|
|
14227
|
-
pending:
|
|
15002
|
+
pending: estateMemberNames(cfg.projectDirs)
|
|
14228
15003
|
}
|
|
14229
15004
|
});
|
|
14230
15005
|
}
|
|
14231
|
-
const
|
|
15006
|
+
const traffic2 = trafficFor(new URL(c.req.url).searchParams, beholdConfig);
|
|
15007
|
+
const est = await composeEstateOverlay(cfg.projectDirs, { ...tierTargetOpts(query), detail, env, ...traffic2 ? { traffic: traffic2 } : {} }, reclassifyOverlay, { fresh: planWanted });
|
|
14232
15008
|
if (est.dropped.length === est.total) {
|
|
14233
15009
|
return c.json({ error: `no project in the estate could be graphed \u2014 ${est.dropped.map((d) => `${d.name}: ${d.reason}`).join("; ")}` }, 500);
|
|
14234
15010
|
}
|
|
@@ -14267,11 +15043,24 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
|
|
|
14267
15043
|
meta: { projectDir: cfg.projectDir, env, mode: "logical", estate: est.total, vocabulary, ...note3 ? { note: note3 } : {} }
|
|
14268
15044
|
});
|
|
14269
15045
|
}
|
|
14270
|
-
const
|
|
15046
|
+
const predicted = traffic2 ? await Promise.all(
|
|
15047
|
+
estateMembers(cfg.projectDirs).map(async (m) => ({ m, prediction: await predictMember(m, env, traffic2, predictDeps, planWanted) }))
|
|
15048
|
+
) : [];
|
|
15049
|
+
const paintNotes2 = [];
|
|
15050
|
+
for (const { m, prediction: prediction2 } of predicted) {
|
|
15051
|
+
if (prediction2.live) est.memberMeta[m.name] = paintLive(ir2, m.name, prediction2.live, paintNotes2);
|
|
15052
|
+
}
|
|
15053
|
+
const attached2 = attachBehaviour(
|
|
14271
15054
|
ir2,
|
|
14272
15055
|
estateMembers(cfg.projectDirs).map((m) => ({ name: m.name, dir: m.dir, meta: est.memberMeta[m.name] })),
|
|
14273
|
-
env
|
|
15056
|
+
env,
|
|
15057
|
+
...traffic2 ? [() => void 0] : []
|
|
14274
15058
|
);
|
|
15059
|
+
const behaviour2 = traffic2 ? attachBehaviourDelta(
|
|
15060
|
+
ir2,
|
|
15061
|
+
predicted.map(({ m, prediction: prediction2 }) => ({ name: m.name, liveMeta: est.memberMeta[m.name], prediction: prediction2 })),
|
|
15062
|
+
paintNotes2.length ? { ...attached2, diagnostics: [...attached2.diagnostics ?? [], ...paintNotes2] } : attached2
|
|
15063
|
+
) : attached2;
|
|
14275
15064
|
const collapsed = new URL(c.req.url).searchParams.get("collapse") === "1" && !runtime ? collapseBoxes(ir2) : void 0;
|
|
14276
15065
|
const expanded = ir2;
|
|
14277
15066
|
if (collapsed) ir2 = collapsed.ir;
|
|
@@ -14317,7 +15106,16 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
|
|
|
14317
15106
|
}
|
|
14318
15107
|
});
|
|
14319
15108
|
}
|
|
14320
|
-
const
|
|
15109
|
+
const traffic = trafficFor(new URL(c.req.url).searchParams, beholdConfig);
|
|
15110
|
+
const ownKind = memberKindOf(cfg.projectDir) ?? "chant";
|
|
15111
|
+
const opts = {
|
|
15112
|
+
...query,
|
|
15113
|
+
live: true,
|
|
15114
|
+
overlay: true,
|
|
15115
|
+
env,
|
|
15116
|
+
...logical || runtime ? { detail: 3 } : {},
|
|
15117
|
+
...traffic && memberTakesTraffic(cfg.projectDir, ownKind) ? { traffic } : {}
|
|
15118
|
+
};
|
|
14321
15119
|
const read = await graphIr(cfg.projectDir, opts);
|
|
14322
15120
|
const readMeta = read.meta;
|
|
14323
15121
|
let ir = joinCarvedSources(reclassifyOverlay(read), (await carveStatesFor(cfg.projectDir)).values(), cfg.projectDir).ir;
|
|
@@ -14358,7 +15156,11 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
|
|
|
14358
15156
|
compositeEdgesAttached = 0;
|
|
14359
15157
|
}
|
|
14360
15158
|
}
|
|
14361
|
-
const
|
|
15159
|
+
const prediction = traffic ? await predictMember({ dir: cfg.projectDir, kind: ownKind }, env, traffic, predictDeps) : void 0;
|
|
15160
|
+
const paintNotes = [];
|
|
15161
|
+
const liveMeta = prediction?.live ? paintLive(ir, void 0, prediction.live, paintNotes) : readMeta;
|
|
15162
|
+
const attached = attachBehaviour(ir, [{ dir: cfg.projectDir, meta: liveMeta }], env, ...traffic ? [() => void 0] : []);
|
|
15163
|
+
const behaviour = prediction ? attachBehaviourDelta(ir, [{ liveMeta, prediction }], paintNotes.length ? { ...attached, diagnostics: [...attached.diagnostics ?? [], ...paintNotes] } : attached) : attached;
|
|
14362
15164
|
const { svg } = renderGraph(ir, { boxes: "byContainer", radial: new URL(c.req.url).searchParams.get("radial") === "1" });
|
|
14363
15165
|
const zoom = runtime ? "runtime" : query.detail === 1 ? "composites" : query.detail === 3 ? "attributes" : "resources";
|
|
14364
15166
|
const tierNote = tierMismatchNote(ir, beholdConfig.tiers, query.tier);
|
|
@@ -14371,7 +15173,14 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
|
|
|
14371
15173
|
}
|
|
14372
15174
|
});
|
|
14373
15175
|
app.post("/api/refresh", async (c) => {
|
|
14374
|
-
const
|
|
15176
|
+
const url = new URL(c.req.url);
|
|
15177
|
+
if (url.searchParams.get("notify") === "1") {
|
|
15178
|
+
invalidateMember();
|
|
15179
|
+
invalidateOverlay();
|
|
15180
|
+
broadcaster.emit("changed", "");
|
|
15181
|
+
return c.json({ notified: true });
|
|
15182
|
+
}
|
|
15183
|
+
const env = optsFromQuery(url).env ?? cfg.env;
|
|
14375
15184
|
const result = await captureFrame(cfg.projectDir, env, frames, broadcaster);
|
|
14376
15185
|
if (!result) return c.json({ error: "refresh failed \u2014 see server log" }, 500);
|
|
14377
15186
|
if (env) reclassifyOverlay(result.ir);
|
|
@@ -14451,9 +15260,9 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
|
|
|
14451
15260
|
});
|
|
14452
15261
|
const choudoufuPlanPath = (plan) => {
|
|
14453
15262
|
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" };
|
|
14454
|
-
const abs =
|
|
15263
|
+
const abs = resolve16(cfg.projectDir, plan);
|
|
14455
15264
|
const members = (cfg.projectDirs ?? [cfg.projectDir]).filter((d) => memberKindOf(d) === "choudoufu");
|
|
14456
|
-
const inside = members.some((d) => abs.startsWith(
|
|
15265
|
+
const inside = members.some((d) => abs.startsWith(resolve16(d) + sep7));
|
|
14457
15266
|
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" };
|
|
14458
15267
|
return { path: abs };
|
|
14459
15268
|
};
|
|
@@ -14584,8 +15393,12 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
|
|
|
14584
15393
|
return c.json({ started: true, component, env });
|
|
14585
15394
|
});
|
|
14586
15395
|
const rel = relative8(process.cwd(), webRoot) || ".";
|
|
15396
|
+
app.use("/*", async (c, next) => {
|
|
15397
|
+
await next();
|
|
15398
|
+
if (!c.req.path.startsWith("/api/") && c.res.ok && !c.res.headers.has("cache-control")) c.res.headers.set("cache-control", "no-cache");
|
|
15399
|
+
});
|
|
14587
15400
|
app.use("/*", serveStatic({ root: rel }));
|
|
14588
|
-
app.get("/", serveStatic({ path:
|
|
15401
|
+
app.get("/", serveStatic({ path: join26(rel, "index.html") }));
|
|
14589
15402
|
return app;
|
|
14590
15403
|
}
|
|
14591
15404
|
async function bootLocalEmulators(dir, who) {
|
|
@@ -14614,7 +15427,7 @@ async function bootLocalEmulators(dir, who) {
|
|
|
14614
15427
|
async function startServer(cfg) {
|
|
14615
15428
|
if (cfg.local) cfg.emulators = await bootLocalEmulators(cfg.projectDir, "behold serve --local");
|
|
14616
15429
|
for (const dir of cfg.projectDirs ?? [cfg.projectDir]) {
|
|
14617
|
-
if (
|
|
15430
|
+
if (existsSync18(join26(dir, "chant.config.ts"))) addRecent(dir);
|
|
14618
15431
|
}
|
|
14619
15432
|
const broadcaster = new Broadcaster();
|
|
14620
15433
|
const frames = new FrameBuffer();
|
|
@@ -14623,6 +15436,7 @@ async function startServer(cfg) {
|
|
|
14623
15436
|
broadcaster,
|
|
14624
15437
|
onDone: (opEnv) => captureFrame(cfg.projectDir, opEnv ?? cfg.env, frames, broadcaster)
|
|
14625
15438
|
});
|
|
15439
|
+
if (!cfg.host && process.env.BEHOLD_HOST) cfg.host = process.env.BEHOLD_HOST;
|
|
14626
15440
|
const app = createApp(cfg, broadcaster, frames, runner);
|
|
14627
15441
|
const autoSync = cfg.autoSync ?? "off";
|
|
14628
15442
|
if (!cfg.carveReport) {
|
|
@@ -14646,7 +15460,7 @@ async function startServer(cfg) {
|
|
|
14646
15460
|
invalidateOverlay(memberDir);
|
|
14647
15461
|
onEstateChange(memberDir);
|
|
14648
15462
|
};
|
|
14649
|
-
const memberTag = (dir) => (cfg.projectDirs?.length ?? 0) > 1 ? ` [${
|
|
15463
|
+
const memberTag = (dir) => (cfg.projectDirs?.length ?? 0) > 1 ? ` [${basename3(dir)}]` : "";
|
|
14650
15464
|
const onPollDrift = (dir, movedLexicons) => {
|
|
14651
15465
|
broadcaster.emit("changed", dir);
|
|
14652
15466
|
if (autoSync === "off") return;
|
|
@@ -14656,7 +15470,7 @@ async function startServer(cfg) {
|
|
|
14656
15470
|
const suspended = autoSync === "pull-request" ? suspendedByRollback(await openRollbackBranches(dir, cfg.env), movedLexicons) : /* @__PURE__ */ new Set();
|
|
14657
15471
|
const { routed, local } = splitForgeRouted(discoverEstateOps(cfg.projectDirs ?? [cfg.projectDir]), executorDesignation);
|
|
14658
15472
|
for (const { op: o, designation: d } of routed) {
|
|
14659
|
-
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 ${
|
|
15473
|
+
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 ${basename3(o.dir)}/.behold.json \u2014 never applied from here`);
|
|
14660
15474
|
}
|
|
14661
15475
|
const { picks, declined } = pickAutoSyncOps(
|
|
14662
15476
|
autoSync,
|
|
@@ -14730,7 +15544,8 @@ async function startServer(cfg) {
|
|
|
14730
15544
|
};
|
|
14731
15545
|
process.on("SIGINT", shutdown);
|
|
14732
15546
|
process.on("SIGTERM", shutdown);
|
|
14733
|
-
const
|
|
15547
|
+
const hostname = cfg.host ?? process.env.BEHOLD_HOST ?? "127.0.0.1";
|
|
15548
|
+
const server = serve({ fetch: app.fetch, port: cfg.port, hostname }, (info) => {
|
|
14734
15549
|
if (carve) {
|
|
14735
15550
|
process.stdout.write(
|
|
14736
15551
|
`behold \u2192 http://localhost:${info.port}
|
|
@@ -14778,9 +15593,132 @@ async function startServer(cfg) {
|
|
|
14778
15593
|
// src/cli.ts
|
|
14779
15594
|
init_chant();
|
|
14780
15595
|
|
|
15596
|
+
// src/workspace-convert.ts
|
|
15597
|
+
init_chant();
|
|
15598
|
+
import { existsSync as existsSync19, mkdirSync as mkdirSync7, readFileSync as readFileSync20, renameSync as renameSync2, writeFileSync as writeFileSync6 } from "node:fs";
|
|
15599
|
+
import { basename as basename4, dirname as dirname8, join as join27, relative as relative9, resolve as resolve17, sep as sep8 } from "node:path";
|
|
15600
|
+
import { shortStackNames as shortStackNames2 } from "@intentius/pinhole";
|
|
15601
|
+
init_project();
|
|
15602
|
+
init_terraform_member();
|
|
15603
|
+
var TERRAFORM_LEXICON2 = "@intentius/chant-lexicon-terraform";
|
|
15604
|
+
function declarationName(raw) {
|
|
15605
|
+
const folded = raw.toLowerCase().replace(/[^a-z0-9-]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 40).replace(/-+$/, "");
|
|
15606
|
+
return folded || "member";
|
|
15607
|
+
}
|
|
15608
|
+
function unique(name, taken) {
|
|
15609
|
+
let out = name;
|
|
15610
|
+
for (let i = 2; taken.has(out); i++) out = `${name.slice(0, 40 - String(i).length - 1)}-${i}`;
|
|
15611
|
+
taken.add(out);
|
|
15612
|
+
return out;
|
|
15613
|
+
}
|
|
15614
|
+
var posix = (p) => p.split(sep8).join("/");
|
|
15615
|
+
function installedVersion(pkg, dir) {
|
|
15616
|
+
for (let at = resolve17(dir); ; at = dirname8(at)) {
|
|
15617
|
+
const manifest = join27(at, "node_modules", ...pkg.split("/"), "package.json");
|
|
15618
|
+
if (existsSync19(manifest)) {
|
|
15619
|
+
try {
|
|
15620
|
+
const v = JSON.parse(readFileSync20(manifest, "utf8")).version;
|
|
15621
|
+
return typeof v === "string" ? v : void 0;
|
|
15622
|
+
} catch {
|
|
15623
|
+
return void 0;
|
|
15624
|
+
}
|
|
15625
|
+
}
|
|
15626
|
+
if (dirname8(at) === at) return void 0;
|
|
15627
|
+
}
|
|
15628
|
+
}
|
|
15629
|
+
function planConversion(rootArg) {
|
|
15630
|
+
const root = resolve17(rootArg);
|
|
15631
|
+
const existing = DECLARATION_FILES.find((f) => existsSync19(join27(root, f)));
|
|
15632
|
+
if (existing) return { ok: false, error: `${root} already has ${existing}; there is nothing to convert.` };
|
|
15633
|
+
const shape = detectProjectShape(root);
|
|
15634
|
+
if (shape.kind !== "estate" || !shape.members?.length || shape.membersFrom === "itself") {
|
|
15635
|
+
return { ok: false, error: `${root} is not an estate root with a member list, so there is no member list to convert.` };
|
|
15636
|
+
}
|
|
15637
|
+
const abs = shape.members.map((m) => resolve17(root, m.dir));
|
|
15638
|
+
const shorts = shortStackNames2(abs);
|
|
15639
|
+
const taken = /* @__PURE__ */ new Set();
|
|
15640
|
+
const members = [];
|
|
15641
|
+
const moves = [];
|
|
15642
|
+
const notes = [];
|
|
15643
|
+
let needsLexicon = false;
|
|
15644
|
+
shape.members.forEach((m, i) => {
|
|
15645
|
+
const short = shorts[i];
|
|
15646
|
+
const dir = posix(relative9(root, abs[i])) || ".";
|
|
15647
|
+
if (m.kind === "terraform") {
|
|
15648
|
+
needsLexicon = true;
|
|
15649
|
+
const scan = discoverTerraformRoots(abs[i]);
|
|
15650
|
+
for (const r of scan.roots) {
|
|
15651
|
+
const name2 = unique(declarationName(r.name), taken);
|
|
15652
|
+
members.push({ name: name2, dir: r.dir === "." ? dir : posix(join27(dir, r.dir)), kind: "terraform" });
|
|
15653
|
+
moves.push({ from: `${short}/${r.name}/`, to: `${name2}/${name2}/` });
|
|
15654
|
+
}
|
|
15655
|
+
for (const s of scan.skipped) notes.push(`${posix(join27(dir, s.dir))} is not declared: ${s.why}.`);
|
|
15656
|
+
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.`);
|
|
15657
|
+
return;
|
|
15658
|
+
}
|
|
15659
|
+
if (m.kind === "choudoufu") needsLexicon = true;
|
|
15660
|
+
const name = unique(declarationName(short), taken);
|
|
15661
|
+
members.push({ name, dir, kind: m.kind });
|
|
15662
|
+
if (name !== short) {
|
|
15663
|
+
moves.push({ from: `${short}/`, to: `${name}/` });
|
|
15664
|
+
notes.push(`member ${short} is declared as ${name}: a declared name is lowercase letters, digits and hyphens.`);
|
|
15665
|
+
}
|
|
15666
|
+
});
|
|
15667
|
+
const pins = [];
|
|
15668
|
+
const chant = resolveChant(root);
|
|
15669
|
+
if (chant.source === "project" && chant.version) pins.push({ package: "@intentius/chant", version: chant.version });
|
|
15670
|
+
if (needsLexicon) {
|
|
15671
|
+
const version = installedVersion(TERRAFORM_LEXICON2, root);
|
|
15672
|
+
if (version) pins.push({ package: TERRAFORM_LEXICON2, version });
|
|
15673
|
+
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.`);
|
|
15674
|
+
}
|
|
15675
|
+
const declaration = {
|
|
15676
|
+
name: declarationName(basename4(root)),
|
|
15677
|
+
schema: 1,
|
|
15678
|
+
...pins.length ? { pins } : {},
|
|
15679
|
+
members
|
|
15680
|
+
};
|
|
15681
|
+
const firstLayout = layoutPath(abs[0]);
|
|
15682
|
+
moves.sort((a, b) => b.from.length - a.from.length);
|
|
15683
|
+
return { ok: true, plan: { root, from: shape.membersFrom === "behold-config" ? "behold-config" : "workspaces", declaration, moves, ...existsSync19(firstLayout) ? { layoutFrom: firstLayout } : {}, notes } };
|
|
15684
|
+
}
|
|
15685
|
+
function moveLayoutIds(file, moves) {
|
|
15686
|
+
const lenses = {};
|
|
15687
|
+
for (const [lens, deltas] of Object.entries(file.lenses)) {
|
|
15688
|
+
const out = {};
|
|
15689
|
+
for (const [id, d] of Object.entries(deltas)) {
|
|
15690
|
+
const move = moves.find((m) => id.startsWith(m.from));
|
|
15691
|
+
out[move ? move.to + id.slice(move.from.length) : id] = d;
|
|
15692
|
+
}
|
|
15693
|
+
lenses[lens] = out;
|
|
15694
|
+
}
|
|
15695
|
+
return { version: 1, lenses };
|
|
15696
|
+
}
|
|
15697
|
+
function applyConversion(plan) {
|
|
15698
|
+
const declarationFile = join27(plan.root, DECLARATION_FILES[0]);
|
|
15699
|
+
if (existsSync19(declarationFile)) throw new Error(`${declarationFile} appeared while converting; nothing was written`);
|
|
15700
|
+
writeFileSync6(declarationFile, `${JSON.stringify(plan.declaration, null, 2)}
|
|
15701
|
+
`);
|
|
15702
|
+
let layout;
|
|
15703
|
+
const target = layoutPath(plan.root);
|
|
15704
|
+
if (!plan.layoutFrom) layout = { skipped: "no saved layout to move" };
|
|
15705
|
+
else if (resolve17(plan.layoutFrom) === resolve17(target)) layout = { skipped: "the saved layout is already in the root" };
|
|
15706
|
+
else if (existsSync19(target)) layout = { skipped: `${target} already exists; the layout in ${plan.layoutFrom} was left alone` };
|
|
15707
|
+
else {
|
|
15708
|
+
const moved = moveLayoutIds(readLayoutFile(dirname8(dirname8(plan.layoutFrom))), plan.moves);
|
|
15709
|
+
mkdirSync7(join27(plan.root, LAYOUT_DIR), { recursive: true });
|
|
15710
|
+
const tmp = `${target}.tmp`;
|
|
15711
|
+
writeFileSync6(tmp, `${JSON.stringify(moved, null, 2)}
|
|
15712
|
+
`);
|
|
15713
|
+
renameSync2(tmp, target);
|
|
15714
|
+
layout = { wrote: target, ids: Object.values(moved.lenses).reduce((n, l) => n + Object.keys(l).length, 0) };
|
|
15715
|
+
}
|
|
15716
|
+
return { declaration: declarationFile, layout };
|
|
15717
|
+
}
|
|
15718
|
+
|
|
14781
15719
|
// src/export.ts
|
|
14782
|
-
import { mkdirSync as
|
|
14783
|
-
import { join as
|
|
15720
|
+
import { mkdirSync as mkdirSync8, writeFileSync as writeFileSync7, copyFileSync, readFileSync as readFileSync21, readdirSync as readdirSync10, existsSync as existsSync20 } from "node:fs";
|
|
15721
|
+
import { join as join28, dirname as dirname9, basename as basename5 } from "node:path";
|
|
14784
15722
|
import { fileURLToPath as fileURLToPath5 } from "node:url";
|
|
14785
15723
|
var LENS_PARAMS = ["collapse", "components", "detail", "env", "logical", "ops", "radial", "stacks", "tier"];
|
|
14786
15724
|
function canonicalKey(path, params) {
|
|
@@ -14829,31 +15767,31 @@ function captureKeys(axes) {
|
|
|
14829
15767
|
return [...keys];
|
|
14830
15768
|
}
|
|
14831
15769
|
function webDir() {
|
|
14832
|
-
return
|
|
15770
|
+
return join28(dirname9(fileURLToPath5(import.meta.url)), "..", "web");
|
|
14833
15771
|
}
|
|
14834
15772
|
function pkgRoot2() {
|
|
14835
|
-
return
|
|
15773
|
+
return join28(dirname9(fileURLToPath5(import.meta.url)), "..");
|
|
14836
15774
|
}
|
|
14837
15775
|
var NOTICE_FILES = ["LICENSE", "THIRD_PARTY.md"];
|
|
14838
15776
|
function copyNotices(outDir, root = pkgRoot2()) {
|
|
14839
15777
|
const copied = [];
|
|
14840
15778
|
for (const f of NOTICE_FILES) {
|
|
14841
|
-
if (!
|
|
14842
|
-
copyFileSync(
|
|
15779
|
+
if (!existsSync20(join28(root, f))) continue;
|
|
15780
|
+
copyFileSync(join28(root, f), join28(outDir, f));
|
|
14843
15781
|
copied.push(f);
|
|
14844
15782
|
}
|
|
14845
|
-
const lic =
|
|
14846
|
-
if (
|
|
14847
|
-
|
|
15783
|
+
const lic = join28(root, "licenses");
|
|
15784
|
+
if (existsSync20(lic)) {
|
|
15785
|
+
mkdirSync8(join28(outDir, "licenses"), { recursive: true });
|
|
14848
15786
|
for (const f of readdirSync10(lic)) {
|
|
14849
|
-
copyFileSync(
|
|
15787
|
+
copyFileSync(join28(lic, f), join28(outDir, "licenses", f));
|
|
14850
15788
|
copied.push(`licenses/${f}`);
|
|
14851
15789
|
}
|
|
14852
15790
|
}
|
|
14853
15791
|
return copied;
|
|
14854
15792
|
}
|
|
14855
15793
|
function workerName(project, override) {
|
|
14856
|
-
const raw = override ?? `behold-${
|
|
15794
|
+
const raw = override ?? `behold-${basename5(project)}`;
|
|
14857
15795
|
const name = raw.toLowerCase().replace(/[^a-z0-9-]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 63);
|
|
14858
15796
|
return name || "behold-export";
|
|
14859
15797
|
}
|
|
@@ -14861,8 +15799,8 @@ async function runExport(cfg, outDir, opts = {}) {
|
|
|
14861
15799
|
const app = createApp({ ...cfg, layoutWrites: false });
|
|
14862
15800
|
const proj = await (await app.request("/api/project")).json();
|
|
14863
15801
|
const axes = { environments: proj.environments ?? [], tiers: proj.tiers ?? [], ...proj.ops ? { ops: proj.ops } : {} };
|
|
14864
|
-
const snapDir =
|
|
14865
|
-
|
|
15802
|
+
const snapDir = join28(outDir, "snapshots");
|
|
15803
|
+
mkdirSync8(snapDir, { recursive: true });
|
|
14866
15804
|
const keyToFile = {};
|
|
14867
15805
|
let ok = 0;
|
|
14868
15806
|
let failed = 0;
|
|
@@ -14870,7 +15808,7 @@ async function runExport(cfg, outDir, opts = {}) {
|
|
|
14870
15808
|
const res = await app.request(`${key}${key.includes("?") ? "&" : "?"}layout=1`);
|
|
14871
15809
|
const body = await res.text();
|
|
14872
15810
|
const file = slug3(key);
|
|
14873
|
-
|
|
15811
|
+
writeFileSync7(join28(snapDir, file), body);
|
|
14874
15812
|
keyToFile[key] = `snapshots/${file}`;
|
|
14875
15813
|
if (res.ok) ok++;
|
|
14876
15814
|
else failed++;
|
|
@@ -14882,22 +15820,22 @@ async function runExport(cfg, outDir, opts = {}) {
|
|
|
14882
15820
|
axes,
|
|
14883
15821
|
keyToFile
|
|
14884
15822
|
};
|
|
14885
|
-
|
|
14886
|
-
const html =
|
|
15823
|
+
writeFileSync7(join28(outDir, "manifest.json"), JSON.stringify(manifest, null, 2));
|
|
15824
|
+
const html = readFileSync21(join28(webDir(), "index.html"), "utf8").replace(
|
|
14887
15825
|
/<\/head>/i,
|
|
14888
15826
|
` <script>window.__BEHOLD_STATIC__ = true;</script>
|
|
14889
15827
|
</head>`
|
|
14890
15828
|
);
|
|
14891
|
-
|
|
15829
|
+
writeFileSync7(join28(outDir, "index.html"), html);
|
|
14892
15830
|
for (const f of readdirSync10(webDir())) {
|
|
14893
15831
|
if (f === "index.html") continue;
|
|
14894
|
-
copyFileSync(
|
|
15832
|
+
copyFileSync(join28(webDir(), f), join28(outDir, f));
|
|
14895
15833
|
}
|
|
14896
|
-
|
|
15834
|
+
writeFileSync7(join28(outDir, "README.md"), BUNDLE_README);
|
|
14897
15835
|
copyNotices(outDir);
|
|
14898
15836
|
const name = workerName(cfg.projectDir, opts.name);
|
|
14899
|
-
|
|
14900
|
-
|
|
15837
|
+
writeFileSync7(
|
|
15838
|
+
join28(outDir, "wrangler.jsonc"),
|
|
14901
15839
|
JSON.stringify(
|
|
14902
15840
|
{ $schema: "node_modules/wrangler/config-schema.json", name, compatibility_date: "2025-06-01", assets: { directory: "." } },
|
|
14903
15841
|
null,
|
|
@@ -14949,15 +15887,16 @@ init_project();
|
|
|
14949
15887
|
init_member_kind();
|
|
14950
15888
|
init_choudoufu_member();
|
|
14951
15889
|
init_carve_lens();
|
|
15890
|
+
init_terraform_member();
|
|
14952
15891
|
var USAGE = `behold \u2014 a live control plane on chant (read-only core)
|
|
14953
15892
|
|
|
14954
15893
|
Usage:
|
|
14955
15894
|
behold demo [name] [target-dir] [--port <n>] [--list]
|
|
14956
15895
|
behold demo carve [--live] [--port <n>]
|
|
14957
|
-
behold doctor [project-dir] [--json]
|
|
15896
|
+
behold doctor [project-dir] [--json] [--fix]
|
|
14958
15897
|
behold preview [project-dir] [--port <n>] [--emulator]
|
|
14959
15898
|
behold export [project-dir] [--out <dir>] [--env <name>] [--name <worker>] [--emulator]
|
|
14960
|
-
behold serve <project-dir\u2026> [--port <n>] [--env <name>] [--poll <secs>] [--local]
|
|
15899
|
+
behold serve <project-dir\u2026> [--port <n>] [--host <addr>] [--allow-host <name,\u2026>] [--env <name>] [--poll <secs>] [--local]
|
|
14961
15900
|
behold carve <report.json> [--port <n>]
|
|
14962
15901
|
|
|
14963
15902
|
carve Render a chant Terraform peelability report \u2014 the JSON from
|
|
@@ -15016,6 +15955,12 @@ Usage:
|
|
|
15016
15955
|
|
|
15017
15956
|
Options:
|
|
15018
15957
|
--port <n> Port (default 4600). preview/serve/carve.
|
|
15958
|
+
--host <addr> serve only: the address to bind (default 127.0.0.1, or
|
|
15959
|
+
BEHOLD_HOST). behold runs writes with your credentials,
|
|
15960
|
+
so it answers this machine unless you say otherwise.
|
|
15961
|
+
--allow-host <n,\u2026> serve only: names besides loopback behold answers to, and
|
|
15962
|
+
whose pages may write (BEHOLD_ALLOWED_HOSTS too). For a
|
|
15963
|
+
proxy that frames behold, or --host 0.0.0.0.
|
|
15019
15964
|
--env <name> Environment name \u2014 turns on the live drift overlay.
|
|
15020
15965
|
export/serve.
|
|
15021
15966
|
--poll <secs> Re-query live drift every <secs> and push updates (needs --env).
|
|
@@ -15048,6 +15993,10 @@ Options:
|
|
|
15048
15993
|
with chant's generic \`chant emulator up\`.
|
|
15049
15994
|
--json doctor only: the report as JSON (stable keys) instead of
|
|
15050
15995
|
the console lines \u2014 the AGENTS.md audience.
|
|
15996
|
+
--fix doctor only: write chant.workspace.json for an estate
|
|
15997
|
+
root that lists its members in .behold.json, move its
|
|
15998
|
+
saved layout to the root, and run chant workspace check.
|
|
15999
|
+
The one doctor that writes (#464).
|
|
15051
16000
|
--out <dir> export only: output directory (default ./behold-export).
|
|
15052
16001
|
--name <worker> export only: Cloudflare Worker name in the generated
|
|
15053
16002
|
wrangler.jsonc.
|
|
@@ -15096,9 +16045,13 @@ ${USAGE}`);
|
|
|
15096
16045
|
let pollSecs;
|
|
15097
16046
|
let autoSync = "off";
|
|
15098
16047
|
let local = false;
|
|
16048
|
+
let host;
|
|
16049
|
+
const allowHosts = [];
|
|
15099
16050
|
for (let i = 0; i < rest.length; i++) {
|
|
15100
16051
|
const a = rest[i];
|
|
15101
16052
|
if (a === "--port") port = Number(rest[++i]);
|
|
16053
|
+
else if (a === "--host") host = rest[++i];
|
|
16054
|
+
else if (a === "--allow-host") allowHosts.push(...(rest[++i] ?? "").split(","));
|
|
15102
16055
|
else if (a === "--env") env = rest[++i];
|
|
15103
16056
|
else if (a === "--poll") pollSecs = Number(rest[++i]);
|
|
15104
16057
|
else if (a === "--local") local = true;
|
|
@@ -15139,7 +16092,24 @@ ${USAGE}`);
|
|
|
15139
16092
|
process.stderr.write("behold serve: --auto-sync needs --env and --poll (it acts on polled drift)\n");
|
|
15140
16093
|
process.exit(2);
|
|
15141
16094
|
}
|
|
15142
|
-
const dirs = projectDirs.map((d) =>
|
|
16095
|
+
const dirs = projectDirs.map((d) => resolve18(d));
|
|
16096
|
+
const workspace = dirs.length === 1 && hasDeclaration(dirs[0]) ? await declaredWorkspace(dirs[0]) : void 0;
|
|
16097
|
+
if (workspace) {
|
|
16098
|
+
const drawn = drawnMembers(workspace).map((m) => m.abs);
|
|
16099
|
+
await startServer({
|
|
16100
|
+
projectDir: drawn[0] ?? workspace.root,
|
|
16101
|
+
projectDirs: drawn,
|
|
16102
|
+
workspace,
|
|
16103
|
+
port,
|
|
16104
|
+
...env ? { env } : {},
|
|
16105
|
+
...pollSecs !== void 0 ? { pollSecs } : {},
|
|
16106
|
+
...autoSync !== "off" ? { autoSync } : {},
|
|
16107
|
+
...local ? { local: true } : {},
|
|
16108
|
+
...host ? { host } : {},
|
|
16109
|
+
...allowHosts.length ? { allowedHosts: allowHosts } : {}
|
|
16110
|
+
});
|
|
16111
|
+
return;
|
|
16112
|
+
}
|
|
15143
16113
|
for (const d of dirs) warnIfNotChantProject(d);
|
|
15144
16114
|
await startServer({
|
|
15145
16115
|
projectDir: dirs[0],
|
|
@@ -15153,9 +16123,33 @@ ${USAGE}`);
|
|
|
15153
16123
|
...env ? { env } : {},
|
|
15154
16124
|
...pollSecs !== void 0 ? { pollSecs } : {},
|
|
15155
16125
|
...autoSync !== "off" ? { autoSync } : {},
|
|
15156
|
-
...local ? { local: true } : {}
|
|
16126
|
+
...local ? { local: true } : {},
|
|
16127
|
+
...host ? { host } : {},
|
|
16128
|
+
...allowHosts.length ? { allowedHosts: allowHosts } : {}
|
|
15157
16129
|
});
|
|
15158
16130
|
}
|
|
16131
|
+
async function declaredWorkspace(root) {
|
|
16132
|
+
const read = await readWorkspace(root);
|
|
16133
|
+
if (!read.ok) {
|
|
16134
|
+
process.stderr.write(`behold serve: ${read.refusal.error}
|
|
16135
|
+
${read.refusal.remedy}
|
|
16136
|
+
`);
|
|
16137
|
+
process.exit(1);
|
|
16138
|
+
}
|
|
16139
|
+
const ws = read.workspace;
|
|
16140
|
+
const drawn = drawnMembers(ws);
|
|
16141
|
+
process.stdout.write(`behold: serving chant workspace ${ws.name} (${ws.file}), ${drawn.length} of ${ws.members.length} members drawn
|
|
16142
|
+
`);
|
|
16143
|
+
for (const m of unreadableMembers(ws)) process.stdout.write(` ${m.name}: unreadable, ${m.reason.code}: ${m.reason.message}
|
|
16144
|
+
`);
|
|
16145
|
+
for (const m of undrawnMembers(ws)) process.stdout.write(` ${m.name}: kind ${m.kind}, listed and not drawn${m.because ? ` (${m.because})` : ""}
|
|
16146
|
+
`);
|
|
16147
|
+
if (loadBeholdConfig(root).members) {
|
|
16148
|
+
process.stderr.write(`behold: warning \u2014 ${root}/.behold.json lists members, and chant.workspace.json does too; the declaration wins and .behold.json's members are ignored.
|
|
16149
|
+
`);
|
|
16150
|
+
}
|
|
16151
|
+
return ws;
|
|
16152
|
+
}
|
|
15159
16153
|
function warnIfNotChantProject(dir) {
|
|
15160
16154
|
const shape = detectProjectShape(dir);
|
|
15161
16155
|
if (shape.kind === "project") return;
|
|
@@ -15163,7 +16157,7 @@ function warnIfNotChantProject(dir) {
|
|
|
15163
16157
|
if (shape.kind === "estate") {
|
|
15164
16158
|
process.stderr.write(
|
|
15165
16159
|
`behold: warning \u2014 ${dir} is an estate root, not a chant project itself.
|
|
15166
|
-
Serve its members composed: behold serve ${shape.members.map((m) =>
|
|
16160
|
+
Serve its members composed: behold serve ${shape.members.map((m) => join29(dir, m.dir)).join(" ")}
|
|
15167
16161
|
`
|
|
15168
16162
|
);
|
|
15169
16163
|
return;
|
|
@@ -15198,8 +16192,8 @@ async function runCarve(rest) {
|
|
|
15198
16192
|
);
|
|
15199
16193
|
process.exit(2);
|
|
15200
16194
|
}
|
|
15201
|
-
const reportPath =
|
|
15202
|
-
const parsed = readCarveReport(reportPath, (p) =>
|
|
16195
|
+
const reportPath = resolve18(fileArg);
|
|
16196
|
+
const parsed = readCarveReport(reportPath, (p) => readFileSync22(p, "utf8"));
|
|
15203
16197
|
if (!parsed.ok) {
|
|
15204
16198
|
process.stderr.write(`behold carve: ${parsed.refusal.error}
|
|
15205
16199
|
${parsed.refusal.remedy}
|
|
@@ -15210,13 +16204,15 @@ async function runCarve(rest) {
|
|
|
15210
16204
|
`behold carve \u2014 ${parsed.report.resources.length} resource(s)/module(s) ranked${parsed.report.from ? ` from ${parsed.report.from}` : ""}
|
|
15211
16205
|
`
|
|
15212
16206
|
);
|
|
15213
|
-
await startServer({ projectDir:
|
|
16207
|
+
await startServer({ projectDir: dirname10(reportPath), carveReport: reportPath, port });
|
|
15214
16208
|
}
|
|
15215
16209
|
async function runDoctor(rest) {
|
|
15216
16210
|
let json = false;
|
|
16211
|
+
let fix = false;
|
|
15217
16212
|
let dirArg;
|
|
15218
16213
|
for (const a of rest) {
|
|
15219
16214
|
if (a === "--json") json = true;
|
|
16215
|
+
else if (a === "--fix") fix = true;
|
|
15220
16216
|
else if (a === "-h" || a === "--help") return void process.stdout.write(USAGE);
|
|
15221
16217
|
else if (!a.startsWith("-")) dirArg = a;
|
|
15222
16218
|
else {
|
|
@@ -15226,17 +16222,44 @@ async function runDoctor(rest) {
|
|
|
15226
16222
|
}
|
|
15227
16223
|
}
|
|
15228
16224
|
const dir = dirArg ?? ".";
|
|
15229
|
-
if (!
|
|
15230
|
-
process.stderr.write(`behold doctor: no such directory: ${
|
|
16225
|
+
if (!existsSync21(resolve18(dir))) {
|
|
16226
|
+
process.stderr.write(`behold doctor: no such directory: ${resolve18(dir)}
|
|
15231
16227
|
`);
|
|
15232
16228
|
process.exit(2);
|
|
15233
16229
|
}
|
|
16230
|
+
if (fix) return runDoctorFix(dir);
|
|
15234
16231
|
const report = await diagnose(dir);
|
|
15235
16232
|
process.stdout.write(json ? JSON.stringify(report, null, 2) + "\n" : formatReport(report));
|
|
15236
16233
|
if (!report.ok) process.exitCode = 1;
|
|
15237
16234
|
}
|
|
16235
|
+
async function runDoctorFix(dir) {
|
|
16236
|
+
const planned = planConversion(dir);
|
|
16237
|
+
if (!planned.ok) {
|
|
16238
|
+
process.stderr.write(`behold doctor --fix: ${planned.error}
|
|
16239
|
+
`);
|
|
16240
|
+
process.exitCode = 1;
|
|
16241
|
+
return;
|
|
16242
|
+
}
|
|
16243
|
+
const { plan } = planned;
|
|
16244
|
+
const wrote = applyConversion(plan);
|
|
16245
|
+
process.stdout.write(`wrote ${wrote.declaration} (${plan.declaration.members.length} members)
|
|
16246
|
+
`);
|
|
16247
|
+
if ("wrote" in wrote.layout) process.stdout.write(`moved the saved layout to ${wrote.layout.wrote} (${wrote.layout.ids} node ids)
|
|
16248
|
+
`);
|
|
16249
|
+
else process.stdout.write(`layout: ${wrote.layout.skipped}
|
|
16250
|
+
`);
|
|
16251
|
+
for (const n of plan.notes) process.stdout.write(`note: ${n}
|
|
16252
|
+
`);
|
|
16253
|
+
if (plan.from === "behold-config") process.stdout.write(`.behold.json's members are ignored from now on, since the declaration lists them; you can delete them.
|
|
16254
|
+
`);
|
|
16255
|
+
const check = await runChantRaw(["workspace", "check", "--format", "json"], plan.root);
|
|
16256
|
+
process.stdout.write(check.code === 0 ? "chant workspace check: passed\n" : `chant workspace check failed (exit ${check.code}):
|
|
16257
|
+
${check.stdout || check.stderr}
|
|
16258
|
+
`);
|
|
16259
|
+
if (check.code !== 0) process.exitCode = 1;
|
|
16260
|
+
}
|
|
15238
16261
|
async function runDemo(rest) {
|
|
15239
|
-
const pkgRoot3 =
|
|
16262
|
+
const pkgRoot3 = join29(dirname10(fileURLToPath6(import.meta.url)), "..");
|
|
15240
16263
|
const registry2 = loadDemoRegistry(pkgRoot3);
|
|
15241
16264
|
let port = 4600;
|
|
15242
16265
|
let live2 = false;
|
|
@@ -15299,7 +16322,7 @@ async function runDemo(rest) {
|
|
|
15299
16322
|
`);
|
|
15300
16323
|
process.exit(2);
|
|
15301
16324
|
}
|
|
15302
|
-
const target = dirArg ?
|
|
16325
|
+
const target = dirArg ? resolve18(dirArg) : demoTargetDir(entry);
|
|
15303
16326
|
const loaded = await loadDemo(entry, { pkgRoot: pkgRoot3, target, log: (line) => process.stdout.write(line + "\n") });
|
|
15304
16327
|
if (!loaded.ok) {
|
|
15305
16328
|
process.stderr.write(`behold demo ${entry.name}: ${loaded.error}
|
|
@@ -15331,7 +16354,7 @@ function spawnStep(cmd, args, cwd) {
|
|
|
15331
16354
|
});
|
|
15332
16355
|
}
|
|
15333
16356
|
async function serveCarveDemo(target, carve, port, live2 = false) {
|
|
15334
|
-
const at = (rel) =>
|
|
16357
|
+
const at = (rel) => resolve18(target, rel);
|
|
15335
16358
|
const project = at(carve.project);
|
|
15336
16359
|
const from = at(carve.from);
|
|
15337
16360
|
const state = carve.state ? at(carve.state) : void 0;
|
|
@@ -15371,7 +16394,7 @@ async function serveCarveDemo(target, carve, port, live2 = false) {
|
|
|
15371
16394
|
};
|
|
15372
16395
|
process.stdout.write("behold demo carve --live \u2192 the tfstate is real now; the advisor reads what terraform wrote.\n");
|
|
15373
16396
|
}
|
|
15374
|
-
if (
|
|
16397
|
+
if (existsSync21(join29(project, "package.json")) && !existsSync21(join29(project, "node_modules"))) {
|
|
15375
16398
|
process.stdout.write(`behold demo carve \u2192 npm install in ${carve.project}/ (the chant this walkthrough shells)\u2026
|
|
15376
16399
|
`);
|
|
15377
16400
|
const code = await spawnStep("npm", ["install"], project);
|
|
@@ -15382,10 +16405,11 @@ async function serveCarveDemo(target, carve, port, live2 = false) {
|
|
|
15382
16405
|
}
|
|
15383
16406
|
}
|
|
15384
16407
|
let degraded;
|
|
15385
|
-
if (!
|
|
15386
|
-
process.stdout.write(
|
|
15387
|
-
|
|
15388
|
-
|
|
16408
|
+
if (!existsSync21(join29(target, "node_modules", ...HCL_PARSER_PKG.split("/")))) {
|
|
16409
|
+
process.stdout.write(`behold demo carve \u2192 npm install ${HCL_PARSER_PKG} (chant's HCL parser, ~2MB, once)\u2026
|
|
16410
|
+
`);
|
|
16411
|
+
const code = await spawnStep("npm", ["install", "--no-save", "--no-package-lock", HCL_PARSER_PKG], target);
|
|
16412
|
+
if (code !== 0) degraded = `couldn't install ${HCL_PARSER_PKG} (chant's HCL parser) \u2014 no network?`;
|
|
15389
16413
|
}
|
|
15390
16414
|
const report = at("carve-report.json");
|
|
15391
16415
|
if (!degraded) {
|
|
@@ -15398,7 +16422,7 @@ async function serveCarveDemo(target, carve, port, live2 = false) {
|
|
|
15398
16422
|
);
|
|
15399
16423
|
if (code !== 0) degraded = `chant carve advise exited ${code}`;
|
|
15400
16424
|
}
|
|
15401
|
-
const serving = !degraded &&
|
|
16425
|
+
const serving = !degraded && existsSync21(report) ? report : committed;
|
|
15402
16426
|
if (degraded) {
|
|
15403
16427
|
process.stderr.write(
|
|
15404
16428
|
`behold demo carve: ${degraded}
|
|
@@ -15406,7 +16430,7 @@ async function serveCarveDemo(target, carve, port, live2 = false) {
|
|
|
15406
16430
|
`
|
|
15407
16431
|
);
|
|
15408
16432
|
}
|
|
15409
|
-
if (!
|
|
16433
|
+
if (!existsSync21(serving)) {
|
|
15410
16434
|
process.stderr.write(`behold demo carve: no carve report at ${serving}
|
|
15411
16435
|
`);
|
|
15412
16436
|
process.exit(2);
|
|
@@ -15451,8 +16475,8 @@ async function runPreview(rest) {
|
|
|
15451
16475
|
process.stderr.write("behold preview: --port must be a number\n");
|
|
15452
16476
|
process.exit(2);
|
|
15453
16477
|
}
|
|
15454
|
-
const projectDir =
|
|
15455
|
-
if (!
|
|
16478
|
+
const projectDir = resolve18(dirArg ?? process.cwd());
|
|
16479
|
+
if (!existsSync21(projectDir)) {
|
|
15456
16480
|
process.stderr.write(`behold preview: project not found at ${projectDir}
|
|
15457
16481
|
`);
|
|
15458
16482
|
process.exit(2);
|
|
@@ -15472,26 +16496,26 @@ async function runPreview(rest) {
|
|
|
15472
16496
|
await startServer({ projectDir, port, env: "local", previewMode: true });
|
|
15473
16497
|
}
|
|
15474
16498
|
async function runExportCmd(rest) {
|
|
15475
|
-
let outDir =
|
|
16499
|
+
let outDir = resolve18("behold-export");
|
|
15476
16500
|
let env;
|
|
15477
16501
|
let name;
|
|
15478
16502
|
let dirArg;
|
|
15479
16503
|
let emulator = false;
|
|
15480
16504
|
for (let i = 0; i < rest.length; i++) {
|
|
15481
16505
|
const a = rest[i];
|
|
15482
|
-
if (a === "--out") outDir =
|
|
16506
|
+
if (a === "--out") outDir = resolve18(rest[++i]);
|
|
15483
16507
|
else if (a === "--env") env = rest[++i];
|
|
15484
16508
|
else if (a === "--name") name = rest[++i];
|
|
15485
16509
|
else if (a === "--emulator") emulator = true;
|
|
15486
16510
|
else if (a === "-h" || a === "--help") return void process.stdout.write(USAGE);
|
|
15487
16511
|
else if (!a.startsWith("-")) dirArg = a;
|
|
15488
16512
|
}
|
|
15489
|
-
const projectDir =
|
|
16513
|
+
const projectDir = resolve18(dirArg ?? process.cwd());
|
|
15490
16514
|
if (emulator) {
|
|
15491
16515
|
injectEmulatorEnv(env);
|
|
15492
16516
|
env ??= "local";
|
|
15493
16517
|
}
|
|
15494
|
-
if (!
|
|
16518
|
+
if (!existsSync21(projectDir)) {
|
|
15495
16519
|
process.stderr.write(`behold export: project not found at ${projectDir}
|
|
15496
16520
|
`);
|
|
15497
16521
|
process.exit(2);
|