@intentius/behold 0.20.0 → 0.22.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/README.md +53 -0
- package/dist/cli.js +301 -65
- package/package.json +1 -1
- package/web/app.js +384 -9
- package/web/embed.js +78 -0
- package/web/embed.test.js +51 -0
- package/web/index.html +20 -0
- package/web/operator.js +14 -1
- package/web/panel.js +11 -3
- package/web/run-playhead.js +13 -1
- package/web/view-url.js +151 -0
- package/web/view-url.test.js +109 -0
- package/web/workspace-gates.js +44 -0
- package/web/workspace-gates.test.js +34 -0
package/dist/cli.js
CHANGED
|
@@ -118,17 +118,17 @@ var init_read_scheduler = __esm({
|
|
|
118
118
|
recordRefused();
|
|
119
119
|
return Promise.reject(new Error("Chant read queue is full; retry after current reads finish"));
|
|
120
120
|
}
|
|
121
|
-
let
|
|
121
|
+
let resolve19;
|
|
122
122
|
let reject;
|
|
123
123
|
const promise = new Promise((yes, no) => {
|
|
124
|
-
|
|
124
|
+
resolve19 = yes;
|
|
125
125
|
reject = no;
|
|
126
126
|
});
|
|
127
127
|
job = {
|
|
128
128
|
key,
|
|
129
129
|
run: run3,
|
|
130
130
|
promise,
|
|
131
|
-
resolve:
|
|
131
|
+
resolve: resolve19,
|
|
132
132
|
reject,
|
|
133
133
|
controller: new AbortController(),
|
|
134
134
|
users: 0,
|
|
@@ -142,7 +142,7 @@ var init_read_scheduler = __esm({
|
|
|
142
142
|
}
|
|
143
143
|
const shared2 = job;
|
|
144
144
|
shared2.users++;
|
|
145
|
-
const result = new Promise((
|
|
145
|
+
const result = new Promise((resolve19, reject) => {
|
|
146
146
|
let done = false;
|
|
147
147
|
const finish = (error, value, failed = false) => {
|
|
148
148
|
if (done) return;
|
|
@@ -150,7 +150,7 @@ var init_read_scheduler = __esm({
|
|
|
150
150
|
signal?.removeEventListener("abort", abort);
|
|
151
151
|
shared2.users--;
|
|
152
152
|
if (failed) reject(error);
|
|
153
|
-
else
|
|
153
|
+
else resolve19(value);
|
|
154
154
|
};
|
|
155
155
|
const abort = () => {
|
|
156
156
|
finish(signal?.reason, void 0, true);
|
|
@@ -1038,11 +1038,11 @@ async function createK8sClient(options = {}) {
|
|
|
1038
1038
|
}
|
|
1039
1039
|
return void 0;
|
|
1040
1040
|
}
|
|
1041
|
-
async function
|
|
1041
|
+
async function resolve19(selector, signal) {
|
|
1042
1042
|
return "apiVersion" in selector ? resolveByGvk(selector.apiVersion, selector.kind, signal) : resolveByResourceString(selector.resource, selector.group, signal);
|
|
1043
1043
|
}
|
|
1044
1044
|
async function resolveOrThrow(selector, signal) {
|
|
1045
|
-
const info = await
|
|
1045
|
+
const info = await resolve19(selector, signal);
|
|
1046
1046
|
if (!info) throw new UnknownResourceError(selectorText(selector));
|
|
1047
1047
|
return info;
|
|
1048
1048
|
}
|
|
@@ -1054,7 +1054,7 @@ async function createK8sClient(options = {}) {
|
|
|
1054
1054
|
return parts.join("/");
|
|
1055
1055
|
}
|
|
1056
1056
|
async function pathFor(ref, signal) {
|
|
1057
|
-
const info = await
|
|
1057
|
+
const info = await resolve19({ apiVersion: ref.apiVersion, kind: ref.kind }, signal);
|
|
1058
1058
|
if (!info) return void 0;
|
|
1059
1059
|
return objectPath(info, ref.name, ref.namespace);
|
|
1060
1060
|
}
|
|
@@ -1168,8 +1168,8 @@ async function createK8sClient(options = {}) {
|
|
|
1168
1168
|
const { lines } = await openStream(listPath, query, controller.signal, target);
|
|
1169
1169
|
let expired = false;
|
|
1170
1170
|
const iterator = lines[Symbol.asyncIterator]();
|
|
1171
|
-
const stopped = new Promise((
|
|
1172
|
-
const finish = () =>
|
|
1171
|
+
const stopped = new Promise((resolve20) => {
|
|
1172
|
+
const finish = () => resolve20({ done: true, value: void 0 });
|
|
1173
1173
|
if (controller.signal.aborted) finish();
|
|
1174
1174
|
else controller.signal.addEventListener("abort", finish, { once: true });
|
|
1175
1175
|
});
|
|
@@ -1196,11 +1196,11 @@ async function createK8sClient(options = {}) {
|
|
|
1196
1196
|
resourceVersion = await listResourceVersion();
|
|
1197
1197
|
continue;
|
|
1198
1198
|
}
|
|
1199
|
-
await new Promise((
|
|
1200
|
-
const timer = setTimeout(
|
|
1199
|
+
await new Promise((resolve20) => {
|
|
1200
|
+
const timer = setTimeout(resolve20, reopenDelayMs);
|
|
1201
1201
|
controller.signal.addEventListener("abort", () => {
|
|
1202
1202
|
clearTimeout(timer);
|
|
1203
|
-
|
|
1203
|
+
resolve20(void 0);
|
|
1204
1204
|
}, { once: true });
|
|
1205
1205
|
});
|
|
1206
1206
|
}
|
|
@@ -1318,7 +1318,7 @@ async function createK8sClient(options = {}) {
|
|
|
1318
1318
|
return {
|
|
1319
1319
|
provenance,
|
|
1320
1320
|
defaultNamespace,
|
|
1321
|
-
resolve:
|
|
1321
|
+
resolve: resolve19,
|
|
1322
1322
|
read,
|
|
1323
1323
|
pathFor,
|
|
1324
1324
|
readIfPresent,
|
|
@@ -4384,7 +4384,7 @@ var init_chant = __esm({
|
|
|
4384
4384
|
});
|
|
4385
4385
|
|
|
4386
4386
|
// src/cli.ts
|
|
4387
|
-
import { resolve as
|
|
4387
|
+
import { resolve as resolve18, dirname as dirname10, join as join29 } from "node:path";
|
|
4388
4388
|
import { realpathSync as realpathSync2, existsSync as existsSync21, readFileSync as readFileSync22, rmSync as rmSync4 } from "node:fs";
|
|
4389
4389
|
import { fileURLToPath as fileURLToPath6 } from "node:url";
|
|
4390
4390
|
import { spawn as spawn7, spawnSync as spawnSync3 } from "node:child_process";
|
|
@@ -4444,7 +4444,7 @@ import { fileURLToPath as fileURLToPath4 } from "node:url";
|
|
|
4444
4444
|
import { execFile as execFile3 } from "node:child_process";
|
|
4445
4445
|
import { promisify as promisify3 } from "node:util";
|
|
4446
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
|
|
4447
|
+
import { basename as basename3, dirname as dirname7, join as join26, relative as relative8, resolve as resolve16, sep as sep7 } from "node:path";
|
|
4448
4448
|
|
|
4449
4449
|
// src/recents.ts
|
|
4450
4450
|
import { readFileSync, writeFileSync, mkdirSync, existsSync } from "node:fs";
|
|
@@ -4686,19 +4686,19 @@ function finishPipelineProgress(state, exitCode) {
|
|
|
4686
4686
|
}
|
|
4687
4687
|
|
|
4688
4688
|
// src/gh-run.ts
|
|
4689
|
-
var defaultGhExec = (args) => new Promise((
|
|
4689
|
+
var defaultGhExec = (args) => new Promise((resolve19) => {
|
|
4690
4690
|
let out = "";
|
|
4691
4691
|
let proc;
|
|
4692
4692
|
try {
|
|
4693
4693
|
proc = spawn3("gh", args, { stdio: ["ignore", "pipe", "pipe"] });
|
|
4694
4694
|
} catch {
|
|
4695
|
-
|
|
4695
|
+
resolve19({ code: 127, out: "" });
|
|
4696
4696
|
return;
|
|
4697
4697
|
}
|
|
4698
4698
|
proc.stdout.on("data", (d) => out += d);
|
|
4699
4699
|
proc.stderr.on("data", (d) => out += d);
|
|
4700
|
-
proc.on("error", () =>
|
|
4701
|
-
proc.on("close", (code) =>
|
|
4700
|
+
proc.on("error", () => resolve19({ code: 127, out }));
|
|
4701
|
+
proc.on("close", (code) => resolve19({ code: code ?? 1, out }));
|
|
4702
4702
|
});
|
|
4703
4703
|
async function ghReady(exec = defaultGhExec) {
|
|
4704
4704
|
const { code, out } = await exec(["auth", "status"]);
|
|
@@ -6600,14 +6600,30 @@ function forMember(dir, opts) {
|
|
|
6600
6600
|
}
|
|
6601
6601
|
var memberSource = (dir, opts) => memberIr(dir, forMember(dir, opts), memberViaFor(dir));
|
|
6602
6602
|
var memberLive = (dir, opts, fresh) => overlayIr(dir, forMember(dir, opts), memberViaFor(dir), fresh);
|
|
6603
|
+
async function memberSourceOrBox(dir, opts) {
|
|
6604
|
+
try {
|
|
6605
|
+
return joinCarvedSources(await memberSource(dir, opts), (await carveStatesFor(dir)).values(), dir).ir;
|
|
6606
|
+
} catch (err) {
|
|
6607
|
+
const box = declaredMemberBox(dir, err);
|
|
6608
|
+
if (!box) throw err;
|
|
6609
|
+
return box;
|
|
6610
|
+
}
|
|
6611
|
+
}
|
|
6612
|
+
function declaredMemberBox(dir, err) {
|
|
6613
|
+
const m = servedWorkspace() ? workspaceMemberOf(dir) : void 0;
|
|
6614
|
+
if (!m) return void 0;
|
|
6615
|
+
const said4 = (e) => {
|
|
6616
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
6617
|
+
return (msg.match(/exited \d+:\s*([\s\S]*)/)?.[1] ?? msg).trim().slice(0, 600);
|
|
6618
|
+
};
|
|
6619
|
+
const reason = err instanceof WorkspaceMemberError ? err.reason : { code: "command-failed", message: said4(err) };
|
|
6620
|
+
return unreadableMemberIr({ ...m, reason: { ...reason, message: reason.message.replace(/\u001b\[[0-9;]*m/g, "") } });
|
|
6621
|
+
}
|
|
6603
6622
|
async function composeEstate(projectDirs, opts = {}) {
|
|
6604
6623
|
const names = estateMemberNames(projectDirs);
|
|
6605
6624
|
const stacks = await mapPool(projectDirs, estateReadPool(projectDirs.length), async (dir, i) => ({
|
|
6606
6625
|
name: names[i],
|
|
6607
|
-
|
|
6608
|
-
// entities were emitted into — joined here, where the member's root is
|
|
6609
|
-
// known, onto a COPY (the member IR is the #312 cache's own object).
|
|
6610
|
-
ir: joinCarvedSources(await memberSource(dir, opts), (await carveStatesFor(dir)).values(), dir).ir
|
|
6626
|
+
ir: await memberSourceOrBox(dir, opts)
|
|
6611
6627
|
}));
|
|
6612
6628
|
return composeStacks([...stacks, ...unreadableStacks(projectDirs)]);
|
|
6613
6629
|
}
|
|
@@ -6615,8 +6631,8 @@ async function composeEstatePending(projectDirs, opts = {}) {
|
|
|
6615
6631
|
const names = estateMemberNames(projectDirs);
|
|
6616
6632
|
const { env: _env, live: _live, overlay: _overlay, ...srcOpts } = opts;
|
|
6617
6633
|
const stacks = await mapPool(projectDirs, estateReadPool(projectDirs.length), async (dir, i) => {
|
|
6618
|
-
const ir =
|
|
6619
|
-
for (const n of ir.nodes) n.attrs = { ...n.attrs, _pendingRead: true };
|
|
6634
|
+
const ir = await memberSourceOrBox(dir, srcOpts);
|
|
6635
|
+
for (const n of ir.nodes) if (n.kind !== "UnreadableMember") n.attrs = { ...n.attrs, _pendingRead: true };
|
|
6620
6636
|
return { name: names[i], ir };
|
|
6621
6637
|
});
|
|
6622
6638
|
return composeStacks([...stacks, ...unreadableStacks(projectDirs)]);
|
|
@@ -6757,7 +6773,11 @@ async function composeEstateOverlay(projectDirs, opts, classify, observe = {}) {
|
|
|
6757
6773
|
observe.onMember?.({ name, dir, ir: src, unobserved: reason });
|
|
6758
6774
|
unobserved.push({ name, reason });
|
|
6759
6775
|
} catch (err2) {
|
|
6760
|
-
|
|
6776
|
+
const box = declaredMemberBox(dir, err2);
|
|
6777
|
+
if (box) {
|
|
6778
|
+
stacks[i] = { name, ir: box };
|
|
6779
|
+
observe.onMember?.({ name, dir, ir: box });
|
|
6780
|
+
} else dropped.push({ name, reason: firstLine2(err2) });
|
|
6761
6781
|
}
|
|
6762
6782
|
}
|
|
6763
6783
|
});
|
|
@@ -11502,19 +11522,19 @@ import { join as join20 } from "node:path";
|
|
|
11502
11522
|
import { platform } from "node:os";
|
|
11503
11523
|
var DOCKER_DOWN_DETAIL = "docker is down";
|
|
11504
11524
|
function probe(cmd, args) {
|
|
11505
|
-
return new Promise((
|
|
11525
|
+
return new Promise((resolve19) => {
|
|
11506
11526
|
let out = "";
|
|
11507
11527
|
let proc;
|
|
11508
11528
|
try {
|
|
11509
11529
|
proc = spawn5(cmd, args, { stdio: ["ignore", "pipe", "pipe"] });
|
|
11510
11530
|
} catch {
|
|
11511
|
-
|
|
11531
|
+
resolve19({ code: 127, out: "" });
|
|
11512
11532
|
return;
|
|
11513
11533
|
}
|
|
11514
11534
|
proc.stdout.on("data", (d) => out += d);
|
|
11515
11535
|
proc.stderr.on("data", (d) => out += d);
|
|
11516
|
-
proc.on("error", () =>
|
|
11517
|
-
proc.on("close", (code) =>
|
|
11536
|
+
proc.on("error", () => resolve19({ code: 127, out }));
|
|
11537
|
+
proc.on("close", (code) => resolve19({ code: code ?? 1, out }));
|
|
11518
11538
|
});
|
|
11519
11539
|
}
|
|
11520
11540
|
async function dockerAvailable(run3) {
|
|
@@ -12611,6 +12631,142 @@ async function moveReceipt(plan, members, run3 = captureChoudoufu) {
|
|
|
12611
12631
|
};
|
|
12612
12632
|
}
|
|
12613
12633
|
|
|
12634
|
+
// src/workspace-gates.ts
|
|
12635
|
+
init_chant();
|
|
12636
|
+
import { resolve as resolve13 } from "node:path";
|
|
12637
|
+
var isRecord11 = (v) => v !== null && typeof v === "object" && !Array.isArray(v);
|
|
12638
|
+
var str7 = (v) => typeof v === "string" && v ? v : null;
|
|
12639
|
+
function gateKey(member, op, gate) {
|
|
12640
|
+
return `${member}/${op}/${gate}`;
|
|
12641
|
+
}
|
|
12642
|
+
function approveArgs(g) {
|
|
12643
|
+
return ["approve", g.op, g.gate, ...g.env ? ["--env", g.env] : [], ...g.planDigest ? ["--plan", g.planDigest] : []];
|
|
12644
|
+
}
|
|
12645
|
+
function parseWorkspaceStatus(text, root) {
|
|
12646
|
+
let doc;
|
|
12647
|
+
try {
|
|
12648
|
+
doc = JSON.parse(text);
|
|
12649
|
+
} catch {
|
|
12650
|
+
return { ok: false, refusal: { error: "chant workspace status printed something that is not JSON.", code: "workspace-status", remedy: "Run `chant workspace status <env> --json` in the workspace root to see what it says." } };
|
|
12651
|
+
}
|
|
12652
|
+
if (!isRecord11(doc)) return { ok: false, refusal: { error: "chant workspace status printed JSON that is not an object.", code: "workspace-status", remedy: "Run `chant workspace status <env> --json` in the workspace root." } };
|
|
12653
|
+
const contract = contractRefusal(doc, "status");
|
|
12654
|
+
if (contract) return { ok: false, refusal: contract };
|
|
12655
|
+
if (isRecord11(doc.error)) {
|
|
12656
|
+
const code = String(doc.error.code ?? "unknown");
|
|
12657
|
+
return { ok: false, refusal: { error: `chant could not read the workspace's status: ${code}: ${String(doc.error.message ?? "")}`, code, remedy: "`chant workspace check` names the problem and where it is." } };
|
|
12658
|
+
}
|
|
12659
|
+
const gates = [];
|
|
12660
|
+
for (const m of Array.isArray(doc.members) ? doc.members : []) {
|
|
12661
|
+
if (!isRecord11(m) || typeof m.name !== "string") continue;
|
|
12662
|
+
const dir = typeof m.dir === "string" ? m.dir === "." ? resolve13(root) : resolve13(root, m.dir) : resolve13(root);
|
|
12663
|
+
for (const g of Array.isArray(m.gates) ? m.gates : []) {
|
|
12664
|
+
if (!isRecord11(g)) continue;
|
|
12665
|
+
if (g.state !== void 0 && g.state !== "pending") continue;
|
|
12666
|
+
const op = str7(g.component);
|
|
12667
|
+
const gate = str7(g.name);
|
|
12668
|
+
if (!op || !gate) continue;
|
|
12669
|
+
const env = str7(g.env);
|
|
12670
|
+
const planDigest = str7(g.planDigest);
|
|
12671
|
+
const approvals = (Array.isArray(g.approvals) ? g.approvals : []).filter(isRecord11).map((a) => ({
|
|
12672
|
+
principal: String(a.principal ?? "unknown"),
|
|
12673
|
+
at: str7(a.at),
|
|
12674
|
+
channel: str7(a.channel)
|
|
12675
|
+
}));
|
|
12676
|
+
gates.push({
|
|
12677
|
+
key: gateKey(m.name, op, gate),
|
|
12678
|
+
member: m.name,
|
|
12679
|
+
dir,
|
|
12680
|
+
op,
|
|
12681
|
+
gate,
|
|
12682
|
+
env,
|
|
12683
|
+
planDigest,
|
|
12684
|
+
approvals,
|
|
12685
|
+
needed: typeof g.needed === "number" ? g.needed : 1,
|
|
12686
|
+
recordedAt: str7(g.recordedAt),
|
|
12687
|
+
expiresAt: str7(g.expiresAt),
|
|
12688
|
+
signed: isRecord11(g.signed),
|
|
12689
|
+
// status's line already carries --sign for a signed gate; only the
|
|
12690
|
+
// line built here needs it added.
|
|
12691
|
+
approve: str7(g.approve) ?? ["chant", ...approveArgs({ op, gate, env, planDigest }), ...isRecord11(g.signed) ? ["--sign"] : []].join(" ")
|
|
12692
|
+
});
|
|
12693
|
+
}
|
|
12694
|
+
}
|
|
12695
|
+
return { ok: true, env: String(doc.env ?? ""), gates };
|
|
12696
|
+
}
|
|
12697
|
+
async function readWorkspaceGates(ws, env) {
|
|
12698
|
+
const run3 = await runChantRaw(["workspace", "status", env, "--json"], ws.root);
|
|
12699
|
+
if (!run3.stdout.trim()) {
|
|
12700
|
+
return {
|
|
12701
|
+
ok: false,
|
|
12702
|
+
refusal: {
|
|
12703
|
+
error: `chant workspace status ${env} failed in ${ws.root} (exit ${run3.code}): ${run3.stderr.trim().split("\n").slice(-3).join(" ")}`,
|
|
12704
|
+
code: "workspace-status",
|
|
12705
|
+
remedy: "Run `chant workspace status <env>` in the workspace root."
|
|
12706
|
+
}
|
|
12707
|
+
};
|
|
12708
|
+
}
|
|
12709
|
+
return parseWorkspaceStatus(run3.stdout, ws.root);
|
|
12710
|
+
}
|
|
12711
|
+
function isEnvName(env) {
|
|
12712
|
+
return /^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(env);
|
|
12713
|
+
}
|
|
12714
|
+
function localApprover(env = process.env) {
|
|
12715
|
+
return env.GITHUB_ACTOR ?? env.GITLAB_USER_LOGIN ?? env.USER ?? "unknown";
|
|
12716
|
+
}
|
|
12717
|
+
|
|
12718
|
+
// src/request-guard.ts
|
|
12719
|
+
var LOOPBACK = /* @__PURE__ */ new Set(["localhost", "127.0.0.1", "::1", "[::1]"]);
|
|
12720
|
+
var SAFE = /* @__PURE__ */ new Set(["GET", "HEAD", "OPTIONS"]);
|
|
12721
|
+
function hostName(hostport) {
|
|
12722
|
+
const h = hostport.trim().toLowerCase();
|
|
12723
|
+
if (h.startsWith("[")) {
|
|
12724
|
+
const end = h.indexOf("]");
|
|
12725
|
+
return end >= 0 ? h.slice(0, end + 1) : h;
|
|
12726
|
+
}
|
|
12727
|
+
const colon = h.lastIndexOf(":");
|
|
12728
|
+
return colon >= 0 && h.indexOf(":") === colon ? h.slice(0, colon) : h;
|
|
12729
|
+
}
|
|
12730
|
+
function isLoopbackName(name) {
|
|
12731
|
+
return LOOPBACK.has(name) || name.endsWith(".localhost");
|
|
12732
|
+
}
|
|
12733
|
+
function allowedName(name, cfg) {
|
|
12734
|
+
if (isLoopbackName(name)) return true;
|
|
12735
|
+
if (cfg.boundHost && !isWildcard(cfg.boundHost) && hostName(cfg.boundHost) === name) return true;
|
|
12736
|
+
return cfg.allowedHosts.some((h) => hostName(h) === name);
|
|
12737
|
+
}
|
|
12738
|
+
function isWildcard(h) {
|
|
12739
|
+
return h === "0.0.0.0" || h === "::" || h === "[::]";
|
|
12740
|
+
}
|
|
12741
|
+
function guardRequest(req, cfg) {
|
|
12742
|
+
const name = hostName(req.host);
|
|
12743
|
+
if (!allowedName(name, cfg)) {
|
|
12744
|
+
return { ok: false, status: 421, error: `behold doesn't answer to host ${name}. Start it with --allow-host ${name} if a proxy of yours sends that name.` };
|
|
12745
|
+
}
|
|
12746
|
+
if (SAFE.has(req.method.toUpperCase())) return { ok: true };
|
|
12747
|
+
if ((req.secFetchSite || "").toLowerCase() === "cross-site") {
|
|
12748
|
+
return { ok: false, status: 403, error: "a cross-site page can't write to behold" };
|
|
12749
|
+
}
|
|
12750
|
+
if (req.origin && req.origin !== "null") {
|
|
12751
|
+
let originHost;
|
|
12752
|
+
try {
|
|
12753
|
+
originHost = new URL(req.origin).host.toLowerCase();
|
|
12754
|
+
} catch {
|
|
12755
|
+
return { ok: false, status: 403, error: "a write with an unreadable Origin is refused" };
|
|
12756
|
+
}
|
|
12757
|
+
const same2 = originHost === req.host.trim().toLowerCase();
|
|
12758
|
+
if (!same2 && !cfg.allowedHosts.some((h) => hostName(h) === hostName(originHost))) {
|
|
12759
|
+
return { ok: false, status: 403, error: `a page from ${originHost} can't write to behold` };
|
|
12760
|
+
}
|
|
12761
|
+
} else if (req.origin === "null") {
|
|
12762
|
+
return { ok: false, status: 403, error: "a write from an opaque origin is refused" };
|
|
12763
|
+
}
|
|
12764
|
+
return { ok: true };
|
|
12765
|
+
}
|
|
12766
|
+
function allowedHostsFrom(flag = [], env = process.env.BEHOLD_ALLOWED_HOSTS) {
|
|
12767
|
+
return [...flag, ...(env || "").split(",")].map((s) => s.trim()).filter(Boolean);
|
|
12768
|
+
}
|
|
12769
|
+
|
|
12614
12770
|
// src/server.ts
|
|
12615
12771
|
init_member_kind();
|
|
12616
12772
|
init_terraform_member();
|
|
@@ -12620,7 +12776,7 @@ init_chant();
|
|
|
12620
12776
|
init_member_kind();
|
|
12621
12777
|
init_choudoufu_member();
|
|
12622
12778
|
init_project();
|
|
12623
|
-
import { relative as relative6, resolve as
|
|
12779
|
+
import { relative as relative6, resolve as resolve14 } from "node:path";
|
|
12624
12780
|
init_terraform_member();
|
|
12625
12781
|
init_targets();
|
|
12626
12782
|
var list2 = (xs) => xs.join(", ");
|
|
@@ -12870,7 +13026,7 @@ function workspaceCheck(ws, beholdMembers) {
|
|
|
12870
13026
|
return { name: "project", status: warn ? "warn" : "pass", detail: parts.join("; "), ...warn ? { fix } : {} };
|
|
12871
13027
|
}
|
|
12872
13028
|
async function diagnose(dir, probes = {}) {
|
|
12873
|
-
const root =
|
|
13029
|
+
const root = resolve14(dir);
|
|
12874
13030
|
const behold = beholdVersion();
|
|
12875
13031
|
const listing = hasDeclaration(root) ? await (probes.readWorkspace ?? readWorkspace)(root) : void 0;
|
|
12876
13032
|
if (listing && !listing.ok) {
|
|
@@ -12909,7 +13065,7 @@ async function diagnose(dir, probes = {}) {
|
|
|
12909
13065
|
};
|
|
12910
13066
|
}
|
|
12911
13067
|
const estate = shape.kind === "estate";
|
|
12912
|
-
const members = (shape.members ?? []).map((m) => ({ ...m, abs:
|
|
13068
|
+
const members = (shape.members ?? []).map((m) => ({ ...m, abs: resolve14(root, m.dir) }));
|
|
12913
13069
|
const targets = estate ? members.map((m) => m.abs) : [root];
|
|
12914
13070
|
const primary = targets[0];
|
|
12915
13071
|
const chantTargets = estate ? members.filter((m) => m.kind === "chant").map((m) => m.abs) : [root];
|
|
@@ -13294,7 +13450,7 @@ async function emulatorDown(projectDir) {
|
|
|
13294
13450
|
|
|
13295
13451
|
// src/demos.ts
|
|
13296
13452
|
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
|
|
13453
|
+
import { dirname as dirname6, isAbsolute as isAbsolute4, join as join25, relative as relative7, resolve as resolve15, sep as sep6 } from "node:path";
|
|
13298
13454
|
import { spawn as spawn6, spawnSync as spawnSync2 } from "node:child_process";
|
|
13299
13455
|
function readCatalog(file, catalog) {
|
|
13300
13456
|
let raw;
|
|
@@ -13305,7 +13461,7 @@ function readCatalog(file, catalog) {
|
|
|
13305
13461
|
}
|
|
13306
13462
|
const list3 = raw?.demos;
|
|
13307
13463
|
if (!Array.isArray(list3)) return [];
|
|
13308
|
-
const catalogDir = dirname6(
|
|
13464
|
+
const catalogDir = dirname6(resolve15(file));
|
|
13309
13465
|
return list3.filter((e) => validEntry(e)).map((e) => ({ ...e, catalog, catalogDir }));
|
|
13310
13466
|
}
|
|
13311
13467
|
function validEntry(e) {
|
|
@@ -13342,7 +13498,7 @@ function loadDemoRegistry(pkgRoot3) {
|
|
|
13342
13498
|
const merged = readCatalog(join25(pkgRoot3, "demos.json"), "demos");
|
|
13343
13499
|
const seen = new Set(merged.map((e) => e.name));
|
|
13344
13500
|
const named = process.env.BEHOLD_WORKBENCH;
|
|
13345
|
-
for (const e of [...readCatalog(join25(pkgRoot3, "workbench.json"), "workbench"), ...named ? readCatalog(
|
|
13501
|
+
for (const e of [...readCatalog(join25(pkgRoot3, "workbench.json"), "workbench"), ...named ? readCatalog(resolve15(named), "workbench") : []]) {
|
|
13346
13502
|
if (seen.has(e.name)) {
|
|
13347
13503
|
process.stderr.write(`behold: workbench entry "${e.name}" collides with a demo already in the catalog \u2014 dropped
|
|
13348
13504
|
`);
|
|
@@ -13355,7 +13511,7 @@ function loadDemoRegistry(pkgRoot3) {
|
|
|
13355
13511
|
}
|
|
13356
13512
|
function demoLocalPath(entry) {
|
|
13357
13513
|
if (entry.source !== "local" || !entry.path) return void 0;
|
|
13358
|
-
return isAbsolute4(entry.path) ? entry.path :
|
|
13514
|
+
return isAbsolute4(entry.path) ? entry.path : resolve15(entry.catalogDir ?? process.cwd(), entry.path);
|
|
13359
13515
|
}
|
|
13360
13516
|
function missingRequirements(entry) {
|
|
13361
13517
|
const bins = entry.source === "git" && !entry.requires.includes("git") ? [...entry.requires, "git"] : entry.requires;
|
|
@@ -13374,9 +13530,9 @@ function fetchesFromNetwork(entry) {
|
|
|
13374
13530
|
function demoTargetDir(entry, cwd = process.cwd()) {
|
|
13375
13531
|
const inPlace = entry.inPlace ? demoLocalPath(entry) : void 0;
|
|
13376
13532
|
if (inPlace) return inPlace;
|
|
13377
|
-
const legacy =
|
|
13533
|
+
const legacy = resolve15(cwd, "behold-demo");
|
|
13378
13534
|
if (entry.name === "writes" && existsSync17(legacy)) return legacy;
|
|
13379
|
-
return
|
|
13535
|
+
return resolve15(cwd, "behold-demos", entry.name);
|
|
13380
13536
|
}
|
|
13381
13537
|
async function loadDemo(entry, opts) {
|
|
13382
13538
|
const { pkgRoot: pkgRoot3, target } = opts;
|
|
@@ -13785,6 +13941,20 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
|
|
|
13785
13941
|
onDone: (opEnv) => captureFrame(cfg.projectDir, opEnv ?? cfg.env, frames, broadcaster)
|
|
13786
13942
|
})) {
|
|
13787
13943
|
const app = new Hono();
|
|
13944
|
+
const guard = { allowedHosts: allowedHostsFrom(cfg.allowedHosts ?? []), ...cfg.host ? { boundHost: cfg.host } : {} };
|
|
13945
|
+
app.use("*", async (c, next) => {
|
|
13946
|
+
const verdict = guardRequest(
|
|
13947
|
+
{
|
|
13948
|
+
method: c.req.method,
|
|
13949
|
+
host: c.req.header("host") ?? new URL(c.req.url).host,
|
|
13950
|
+
origin: c.req.header("origin") ?? null,
|
|
13951
|
+
secFetchSite: c.req.header("sec-fetch-site") ?? null
|
|
13952
|
+
},
|
|
13953
|
+
guard
|
|
13954
|
+
);
|
|
13955
|
+
if (!verdict.ok) return c.json({ error: verdict.error, code: "request-refused" }, verdict.status);
|
|
13956
|
+
await next();
|
|
13957
|
+
});
|
|
13788
13958
|
setChoudoufuRunner(cfg.choudoufu?.run);
|
|
13789
13959
|
setEstateLexiconRead(cfg.choudoufu ? cfg.choudoufu.lexicon ?? null : void 0);
|
|
13790
13960
|
setServedWorkspace(cfg.workspace);
|
|
@@ -14006,6 +14176,7 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
|
|
|
14006
14176
|
return c.json({ history });
|
|
14007
14177
|
});
|
|
14008
14178
|
app.post("/api/operator/approve/:op/:gate", async (c) => {
|
|
14179
|
+
if (cfg.previewMode) return c.json({ error: "approving is disabled in preview mode", code: "preview" }, 403);
|
|
14009
14180
|
const { op, gate } = c.req.param();
|
|
14010
14181
|
const info = estateOps().find((o) => o.name === op);
|
|
14011
14182
|
broadcaster.emit("op", `\u270E approve ${op} ${gate} \u2014 records a fact; the next tick acts on it`);
|
|
@@ -14016,6 +14187,7 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
|
|
|
14016
14187
|
return c.json({ recorded: true, op, gate, semantics: APPROVE_SEMANTICS });
|
|
14017
14188
|
});
|
|
14018
14189
|
app.post("/api/ops/:name/signal/:gate", async (c) => {
|
|
14190
|
+
if (cfg.previewMode) return c.json({ error: "approving is disabled in preview mode", code: "preview" }, 403);
|
|
14019
14191
|
const { name, gate } = c.req.param();
|
|
14020
14192
|
const info = estateOps().find((o) => o.name === name);
|
|
14021
14193
|
broadcaster.emit("op", `\u270E signal ${name} ${gate}`);
|
|
@@ -14023,6 +14195,38 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
|
|
|
14023
14195
|
if (code !== 0) return c.json({ error: stderr.trim() || `signal exited ${code}` }, 500);
|
|
14024
14196
|
return c.json({ signalled: true });
|
|
14025
14197
|
});
|
|
14198
|
+
app.get("/api/workspace/gates", async (c) => {
|
|
14199
|
+
const ws = servedWorkspace();
|
|
14200
|
+
const env = c.req.query("env") || cfg.env || "local";
|
|
14201
|
+
if (!isEnvName(env)) return c.json({ error: `${JSON.stringify(env)} isn't an env name`, code: "env" }, 400);
|
|
14202
|
+
if (!ws) return c.json({ workspace: false, env, gates: [], approver: localApprover() });
|
|
14203
|
+
const read = await readWorkspaceGates(ws, env);
|
|
14204
|
+
if (!read.ok) return c.json({ workspace: true, env, gates: [], refusal: read.refusal, approver: localApprover() });
|
|
14205
|
+
return c.json({ workspace: true, env, gates: read.gates.map(({ dir: _dir, ...g }) => g), approver: localApprover() });
|
|
14206
|
+
});
|
|
14207
|
+
app.post("/api/workspace/gates/approve", async (c) => {
|
|
14208
|
+
if (cfg.previewMode) return c.json({ error: "approving is disabled in preview mode", code: "preview" }, 403);
|
|
14209
|
+
if (!(c.req.header("content-type") ?? "").toLowerCase().startsWith("application/json")) {
|
|
14210
|
+
return c.json({ error: "send the body as application/json", code: "content-type" }, 415);
|
|
14211
|
+
}
|
|
14212
|
+
const ws = servedWorkspace();
|
|
14213
|
+
if (!ws) return c.json({ error: "behold isn't serving a declared workspace", code: "no-workspace" }, 404);
|
|
14214
|
+
const body = await c.req.json().catch(() => ({}));
|
|
14215
|
+
const key = typeof body?.key === "string" ? body.key : "";
|
|
14216
|
+
const env = typeof body?.env === "string" && body.env ? body.env : cfg.env || "local";
|
|
14217
|
+
if (!isEnvName(env)) return c.json({ error: `${JSON.stringify(env)} isn't an env name`, code: "env" }, 400);
|
|
14218
|
+
const read = await readWorkspaceGates(ws, env);
|
|
14219
|
+
if (!read.ok) return c.json({ error: read.refusal.error, code: read.refusal.code }, 422);
|
|
14220
|
+
const g = read.gates.find((x) => x.key === key);
|
|
14221
|
+
if (!g) return c.json({ error: `no gate ${key} is waiting in ${env}`, code: "no-gate" }, 404);
|
|
14222
|
+
if (g.signed) return c.json({ error: `${key} wants a signed approval, which behold can't make. Run: ${g.approve}`, code: "signed" }, 422);
|
|
14223
|
+
const approver = localApprover();
|
|
14224
|
+
broadcaster.emit("op", `\u270E approve ${g.key} as ${approver}`);
|
|
14225
|
+
const { code, stderr, stdout } = await runChantRaw(approveArgs(g), g.dir);
|
|
14226
|
+
if (code !== 0) return c.json({ error: stderr.trim() || stdout.trim() || `approve exited ${code}`, code: "approve" }, 500);
|
|
14227
|
+
broadcaster.emit("changed", "");
|
|
14228
|
+
return c.json({ recorded: true, key: g.key, approver });
|
|
14229
|
+
});
|
|
14026
14230
|
app.get(
|
|
14027
14231
|
"/api/events",
|
|
14028
14232
|
(c) => streamSSE(c, async (stream) => {
|
|
@@ -14089,6 +14293,9 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
|
|
|
14089
14293
|
currentEnv: cfg.env ?? null,
|
|
14090
14294
|
// v0.1.0 preview: the SPA hides git/PR ops + arbitrary-project affordances.
|
|
14091
14295
|
...cfg.previewMode ? { previewMode: true } : {},
|
|
14296
|
+
// #477: who `chant approve` records when behold runs it. The approve
|
|
14297
|
+
// buttons say it before the click.
|
|
14298
|
+
approver: localApprover(),
|
|
14092
14299
|
// The tier picker's options (M2 #54, sourced #70): gated on the served
|
|
14093
14300
|
// project's `.behold.json` declaring a `tiers` block at all — NOT on
|
|
14094
14301
|
// whether its env var happens to be set in behold's own launch env
|
|
@@ -14139,7 +14346,7 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
|
|
|
14139
14346
|
app.post("/api/project/open", async (c) => {
|
|
14140
14347
|
if (cfg.previewMode) return c.json({ error: "switching projects is locked in preview mode" }, 403);
|
|
14141
14348
|
const body = await c.req.json().catch(() => ({}));
|
|
14142
|
-
const dir = typeof body.dir === "string" && body.dir.trim() ?
|
|
14349
|
+
const dir = typeof body.dir === "string" && body.dir.trim() ? resolve16(body.dir.trim()) : "";
|
|
14143
14350
|
if (!dir || !existsSync18(dir)) return c.json({ error: `no such directory: ${dir || "(no dir given)"}` }, 400);
|
|
14144
14351
|
if (!existsSync18(join26(dir, "chant.config.ts"))) {
|
|
14145
14352
|
return c.json({ error: `${dir} doesn't look like a chant project \u2014 no chant.config.ts` }, 400);
|
|
@@ -14164,7 +14371,7 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
|
|
|
14164
14371
|
};
|
|
14165
14372
|
app.post("/api/project/reveal", async (c) => {
|
|
14166
14373
|
const body = await c.req.json().catch(() => ({}));
|
|
14167
|
-
const dir = typeof body.dir === "string" && body.dir.trim() ?
|
|
14374
|
+
const dir = typeof body.dir === "string" && body.dir.trim() ? resolve16(body.dir.trim()) : cfg.projectDir;
|
|
14168
14375
|
const known = /* @__PURE__ */ new Set([cfg.projectDir, ...cfg.projectDirs ?? [], ...listRecents().map((r) => r.dir)]);
|
|
14169
14376
|
if (!known.has(dir)) return c.json({ error: "not a served or recent project directory" }, 400);
|
|
14170
14377
|
const opener = process.platform === "darwin" ? "open" : process.platform === "win32" ? "explorer" : "xdg-open";
|
|
@@ -14375,10 +14582,12 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
|
|
|
14375
14582
|
{ method: "GET", path: "/api/history", desc: "recent source commits (rollback targets)" },
|
|
14376
14583
|
{ method: "GET", path: "/api/frames", desc: "captured lanes frames" },
|
|
14377
14584
|
{ method: "GET", path: "/api/events", desc: "SSE: changed / op / apply / run / pr" },
|
|
14378
|
-
{ method: "POST", path: "/api/refresh", desc: "re-observe live now (?env=) \u2014 returns the fresh graph" },
|
|
14585
|
+
{ method: "POST", path: "/api/refresh", desc: "re-observe live now (?env=) \u2014 returns the fresh graph. With ?notify=1 it is a host's notice that the workspace changed: cached reads are dropped and every open page re-pulls (#476)" },
|
|
14379
14586
|
{ method: "POST", path: "/api/apply", desc: "delegated apply: ?env=&component=<name|all> (guarded, preview-locked)" },
|
|
14380
14587
|
{ method: "POST", path: "/api/ops/:name/run", desc: "run a committed Op (delegated write)" },
|
|
14381
14588
|
{ method: "POST", path: "/api/ops/:name/signal/:gate", desc: "approve an Op's gate (releases the waiting run)" },
|
|
14589
|
+
{ method: "GET", path: "/api/workspace/gates", desc: "a declared workspace's waiting gates from chant workspace status <env> --json (?env=), keyed member/op/gate, with the approver behold would record (#477)" },
|
|
14590
|
+
{ method: "POST", path: "/api/workspace/gates/approve", desc: "approve one workspace gate: JSON body {key, env}; env and plan come from a fresh status read; recorded as the user running behold; preview-locked (#477)" },
|
|
14382
14591
|
{ method: "POST", path: "/api/operator/approve/:op/:gate", desc: "record a converge gate's resolution (chant approve \u2014 a fact for the next tick, not an unblock)" },
|
|
14383
14592
|
{ method: "POST", path: "/api/rollback", desc: "open a rollback PR: ?to=<sha>" },
|
|
14384
14593
|
{ method: "POST", path: "/api/substrates/:name/up", desc: "bring a substrate up" },
|
|
@@ -14984,7 +15193,14 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
|
|
|
14984
15193
|
}
|
|
14985
15194
|
});
|
|
14986
15195
|
app.post("/api/refresh", async (c) => {
|
|
14987
|
-
const
|
|
15196
|
+
const url = new URL(c.req.url);
|
|
15197
|
+
if (url.searchParams.get("notify") === "1") {
|
|
15198
|
+
invalidateMember();
|
|
15199
|
+
invalidateOverlay();
|
|
15200
|
+
broadcaster.emit("changed", "");
|
|
15201
|
+
return c.json({ notified: true });
|
|
15202
|
+
}
|
|
15203
|
+
const env = optsFromQuery(url).env ?? cfg.env;
|
|
14988
15204
|
const result = await captureFrame(cfg.projectDir, env, frames, broadcaster);
|
|
14989
15205
|
if (!result) return c.json({ error: "refresh failed \u2014 see server log" }, 500);
|
|
14990
15206
|
if (env) reclassifyOverlay(result.ir);
|
|
@@ -15064,9 +15280,9 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
|
|
|
15064
15280
|
});
|
|
15065
15281
|
const choudoufuPlanPath = (plan) => {
|
|
15066
15282
|
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" };
|
|
15067
|
-
const abs =
|
|
15283
|
+
const abs = resolve16(cfg.projectDir, plan);
|
|
15068
15284
|
const members = (cfg.projectDirs ?? [cfg.projectDir]).filter((d) => memberKindOf(d) === "choudoufu");
|
|
15069
|
-
const inside = members.some((d) => abs.startsWith(
|
|
15285
|
+
const inside = members.some((d) => abs.startsWith(resolve16(d) + sep7));
|
|
15070
15286
|
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" };
|
|
15071
15287
|
return { path: abs };
|
|
15072
15288
|
};
|
|
@@ -15197,6 +15413,10 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
|
|
|
15197
15413
|
return c.json({ started: true, component, env });
|
|
15198
15414
|
});
|
|
15199
15415
|
const rel = relative8(process.cwd(), webRoot) || ".";
|
|
15416
|
+
app.use("/*", async (c, next) => {
|
|
15417
|
+
await next();
|
|
15418
|
+
if (!c.req.path.startsWith("/api/") && c.res.ok && !c.res.headers.has("cache-control")) c.res.headers.set("cache-control", "no-cache");
|
|
15419
|
+
});
|
|
15200
15420
|
app.use("/*", serveStatic({ root: rel }));
|
|
15201
15421
|
app.get("/", serveStatic({ path: join26(rel, "index.html") }));
|
|
15202
15422
|
return app;
|
|
@@ -15236,6 +15456,7 @@ async function startServer(cfg) {
|
|
|
15236
15456
|
broadcaster,
|
|
15237
15457
|
onDone: (opEnv) => captureFrame(cfg.projectDir, opEnv ?? cfg.env, frames, broadcaster)
|
|
15238
15458
|
});
|
|
15459
|
+
if (!cfg.host && process.env.BEHOLD_HOST) cfg.host = process.env.BEHOLD_HOST;
|
|
15239
15460
|
const app = createApp(cfg, broadcaster, frames, runner);
|
|
15240
15461
|
const autoSync = cfg.autoSync ?? "off";
|
|
15241
15462
|
if (!cfg.carveReport) {
|
|
@@ -15343,7 +15564,8 @@ async function startServer(cfg) {
|
|
|
15343
15564
|
};
|
|
15344
15565
|
process.on("SIGINT", shutdown);
|
|
15345
15566
|
process.on("SIGTERM", shutdown);
|
|
15346
|
-
const
|
|
15567
|
+
const hostname = cfg.host ?? process.env.BEHOLD_HOST ?? "127.0.0.1";
|
|
15568
|
+
const server = serve({ fetch: app.fetch, port: cfg.port, hostname }, (info) => {
|
|
15347
15569
|
if (carve) {
|
|
15348
15570
|
process.stdout.write(
|
|
15349
15571
|
`behold \u2192 http://localhost:${info.port}
|
|
@@ -15394,7 +15616,7 @@ init_chant();
|
|
|
15394
15616
|
// src/workspace-convert.ts
|
|
15395
15617
|
init_chant();
|
|
15396
15618
|
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
|
|
15619
|
+
import { basename as basename4, dirname as dirname8, join as join27, relative as relative9, resolve as resolve17, sep as sep8 } from "node:path";
|
|
15398
15620
|
import { shortStackNames as shortStackNames2 } from "@intentius/pinhole";
|
|
15399
15621
|
init_project();
|
|
15400
15622
|
init_terraform_member();
|
|
@@ -15411,7 +15633,7 @@ function unique(name, taken) {
|
|
|
15411
15633
|
}
|
|
15412
15634
|
var posix = (p) => p.split(sep8).join("/");
|
|
15413
15635
|
function installedVersion(pkg, dir) {
|
|
15414
|
-
for (let at =
|
|
15636
|
+
for (let at = resolve17(dir); ; at = dirname8(at)) {
|
|
15415
15637
|
const manifest = join27(at, "node_modules", ...pkg.split("/"), "package.json");
|
|
15416
15638
|
if (existsSync19(manifest)) {
|
|
15417
15639
|
try {
|
|
@@ -15425,14 +15647,14 @@ function installedVersion(pkg, dir) {
|
|
|
15425
15647
|
}
|
|
15426
15648
|
}
|
|
15427
15649
|
function planConversion(rootArg) {
|
|
15428
|
-
const root =
|
|
15650
|
+
const root = resolve17(rootArg);
|
|
15429
15651
|
const existing = DECLARATION_FILES.find((f) => existsSync19(join27(root, f)));
|
|
15430
15652
|
if (existing) return { ok: false, error: `${root} already has ${existing}; there is nothing to convert.` };
|
|
15431
15653
|
const shape = detectProjectShape(root);
|
|
15432
15654
|
if (shape.kind !== "estate" || !shape.members?.length || shape.membersFrom === "itself") {
|
|
15433
15655
|
return { ok: false, error: `${root} is not an estate root with a member list, so there is no member list to convert.` };
|
|
15434
15656
|
}
|
|
15435
|
-
const abs = shape.members.map((m) =>
|
|
15657
|
+
const abs = shape.members.map((m) => resolve17(root, m.dir));
|
|
15436
15658
|
const shorts = shortStackNames2(abs);
|
|
15437
15659
|
const taken = /* @__PURE__ */ new Set();
|
|
15438
15660
|
const members = [];
|
|
@@ -15500,7 +15722,7 @@ function applyConversion(plan) {
|
|
|
15500
15722
|
let layout;
|
|
15501
15723
|
const target = layoutPath(plan.root);
|
|
15502
15724
|
if (!plan.layoutFrom) layout = { skipped: "no saved layout to move" };
|
|
15503
|
-
else if (
|
|
15725
|
+
else if (resolve17(plan.layoutFrom) === resolve17(target)) layout = { skipped: "the saved layout is already in the root" };
|
|
15504
15726
|
else if (existsSync19(target)) layout = { skipped: `${target} already exists; the layout in ${plan.layoutFrom} was left alone` };
|
|
15505
15727
|
else {
|
|
15506
15728
|
const moved = moveLayoutIds(readLayoutFile(dirname8(dirname8(plan.layoutFrom))), plan.moves);
|
|
@@ -15694,7 +15916,7 @@ Usage:
|
|
|
15694
15916
|
behold doctor [project-dir] [--json] [--fix]
|
|
15695
15917
|
behold preview [project-dir] [--port <n>] [--emulator]
|
|
15696
15918
|
behold export [project-dir] [--out <dir>] [--env <name>] [--name <worker>] [--emulator]
|
|
15697
|
-
behold serve <project-dir\u2026> [--port <n>] [--env <name>] [--poll <secs>] [--local]
|
|
15919
|
+
behold serve <project-dir\u2026> [--port <n>] [--host <addr>] [--allow-host <name,\u2026>] [--env <name>] [--poll <secs>] [--local]
|
|
15698
15920
|
behold carve <report.json> [--port <n>]
|
|
15699
15921
|
|
|
15700
15922
|
carve Render a chant Terraform peelability report \u2014 the JSON from
|
|
@@ -15753,6 +15975,12 @@ Usage:
|
|
|
15753
15975
|
|
|
15754
15976
|
Options:
|
|
15755
15977
|
--port <n> Port (default 4600). preview/serve/carve.
|
|
15978
|
+
--host <addr> serve only: the address to bind (default 127.0.0.1, or
|
|
15979
|
+
BEHOLD_HOST). behold runs writes with your credentials,
|
|
15980
|
+
so it answers this machine unless you say otherwise.
|
|
15981
|
+
--allow-host <n,\u2026> serve only: names besides loopback behold answers to, and
|
|
15982
|
+
whose pages may write (BEHOLD_ALLOWED_HOSTS too). For a
|
|
15983
|
+
proxy that frames behold, or --host 0.0.0.0.
|
|
15756
15984
|
--env <name> Environment name \u2014 turns on the live drift overlay.
|
|
15757
15985
|
export/serve.
|
|
15758
15986
|
--poll <secs> Re-query live drift every <secs> and push updates (needs --env).
|
|
@@ -15837,9 +16065,13 @@ ${USAGE}`);
|
|
|
15837
16065
|
let pollSecs;
|
|
15838
16066
|
let autoSync = "off";
|
|
15839
16067
|
let local = false;
|
|
16068
|
+
let host;
|
|
16069
|
+
const allowHosts = [];
|
|
15840
16070
|
for (let i = 0; i < rest.length; i++) {
|
|
15841
16071
|
const a = rest[i];
|
|
15842
16072
|
if (a === "--port") port = Number(rest[++i]);
|
|
16073
|
+
else if (a === "--host") host = rest[++i];
|
|
16074
|
+
else if (a === "--allow-host") allowHosts.push(...(rest[++i] ?? "").split(","));
|
|
15843
16075
|
else if (a === "--env") env = rest[++i];
|
|
15844
16076
|
else if (a === "--poll") pollSecs = Number(rest[++i]);
|
|
15845
16077
|
else if (a === "--local") local = true;
|
|
@@ -15880,7 +16112,7 @@ ${USAGE}`);
|
|
|
15880
16112
|
process.stderr.write("behold serve: --auto-sync needs --env and --poll (it acts on polled drift)\n");
|
|
15881
16113
|
process.exit(2);
|
|
15882
16114
|
}
|
|
15883
|
-
const dirs = projectDirs.map((d) =>
|
|
16115
|
+
const dirs = projectDirs.map((d) => resolve18(d));
|
|
15884
16116
|
const workspace = dirs.length === 1 && hasDeclaration(dirs[0]) ? await declaredWorkspace(dirs[0]) : void 0;
|
|
15885
16117
|
if (workspace) {
|
|
15886
16118
|
const drawn = drawnMembers(workspace).map((m) => m.abs);
|
|
@@ -15892,7 +16124,9 @@ ${USAGE}`);
|
|
|
15892
16124
|
...env ? { env } : {},
|
|
15893
16125
|
...pollSecs !== void 0 ? { pollSecs } : {},
|
|
15894
16126
|
...autoSync !== "off" ? { autoSync } : {},
|
|
15895
|
-
...local ? { local: true } : {}
|
|
16127
|
+
...local ? { local: true } : {},
|
|
16128
|
+
...host ? { host } : {},
|
|
16129
|
+
...allowHosts.length ? { allowedHosts: allowHosts } : {}
|
|
15896
16130
|
});
|
|
15897
16131
|
return;
|
|
15898
16132
|
}
|
|
@@ -15909,7 +16143,9 @@ ${USAGE}`);
|
|
|
15909
16143
|
...env ? { env } : {},
|
|
15910
16144
|
...pollSecs !== void 0 ? { pollSecs } : {},
|
|
15911
16145
|
...autoSync !== "off" ? { autoSync } : {},
|
|
15912
|
-
...local ? { local: true } : {}
|
|
16146
|
+
...local ? { local: true } : {},
|
|
16147
|
+
...host ? { host } : {},
|
|
16148
|
+
...allowHosts.length ? { allowedHosts: allowHosts } : {}
|
|
15913
16149
|
});
|
|
15914
16150
|
}
|
|
15915
16151
|
async function declaredWorkspace(root) {
|
|
@@ -15976,7 +16212,7 @@ async function runCarve(rest) {
|
|
|
15976
16212
|
);
|
|
15977
16213
|
process.exit(2);
|
|
15978
16214
|
}
|
|
15979
|
-
const reportPath =
|
|
16215
|
+
const reportPath = resolve18(fileArg);
|
|
15980
16216
|
const parsed = readCarveReport(reportPath, (p) => readFileSync22(p, "utf8"));
|
|
15981
16217
|
if (!parsed.ok) {
|
|
15982
16218
|
process.stderr.write(`behold carve: ${parsed.refusal.error}
|
|
@@ -16006,8 +16242,8 @@ async function runDoctor(rest) {
|
|
|
16006
16242
|
}
|
|
16007
16243
|
}
|
|
16008
16244
|
const dir = dirArg ?? ".";
|
|
16009
|
-
if (!existsSync21(
|
|
16010
|
-
process.stderr.write(`behold doctor: no such directory: ${
|
|
16245
|
+
if (!existsSync21(resolve18(dir))) {
|
|
16246
|
+
process.stderr.write(`behold doctor: no such directory: ${resolve18(dir)}
|
|
16011
16247
|
`);
|
|
16012
16248
|
process.exit(2);
|
|
16013
16249
|
}
|
|
@@ -16106,7 +16342,7 @@ async function runDemo(rest) {
|
|
|
16106
16342
|
`);
|
|
16107
16343
|
process.exit(2);
|
|
16108
16344
|
}
|
|
16109
|
-
const target = dirArg ?
|
|
16345
|
+
const target = dirArg ? resolve18(dirArg) : demoTargetDir(entry);
|
|
16110
16346
|
const loaded = await loadDemo(entry, { pkgRoot: pkgRoot3, target, log: (line) => process.stdout.write(line + "\n") });
|
|
16111
16347
|
if (!loaded.ok) {
|
|
16112
16348
|
process.stderr.write(`behold demo ${entry.name}: ${loaded.error}
|
|
@@ -16138,7 +16374,7 @@ function spawnStep(cmd, args, cwd) {
|
|
|
16138
16374
|
});
|
|
16139
16375
|
}
|
|
16140
16376
|
async function serveCarveDemo(target, carve, port, live2 = false) {
|
|
16141
|
-
const at = (rel) =>
|
|
16377
|
+
const at = (rel) => resolve18(target, rel);
|
|
16142
16378
|
const project = at(carve.project);
|
|
16143
16379
|
const from = at(carve.from);
|
|
16144
16380
|
const state = carve.state ? at(carve.state) : void 0;
|
|
@@ -16259,7 +16495,7 @@ async function runPreview(rest) {
|
|
|
16259
16495
|
process.stderr.write("behold preview: --port must be a number\n");
|
|
16260
16496
|
process.exit(2);
|
|
16261
16497
|
}
|
|
16262
|
-
const projectDir =
|
|
16498
|
+
const projectDir = resolve18(dirArg ?? process.cwd());
|
|
16263
16499
|
if (!existsSync21(projectDir)) {
|
|
16264
16500
|
process.stderr.write(`behold preview: project not found at ${projectDir}
|
|
16265
16501
|
`);
|
|
@@ -16280,21 +16516,21 @@ async function runPreview(rest) {
|
|
|
16280
16516
|
await startServer({ projectDir, port, env: "local", previewMode: true });
|
|
16281
16517
|
}
|
|
16282
16518
|
async function runExportCmd(rest) {
|
|
16283
|
-
let outDir =
|
|
16519
|
+
let outDir = resolve18("behold-export");
|
|
16284
16520
|
let env;
|
|
16285
16521
|
let name;
|
|
16286
16522
|
let dirArg;
|
|
16287
16523
|
let emulator = false;
|
|
16288
16524
|
for (let i = 0; i < rest.length; i++) {
|
|
16289
16525
|
const a = rest[i];
|
|
16290
|
-
if (a === "--out") outDir =
|
|
16526
|
+
if (a === "--out") outDir = resolve18(rest[++i]);
|
|
16291
16527
|
else if (a === "--env") env = rest[++i];
|
|
16292
16528
|
else if (a === "--name") name = rest[++i];
|
|
16293
16529
|
else if (a === "--emulator") emulator = true;
|
|
16294
16530
|
else if (a === "-h" || a === "--help") return void process.stdout.write(USAGE);
|
|
16295
16531
|
else if (!a.startsWith("-")) dirArg = a;
|
|
16296
16532
|
}
|
|
16297
|
-
const projectDir =
|
|
16533
|
+
const projectDir = resolve18(dirArg ?? process.cwd());
|
|
16298
16534
|
if (emulator) {
|
|
16299
16535
|
injectEmulatorEnv(env);
|
|
16300
16536
|
env ??= "local";
|