@intentius/behold 0.19.0 → 0.20.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 +5 -1
- package/demos.json +3 -2
- package/dist/cli.js +1106 -298
- 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 +54 -8
- package/web/behaviour-scale.js +32 -0
- package/web/behaviour-scale.test.js +39 -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 resolve18;
|
|
122
122
|
let reject;
|
|
123
123
|
const promise = new Promise((yes, no) => {
|
|
124
|
-
|
|
124
|
+
resolve18 = yes;
|
|
125
125
|
reject = no;
|
|
126
126
|
});
|
|
127
127
|
job = {
|
|
128
128
|
key,
|
|
129
129
|
run: run3,
|
|
130
130
|
promise,
|
|
131
|
-
resolve:
|
|
131
|
+
resolve: resolve18,
|
|
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((resolve18, 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 resolve18(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 resolve18(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 resolve18(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 resolve18({ 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((resolve19) => {
|
|
1172
|
+
const finish = () => resolve19({ 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((resolve19) => {
|
|
1200
|
+
const timer = setTimeout(resolve19, reopenDelayMs);
|
|
1201
1201
|
controller.signal.addEventListener("abort", () => {
|
|
1202
1202
|
clearTimeout(timer);
|
|
1203
|
-
|
|
1203
|
+
resolve19(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: resolve18,
|
|
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 resolve17, 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 resolve15, 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((resolve18) => {
|
|
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
|
+
resolve18({ 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", () => resolve18({ code: 127, out }));
|
|
4701
|
+
proc.on("close", (code) => resolve18({ 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((resolve18) => {
|
|
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
|
+
resolve18({ 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", () => resolve18({ code: 127, out }));
|
|
11517
|
+
proc.on("close", (code) => resolve18({ 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
|
}
|
|
@@ -12079,8 +12620,8 @@ init_chant();
|
|
|
12079
12620
|
init_member_kind();
|
|
12080
12621
|
init_choudoufu_member();
|
|
12081
12622
|
init_project();
|
|
12623
|
+
import { relative as relative6, resolve as resolve13 } from "node:path";
|
|
12082
12624
|
init_terraform_member();
|
|
12083
|
-
import { relative as relative6, resolve as resolve11 } from "node:path";
|
|
12084
12625
|
init_targets();
|
|
12085
12626
|
var list2 = (xs) => xs.join(", ");
|
|
12086
12627
|
var EMPTY_KUBECONFIG = { contexts: /* @__PURE__ */ new Map(), servers: /* @__PURE__ */ new Map() };
|
|
@@ -12262,11 +12803,11 @@ async function choudoufuCheck(root, members, probe2) {
|
|
|
12262
12803
|
};
|
|
12263
12804
|
}
|
|
12264
12805
|
if (!choudoufuMeetsFloor(v)) {
|
|
12265
|
-
const
|
|
12806
|
+
const said4 = v.forkField ? `choudoufu ${v.version}` : `a choudoufu whose \`version -json\` carries no \`choudoufu_version\` (v0.15.0 or older)`;
|
|
12266
12807
|
return {
|
|
12267
12808
|
name: "choudoufu",
|
|
12268
12809
|
status: "fail",
|
|
12269
|
-
detail: `${
|
|
12810
|
+
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
12811
|
fix: `Upgrade choudoufu to ${CHOUDOUFU_FLOOR} or newer (a build from main after 2026-09-08 also carries them).`
|
|
12271
12812
|
};
|
|
12272
12813
|
}
|
|
@@ -12308,10 +12849,41 @@ function terraformCheck(members, probe2) {
|
|
|
12308
12849
|
const reader = `${TERRAFORM_LEXICON_PKG} ${state.lexicon.version || "dev"} (${HCL_PARSER_PKG} ${state.parser.version || "dev"})`;
|
|
12309
12850
|
return { name: "terraform", status: "pass", detail: `${reader}; ${perMember}` };
|
|
12310
12851
|
}
|
|
12852
|
+
function workspaceShape(ws) {
|
|
12853
|
+
return { kind: "estate", members: drawnMembers(ws).map((m) => ({ dir: m.dir, kind: m.kind })) };
|
|
12854
|
+
}
|
|
12855
|
+
function workspaceCheck(ws, beholdMembers) {
|
|
12856
|
+
const drawn = drawnMembers(ws);
|
|
12857
|
+
const unreadable = unreadableMembers(ws);
|
|
12858
|
+
const undrawn = undrawnMembers(ws);
|
|
12859
|
+
const parts = [
|
|
12860
|
+
`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})`))}`,
|
|
12861
|
+
...unreadable.length ? [`unreadable: ${list2(unreadable.map((m) => `${m.name} ${m.reason.code}: ${m.reason.message}`))}`] : [],
|
|
12862
|
+
...undrawn.length ? [`listed, not drawn: ${list2(undrawn.map((m) => `${m.name} (${m.kind})`))}`] : [],
|
|
12863
|
+
...beholdMembers ? [".behold.json also lists members, which are ignored beside the declaration"] : []
|
|
12864
|
+
];
|
|
12865
|
+
const warn = unreadable.length > 0 || beholdMembers;
|
|
12866
|
+
const fix = [
|
|
12867
|
+
...unreadable.length ? ["`chant workspace check` says what each unreadable member needs."] : [],
|
|
12868
|
+
...beholdMembers ? ["Delete `members` from .behold.json; chant.workspace.json lists them now."] : []
|
|
12869
|
+
].join(" ");
|
|
12870
|
+
return { name: "project", status: warn ? "warn" : "pass", detail: parts.join("; "), ...warn ? { fix } : {} };
|
|
12871
|
+
}
|
|
12311
12872
|
async function diagnose(dir, probes = {}) {
|
|
12312
|
-
const root =
|
|
12313
|
-
const shape = detectProjectShape(root);
|
|
12873
|
+
const root = resolve13(dir);
|
|
12314
12874
|
const behold = beholdVersion();
|
|
12875
|
+
const listing = hasDeclaration(root) ? await (probes.readWorkspace ?? readWorkspace)(root) : void 0;
|
|
12876
|
+
if (listing && !listing.ok) {
|
|
12877
|
+
return {
|
|
12878
|
+
behold,
|
|
12879
|
+
dir: root,
|
|
12880
|
+
kind: "estate",
|
|
12881
|
+
ok: false,
|
|
12882
|
+
checks: [{ name: "project", status: "fail", detail: listing.refusal.error, fix: listing.refusal.remedy }]
|
|
12883
|
+
};
|
|
12884
|
+
}
|
|
12885
|
+
const workspace = listing?.ok ? listing.workspace : void 0;
|
|
12886
|
+
const shape = workspace ? workspaceShape(workspace) : detectProjectShape(root);
|
|
12315
12887
|
const invalid = shape.invalidMembers ?? [];
|
|
12316
12888
|
const invalidDetail = invalid.map((m) => `${m.dir} \u2014 ${m.invalid}`);
|
|
12317
12889
|
const kindsFix = `Member kinds this behold reads: ${registeredMemberKinds().join(", ")}. Fix or remove the entry in .behold.json \`members\`.`;
|
|
@@ -12337,14 +12909,14 @@ async function diagnose(dir, probes = {}) {
|
|
|
12337
12909
|
};
|
|
12338
12910
|
}
|
|
12339
12911
|
const estate = shape.kind === "estate";
|
|
12340
|
-
const members = (shape.members ?? []).map((m) => ({ ...m, abs:
|
|
12912
|
+
const members = (shape.members ?? []).map((m) => ({ ...m, abs: resolve13(root, m.dir) }));
|
|
12341
12913
|
const targets = estate ? members.map((m) => m.abs) : [root];
|
|
12342
12914
|
const primary = targets[0];
|
|
12343
12915
|
const chantTargets = estate ? members.filter((m) => m.kind === "chant").map((m) => m.abs) : [root];
|
|
12344
12916
|
const memberList = list2(members.map((m) => `${m.dir} (${m.kind})`));
|
|
12345
12917
|
const membersFrom = shape.membersFrom === "behold-config" ? ".behold.json members" : "npm workspaces";
|
|
12346
12918
|
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 ? {
|
|
12919
|
+
const projectCheck = workspace ? workspaceCheck(workspace, !!loadBeholdConfig(root).members) : estate ? invalid.length ? {
|
|
12348
12920
|
name: "project",
|
|
12349
12921
|
status: "fail",
|
|
12350
12922
|
detail: `${estateDetail}; invalid: ${list2(invalidDetail)}`,
|
|
@@ -12397,12 +12969,11 @@ function formatReport(report) {
|
|
|
12397
12969
|
|
|
12398
12970
|
// src/server.ts
|
|
12399
12971
|
init_read_stats();
|
|
12400
|
-
import { shortStackNames as shortStackNames2 } from "@intentius/pinhole";
|
|
12401
12972
|
init_foreign();
|
|
12402
12973
|
|
|
12403
12974
|
// src/events.ts
|
|
12404
|
-
import { watch, existsSync as
|
|
12405
|
-
import { join as
|
|
12975
|
+
import { watch, existsSync as existsSync16 } from "node:fs";
|
|
12976
|
+
import { join as join24 } from "node:path";
|
|
12406
12977
|
var Broadcaster = class {
|
|
12407
12978
|
listeners = /* @__PURE__ */ new Set();
|
|
12408
12979
|
subscribe(fn) {
|
|
@@ -12420,7 +12991,7 @@ var Broadcaster = class {
|
|
|
12420
12991
|
};
|
|
12421
12992
|
var IGNORE = /(^|[\\/])(node_modules|dist|\.git)([\\/]|$)/;
|
|
12422
12993
|
function watchSource(projectDir, onChange, debounceMs = 200) {
|
|
12423
|
-
const dir =
|
|
12994
|
+
const dir = existsSync16(join24(projectDir, "src")) ? join24(projectDir, "src") : projectDir;
|
|
12424
12995
|
let timer;
|
|
12425
12996
|
const watcher = watch(dir, { recursive: true }, (_event, file) => {
|
|
12426
12997
|
const name = typeof file === "string" ? file : "";
|
|
@@ -12722,8 +13293,8 @@ async function emulatorDown(projectDir) {
|
|
|
12722
13293
|
}
|
|
12723
13294
|
|
|
12724
13295
|
// src/demos.ts
|
|
12725
|
-
import { readFileSync as readFileSync18, existsSync as
|
|
12726
|
-
import { dirname as dirname6, isAbsolute as isAbsolute4, join as
|
|
13296
|
+
import { readFileSync as readFileSync18, existsSync as existsSync17, cpSync, mkdirSync as mkdirSync6 } from "node:fs";
|
|
13297
|
+
import { dirname as dirname6, isAbsolute as isAbsolute4, join as join25, relative as relative7, resolve as resolve14, sep as sep6 } from "node:path";
|
|
12727
13298
|
import { spawn as spawn6, spawnSync as spawnSync2 } from "node:child_process";
|
|
12728
13299
|
function readCatalog(file, catalog) {
|
|
12729
13300
|
let raw;
|
|
@@ -12734,7 +13305,7 @@ function readCatalog(file, catalog) {
|
|
|
12734
13305
|
}
|
|
12735
13306
|
const list3 = raw?.demos;
|
|
12736
13307
|
if (!Array.isArray(list3)) return [];
|
|
12737
|
-
const catalogDir = dirname6(
|
|
13308
|
+
const catalogDir = dirname6(resolve14(file));
|
|
12738
13309
|
return list3.filter((e) => validEntry(e)).map((e) => ({ ...e, catalog, catalogDir }));
|
|
12739
13310
|
}
|
|
12740
13311
|
function validEntry(e) {
|
|
@@ -12768,10 +13339,10 @@ function validEntry(e) {
|
|
|
12768
13339
|
return true;
|
|
12769
13340
|
}
|
|
12770
13341
|
function loadDemoRegistry(pkgRoot3) {
|
|
12771
|
-
const merged = readCatalog(
|
|
13342
|
+
const merged = readCatalog(join25(pkgRoot3, "demos.json"), "demos");
|
|
12772
13343
|
const seen = new Set(merged.map((e) => e.name));
|
|
12773
13344
|
const named = process.env.BEHOLD_WORKBENCH;
|
|
12774
|
-
for (const e of [...readCatalog(
|
|
13345
|
+
for (const e of [...readCatalog(join25(pkgRoot3, "workbench.json"), "workbench"), ...named ? readCatalog(resolve14(named), "workbench") : []]) {
|
|
12775
13346
|
if (seen.has(e.name)) {
|
|
12776
13347
|
process.stderr.write(`behold: workbench entry "${e.name}" collides with a demo already in the catalog \u2014 dropped
|
|
12777
13348
|
`);
|
|
@@ -12784,17 +13355,17 @@ function loadDemoRegistry(pkgRoot3) {
|
|
|
12784
13355
|
}
|
|
12785
13356
|
function demoLocalPath(entry) {
|
|
12786
13357
|
if (entry.source !== "local" || !entry.path) return void 0;
|
|
12787
|
-
return isAbsolute4(entry.path) ? entry.path :
|
|
13358
|
+
return isAbsolute4(entry.path) ? entry.path : resolve14(entry.catalogDir ?? process.cwd(), entry.path);
|
|
12788
13359
|
}
|
|
12789
13360
|
function missingRequirements(entry) {
|
|
12790
13361
|
const bins = entry.source === "git" && !entry.requires.includes("git") ? [...entry.requires, "git"] : entry.requires;
|
|
12791
13362
|
const finder = process.platform === "win32" ? "where" : "which";
|
|
12792
13363
|
const missing = bins.filter((bin) => {
|
|
12793
|
-
if (bin === "choudoufu" && process.env.CHOUDOUFU_BIN &&
|
|
13364
|
+
if (bin === "choudoufu" && process.env.CHOUDOUFU_BIN && existsSync17(process.env.CHOUDOUFU_BIN)) return false;
|
|
12794
13365
|
return spawnSync2(finder, [bin], { stdio: "ignore" }).status !== 0;
|
|
12795
13366
|
});
|
|
12796
13367
|
const local = demoLocalPath(entry);
|
|
12797
|
-
if (local && !
|
|
13368
|
+
if (local && !existsSync17(local)) missing.push(`${entry.path} (not checked out)`);
|
|
12798
13369
|
return missing;
|
|
12799
13370
|
}
|
|
12800
13371
|
function fetchesFromNetwork(entry) {
|
|
@@ -12803,9 +13374,9 @@ function fetchesFromNetwork(entry) {
|
|
|
12803
13374
|
function demoTargetDir(entry, cwd = process.cwd()) {
|
|
12804
13375
|
const inPlace = entry.inPlace ? demoLocalPath(entry) : void 0;
|
|
12805
13376
|
if (inPlace) return inPlace;
|
|
12806
|
-
const legacy =
|
|
12807
|
-
if (entry.name === "writes" &&
|
|
12808
|
-
return
|
|
13377
|
+
const legacy = resolve14(cwd, "behold-demo");
|
|
13378
|
+
if (entry.name === "writes" && existsSync17(legacy)) return legacy;
|
|
13379
|
+
return resolve14(cwd, "behold-demos", entry.name);
|
|
12809
13380
|
}
|
|
12810
13381
|
async function loadDemo(entry, opts) {
|
|
12811
13382
|
const { pkgRoot: pkgRoot3, target } = opts;
|
|
@@ -12814,12 +13385,12 @@ async function loadDemo(entry, opts) {
|
|
|
12814
13385
|
const inPlace = entry.source === "local" && entry.inPlace;
|
|
12815
13386
|
const work = inPlace ? local : target;
|
|
12816
13387
|
if (inPlace) {
|
|
12817
|
-
if (!
|
|
13388
|
+
if (!existsSync17(work)) return { ok: false, error: `${entry.path} is not checked out (looked in ${work})` };
|
|
12818
13389
|
say(`serving ${work} where it is \u2014 nothing copied`);
|
|
12819
|
-
} else if (!
|
|
13390
|
+
} else if (!existsSync17(target)) {
|
|
12820
13391
|
if (entry.source === "bundled" || entry.source === "local" && local) {
|
|
12821
|
-
const from = entry.source === "bundled" ?
|
|
12822
|
-
if (!
|
|
13392
|
+
const from = entry.source === "bundled" ? join25(pkgRoot3, entry.dir) : local;
|
|
13393
|
+
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
13394
|
say(`copying to ${target} (it's yours \u2014 edit it)`);
|
|
12824
13395
|
try {
|
|
12825
13396
|
cpSync(from, target, {
|
|
@@ -12845,7 +13416,7 @@ async function loadDemo(entry, opts) {
|
|
|
12845
13416
|
} else {
|
|
12846
13417
|
say(`reusing ${target}`);
|
|
12847
13418
|
}
|
|
12848
|
-
if (!inPlace &&
|
|
13419
|
+
if (!inPlace && existsSync17(join25(work, "package.json")) && !existsSync17(join25(work, "node_modules"))) {
|
|
12849
13420
|
say("npm install\u2026");
|
|
12850
13421
|
if (await runStep("npm", ["install"], { cwd: work, shell: process.platform === "win32" }) !== 0) {
|
|
12851
13422
|
return { ok: false, error: `npm install failed in ${work}` };
|
|
@@ -12862,7 +13433,7 @@ async function loadDemo(entry, opts) {
|
|
|
12862
13433
|
return { ok: false, error: `setup failed (${entry.setup})` };
|
|
12863
13434
|
}
|
|
12864
13435
|
}
|
|
12865
|
-
return { ok: true, serveDirs: entry.serve.dirs?.length ? entry.serve.dirs.map((d) =>
|
|
13436
|
+
return { ok: true, serveDirs: entry.serve.dirs?.length ? entry.serve.dirs.map((d) => join25(work, d)) : [work] };
|
|
12866
13437
|
}
|
|
12867
13438
|
function runStep(cmd, args, opts = {}) {
|
|
12868
13439
|
return new Promise((res) => {
|
|
@@ -12873,8 +13444,8 @@ function runStep(cmd, args, opts = {}) {
|
|
|
12873
13444
|
}
|
|
12874
13445
|
|
|
12875
13446
|
// src/server.ts
|
|
12876
|
-
var pkgRoot =
|
|
12877
|
-
var webRoot =
|
|
13447
|
+
var pkgRoot = join26(dirname7(fileURLToPath4(import.meta.url)), "..");
|
|
13448
|
+
var webRoot = join26(pkgRoot, "web");
|
|
12878
13449
|
var execFileP = async (cmd, args) => (await promisify3(execFile3)(cmd, args, { encoding: "utf8", timeout: 1e4 })).stdout;
|
|
12879
13450
|
function optsFromQuery(url, tierEnvVar, projectDir) {
|
|
12880
13451
|
const q = url.searchParams;
|
|
@@ -12925,7 +13496,7 @@ function tierFailure(tier, message) {
|
|
|
12925
13496
|
function beholdVersion() {
|
|
12926
13497
|
try {
|
|
12927
13498
|
const here = dirname7(fileURLToPath4(import.meta.url));
|
|
12928
|
-
return JSON.parse(readFileSync19(
|
|
13499
|
+
return JSON.parse(readFileSync19(join26(here, "..", "package.json"), "utf8")).version ?? "unknown";
|
|
12929
13500
|
} catch {
|
|
12930
13501
|
return "unknown";
|
|
12931
13502
|
}
|
|
@@ -13216,11 +13787,13 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
|
|
|
13216
13787
|
const app = new Hono();
|
|
13217
13788
|
setChoudoufuRunner(cfg.choudoufu?.run);
|
|
13218
13789
|
setEstateLexiconRead(cfg.choudoufu ? cfg.choudoufu.lexicon ?? null : void 0);
|
|
13790
|
+
setServedWorkspace(cfg.workspace);
|
|
13219
13791
|
app.use("/api/*", async (c, next) => {
|
|
13220
13792
|
if (c.req.method === "GET") return withReadSignal(c.req.raw.signal, next);
|
|
13221
13793
|
await next();
|
|
13222
13794
|
});
|
|
13223
13795
|
if (cfg.carveReport) carveRoutes(app, cfg.carveReport, cfg.carveDemo);
|
|
13796
|
+
const predictDeps = cfg.behaviour ?? { read: graphIr };
|
|
13224
13797
|
let beholdConfig = loadBeholdConfig(cfg.projectDir);
|
|
13225
13798
|
let tierEnvVar = beholdConfig.tiers?.envVar;
|
|
13226
13799
|
const executorFor = (env) => env ? beholdConfig.executor?.[env] : void 0;
|
|
@@ -13331,9 +13904,9 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
|
|
|
13331
13904
|
return c.json({ started: true, name, ran: label });
|
|
13332
13905
|
});
|
|
13333
13906
|
app.post("/api/local/reset", (c) => {
|
|
13334
|
-
const down =
|
|
13335
|
-
const up =
|
|
13336
|
-
if (!
|
|
13907
|
+
const down = join26(cfg.projectDir, "scripts/local/local-down.sh");
|
|
13908
|
+
const up = join26(cfg.projectDir, "scripts/local/local-up.sh");
|
|
13909
|
+
if (!existsSync18(down) || !existsSync18(up)) {
|
|
13337
13910
|
return c.json({ error: "no local-down.sh / local-up.sh in scripts/local \u2014 reset is only for local emulator projects" }, 400);
|
|
13338
13911
|
}
|
|
13339
13912
|
if (!runner.bringUp("reset local emulator", "bash", ["-c", "bash scripts/local/local-down.sh && bash scripts/local/local-up.sh"], cfg.projectDir)) {
|
|
@@ -13566,9 +14139,9 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
|
|
|
13566
14139
|
app.post("/api/project/open", async (c) => {
|
|
13567
14140
|
if (cfg.previewMode) return c.json({ error: "switching projects is locked in preview mode" }, 403);
|
|
13568
14141
|
const body = await c.req.json().catch(() => ({}));
|
|
13569
|
-
const dir = typeof body.dir === "string" && body.dir.trim() ?
|
|
13570
|
-
if (!dir || !
|
|
13571
|
-
if (!
|
|
14142
|
+
const dir = typeof body.dir === "string" && body.dir.trim() ? resolve15(body.dir.trim()) : "";
|
|
14143
|
+
if (!dir || !existsSync18(dir)) return c.json({ error: `no such directory: ${dir || "(no dir given)"}` }, 400);
|
|
14144
|
+
if (!existsSync18(join26(dir, "chant.config.ts"))) {
|
|
13572
14145
|
return c.json({ error: `${dir} doesn't look like a chant project \u2014 no chant.config.ts` }, 400);
|
|
13573
14146
|
}
|
|
13574
14147
|
switchServedProject([dir]);
|
|
@@ -13577,6 +14150,8 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
|
|
|
13577
14150
|
const switchServedProject = (dirs, env, spawnEnv) => {
|
|
13578
14151
|
addRecent(cfg.projectDir);
|
|
13579
14152
|
cfg.projectDir = dirs[0];
|
|
14153
|
+
cfg.workspace = void 0;
|
|
14154
|
+
setServedWorkspace(void 0);
|
|
13580
14155
|
cfg.projectDirs = servesAsEstate(dirs) ? dirs : void 0;
|
|
13581
14156
|
setChoudoufuSpawnEnv(spawnEnv);
|
|
13582
14157
|
cfg.env = env;
|
|
@@ -13589,7 +14164,7 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
|
|
|
13589
14164
|
};
|
|
13590
14165
|
app.post("/api/project/reveal", async (c) => {
|
|
13591
14166
|
const body = await c.req.json().catch(() => ({}));
|
|
13592
|
-
const dir = typeof body.dir === "string" && body.dir.trim() ?
|
|
14167
|
+
const dir = typeof body.dir === "string" && body.dir.trim() ? resolve15(body.dir.trim()) : cfg.projectDir;
|
|
13593
14168
|
const known = /* @__PURE__ */ new Set([cfg.projectDir, ...cfg.projectDirs ?? [], ...listRecents().map((r) => r.dir)]);
|
|
13594
14169
|
if (!known.has(dir)) return c.json({ error: "not a served or recent project directory" }, 400);
|
|
13595
14170
|
const opener = process.platform === "darwin" ? "open" : process.platform === "win32" ? "explorer" : "xdg-open";
|
|
@@ -13617,7 +14192,7 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
|
|
|
13617
14192
|
fetches: fetchesFromNetwork(e),
|
|
13618
14193
|
...e.repo ? { repo: e.repo } : {},
|
|
13619
14194
|
target,
|
|
13620
|
-
loaded:
|
|
14195
|
+
loaded: existsSync18(target),
|
|
13621
14196
|
satisfiable: missing.length === 0,
|
|
13622
14197
|
// #254: the carve walkthrough runs fine here — it just isn't a project
|
|
13623
14198
|
// to switch INTO. Carve mode claims `/api/graph` and `/api/project` at
|
|
@@ -13676,20 +14251,21 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
|
|
|
13676
14251
|
loadingDemo = null;
|
|
13677
14252
|
}
|
|
13678
14253
|
});
|
|
14254
|
+
const layoutDir = () => cfg.workspace?.root ?? cfg.projectDir;
|
|
13679
14255
|
const layoutWriteBlock = () => {
|
|
13680
14256
|
if (cfg.previewMode) return "the layout sidecar is read-only in preview mode";
|
|
13681
14257
|
if (cfg.carveReport) return "a carve report isn't a project \u2014 there's nowhere to keep a hand layout";
|
|
13682
14258
|
if (cfg.layoutWrites === false) return "a static export captures a snapshot \u2014 it doesn't write to the project";
|
|
13683
|
-
return unwritableReason(
|
|
14259
|
+
return unwritableReason(layoutDir());
|
|
13684
14260
|
};
|
|
13685
14261
|
app.get("/api/layout", (c) => {
|
|
13686
14262
|
const block = layoutWriteBlock();
|
|
13687
|
-
const shared2 = { path: layoutPath(
|
|
14263
|
+
const shared2 = { path: layoutPath(layoutDir()), writable: !block, ...block ? { reason: block } : {} };
|
|
13688
14264
|
const raw = new URL(c.req.url).searchParams.get("lens");
|
|
13689
|
-
if (raw === null) return c.json({ ...shared2, lenses: readLayoutFile(
|
|
14265
|
+
if (raw === null) return c.json({ ...shared2, lenses: readLayoutFile(layoutDir()).lenses });
|
|
13690
14266
|
const lens = normalizeLens(raw);
|
|
13691
14267
|
if (!lens) return c.json({ error: `not a lens key: ${raw}`, code: "bad-layout" }, 400);
|
|
13692
|
-
return c.json({ ...shared2, lens, deltas: readLens(
|
|
14268
|
+
return c.json({ ...shared2, lens, deltas: readLens(layoutDir(), lens) });
|
|
13693
14269
|
});
|
|
13694
14270
|
app.post("/api/layout", async (c) => {
|
|
13695
14271
|
const block = layoutWriteBlock();
|
|
@@ -13713,8 +14289,8 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
|
|
|
13713
14289
|
return c.json({ error: "body needs `deltas`: {<node id>: {dx,dy,dw,dh}}", code: "bad-layout" }, 400);
|
|
13714
14290
|
}
|
|
13715
14291
|
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(
|
|
14292
|
+
const stored = writeLens(layoutDir(), lens, body.deltas);
|
|
14293
|
+
return c.json({ ok: true, lens: stored.lens, deltas: stored.deltas, count: Object.keys(stored.deltas).length, path: layoutPath(layoutDir()) });
|
|
13718
14294
|
} catch (err) {
|
|
13719
14295
|
if (err instanceof LayoutTooLarge) return c.json({ error: err.message, code: "too-large" }, 413);
|
|
13720
14296
|
return c.json({ error: err instanceof Error ? err.message : String(err) }, 500);
|
|
@@ -13727,7 +14303,7 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
|
|
|
13727
14303
|
const res = c.res;
|
|
13728
14304
|
if (!res || res.status !== 200 || !(res.headers.get("content-type") ?? "").includes("json")) return;
|
|
13729
14305
|
const lens = lensFromQuery(url.searchParams);
|
|
13730
|
-
const deltas = readLens(
|
|
14306
|
+
const deltas = readLens(layoutDir(), lens);
|
|
13731
14307
|
if (!Object.keys(deltas).length) return;
|
|
13732
14308
|
let body;
|
|
13733
14309
|
try {
|
|
@@ -13776,7 +14352,7 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
|
|
|
13776
14352
|
{
|
|
13777
14353
|
method: "GET",
|
|
13778
14354
|
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"
|
|
14355
|
+
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
14356
|
},
|
|
13781
14357
|
{ method: "GET", path: "/api/layout", desc: "hand-layout sidecar (.behold/layout.json): ?lens=<key> \u2192 {lens, deltas, writable}; no lens \u2192 every lens" },
|
|
13782
14358
|
{ 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)" },
|
|
@@ -13878,8 +14454,8 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
|
|
|
13878
14454
|
}
|
|
13879
14455
|
const runState = runner.runState;
|
|
13880
14456
|
const ranOp = runState.op ? parsedOps.find((o) => o.name === runState.op) : void 0;
|
|
13881
|
-
const
|
|
13882
|
-
const opsIr =
|
|
14457
|
+
const join30 = ranOp ? joinRun(ranOp, runState.records, runState.gate) : null;
|
|
14458
|
+
const opsIr = join30 ? paintPlayhead(declaredIr, join30, runState.gate) : declaredIr;
|
|
13883
14459
|
const { svg: svg2 } = renderGraph(opsIr, { boxes: "byStack" });
|
|
13884
14460
|
const operator = runner.noteDeclaredConvergeOps(declaredConvergeOps(parsedOps));
|
|
13885
14461
|
return c.json({
|
|
@@ -13892,7 +14468,7 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
|
|
|
13892
14468
|
target: null,
|
|
13893
14469
|
mode: "ops",
|
|
13894
14470
|
ops: parsedOps.length,
|
|
13895
|
-
note: `${opsNote(parsedOps, opsIr)} ${playheadNote(runState,
|
|
14471
|
+
note: `${opsNote(parsedOps, opsIr)} ${playheadNote(runState, join30)} ${operatorNote(operator)}`,
|
|
13896
14472
|
// The operator strip's own model: the ConvergeOps this project
|
|
13897
14473
|
// declares, plus whatever the last status read said. `declared`
|
|
13898
14474
|
// without a `strip` is the honest starting state — a loop exists and
|
|
@@ -13999,12 +14575,21 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
|
|
|
13999
14575
|
srcCompositeEdgesAttached = 0;
|
|
14000
14576
|
}
|
|
14001
14577
|
}
|
|
14002
|
-
if (!multi && !components && !logical && !opts.lens && !ownKindVia && ir.nodes.length === 0 && !
|
|
14578
|
+
if (!multi && !components && !logical && !opts.lens && !ownKindVia && ir.nodes.length === 0 && !existsSync18(join26(cfg.projectDir, "chant.config.ts"))) {
|
|
14003
14579
|
return c.json(noProjectError(cfg.projectDir), 404);
|
|
14004
14580
|
}
|
|
14005
14581
|
const radial = new URL(c.req.url).searchParams.get("radial") === "1";
|
|
14006
14582
|
const collapse = new URL(c.req.url).searchParams.get("collapse") === "1";
|
|
14007
14583
|
let collapsedNote;
|
|
14584
|
+
const srcTraffic = components ? void 0 : trafficFor(new URL(c.req.url).searchParams, beholdConfig);
|
|
14585
|
+
const srcBehaviour = srcTraffic ? attachDeclaredBehaviour(
|
|
14586
|
+
ir,
|
|
14587
|
+
await Promise.all(
|
|
14588
|
+
(multi ? estateMembers(cfg.projectDirs) : [{ name: void 0, dir: cfg.projectDir, kind: memberKindOf(cfg.projectDir) ?? "chant" }]).map(
|
|
14589
|
+
async (m) => ({ name: m.name, prediction: await predictMember(m, void 0, srcTraffic, predictDeps) })
|
|
14590
|
+
)
|
|
14591
|
+
)
|
|
14592
|
+
) : void 0;
|
|
14008
14593
|
const expanded = ir;
|
|
14009
14594
|
if (multi && collapse) {
|
|
14010
14595
|
const shut = collapseBoxes(ir);
|
|
@@ -14029,6 +14614,7 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
|
|
|
14029
14614
|
meta: {
|
|
14030
14615
|
projectDir: cfg.projectDir,
|
|
14031
14616
|
env: metaEnv,
|
|
14617
|
+
...srcBehaviour ? { behaviour: srcBehaviour } : {},
|
|
14032
14618
|
...srcNote ? { note: srcNote } : {},
|
|
14033
14619
|
// #393 item 7: the strip's form of the same note, when there is a
|
|
14034
14620
|
// shorter true one. Absent means "the note fits" — the SPA shows
|
|
@@ -14162,7 +14748,7 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
|
|
|
14162
14748
|
});
|
|
14163
14749
|
const choudoufuPlanDrift = async (dirs, wanted) => {
|
|
14164
14750
|
const members = estateMembers(dirs).filter((m) => m.kind === "choudoufu");
|
|
14165
|
-
const
|
|
14751
|
+
const byDir2 = /* @__PURE__ */ new Map();
|
|
14166
14752
|
let read = false;
|
|
14167
14753
|
for (const m of members) {
|
|
14168
14754
|
const stamp = memberSourceStamp(m.dir);
|
|
@@ -14170,7 +14756,7 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
|
|
|
14170
14756
|
const hit = cachedChoudoufuPlan(m.dir, stamp);
|
|
14171
14757
|
if (hit) {
|
|
14172
14758
|
read = true;
|
|
14173
|
-
|
|
14759
|
+
byDir2.set(m.dir, { name: m.name, drift: hit });
|
|
14174
14760
|
}
|
|
14175
14761
|
continue;
|
|
14176
14762
|
}
|
|
@@ -14178,11 +14764,11 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
|
|
|
14178
14764
|
const drift = await readChoudoufuPlan(m.dir, cfg.choudoufu?.run);
|
|
14179
14765
|
cacheChoudoufuPlan(m.dir, stamp, drift);
|
|
14180
14766
|
read = true;
|
|
14181
|
-
|
|
14767
|
+
byDir2.set(m.dir, { name: m.name, drift });
|
|
14182
14768
|
} catch {
|
|
14183
14769
|
}
|
|
14184
14770
|
}
|
|
14185
|
-
return { read, byDir };
|
|
14771
|
+
return { read, byDir: byDir2 };
|
|
14186
14772
|
};
|
|
14187
14773
|
app.get("/api/overlay", async (c) => {
|
|
14188
14774
|
const query = optsFromQuery(new URL(c.req.url), tierEnvVar, cfg.projectDir);
|
|
@@ -14224,11 +14810,12 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
|
|
|
14224
14810
|
mode: "progressive",
|
|
14225
14811
|
// Which members the picture is still waiting on, so a client can
|
|
14226
14812
|
// say so without inferring it from the attrs.
|
|
14227
|
-
pending:
|
|
14813
|
+
pending: estateMemberNames(cfg.projectDirs)
|
|
14228
14814
|
}
|
|
14229
14815
|
});
|
|
14230
14816
|
}
|
|
14231
|
-
const
|
|
14817
|
+
const traffic2 = trafficFor(new URL(c.req.url).searchParams, beholdConfig);
|
|
14818
|
+
const est = await composeEstateOverlay(cfg.projectDirs, { ...tierTargetOpts(query), detail, env, ...traffic2 ? { traffic: traffic2 } : {} }, reclassifyOverlay, { fresh: planWanted });
|
|
14232
14819
|
if (est.dropped.length === est.total) {
|
|
14233
14820
|
return c.json({ error: `no project in the estate could be graphed \u2014 ${est.dropped.map((d) => `${d.name}: ${d.reason}`).join("; ")}` }, 500);
|
|
14234
14821
|
}
|
|
@@ -14267,11 +14854,24 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
|
|
|
14267
14854
|
meta: { projectDir: cfg.projectDir, env, mode: "logical", estate: est.total, vocabulary, ...note3 ? { note: note3 } : {} }
|
|
14268
14855
|
});
|
|
14269
14856
|
}
|
|
14270
|
-
const
|
|
14857
|
+
const predicted = traffic2 ? await Promise.all(
|
|
14858
|
+
estateMembers(cfg.projectDirs).map(async (m) => ({ m, prediction: await predictMember(m, env, traffic2, predictDeps, planWanted) }))
|
|
14859
|
+
) : [];
|
|
14860
|
+
const paintNotes2 = [];
|
|
14861
|
+
for (const { m, prediction: prediction2 } of predicted) {
|
|
14862
|
+
if (prediction2.live) est.memberMeta[m.name] = paintLive(ir2, m.name, prediction2.live, paintNotes2);
|
|
14863
|
+
}
|
|
14864
|
+
const attached2 = attachBehaviour(
|
|
14271
14865
|
ir2,
|
|
14272
14866
|
estateMembers(cfg.projectDirs).map((m) => ({ name: m.name, dir: m.dir, meta: est.memberMeta[m.name] })),
|
|
14273
|
-
env
|
|
14867
|
+
env,
|
|
14868
|
+
...traffic2 ? [() => void 0] : []
|
|
14274
14869
|
);
|
|
14870
|
+
const behaviour2 = traffic2 ? attachBehaviourDelta(
|
|
14871
|
+
ir2,
|
|
14872
|
+
predicted.map(({ m, prediction: prediction2 }) => ({ name: m.name, liveMeta: est.memberMeta[m.name], prediction: prediction2 })),
|
|
14873
|
+
paintNotes2.length ? { ...attached2, diagnostics: [...attached2.diagnostics ?? [], ...paintNotes2] } : attached2
|
|
14874
|
+
) : attached2;
|
|
14275
14875
|
const collapsed = new URL(c.req.url).searchParams.get("collapse") === "1" && !runtime ? collapseBoxes(ir2) : void 0;
|
|
14276
14876
|
const expanded = ir2;
|
|
14277
14877
|
if (collapsed) ir2 = collapsed.ir;
|
|
@@ -14317,7 +14917,16 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
|
|
|
14317
14917
|
}
|
|
14318
14918
|
});
|
|
14319
14919
|
}
|
|
14320
|
-
const
|
|
14920
|
+
const traffic = trafficFor(new URL(c.req.url).searchParams, beholdConfig);
|
|
14921
|
+
const ownKind = memberKindOf(cfg.projectDir) ?? "chant";
|
|
14922
|
+
const opts = {
|
|
14923
|
+
...query,
|
|
14924
|
+
live: true,
|
|
14925
|
+
overlay: true,
|
|
14926
|
+
env,
|
|
14927
|
+
...logical || runtime ? { detail: 3 } : {},
|
|
14928
|
+
...traffic && memberTakesTraffic(cfg.projectDir, ownKind) ? { traffic } : {}
|
|
14929
|
+
};
|
|
14321
14930
|
const read = await graphIr(cfg.projectDir, opts);
|
|
14322
14931
|
const readMeta = read.meta;
|
|
14323
14932
|
let ir = joinCarvedSources(reclassifyOverlay(read), (await carveStatesFor(cfg.projectDir)).values(), cfg.projectDir).ir;
|
|
@@ -14358,7 +14967,11 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
|
|
|
14358
14967
|
compositeEdgesAttached = 0;
|
|
14359
14968
|
}
|
|
14360
14969
|
}
|
|
14361
|
-
const
|
|
14970
|
+
const prediction = traffic ? await predictMember({ dir: cfg.projectDir, kind: ownKind }, env, traffic, predictDeps) : void 0;
|
|
14971
|
+
const paintNotes = [];
|
|
14972
|
+
const liveMeta = prediction?.live ? paintLive(ir, void 0, prediction.live, paintNotes) : readMeta;
|
|
14973
|
+
const attached = attachBehaviour(ir, [{ dir: cfg.projectDir, meta: liveMeta }], env, ...traffic ? [() => void 0] : []);
|
|
14974
|
+
const behaviour = prediction ? attachBehaviourDelta(ir, [{ liveMeta, prediction }], paintNotes.length ? { ...attached, diagnostics: [...attached.diagnostics ?? [], ...paintNotes] } : attached) : attached;
|
|
14362
14975
|
const { svg } = renderGraph(ir, { boxes: "byContainer", radial: new URL(c.req.url).searchParams.get("radial") === "1" });
|
|
14363
14976
|
const zoom = runtime ? "runtime" : query.detail === 1 ? "composites" : query.detail === 3 ? "attributes" : "resources";
|
|
14364
14977
|
const tierNote = tierMismatchNote(ir, beholdConfig.tiers, query.tier);
|
|
@@ -14451,9 +15064,9 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
|
|
|
14451
15064
|
});
|
|
14452
15065
|
const choudoufuPlanPath = (plan) => {
|
|
14453
15066
|
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 =
|
|
15067
|
+
const abs = resolve15(cfg.projectDir, plan);
|
|
14455
15068
|
const members = (cfg.projectDirs ?? [cfg.projectDir]).filter((d) => memberKindOf(d) === "choudoufu");
|
|
14456
|
-
const inside = members.some((d) => abs.startsWith(
|
|
15069
|
+
const inside = members.some((d) => abs.startsWith(resolve15(d) + sep7));
|
|
14457
15070
|
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
15071
|
return { path: abs };
|
|
14459
15072
|
};
|
|
@@ -14585,7 +15198,7 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
|
|
|
14585
15198
|
});
|
|
14586
15199
|
const rel = relative8(process.cwd(), webRoot) || ".";
|
|
14587
15200
|
app.use("/*", serveStatic({ root: rel }));
|
|
14588
|
-
app.get("/", serveStatic({ path:
|
|
15201
|
+
app.get("/", serveStatic({ path: join26(rel, "index.html") }));
|
|
14589
15202
|
return app;
|
|
14590
15203
|
}
|
|
14591
15204
|
async function bootLocalEmulators(dir, who) {
|
|
@@ -14614,7 +15227,7 @@ async function bootLocalEmulators(dir, who) {
|
|
|
14614
15227
|
async function startServer(cfg) {
|
|
14615
15228
|
if (cfg.local) cfg.emulators = await bootLocalEmulators(cfg.projectDir, "behold serve --local");
|
|
14616
15229
|
for (const dir of cfg.projectDirs ?? [cfg.projectDir]) {
|
|
14617
|
-
if (
|
|
15230
|
+
if (existsSync18(join26(dir, "chant.config.ts"))) addRecent(dir);
|
|
14618
15231
|
}
|
|
14619
15232
|
const broadcaster = new Broadcaster();
|
|
14620
15233
|
const frames = new FrameBuffer();
|
|
@@ -14646,7 +15259,7 @@ async function startServer(cfg) {
|
|
|
14646
15259
|
invalidateOverlay(memberDir);
|
|
14647
15260
|
onEstateChange(memberDir);
|
|
14648
15261
|
};
|
|
14649
|
-
const memberTag = (dir) => (cfg.projectDirs?.length ?? 0) > 1 ? ` [${
|
|
15262
|
+
const memberTag = (dir) => (cfg.projectDirs?.length ?? 0) > 1 ? ` [${basename3(dir)}]` : "";
|
|
14650
15263
|
const onPollDrift = (dir, movedLexicons) => {
|
|
14651
15264
|
broadcaster.emit("changed", dir);
|
|
14652
15265
|
if (autoSync === "off") return;
|
|
@@ -14656,7 +15269,7 @@ async function startServer(cfg) {
|
|
|
14656
15269
|
const suspended = autoSync === "pull-request" ? suspendedByRollback(await openRollbackBranches(dir, cfg.env), movedLexicons) : /* @__PURE__ */ new Set();
|
|
14657
15270
|
const { routed, local } = splitForgeRouted(discoverEstateOps(cfg.projectDirs ?? [cfg.projectDir]), executorDesignation);
|
|
14658
15271
|
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 ${
|
|
15272
|
+
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
15273
|
}
|
|
14661
15274
|
const { picks, declined } = pickAutoSyncOps(
|
|
14662
15275
|
autoSync,
|
|
@@ -14778,9 +15391,132 @@ async function startServer(cfg) {
|
|
|
14778
15391
|
// src/cli.ts
|
|
14779
15392
|
init_chant();
|
|
14780
15393
|
|
|
15394
|
+
// src/workspace-convert.ts
|
|
15395
|
+
init_chant();
|
|
15396
|
+
import { existsSync as existsSync19, mkdirSync as mkdirSync7, readFileSync as readFileSync20, renameSync as renameSync2, writeFileSync as writeFileSync6 } from "node:fs";
|
|
15397
|
+
import { basename as basename4, dirname as dirname8, join as join27, relative as relative9, resolve as resolve16, sep as sep8 } from "node:path";
|
|
15398
|
+
import { shortStackNames as shortStackNames2 } from "@intentius/pinhole";
|
|
15399
|
+
init_project();
|
|
15400
|
+
init_terraform_member();
|
|
15401
|
+
var TERRAFORM_LEXICON2 = "@intentius/chant-lexicon-terraform";
|
|
15402
|
+
function declarationName(raw) {
|
|
15403
|
+
const folded = raw.toLowerCase().replace(/[^a-z0-9-]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 40).replace(/-+$/, "");
|
|
15404
|
+
return folded || "member";
|
|
15405
|
+
}
|
|
15406
|
+
function unique(name, taken) {
|
|
15407
|
+
let out = name;
|
|
15408
|
+
for (let i = 2; taken.has(out); i++) out = `${name.slice(0, 40 - String(i).length - 1)}-${i}`;
|
|
15409
|
+
taken.add(out);
|
|
15410
|
+
return out;
|
|
15411
|
+
}
|
|
15412
|
+
var posix = (p) => p.split(sep8).join("/");
|
|
15413
|
+
function installedVersion(pkg, dir) {
|
|
15414
|
+
for (let at = resolve16(dir); ; at = dirname8(at)) {
|
|
15415
|
+
const manifest = join27(at, "node_modules", ...pkg.split("/"), "package.json");
|
|
15416
|
+
if (existsSync19(manifest)) {
|
|
15417
|
+
try {
|
|
15418
|
+
const v = JSON.parse(readFileSync20(manifest, "utf8")).version;
|
|
15419
|
+
return typeof v === "string" ? v : void 0;
|
|
15420
|
+
} catch {
|
|
15421
|
+
return void 0;
|
|
15422
|
+
}
|
|
15423
|
+
}
|
|
15424
|
+
if (dirname8(at) === at) return void 0;
|
|
15425
|
+
}
|
|
15426
|
+
}
|
|
15427
|
+
function planConversion(rootArg) {
|
|
15428
|
+
const root = resolve16(rootArg);
|
|
15429
|
+
const existing = DECLARATION_FILES.find((f) => existsSync19(join27(root, f)));
|
|
15430
|
+
if (existing) return { ok: false, error: `${root} already has ${existing}; there is nothing to convert.` };
|
|
15431
|
+
const shape = detectProjectShape(root);
|
|
15432
|
+
if (shape.kind !== "estate" || !shape.members?.length || shape.membersFrom === "itself") {
|
|
15433
|
+
return { ok: false, error: `${root} is not an estate root with a member list, so there is no member list to convert.` };
|
|
15434
|
+
}
|
|
15435
|
+
const abs = shape.members.map((m) => resolve16(root, m.dir));
|
|
15436
|
+
const shorts = shortStackNames2(abs);
|
|
15437
|
+
const taken = /* @__PURE__ */ new Set();
|
|
15438
|
+
const members = [];
|
|
15439
|
+
const moves = [];
|
|
15440
|
+
const notes = [];
|
|
15441
|
+
let needsLexicon = false;
|
|
15442
|
+
shape.members.forEach((m, i) => {
|
|
15443
|
+
const short = shorts[i];
|
|
15444
|
+
const dir = posix(relative9(root, abs[i])) || ".";
|
|
15445
|
+
if (m.kind === "terraform") {
|
|
15446
|
+
needsLexicon = true;
|
|
15447
|
+
const scan = discoverTerraformRoots(abs[i]);
|
|
15448
|
+
for (const r of scan.roots) {
|
|
15449
|
+
const name2 = unique(declarationName(r.name), taken);
|
|
15450
|
+
members.push({ name: name2, dir: r.dir === "." ? dir : posix(join27(dir, r.dir)), kind: "terraform" });
|
|
15451
|
+
moves.push({ from: `${short}/${r.name}/`, to: `${name2}/${name2}/` });
|
|
15452
|
+
}
|
|
15453
|
+
for (const s of scan.skipped) notes.push(`${posix(join27(dir, s.dir))} is not declared: ${s.why}.`);
|
|
15454
|
+
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.`);
|
|
15455
|
+
return;
|
|
15456
|
+
}
|
|
15457
|
+
if (m.kind === "choudoufu") needsLexicon = true;
|
|
15458
|
+
const name = unique(declarationName(short), taken);
|
|
15459
|
+
members.push({ name, dir, kind: m.kind });
|
|
15460
|
+
if (name !== short) {
|
|
15461
|
+
moves.push({ from: `${short}/`, to: `${name}/` });
|
|
15462
|
+
notes.push(`member ${short} is declared as ${name}: a declared name is lowercase letters, digits and hyphens.`);
|
|
15463
|
+
}
|
|
15464
|
+
});
|
|
15465
|
+
const pins = [];
|
|
15466
|
+
const chant = resolveChant(root);
|
|
15467
|
+
if (chant.source === "project" && chant.version) pins.push({ package: "@intentius/chant", version: chant.version });
|
|
15468
|
+
if (needsLexicon) {
|
|
15469
|
+
const version = installedVersion(TERRAFORM_LEXICON2, root);
|
|
15470
|
+
if (version) pins.push({ package: TERRAFORM_LEXICON2, version });
|
|
15471
|
+
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.`);
|
|
15472
|
+
}
|
|
15473
|
+
const declaration = {
|
|
15474
|
+
name: declarationName(basename4(root)),
|
|
15475
|
+
schema: 1,
|
|
15476
|
+
...pins.length ? { pins } : {},
|
|
15477
|
+
members
|
|
15478
|
+
};
|
|
15479
|
+
const firstLayout = layoutPath(abs[0]);
|
|
15480
|
+
moves.sort((a, b) => b.from.length - a.from.length);
|
|
15481
|
+
return { ok: true, plan: { root, from: shape.membersFrom === "behold-config" ? "behold-config" : "workspaces", declaration, moves, ...existsSync19(firstLayout) ? { layoutFrom: firstLayout } : {}, notes } };
|
|
15482
|
+
}
|
|
15483
|
+
function moveLayoutIds(file, moves) {
|
|
15484
|
+
const lenses = {};
|
|
15485
|
+
for (const [lens, deltas] of Object.entries(file.lenses)) {
|
|
15486
|
+
const out = {};
|
|
15487
|
+
for (const [id, d] of Object.entries(deltas)) {
|
|
15488
|
+
const move = moves.find((m) => id.startsWith(m.from));
|
|
15489
|
+
out[move ? move.to + id.slice(move.from.length) : id] = d;
|
|
15490
|
+
}
|
|
15491
|
+
lenses[lens] = out;
|
|
15492
|
+
}
|
|
15493
|
+
return { version: 1, lenses };
|
|
15494
|
+
}
|
|
15495
|
+
function applyConversion(plan) {
|
|
15496
|
+
const declarationFile = join27(plan.root, DECLARATION_FILES[0]);
|
|
15497
|
+
if (existsSync19(declarationFile)) throw new Error(`${declarationFile} appeared while converting; nothing was written`);
|
|
15498
|
+
writeFileSync6(declarationFile, `${JSON.stringify(plan.declaration, null, 2)}
|
|
15499
|
+
`);
|
|
15500
|
+
let layout;
|
|
15501
|
+
const target = layoutPath(plan.root);
|
|
15502
|
+
if (!plan.layoutFrom) layout = { skipped: "no saved layout to move" };
|
|
15503
|
+
else if (resolve16(plan.layoutFrom) === resolve16(target)) layout = { skipped: "the saved layout is already in the root" };
|
|
15504
|
+
else if (existsSync19(target)) layout = { skipped: `${target} already exists; the layout in ${plan.layoutFrom} was left alone` };
|
|
15505
|
+
else {
|
|
15506
|
+
const moved = moveLayoutIds(readLayoutFile(dirname8(dirname8(plan.layoutFrom))), plan.moves);
|
|
15507
|
+
mkdirSync7(join27(plan.root, LAYOUT_DIR), { recursive: true });
|
|
15508
|
+
const tmp = `${target}.tmp`;
|
|
15509
|
+
writeFileSync6(tmp, `${JSON.stringify(moved, null, 2)}
|
|
15510
|
+
`);
|
|
15511
|
+
renameSync2(tmp, target);
|
|
15512
|
+
layout = { wrote: target, ids: Object.values(moved.lenses).reduce((n, l) => n + Object.keys(l).length, 0) };
|
|
15513
|
+
}
|
|
15514
|
+
return { declaration: declarationFile, layout };
|
|
15515
|
+
}
|
|
15516
|
+
|
|
14781
15517
|
// src/export.ts
|
|
14782
|
-
import { mkdirSync as
|
|
14783
|
-
import { join as
|
|
15518
|
+
import { mkdirSync as mkdirSync8, writeFileSync as writeFileSync7, copyFileSync, readFileSync as readFileSync21, readdirSync as readdirSync10, existsSync as existsSync20 } from "node:fs";
|
|
15519
|
+
import { join as join28, dirname as dirname9, basename as basename5 } from "node:path";
|
|
14784
15520
|
import { fileURLToPath as fileURLToPath5 } from "node:url";
|
|
14785
15521
|
var LENS_PARAMS = ["collapse", "components", "detail", "env", "logical", "ops", "radial", "stacks", "tier"];
|
|
14786
15522
|
function canonicalKey(path, params) {
|
|
@@ -14829,31 +15565,31 @@ function captureKeys(axes) {
|
|
|
14829
15565
|
return [...keys];
|
|
14830
15566
|
}
|
|
14831
15567
|
function webDir() {
|
|
14832
|
-
return
|
|
15568
|
+
return join28(dirname9(fileURLToPath5(import.meta.url)), "..", "web");
|
|
14833
15569
|
}
|
|
14834
15570
|
function pkgRoot2() {
|
|
14835
|
-
return
|
|
15571
|
+
return join28(dirname9(fileURLToPath5(import.meta.url)), "..");
|
|
14836
15572
|
}
|
|
14837
15573
|
var NOTICE_FILES = ["LICENSE", "THIRD_PARTY.md"];
|
|
14838
15574
|
function copyNotices(outDir, root = pkgRoot2()) {
|
|
14839
15575
|
const copied = [];
|
|
14840
15576
|
for (const f of NOTICE_FILES) {
|
|
14841
|
-
if (!
|
|
14842
|
-
copyFileSync(
|
|
15577
|
+
if (!existsSync20(join28(root, f))) continue;
|
|
15578
|
+
copyFileSync(join28(root, f), join28(outDir, f));
|
|
14843
15579
|
copied.push(f);
|
|
14844
15580
|
}
|
|
14845
|
-
const lic =
|
|
14846
|
-
if (
|
|
14847
|
-
|
|
15581
|
+
const lic = join28(root, "licenses");
|
|
15582
|
+
if (existsSync20(lic)) {
|
|
15583
|
+
mkdirSync8(join28(outDir, "licenses"), { recursive: true });
|
|
14848
15584
|
for (const f of readdirSync10(lic)) {
|
|
14849
|
-
copyFileSync(
|
|
15585
|
+
copyFileSync(join28(lic, f), join28(outDir, "licenses", f));
|
|
14850
15586
|
copied.push(`licenses/${f}`);
|
|
14851
15587
|
}
|
|
14852
15588
|
}
|
|
14853
15589
|
return copied;
|
|
14854
15590
|
}
|
|
14855
15591
|
function workerName(project, override) {
|
|
14856
|
-
const raw = override ?? `behold-${
|
|
15592
|
+
const raw = override ?? `behold-${basename5(project)}`;
|
|
14857
15593
|
const name = raw.toLowerCase().replace(/[^a-z0-9-]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 63);
|
|
14858
15594
|
return name || "behold-export";
|
|
14859
15595
|
}
|
|
@@ -14861,8 +15597,8 @@ async function runExport(cfg, outDir, opts = {}) {
|
|
|
14861
15597
|
const app = createApp({ ...cfg, layoutWrites: false });
|
|
14862
15598
|
const proj = await (await app.request("/api/project")).json();
|
|
14863
15599
|
const axes = { environments: proj.environments ?? [], tiers: proj.tiers ?? [], ...proj.ops ? { ops: proj.ops } : {} };
|
|
14864
|
-
const snapDir =
|
|
14865
|
-
|
|
15600
|
+
const snapDir = join28(outDir, "snapshots");
|
|
15601
|
+
mkdirSync8(snapDir, { recursive: true });
|
|
14866
15602
|
const keyToFile = {};
|
|
14867
15603
|
let ok = 0;
|
|
14868
15604
|
let failed = 0;
|
|
@@ -14870,7 +15606,7 @@ async function runExport(cfg, outDir, opts = {}) {
|
|
|
14870
15606
|
const res = await app.request(`${key}${key.includes("?") ? "&" : "?"}layout=1`);
|
|
14871
15607
|
const body = await res.text();
|
|
14872
15608
|
const file = slug3(key);
|
|
14873
|
-
|
|
15609
|
+
writeFileSync7(join28(snapDir, file), body);
|
|
14874
15610
|
keyToFile[key] = `snapshots/${file}`;
|
|
14875
15611
|
if (res.ok) ok++;
|
|
14876
15612
|
else failed++;
|
|
@@ -14882,22 +15618,22 @@ async function runExport(cfg, outDir, opts = {}) {
|
|
|
14882
15618
|
axes,
|
|
14883
15619
|
keyToFile
|
|
14884
15620
|
};
|
|
14885
|
-
|
|
14886
|
-
const html =
|
|
15621
|
+
writeFileSync7(join28(outDir, "manifest.json"), JSON.stringify(manifest, null, 2));
|
|
15622
|
+
const html = readFileSync21(join28(webDir(), "index.html"), "utf8").replace(
|
|
14887
15623
|
/<\/head>/i,
|
|
14888
15624
|
` <script>window.__BEHOLD_STATIC__ = true;</script>
|
|
14889
15625
|
</head>`
|
|
14890
15626
|
);
|
|
14891
|
-
|
|
15627
|
+
writeFileSync7(join28(outDir, "index.html"), html);
|
|
14892
15628
|
for (const f of readdirSync10(webDir())) {
|
|
14893
15629
|
if (f === "index.html") continue;
|
|
14894
|
-
copyFileSync(
|
|
15630
|
+
copyFileSync(join28(webDir(), f), join28(outDir, f));
|
|
14895
15631
|
}
|
|
14896
|
-
|
|
15632
|
+
writeFileSync7(join28(outDir, "README.md"), BUNDLE_README);
|
|
14897
15633
|
copyNotices(outDir);
|
|
14898
15634
|
const name = workerName(cfg.projectDir, opts.name);
|
|
14899
|
-
|
|
14900
|
-
|
|
15635
|
+
writeFileSync7(
|
|
15636
|
+
join28(outDir, "wrangler.jsonc"),
|
|
14901
15637
|
JSON.stringify(
|
|
14902
15638
|
{ $schema: "node_modules/wrangler/config-schema.json", name, compatibility_date: "2025-06-01", assets: { directory: "." } },
|
|
14903
15639
|
null,
|
|
@@ -14949,12 +15685,13 @@ init_project();
|
|
|
14949
15685
|
init_member_kind();
|
|
14950
15686
|
init_choudoufu_member();
|
|
14951
15687
|
init_carve_lens();
|
|
15688
|
+
init_terraform_member();
|
|
14952
15689
|
var USAGE = `behold \u2014 a live control plane on chant (read-only core)
|
|
14953
15690
|
|
|
14954
15691
|
Usage:
|
|
14955
15692
|
behold demo [name] [target-dir] [--port <n>] [--list]
|
|
14956
15693
|
behold demo carve [--live] [--port <n>]
|
|
14957
|
-
behold doctor [project-dir] [--json]
|
|
15694
|
+
behold doctor [project-dir] [--json] [--fix]
|
|
14958
15695
|
behold preview [project-dir] [--port <n>] [--emulator]
|
|
14959
15696
|
behold export [project-dir] [--out <dir>] [--env <name>] [--name <worker>] [--emulator]
|
|
14960
15697
|
behold serve <project-dir\u2026> [--port <n>] [--env <name>] [--poll <secs>] [--local]
|
|
@@ -15048,6 +15785,10 @@ Options:
|
|
|
15048
15785
|
with chant's generic \`chant emulator up\`.
|
|
15049
15786
|
--json doctor only: the report as JSON (stable keys) instead of
|
|
15050
15787
|
the console lines \u2014 the AGENTS.md audience.
|
|
15788
|
+
--fix doctor only: write chant.workspace.json for an estate
|
|
15789
|
+
root that lists its members in .behold.json, move its
|
|
15790
|
+
saved layout to the root, and run chant workspace check.
|
|
15791
|
+
The one doctor that writes (#464).
|
|
15051
15792
|
--out <dir> export only: output directory (default ./behold-export).
|
|
15052
15793
|
--name <worker> export only: Cloudflare Worker name in the generated
|
|
15053
15794
|
wrangler.jsonc.
|
|
@@ -15139,7 +15880,22 @@ ${USAGE}`);
|
|
|
15139
15880
|
process.stderr.write("behold serve: --auto-sync needs --env and --poll (it acts on polled drift)\n");
|
|
15140
15881
|
process.exit(2);
|
|
15141
15882
|
}
|
|
15142
|
-
const dirs = projectDirs.map((d) =>
|
|
15883
|
+
const dirs = projectDirs.map((d) => resolve17(d));
|
|
15884
|
+
const workspace = dirs.length === 1 && hasDeclaration(dirs[0]) ? await declaredWorkspace(dirs[0]) : void 0;
|
|
15885
|
+
if (workspace) {
|
|
15886
|
+
const drawn = drawnMembers(workspace).map((m) => m.abs);
|
|
15887
|
+
await startServer({
|
|
15888
|
+
projectDir: drawn[0] ?? workspace.root,
|
|
15889
|
+
projectDirs: drawn,
|
|
15890
|
+
workspace,
|
|
15891
|
+
port,
|
|
15892
|
+
...env ? { env } : {},
|
|
15893
|
+
...pollSecs !== void 0 ? { pollSecs } : {},
|
|
15894
|
+
...autoSync !== "off" ? { autoSync } : {},
|
|
15895
|
+
...local ? { local: true } : {}
|
|
15896
|
+
});
|
|
15897
|
+
return;
|
|
15898
|
+
}
|
|
15143
15899
|
for (const d of dirs) warnIfNotChantProject(d);
|
|
15144
15900
|
await startServer({
|
|
15145
15901
|
projectDir: dirs[0],
|
|
@@ -15156,6 +15912,28 @@ ${USAGE}`);
|
|
|
15156
15912
|
...local ? { local: true } : {}
|
|
15157
15913
|
});
|
|
15158
15914
|
}
|
|
15915
|
+
async function declaredWorkspace(root) {
|
|
15916
|
+
const read = await readWorkspace(root);
|
|
15917
|
+
if (!read.ok) {
|
|
15918
|
+
process.stderr.write(`behold serve: ${read.refusal.error}
|
|
15919
|
+
${read.refusal.remedy}
|
|
15920
|
+
`);
|
|
15921
|
+
process.exit(1);
|
|
15922
|
+
}
|
|
15923
|
+
const ws = read.workspace;
|
|
15924
|
+
const drawn = drawnMembers(ws);
|
|
15925
|
+
process.stdout.write(`behold: serving chant workspace ${ws.name} (${ws.file}), ${drawn.length} of ${ws.members.length} members drawn
|
|
15926
|
+
`);
|
|
15927
|
+
for (const m of unreadableMembers(ws)) process.stdout.write(` ${m.name}: unreadable, ${m.reason.code}: ${m.reason.message}
|
|
15928
|
+
`);
|
|
15929
|
+
for (const m of undrawnMembers(ws)) process.stdout.write(` ${m.name}: kind ${m.kind}, listed and not drawn${m.because ? ` (${m.because})` : ""}
|
|
15930
|
+
`);
|
|
15931
|
+
if (loadBeholdConfig(root).members) {
|
|
15932
|
+
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.
|
|
15933
|
+
`);
|
|
15934
|
+
}
|
|
15935
|
+
return ws;
|
|
15936
|
+
}
|
|
15159
15937
|
function warnIfNotChantProject(dir) {
|
|
15160
15938
|
const shape = detectProjectShape(dir);
|
|
15161
15939
|
if (shape.kind === "project") return;
|
|
@@ -15163,7 +15941,7 @@ function warnIfNotChantProject(dir) {
|
|
|
15163
15941
|
if (shape.kind === "estate") {
|
|
15164
15942
|
process.stderr.write(
|
|
15165
15943
|
`behold: warning \u2014 ${dir} is an estate root, not a chant project itself.
|
|
15166
|
-
Serve its members composed: behold serve ${shape.members.map((m) =>
|
|
15944
|
+
Serve its members composed: behold serve ${shape.members.map((m) => join29(dir, m.dir)).join(" ")}
|
|
15167
15945
|
`
|
|
15168
15946
|
);
|
|
15169
15947
|
return;
|
|
@@ -15198,8 +15976,8 @@ async function runCarve(rest) {
|
|
|
15198
15976
|
);
|
|
15199
15977
|
process.exit(2);
|
|
15200
15978
|
}
|
|
15201
|
-
const reportPath =
|
|
15202
|
-
const parsed = readCarveReport(reportPath, (p) =>
|
|
15979
|
+
const reportPath = resolve17(fileArg);
|
|
15980
|
+
const parsed = readCarveReport(reportPath, (p) => readFileSync22(p, "utf8"));
|
|
15203
15981
|
if (!parsed.ok) {
|
|
15204
15982
|
process.stderr.write(`behold carve: ${parsed.refusal.error}
|
|
15205
15983
|
${parsed.refusal.remedy}
|
|
@@ -15210,13 +15988,15 @@ async function runCarve(rest) {
|
|
|
15210
15988
|
`behold carve \u2014 ${parsed.report.resources.length} resource(s)/module(s) ranked${parsed.report.from ? ` from ${parsed.report.from}` : ""}
|
|
15211
15989
|
`
|
|
15212
15990
|
);
|
|
15213
|
-
await startServer({ projectDir:
|
|
15991
|
+
await startServer({ projectDir: dirname10(reportPath), carveReport: reportPath, port });
|
|
15214
15992
|
}
|
|
15215
15993
|
async function runDoctor(rest) {
|
|
15216
15994
|
let json = false;
|
|
15995
|
+
let fix = false;
|
|
15217
15996
|
let dirArg;
|
|
15218
15997
|
for (const a of rest) {
|
|
15219
15998
|
if (a === "--json") json = true;
|
|
15999
|
+
else if (a === "--fix") fix = true;
|
|
15220
16000
|
else if (a === "-h" || a === "--help") return void process.stdout.write(USAGE);
|
|
15221
16001
|
else if (!a.startsWith("-")) dirArg = a;
|
|
15222
16002
|
else {
|
|
@@ -15226,17 +16006,44 @@ async function runDoctor(rest) {
|
|
|
15226
16006
|
}
|
|
15227
16007
|
}
|
|
15228
16008
|
const dir = dirArg ?? ".";
|
|
15229
|
-
if (!
|
|
15230
|
-
process.stderr.write(`behold doctor: no such directory: ${
|
|
16009
|
+
if (!existsSync21(resolve17(dir))) {
|
|
16010
|
+
process.stderr.write(`behold doctor: no such directory: ${resolve17(dir)}
|
|
15231
16011
|
`);
|
|
15232
16012
|
process.exit(2);
|
|
15233
16013
|
}
|
|
16014
|
+
if (fix) return runDoctorFix(dir);
|
|
15234
16015
|
const report = await diagnose(dir);
|
|
15235
16016
|
process.stdout.write(json ? JSON.stringify(report, null, 2) + "\n" : formatReport(report));
|
|
15236
16017
|
if (!report.ok) process.exitCode = 1;
|
|
15237
16018
|
}
|
|
16019
|
+
async function runDoctorFix(dir) {
|
|
16020
|
+
const planned = planConversion(dir);
|
|
16021
|
+
if (!planned.ok) {
|
|
16022
|
+
process.stderr.write(`behold doctor --fix: ${planned.error}
|
|
16023
|
+
`);
|
|
16024
|
+
process.exitCode = 1;
|
|
16025
|
+
return;
|
|
16026
|
+
}
|
|
16027
|
+
const { plan } = planned;
|
|
16028
|
+
const wrote = applyConversion(plan);
|
|
16029
|
+
process.stdout.write(`wrote ${wrote.declaration} (${plan.declaration.members.length} members)
|
|
16030
|
+
`);
|
|
16031
|
+
if ("wrote" in wrote.layout) process.stdout.write(`moved the saved layout to ${wrote.layout.wrote} (${wrote.layout.ids} node ids)
|
|
16032
|
+
`);
|
|
16033
|
+
else process.stdout.write(`layout: ${wrote.layout.skipped}
|
|
16034
|
+
`);
|
|
16035
|
+
for (const n of plan.notes) process.stdout.write(`note: ${n}
|
|
16036
|
+
`);
|
|
16037
|
+
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.
|
|
16038
|
+
`);
|
|
16039
|
+
const check = await runChantRaw(["workspace", "check", "--format", "json"], plan.root);
|
|
16040
|
+
process.stdout.write(check.code === 0 ? "chant workspace check: passed\n" : `chant workspace check failed (exit ${check.code}):
|
|
16041
|
+
${check.stdout || check.stderr}
|
|
16042
|
+
`);
|
|
16043
|
+
if (check.code !== 0) process.exitCode = 1;
|
|
16044
|
+
}
|
|
15238
16045
|
async function runDemo(rest) {
|
|
15239
|
-
const pkgRoot3 =
|
|
16046
|
+
const pkgRoot3 = join29(dirname10(fileURLToPath6(import.meta.url)), "..");
|
|
15240
16047
|
const registry2 = loadDemoRegistry(pkgRoot3);
|
|
15241
16048
|
let port = 4600;
|
|
15242
16049
|
let live2 = false;
|
|
@@ -15299,7 +16106,7 @@ async function runDemo(rest) {
|
|
|
15299
16106
|
`);
|
|
15300
16107
|
process.exit(2);
|
|
15301
16108
|
}
|
|
15302
|
-
const target = dirArg ?
|
|
16109
|
+
const target = dirArg ? resolve17(dirArg) : demoTargetDir(entry);
|
|
15303
16110
|
const loaded = await loadDemo(entry, { pkgRoot: pkgRoot3, target, log: (line) => process.stdout.write(line + "\n") });
|
|
15304
16111
|
if (!loaded.ok) {
|
|
15305
16112
|
process.stderr.write(`behold demo ${entry.name}: ${loaded.error}
|
|
@@ -15331,7 +16138,7 @@ function spawnStep(cmd, args, cwd) {
|
|
|
15331
16138
|
});
|
|
15332
16139
|
}
|
|
15333
16140
|
async function serveCarveDemo(target, carve, port, live2 = false) {
|
|
15334
|
-
const at = (rel) =>
|
|
16141
|
+
const at = (rel) => resolve17(target, rel);
|
|
15335
16142
|
const project = at(carve.project);
|
|
15336
16143
|
const from = at(carve.from);
|
|
15337
16144
|
const state = carve.state ? at(carve.state) : void 0;
|
|
@@ -15371,7 +16178,7 @@ async function serveCarveDemo(target, carve, port, live2 = false) {
|
|
|
15371
16178
|
};
|
|
15372
16179
|
process.stdout.write("behold demo carve --live \u2192 the tfstate is real now; the advisor reads what terraform wrote.\n");
|
|
15373
16180
|
}
|
|
15374
|
-
if (
|
|
16181
|
+
if (existsSync21(join29(project, "package.json")) && !existsSync21(join29(project, "node_modules"))) {
|
|
15375
16182
|
process.stdout.write(`behold demo carve \u2192 npm install in ${carve.project}/ (the chant this walkthrough shells)\u2026
|
|
15376
16183
|
`);
|
|
15377
16184
|
const code = await spawnStep("npm", ["install"], project);
|
|
@@ -15382,10 +16189,11 @@ async function serveCarveDemo(target, carve, port, live2 = false) {
|
|
|
15382
16189
|
}
|
|
15383
16190
|
}
|
|
15384
16191
|
let degraded;
|
|
15385
|
-
if (!
|
|
15386
|
-
process.stdout.write(
|
|
15387
|
-
|
|
15388
|
-
|
|
16192
|
+
if (!existsSync21(join29(target, "node_modules", ...HCL_PARSER_PKG.split("/")))) {
|
|
16193
|
+
process.stdout.write(`behold demo carve \u2192 npm install ${HCL_PARSER_PKG} (chant's HCL parser, ~2MB, once)\u2026
|
|
16194
|
+
`);
|
|
16195
|
+
const code = await spawnStep("npm", ["install", "--no-save", "--no-package-lock", HCL_PARSER_PKG], target);
|
|
16196
|
+
if (code !== 0) degraded = `couldn't install ${HCL_PARSER_PKG} (chant's HCL parser) \u2014 no network?`;
|
|
15389
16197
|
}
|
|
15390
16198
|
const report = at("carve-report.json");
|
|
15391
16199
|
if (!degraded) {
|
|
@@ -15398,7 +16206,7 @@ async function serveCarveDemo(target, carve, port, live2 = false) {
|
|
|
15398
16206
|
);
|
|
15399
16207
|
if (code !== 0) degraded = `chant carve advise exited ${code}`;
|
|
15400
16208
|
}
|
|
15401
|
-
const serving = !degraded &&
|
|
16209
|
+
const serving = !degraded && existsSync21(report) ? report : committed;
|
|
15402
16210
|
if (degraded) {
|
|
15403
16211
|
process.stderr.write(
|
|
15404
16212
|
`behold demo carve: ${degraded}
|
|
@@ -15406,7 +16214,7 @@ async function serveCarveDemo(target, carve, port, live2 = false) {
|
|
|
15406
16214
|
`
|
|
15407
16215
|
);
|
|
15408
16216
|
}
|
|
15409
|
-
if (!
|
|
16217
|
+
if (!existsSync21(serving)) {
|
|
15410
16218
|
process.stderr.write(`behold demo carve: no carve report at ${serving}
|
|
15411
16219
|
`);
|
|
15412
16220
|
process.exit(2);
|
|
@@ -15451,8 +16259,8 @@ async function runPreview(rest) {
|
|
|
15451
16259
|
process.stderr.write("behold preview: --port must be a number\n");
|
|
15452
16260
|
process.exit(2);
|
|
15453
16261
|
}
|
|
15454
|
-
const projectDir =
|
|
15455
|
-
if (!
|
|
16262
|
+
const projectDir = resolve17(dirArg ?? process.cwd());
|
|
16263
|
+
if (!existsSync21(projectDir)) {
|
|
15456
16264
|
process.stderr.write(`behold preview: project not found at ${projectDir}
|
|
15457
16265
|
`);
|
|
15458
16266
|
process.exit(2);
|
|
@@ -15472,26 +16280,26 @@ async function runPreview(rest) {
|
|
|
15472
16280
|
await startServer({ projectDir, port, env: "local", previewMode: true });
|
|
15473
16281
|
}
|
|
15474
16282
|
async function runExportCmd(rest) {
|
|
15475
|
-
let outDir =
|
|
16283
|
+
let outDir = resolve17("behold-export");
|
|
15476
16284
|
let env;
|
|
15477
16285
|
let name;
|
|
15478
16286
|
let dirArg;
|
|
15479
16287
|
let emulator = false;
|
|
15480
16288
|
for (let i = 0; i < rest.length; i++) {
|
|
15481
16289
|
const a = rest[i];
|
|
15482
|
-
if (a === "--out") outDir =
|
|
16290
|
+
if (a === "--out") outDir = resolve17(rest[++i]);
|
|
15483
16291
|
else if (a === "--env") env = rest[++i];
|
|
15484
16292
|
else if (a === "--name") name = rest[++i];
|
|
15485
16293
|
else if (a === "--emulator") emulator = true;
|
|
15486
16294
|
else if (a === "-h" || a === "--help") return void process.stdout.write(USAGE);
|
|
15487
16295
|
else if (!a.startsWith("-")) dirArg = a;
|
|
15488
16296
|
}
|
|
15489
|
-
const projectDir =
|
|
16297
|
+
const projectDir = resolve17(dirArg ?? process.cwd());
|
|
15490
16298
|
if (emulator) {
|
|
15491
16299
|
injectEmulatorEnv(env);
|
|
15492
16300
|
env ??= "local";
|
|
15493
16301
|
}
|
|
15494
|
-
if (!
|
|
16302
|
+
if (!existsSync21(projectDir)) {
|
|
15495
16303
|
process.stderr.write(`behold export: project not found at ${projectDir}
|
|
15496
16304
|
`);
|
|
15497
16305
|
process.exit(2);
|