@intentius/behold 0.20.0 → 0.21.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +49 -0
- package/dist/cli.js +274 -58
- package/package.json +1 -1
- package/web/app.js +377 -9
- package/web/embed.js +76 -0
- package/web/embed.test.js +45 -0
- package/web/index.html +20 -0
- package/web/operator.js +14 -1
- package/web/panel.js +11 -3
- package/web/run-playhead.js +13 -1
- package/web/view-url.js +138 -0
- package/web/view-url.test.js +84 -0
- package/web/workspace-gates.js +44 -0
- package/web/workspace-gates.test.js +34 -0
package/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"]);
|
|
@@ -11502,19 +11502,19 @@ import { join as join20 } from "node:path";
|
|
|
11502
11502
|
import { platform } from "node:os";
|
|
11503
11503
|
var DOCKER_DOWN_DETAIL = "docker is down";
|
|
11504
11504
|
function probe(cmd, args) {
|
|
11505
|
-
return new Promise((
|
|
11505
|
+
return new Promise((resolve19) => {
|
|
11506
11506
|
let out = "";
|
|
11507
11507
|
let proc;
|
|
11508
11508
|
try {
|
|
11509
11509
|
proc = spawn5(cmd, args, { stdio: ["ignore", "pipe", "pipe"] });
|
|
11510
11510
|
} catch {
|
|
11511
|
-
|
|
11511
|
+
resolve19({ code: 127, out: "" });
|
|
11512
11512
|
return;
|
|
11513
11513
|
}
|
|
11514
11514
|
proc.stdout.on("data", (d) => out += d);
|
|
11515
11515
|
proc.stderr.on("data", (d) => out += d);
|
|
11516
|
-
proc.on("error", () =>
|
|
11517
|
-
proc.on("close", (code) =>
|
|
11516
|
+
proc.on("error", () => resolve19({ code: 127, out }));
|
|
11517
|
+
proc.on("close", (code) => resolve19({ code: code ?? 1, out }));
|
|
11518
11518
|
});
|
|
11519
11519
|
}
|
|
11520
11520
|
async function dockerAvailable(run3) {
|
|
@@ -12611,6 +12611,142 @@ async function moveReceipt(plan, members, run3 = captureChoudoufu) {
|
|
|
12611
12611
|
};
|
|
12612
12612
|
}
|
|
12613
12613
|
|
|
12614
|
+
// src/workspace-gates.ts
|
|
12615
|
+
init_chant();
|
|
12616
|
+
import { resolve as resolve13 } from "node:path";
|
|
12617
|
+
var isRecord11 = (v) => v !== null && typeof v === "object" && !Array.isArray(v);
|
|
12618
|
+
var str7 = (v) => typeof v === "string" && v ? v : null;
|
|
12619
|
+
function gateKey(member, op, gate) {
|
|
12620
|
+
return `${member}/${op}/${gate}`;
|
|
12621
|
+
}
|
|
12622
|
+
function approveArgs(g) {
|
|
12623
|
+
return ["approve", g.op, g.gate, ...g.env ? ["--env", g.env] : [], ...g.planDigest ? ["--plan", g.planDigest] : []];
|
|
12624
|
+
}
|
|
12625
|
+
function parseWorkspaceStatus(text, root) {
|
|
12626
|
+
let doc;
|
|
12627
|
+
try {
|
|
12628
|
+
doc = JSON.parse(text);
|
|
12629
|
+
} catch {
|
|
12630
|
+
return { ok: false, refusal: { error: "chant workspace status printed something that is not JSON.", code: "workspace-status", remedy: "Run `chant workspace status <env> --json` in the workspace root to see what it says." } };
|
|
12631
|
+
}
|
|
12632
|
+
if (!isRecord11(doc)) return { ok: false, refusal: { error: "chant workspace status printed JSON that is not an object.", code: "workspace-status", remedy: "Run `chant workspace status <env> --json` in the workspace root." } };
|
|
12633
|
+
const contract = contractRefusal(doc, "status");
|
|
12634
|
+
if (contract) return { ok: false, refusal: contract };
|
|
12635
|
+
if (isRecord11(doc.error)) {
|
|
12636
|
+
const code = String(doc.error.code ?? "unknown");
|
|
12637
|
+
return { ok: false, refusal: { error: `chant could not read the workspace's status: ${code}: ${String(doc.error.message ?? "")}`, code, remedy: "`chant workspace check` names the problem and where it is." } };
|
|
12638
|
+
}
|
|
12639
|
+
const gates = [];
|
|
12640
|
+
for (const m of Array.isArray(doc.members) ? doc.members : []) {
|
|
12641
|
+
if (!isRecord11(m) || typeof m.name !== "string") continue;
|
|
12642
|
+
const dir = typeof m.dir === "string" ? m.dir === "." ? resolve13(root) : resolve13(root, m.dir) : resolve13(root);
|
|
12643
|
+
for (const g of Array.isArray(m.gates) ? m.gates : []) {
|
|
12644
|
+
if (!isRecord11(g)) continue;
|
|
12645
|
+
if (g.state !== void 0 && g.state !== "pending") continue;
|
|
12646
|
+
const op = str7(g.component);
|
|
12647
|
+
const gate = str7(g.name);
|
|
12648
|
+
if (!op || !gate) continue;
|
|
12649
|
+
const env = str7(g.env);
|
|
12650
|
+
const planDigest = str7(g.planDigest);
|
|
12651
|
+
const approvals = (Array.isArray(g.approvals) ? g.approvals : []).filter(isRecord11).map((a) => ({
|
|
12652
|
+
principal: String(a.principal ?? "unknown"),
|
|
12653
|
+
at: str7(a.at),
|
|
12654
|
+
channel: str7(a.channel)
|
|
12655
|
+
}));
|
|
12656
|
+
gates.push({
|
|
12657
|
+
key: gateKey(m.name, op, gate),
|
|
12658
|
+
member: m.name,
|
|
12659
|
+
dir,
|
|
12660
|
+
op,
|
|
12661
|
+
gate,
|
|
12662
|
+
env,
|
|
12663
|
+
planDigest,
|
|
12664
|
+
approvals,
|
|
12665
|
+
needed: typeof g.needed === "number" ? g.needed : 1,
|
|
12666
|
+
recordedAt: str7(g.recordedAt),
|
|
12667
|
+
expiresAt: str7(g.expiresAt),
|
|
12668
|
+
signed: isRecord11(g.signed),
|
|
12669
|
+
// status's line already carries --sign for a signed gate; only the
|
|
12670
|
+
// line built here needs it added.
|
|
12671
|
+
approve: str7(g.approve) ?? ["chant", ...approveArgs({ op, gate, env, planDigest }), ...isRecord11(g.signed) ? ["--sign"] : []].join(" ")
|
|
12672
|
+
});
|
|
12673
|
+
}
|
|
12674
|
+
}
|
|
12675
|
+
return { ok: true, env: String(doc.env ?? ""), gates };
|
|
12676
|
+
}
|
|
12677
|
+
async function readWorkspaceGates(ws, env) {
|
|
12678
|
+
const run3 = await runChantRaw(["workspace", "status", env, "--json"], ws.root);
|
|
12679
|
+
if (!run3.stdout.trim()) {
|
|
12680
|
+
return {
|
|
12681
|
+
ok: false,
|
|
12682
|
+
refusal: {
|
|
12683
|
+
error: `chant workspace status ${env} failed in ${ws.root} (exit ${run3.code}): ${run3.stderr.trim().split("\n").slice(-3).join(" ")}`,
|
|
12684
|
+
code: "workspace-status",
|
|
12685
|
+
remedy: "Run `chant workspace status <env>` in the workspace root."
|
|
12686
|
+
}
|
|
12687
|
+
};
|
|
12688
|
+
}
|
|
12689
|
+
return parseWorkspaceStatus(run3.stdout, ws.root);
|
|
12690
|
+
}
|
|
12691
|
+
function isEnvName(env) {
|
|
12692
|
+
return /^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(env);
|
|
12693
|
+
}
|
|
12694
|
+
function localApprover(env = process.env) {
|
|
12695
|
+
return env.GITHUB_ACTOR ?? env.GITLAB_USER_LOGIN ?? env.USER ?? "unknown";
|
|
12696
|
+
}
|
|
12697
|
+
|
|
12698
|
+
// src/request-guard.ts
|
|
12699
|
+
var LOOPBACK = /* @__PURE__ */ new Set(["localhost", "127.0.0.1", "::1", "[::1]"]);
|
|
12700
|
+
var SAFE = /* @__PURE__ */ new Set(["GET", "HEAD", "OPTIONS"]);
|
|
12701
|
+
function hostName(hostport) {
|
|
12702
|
+
const h = hostport.trim().toLowerCase();
|
|
12703
|
+
if (h.startsWith("[")) {
|
|
12704
|
+
const end = h.indexOf("]");
|
|
12705
|
+
return end >= 0 ? h.slice(0, end + 1) : h;
|
|
12706
|
+
}
|
|
12707
|
+
const colon = h.lastIndexOf(":");
|
|
12708
|
+
return colon >= 0 && h.indexOf(":") === colon ? h.slice(0, colon) : h;
|
|
12709
|
+
}
|
|
12710
|
+
function isLoopbackName(name) {
|
|
12711
|
+
return LOOPBACK.has(name) || name.endsWith(".localhost");
|
|
12712
|
+
}
|
|
12713
|
+
function allowedName(name, cfg) {
|
|
12714
|
+
if (isLoopbackName(name)) return true;
|
|
12715
|
+
if (cfg.boundHost && !isWildcard(cfg.boundHost) && hostName(cfg.boundHost) === name) return true;
|
|
12716
|
+
return cfg.allowedHosts.some((h) => hostName(h) === name);
|
|
12717
|
+
}
|
|
12718
|
+
function isWildcard(h) {
|
|
12719
|
+
return h === "0.0.0.0" || h === "::" || h === "[::]";
|
|
12720
|
+
}
|
|
12721
|
+
function guardRequest(req, cfg) {
|
|
12722
|
+
const name = hostName(req.host);
|
|
12723
|
+
if (!allowedName(name, cfg)) {
|
|
12724
|
+
return { ok: false, status: 421, error: `behold doesn't answer to host ${name}. Start it with --allow-host ${name} if a proxy of yours sends that name.` };
|
|
12725
|
+
}
|
|
12726
|
+
if (SAFE.has(req.method.toUpperCase())) return { ok: true };
|
|
12727
|
+
if ((req.secFetchSite || "").toLowerCase() === "cross-site") {
|
|
12728
|
+
return { ok: false, status: 403, error: "a cross-site page can't write to behold" };
|
|
12729
|
+
}
|
|
12730
|
+
if (req.origin && req.origin !== "null") {
|
|
12731
|
+
let originHost;
|
|
12732
|
+
try {
|
|
12733
|
+
originHost = new URL(req.origin).host.toLowerCase();
|
|
12734
|
+
} catch {
|
|
12735
|
+
return { ok: false, status: 403, error: "a write with an unreadable Origin is refused" };
|
|
12736
|
+
}
|
|
12737
|
+
const same2 = originHost === req.host.trim().toLowerCase();
|
|
12738
|
+
if (!same2 && !cfg.allowedHosts.some((h) => hostName(h) === hostName(originHost))) {
|
|
12739
|
+
return { ok: false, status: 403, error: `a page from ${originHost} can't write to behold` };
|
|
12740
|
+
}
|
|
12741
|
+
} else if (req.origin === "null") {
|
|
12742
|
+
return { ok: false, status: 403, error: "a write from an opaque origin is refused" };
|
|
12743
|
+
}
|
|
12744
|
+
return { ok: true };
|
|
12745
|
+
}
|
|
12746
|
+
function allowedHostsFrom(flag = [], env = process.env.BEHOLD_ALLOWED_HOSTS) {
|
|
12747
|
+
return [...flag, ...(env || "").split(",")].map((s) => s.trim()).filter(Boolean);
|
|
12748
|
+
}
|
|
12749
|
+
|
|
12614
12750
|
// src/server.ts
|
|
12615
12751
|
init_member_kind();
|
|
12616
12752
|
init_terraform_member();
|
|
@@ -12620,7 +12756,7 @@ init_chant();
|
|
|
12620
12756
|
init_member_kind();
|
|
12621
12757
|
init_choudoufu_member();
|
|
12622
12758
|
init_project();
|
|
12623
|
-
import { relative as relative6, resolve as
|
|
12759
|
+
import { relative as relative6, resolve as resolve14 } from "node:path";
|
|
12624
12760
|
init_terraform_member();
|
|
12625
12761
|
init_targets();
|
|
12626
12762
|
var list2 = (xs) => xs.join(", ");
|
|
@@ -12870,7 +13006,7 @@ function workspaceCheck(ws, beholdMembers) {
|
|
|
12870
13006
|
return { name: "project", status: warn ? "warn" : "pass", detail: parts.join("; "), ...warn ? { fix } : {} };
|
|
12871
13007
|
}
|
|
12872
13008
|
async function diagnose(dir, probes = {}) {
|
|
12873
|
-
const root =
|
|
13009
|
+
const root = resolve14(dir);
|
|
12874
13010
|
const behold = beholdVersion();
|
|
12875
13011
|
const listing = hasDeclaration(root) ? await (probes.readWorkspace ?? readWorkspace)(root) : void 0;
|
|
12876
13012
|
if (listing && !listing.ok) {
|
|
@@ -12909,7 +13045,7 @@ async function diagnose(dir, probes = {}) {
|
|
|
12909
13045
|
};
|
|
12910
13046
|
}
|
|
12911
13047
|
const estate = shape.kind === "estate";
|
|
12912
|
-
const members = (shape.members ?? []).map((m) => ({ ...m, abs:
|
|
13048
|
+
const members = (shape.members ?? []).map((m) => ({ ...m, abs: resolve14(root, m.dir) }));
|
|
12913
13049
|
const targets = estate ? members.map((m) => m.abs) : [root];
|
|
12914
13050
|
const primary = targets[0];
|
|
12915
13051
|
const chantTargets = estate ? members.filter((m) => m.kind === "chant").map((m) => m.abs) : [root];
|
|
@@ -13294,7 +13430,7 @@ async function emulatorDown(projectDir) {
|
|
|
13294
13430
|
|
|
13295
13431
|
// src/demos.ts
|
|
13296
13432
|
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
|
|
13433
|
+
import { dirname as dirname6, isAbsolute as isAbsolute4, join as join25, relative as relative7, resolve as resolve15, sep as sep6 } from "node:path";
|
|
13298
13434
|
import { spawn as spawn6, spawnSync as spawnSync2 } from "node:child_process";
|
|
13299
13435
|
function readCatalog(file, catalog) {
|
|
13300
13436
|
let raw;
|
|
@@ -13305,7 +13441,7 @@ function readCatalog(file, catalog) {
|
|
|
13305
13441
|
}
|
|
13306
13442
|
const list3 = raw?.demos;
|
|
13307
13443
|
if (!Array.isArray(list3)) return [];
|
|
13308
|
-
const catalogDir = dirname6(
|
|
13444
|
+
const catalogDir = dirname6(resolve15(file));
|
|
13309
13445
|
return list3.filter((e) => validEntry(e)).map((e) => ({ ...e, catalog, catalogDir }));
|
|
13310
13446
|
}
|
|
13311
13447
|
function validEntry(e) {
|
|
@@ -13342,7 +13478,7 @@ function loadDemoRegistry(pkgRoot3) {
|
|
|
13342
13478
|
const merged = readCatalog(join25(pkgRoot3, "demos.json"), "demos");
|
|
13343
13479
|
const seen = new Set(merged.map((e) => e.name));
|
|
13344
13480
|
const named = process.env.BEHOLD_WORKBENCH;
|
|
13345
|
-
for (const e of [...readCatalog(join25(pkgRoot3, "workbench.json"), "workbench"), ...named ? readCatalog(
|
|
13481
|
+
for (const e of [...readCatalog(join25(pkgRoot3, "workbench.json"), "workbench"), ...named ? readCatalog(resolve15(named), "workbench") : []]) {
|
|
13346
13482
|
if (seen.has(e.name)) {
|
|
13347
13483
|
process.stderr.write(`behold: workbench entry "${e.name}" collides with a demo already in the catalog \u2014 dropped
|
|
13348
13484
|
`);
|
|
@@ -13355,7 +13491,7 @@ function loadDemoRegistry(pkgRoot3) {
|
|
|
13355
13491
|
}
|
|
13356
13492
|
function demoLocalPath(entry) {
|
|
13357
13493
|
if (entry.source !== "local" || !entry.path) return void 0;
|
|
13358
|
-
return isAbsolute4(entry.path) ? entry.path :
|
|
13494
|
+
return isAbsolute4(entry.path) ? entry.path : resolve15(entry.catalogDir ?? process.cwd(), entry.path);
|
|
13359
13495
|
}
|
|
13360
13496
|
function missingRequirements(entry) {
|
|
13361
13497
|
const bins = entry.source === "git" && !entry.requires.includes("git") ? [...entry.requires, "git"] : entry.requires;
|
|
@@ -13374,9 +13510,9 @@ function fetchesFromNetwork(entry) {
|
|
|
13374
13510
|
function demoTargetDir(entry, cwd = process.cwd()) {
|
|
13375
13511
|
const inPlace = entry.inPlace ? demoLocalPath(entry) : void 0;
|
|
13376
13512
|
if (inPlace) return inPlace;
|
|
13377
|
-
const legacy =
|
|
13513
|
+
const legacy = resolve15(cwd, "behold-demo");
|
|
13378
13514
|
if (entry.name === "writes" && existsSync17(legacy)) return legacy;
|
|
13379
|
-
return
|
|
13515
|
+
return resolve15(cwd, "behold-demos", entry.name);
|
|
13380
13516
|
}
|
|
13381
13517
|
async function loadDemo(entry, opts) {
|
|
13382
13518
|
const { pkgRoot: pkgRoot3, target } = opts;
|
|
@@ -13785,6 +13921,20 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
|
|
|
13785
13921
|
onDone: (opEnv) => captureFrame(cfg.projectDir, opEnv ?? cfg.env, frames, broadcaster)
|
|
13786
13922
|
})) {
|
|
13787
13923
|
const app = new Hono();
|
|
13924
|
+
const guard = { allowedHosts: allowedHostsFrom(cfg.allowedHosts ?? []), ...cfg.host ? { boundHost: cfg.host } : {} };
|
|
13925
|
+
app.use("*", async (c, next) => {
|
|
13926
|
+
const verdict = guardRequest(
|
|
13927
|
+
{
|
|
13928
|
+
method: c.req.method,
|
|
13929
|
+
host: c.req.header("host") ?? new URL(c.req.url).host,
|
|
13930
|
+
origin: c.req.header("origin") ?? null,
|
|
13931
|
+
secFetchSite: c.req.header("sec-fetch-site") ?? null
|
|
13932
|
+
},
|
|
13933
|
+
guard
|
|
13934
|
+
);
|
|
13935
|
+
if (!verdict.ok) return c.json({ error: verdict.error, code: "request-refused" }, verdict.status);
|
|
13936
|
+
await next();
|
|
13937
|
+
});
|
|
13788
13938
|
setChoudoufuRunner(cfg.choudoufu?.run);
|
|
13789
13939
|
setEstateLexiconRead(cfg.choudoufu ? cfg.choudoufu.lexicon ?? null : void 0);
|
|
13790
13940
|
setServedWorkspace(cfg.workspace);
|
|
@@ -14006,6 +14156,7 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
|
|
|
14006
14156
|
return c.json({ history });
|
|
14007
14157
|
});
|
|
14008
14158
|
app.post("/api/operator/approve/:op/:gate", async (c) => {
|
|
14159
|
+
if (cfg.previewMode) return c.json({ error: "approving is disabled in preview mode", code: "preview" }, 403);
|
|
14009
14160
|
const { op, gate } = c.req.param();
|
|
14010
14161
|
const info = estateOps().find((o) => o.name === op);
|
|
14011
14162
|
broadcaster.emit("op", `\u270E approve ${op} ${gate} \u2014 records a fact; the next tick acts on it`);
|
|
@@ -14016,6 +14167,7 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
|
|
|
14016
14167
|
return c.json({ recorded: true, op, gate, semantics: APPROVE_SEMANTICS });
|
|
14017
14168
|
});
|
|
14018
14169
|
app.post("/api/ops/:name/signal/:gate", async (c) => {
|
|
14170
|
+
if (cfg.previewMode) return c.json({ error: "approving is disabled in preview mode", code: "preview" }, 403);
|
|
14019
14171
|
const { name, gate } = c.req.param();
|
|
14020
14172
|
const info = estateOps().find((o) => o.name === name);
|
|
14021
14173
|
broadcaster.emit("op", `\u270E signal ${name} ${gate}`);
|
|
@@ -14023,6 +14175,38 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
|
|
|
14023
14175
|
if (code !== 0) return c.json({ error: stderr.trim() || `signal exited ${code}` }, 500);
|
|
14024
14176
|
return c.json({ signalled: true });
|
|
14025
14177
|
});
|
|
14178
|
+
app.get("/api/workspace/gates", async (c) => {
|
|
14179
|
+
const ws = servedWorkspace();
|
|
14180
|
+
const env = c.req.query("env") || cfg.env || "local";
|
|
14181
|
+
if (!isEnvName(env)) return c.json({ error: `${JSON.stringify(env)} isn't an env name`, code: "env" }, 400);
|
|
14182
|
+
if (!ws) return c.json({ workspace: false, env, gates: [], approver: localApprover() });
|
|
14183
|
+
const read = await readWorkspaceGates(ws, env);
|
|
14184
|
+
if (!read.ok) return c.json({ workspace: true, env, gates: [], refusal: read.refusal, approver: localApprover() });
|
|
14185
|
+
return c.json({ workspace: true, env, gates: read.gates.map(({ dir: _dir, ...g }) => g), approver: localApprover() });
|
|
14186
|
+
});
|
|
14187
|
+
app.post("/api/workspace/gates/approve", async (c) => {
|
|
14188
|
+
if (cfg.previewMode) return c.json({ error: "approving is disabled in preview mode", code: "preview" }, 403);
|
|
14189
|
+
if (!(c.req.header("content-type") ?? "").toLowerCase().startsWith("application/json")) {
|
|
14190
|
+
return c.json({ error: "send the body as application/json", code: "content-type" }, 415);
|
|
14191
|
+
}
|
|
14192
|
+
const ws = servedWorkspace();
|
|
14193
|
+
if (!ws) return c.json({ error: "behold isn't serving a declared workspace", code: "no-workspace" }, 404);
|
|
14194
|
+
const body = await c.req.json().catch(() => ({}));
|
|
14195
|
+
const key = typeof body?.key === "string" ? body.key : "";
|
|
14196
|
+
const env = typeof body?.env === "string" && body.env ? body.env : cfg.env || "local";
|
|
14197
|
+
if (!isEnvName(env)) return c.json({ error: `${JSON.stringify(env)} isn't an env name`, code: "env" }, 400);
|
|
14198
|
+
const read = await readWorkspaceGates(ws, env);
|
|
14199
|
+
if (!read.ok) return c.json({ error: read.refusal.error, code: read.refusal.code }, 422);
|
|
14200
|
+
const g = read.gates.find((x) => x.key === key);
|
|
14201
|
+
if (!g) return c.json({ error: `no gate ${key} is waiting in ${env}`, code: "no-gate" }, 404);
|
|
14202
|
+
if (g.signed) return c.json({ error: `${key} wants a signed approval, which behold can't make. Run: ${g.approve}`, code: "signed" }, 422);
|
|
14203
|
+
const approver = localApprover();
|
|
14204
|
+
broadcaster.emit("op", `\u270E approve ${g.key} as ${approver}`);
|
|
14205
|
+
const { code, stderr, stdout } = await runChantRaw(approveArgs(g), g.dir);
|
|
14206
|
+
if (code !== 0) return c.json({ error: stderr.trim() || stdout.trim() || `approve exited ${code}`, code: "approve" }, 500);
|
|
14207
|
+
broadcaster.emit("changed", "");
|
|
14208
|
+
return c.json({ recorded: true, key: g.key, approver });
|
|
14209
|
+
});
|
|
14026
14210
|
app.get(
|
|
14027
14211
|
"/api/events",
|
|
14028
14212
|
(c) => streamSSE(c, async (stream) => {
|
|
@@ -14089,6 +14273,9 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
|
|
|
14089
14273
|
currentEnv: cfg.env ?? null,
|
|
14090
14274
|
// v0.1.0 preview: the SPA hides git/PR ops + arbitrary-project affordances.
|
|
14091
14275
|
...cfg.previewMode ? { previewMode: true } : {},
|
|
14276
|
+
// #477: who `chant approve` records when behold runs it. The approve
|
|
14277
|
+
// buttons say it before the click.
|
|
14278
|
+
approver: localApprover(),
|
|
14092
14279
|
// The tier picker's options (M2 #54, sourced #70): gated on the served
|
|
14093
14280
|
// project's `.behold.json` declaring a `tiers` block at all — NOT on
|
|
14094
14281
|
// whether its env var happens to be set in behold's own launch env
|
|
@@ -14139,7 +14326,7 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
|
|
|
14139
14326
|
app.post("/api/project/open", async (c) => {
|
|
14140
14327
|
if (cfg.previewMode) return c.json({ error: "switching projects is locked in preview mode" }, 403);
|
|
14141
14328
|
const body = await c.req.json().catch(() => ({}));
|
|
14142
|
-
const dir = typeof body.dir === "string" && body.dir.trim() ?
|
|
14329
|
+
const dir = typeof body.dir === "string" && body.dir.trim() ? resolve16(body.dir.trim()) : "";
|
|
14143
14330
|
if (!dir || !existsSync18(dir)) return c.json({ error: `no such directory: ${dir || "(no dir given)"}` }, 400);
|
|
14144
14331
|
if (!existsSync18(join26(dir, "chant.config.ts"))) {
|
|
14145
14332
|
return c.json({ error: `${dir} doesn't look like a chant project \u2014 no chant.config.ts` }, 400);
|
|
@@ -14164,7 +14351,7 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
|
|
|
14164
14351
|
};
|
|
14165
14352
|
app.post("/api/project/reveal", async (c) => {
|
|
14166
14353
|
const body = await c.req.json().catch(() => ({}));
|
|
14167
|
-
const dir = typeof body.dir === "string" && body.dir.trim() ?
|
|
14354
|
+
const dir = typeof body.dir === "string" && body.dir.trim() ? resolve16(body.dir.trim()) : cfg.projectDir;
|
|
14168
14355
|
const known = /* @__PURE__ */ new Set([cfg.projectDir, ...cfg.projectDirs ?? [], ...listRecents().map((r) => r.dir)]);
|
|
14169
14356
|
if (!known.has(dir)) return c.json({ error: "not a served or recent project directory" }, 400);
|
|
14170
14357
|
const opener = process.platform === "darwin" ? "open" : process.platform === "win32" ? "explorer" : "xdg-open";
|
|
@@ -14375,10 +14562,12 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
|
|
|
14375
14562
|
{ method: "GET", path: "/api/history", desc: "recent source commits (rollback targets)" },
|
|
14376
14563
|
{ method: "GET", path: "/api/frames", desc: "captured lanes frames" },
|
|
14377
14564
|
{ 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" },
|
|
14565
|
+
{ method: "POST", path: "/api/refresh", desc: "re-observe live now (?env=) \u2014 returns the fresh graph. With ?notify=1 it is a host's notice that the workspace changed: cached reads are dropped and every open page re-pulls (#476)" },
|
|
14379
14566
|
{ method: "POST", path: "/api/apply", desc: "delegated apply: ?env=&component=<name|all> (guarded, preview-locked)" },
|
|
14380
14567
|
{ method: "POST", path: "/api/ops/:name/run", desc: "run a committed Op (delegated write)" },
|
|
14381
14568
|
{ method: "POST", path: "/api/ops/:name/signal/:gate", desc: "approve an Op's gate (releases the waiting run)" },
|
|
14569
|
+
{ method: "GET", path: "/api/workspace/gates", desc: "a declared workspace's waiting gates from chant workspace status <env> --json (?env=), keyed member/op/gate, with the approver behold would record (#477)" },
|
|
14570
|
+
{ method: "POST", path: "/api/workspace/gates/approve", desc: "approve one workspace gate: JSON body {key, env}; env and plan come from a fresh status read; recorded as the user running behold; preview-locked (#477)" },
|
|
14382
14571
|
{ method: "POST", path: "/api/operator/approve/:op/:gate", desc: "record a converge gate's resolution (chant approve \u2014 a fact for the next tick, not an unblock)" },
|
|
14383
14572
|
{ method: "POST", path: "/api/rollback", desc: "open a rollback PR: ?to=<sha>" },
|
|
14384
14573
|
{ method: "POST", path: "/api/substrates/:name/up", desc: "bring a substrate up" },
|
|
@@ -14984,7 +15173,14 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
|
|
|
14984
15173
|
}
|
|
14985
15174
|
});
|
|
14986
15175
|
app.post("/api/refresh", async (c) => {
|
|
14987
|
-
const
|
|
15176
|
+
const url = new URL(c.req.url);
|
|
15177
|
+
if (url.searchParams.get("notify") === "1") {
|
|
15178
|
+
invalidateMember();
|
|
15179
|
+
invalidateOverlay();
|
|
15180
|
+
broadcaster.emit("changed", "");
|
|
15181
|
+
return c.json({ notified: true });
|
|
15182
|
+
}
|
|
15183
|
+
const env = optsFromQuery(url).env ?? cfg.env;
|
|
14988
15184
|
const result = await captureFrame(cfg.projectDir, env, frames, broadcaster);
|
|
14989
15185
|
if (!result) return c.json({ error: "refresh failed \u2014 see server log" }, 500);
|
|
14990
15186
|
if (env) reclassifyOverlay(result.ir);
|
|
@@ -15064,9 +15260,9 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
|
|
|
15064
15260
|
});
|
|
15065
15261
|
const choudoufuPlanPath = (plan) => {
|
|
15066
15262
|
if (!plan) return { error: "no plan named", code: "choudoufu_no_plan", remedy: "pass ?plan=<path to a carve.json inside a served choudoufu member> \u2014 GET /api/project lists the ones found" };
|
|
15067
|
-
const abs =
|
|
15263
|
+
const abs = resolve16(cfg.projectDir, plan);
|
|
15068
15264
|
const members = (cfg.projectDirs ?? [cfg.projectDir]).filter((d) => memberKindOf(d) === "choudoufu");
|
|
15069
|
-
const inside = members.some((d) => abs.startsWith(
|
|
15265
|
+
const inside = members.some((d) => abs.startsWith(resolve16(d) + sep7));
|
|
15070
15266
|
if (!inside) return { error: `${plan} is not inside a served choudoufu member`, code: "choudoufu_plan_outside", remedy: "a plan lives in the estate it moves; point ?plan= at a carve.json under one of the served members" };
|
|
15071
15267
|
return { path: abs };
|
|
15072
15268
|
};
|
|
@@ -15197,6 +15393,10 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
|
|
|
15197
15393
|
return c.json({ started: true, component, env });
|
|
15198
15394
|
});
|
|
15199
15395
|
const rel = relative8(process.cwd(), webRoot) || ".";
|
|
15396
|
+
app.use("/*", async (c, next) => {
|
|
15397
|
+
await next();
|
|
15398
|
+
if (!c.req.path.startsWith("/api/") && c.res.ok && !c.res.headers.has("cache-control")) c.res.headers.set("cache-control", "no-cache");
|
|
15399
|
+
});
|
|
15200
15400
|
app.use("/*", serveStatic({ root: rel }));
|
|
15201
15401
|
app.get("/", serveStatic({ path: join26(rel, "index.html") }));
|
|
15202
15402
|
return app;
|
|
@@ -15236,6 +15436,7 @@ async function startServer(cfg) {
|
|
|
15236
15436
|
broadcaster,
|
|
15237
15437
|
onDone: (opEnv) => captureFrame(cfg.projectDir, opEnv ?? cfg.env, frames, broadcaster)
|
|
15238
15438
|
});
|
|
15439
|
+
if (!cfg.host && process.env.BEHOLD_HOST) cfg.host = process.env.BEHOLD_HOST;
|
|
15239
15440
|
const app = createApp(cfg, broadcaster, frames, runner);
|
|
15240
15441
|
const autoSync = cfg.autoSync ?? "off";
|
|
15241
15442
|
if (!cfg.carveReport) {
|
|
@@ -15343,7 +15544,8 @@ async function startServer(cfg) {
|
|
|
15343
15544
|
};
|
|
15344
15545
|
process.on("SIGINT", shutdown);
|
|
15345
15546
|
process.on("SIGTERM", shutdown);
|
|
15346
|
-
const
|
|
15547
|
+
const hostname = cfg.host ?? process.env.BEHOLD_HOST ?? "127.0.0.1";
|
|
15548
|
+
const server = serve({ fetch: app.fetch, port: cfg.port, hostname }, (info) => {
|
|
15347
15549
|
if (carve) {
|
|
15348
15550
|
process.stdout.write(
|
|
15349
15551
|
`behold \u2192 http://localhost:${info.port}
|
|
@@ -15394,7 +15596,7 @@ init_chant();
|
|
|
15394
15596
|
// src/workspace-convert.ts
|
|
15395
15597
|
init_chant();
|
|
15396
15598
|
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
|
|
15599
|
+
import { basename as basename4, dirname as dirname8, join as join27, relative as relative9, resolve as resolve17, sep as sep8 } from "node:path";
|
|
15398
15600
|
import { shortStackNames as shortStackNames2 } from "@intentius/pinhole";
|
|
15399
15601
|
init_project();
|
|
15400
15602
|
init_terraform_member();
|
|
@@ -15411,7 +15613,7 @@ function unique(name, taken) {
|
|
|
15411
15613
|
}
|
|
15412
15614
|
var posix = (p) => p.split(sep8).join("/");
|
|
15413
15615
|
function installedVersion(pkg, dir) {
|
|
15414
|
-
for (let at =
|
|
15616
|
+
for (let at = resolve17(dir); ; at = dirname8(at)) {
|
|
15415
15617
|
const manifest = join27(at, "node_modules", ...pkg.split("/"), "package.json");
|
|
15416
15618
|
if (existsSync19(manifest)) {
|
|
15417
15619
|
try {
|
|
@@ -15425,14 +15627,14 @@ function installedVersion(pkg, dir) {
|
|
|
15425
15627
|
}
|
|
15426
15628
|
}
|
|
15427
15629
|
function planConversion(rootArg) {
|
|
15428
|
-
const root =
|
|
15630
|
+
const root = resolve17(rootArg);
|
|
15429
15631
|
const existing = DECLARATION_FILES.find((f) => existsSync19(join27(root, f)));
|
|
15430
15632
|
if (existing) return { ok: false, error: `${root} already has ${existing}; there is nothing to convert.` };
|
|
15431
15633
|
const shape = detectProjectShape(root);
|
|
15432
15634
|
if (shape.kind !== "estate" || !shape.members?.length || shape.membersFrom === "itself") {
|
|
15433
15635
|
return { ok: false, error: `${root} is not an estate root with a member list, so there is no member list to convert.` };
|
|
15434
15636
|
}
|
|
15435
|
-
const abs = shape.members.map((m) =>
|
|
15637
|
+
const abs = shape.members.map((m) => resolve17(root, m.dir));
|
|
15436
15638
|
const shorts = shortStackNames2(abs);
|
|
15437
15639
|
const taken = /* @__PURE__ */ new Set();
|
|
15438
15640
|
const members = [];
|
|
@@ -15500,7 +15702,7 @@ function applyConversion(plan) {
|
|
|
15500
15702
|
let layout;
|
|
15501
15703
|
const target = layoutPath(plan.root);
|
|
15502
15704
|
if (!plan.layoutFrom) layout = { skipped: "no saved layout to move" };
|
|
15503
|
-
else if (
|
|
15705
|
+
else if (resolve17(plan.layoutFrom) === resolve17(target)) layout = { skipped: "the saved layout is already in the root" };
|
|
15504
15706
|
else if (existsSync19(target)) layout = { skipped: `${target} already exists; the layout in ${plan.layoutFrom} was left alone` };
|
|
15505
15707
|
else {
|
|
15506
15708
|
const moved = moveLayoutIds(readLayoutFile(dirname8(dirname8(plan.layoutFrom))), plan.moves);
|
|
@@ -15694,7 +15896,7 @@ Usage:
|
|
|
15694
15896
|
behold doctor [project-dir] [--json] [--fix]
|
|
15695
15897
|
behold preview [project-dir] [--port <n>] [--emulator]
|
|
15696
15898
|
behold export [project-dir] [--out <dir>] [--env <name>] [--name <worker>] [--emulator]
|
|
15697
|
-
behold serve <project-dir\u2026> [--port <n>] [--env <name>] [--poll <secs>] [--local]
|
|
15899
|
+
behold serve <project-dir\u2026> [--port <n>] [--host <addr>] [--allow-host <name,\u2026>] [--env <name>] [--poll <secs>] [--local]
|
|
15698
15900
|
behold carve <report.json> [--port <n>]
|
|
15699
15901
|
|
|
15700
15902
|
carve Render a chant Terraform peelability report \u2014 the JSON from
|
|
@@ -15753,6 +15955,12 @@ Usage:
|
|
|
15753
15955
|
|
|
15754
15956
|
Options:
|
|
15755
15957
|
--port <n> Port (default 4600). preview/serve/carve.
|
|
15958
|
+
--host <addr> serve only: the address to bind (default 127.0.0.1, or
|
|
15959
|
+
BEHOLD_HOST). behold runs writes with your credentials,
|
|
15960
|
+
so it answers this machine unless you say otherwise.
|
|
15961
|
+
--allow-host <n,\u2026> serve only: names besides loopback behold answers to, and
|
|
15962
|
+
whose pages may write (BEHOLD_ALLOWED_HOSTS too). For a
|
|
15963
|
+
proxy that frames behold, or --host 0.0.0.0.
|
|
15756
15964
|
--env <name> Environment name \u2014 turns on the live drift overlay.
|
|
15757
15965
|
export/serve.
|
|
15758
15966
|
--poll <secs> Re-query live drift every <secs> and push updates (needs --env).
|
|
@@ -15837,9 +16045,13 @@ ${USAGE}`);
|
|
|
15837
16045
|
let pollSecs;
|
|
15838
16046
|
let autoSync = "off";
|
|
15839
16047
|
let local = false;
|
|
16048
|
+
let host;
|
|
16049
|
+
const allowHosts = [];
|
|
15840
16050
|
for (let i = 0; i < rest.length; i++) {
|
|
15841
16051
|
const a = rest[i];
|
|
15842
16052
|
if (a === "--port") port = Number(rest[++i]);
|
|
16053
|
+
else if (a === "--host") host = rest[++i];
|
|
16054
|
+
else if (a === "--allow-host") allowHosts.push(...(rest[++i] ?? "").split(","));
|
|
15843
16055
|
else if (a === "--env") env = rest[++i];
|
|
15844
16056
|
else if (a === "--poll") pollSecs = Number(rest[++i]);
|
|
15845
16057
|
else if (a === "--local") local = true;
|
|
@@ -15880,7 +16092,7 @@ ${USAGE}`);
|
|
|
15880
16092
|
process.stderr.write("behold serve: --auto-sync needs --env and --poll (it acts on polled drift)\n");
|
|
15881
16093
|
process.exit(2);
|
|
15882
16094
|
}
|
|
15883
|
-
const dirs = projectDirs.map((d) =>
|
|
16095
|
+
const dirs = projectDirs.map((d) => resolve18(d));
|
|
15884
16096
|
const workspace = dirs.length === 1 && hasDeclaration(dirs[0]) ? await declaredWorkspace(dirs[0]) : void 0;
|
|
15885
16097
|
if (workspace) {
|
|
15886
16098
|
const drawn = drawnMembers(workspace).map((m) => m.abs);
|
|
@@ -15892,7 +16104,9 @@ ${USAGE}`);
|
|
|
15892
16104
|
...env ? { env } : {},
|
|
15893
16105
|
...pollSecs !== void 0 ? { pollSecs } : {},
|
|
15894
16106
|
...autoSync !== "off" ? { autoSync } : {},
|
|
15895
|
-
...local ? { local: true } : {}
|
|
16107
|
+
...local ? { local: true } : {},
|
|
16108
|
+
...host ? { host } : {},
|
|
16109
|
+
...allowHosts.length ? { allowedHosts: allowHosts } : {}
|
|
15896
16110
|
});
|
|
15897
16111
|
return;
|
|
15898
16112
|
}
|
|
@@ -15909,7 +16123,9 @@ ${USAGE}`);
|
|
|
15909
16123
|
...env ? { env } : {},
|
|
15910
16124
|
...pollSecs !== void 0 ? { pollSecs } : {},
|
|
15911
16125
|
...autoSync !== "off" ? { autoSync } : {},
|
|
15912
|
-
...local ? { local: true } : {}
|
|
16126
|
+
...local ? { local: true } : {},
|
|
16127
|
+
...host ? { host } : {},
|
|
16128
|
+
...allowHosts.length ? { allowedHosts: allowHosts } : {}
|
|
15913
16129
|
});
|
|
15914
16130
|
}
|
|
15915
16131
|
async function declaredWorkspace(root) {
|
|
@@ -15976,7 +16192,7 @@ async function runCarve(rest) {
|
|
|
15976
16192
|
);
|
|
15977
16193
|
process.exit(2);
|
|
15978
16194
|
}
|
|
15979
|
-
const reportPath =
|
|
16195
|
+
const reportPath = resolve18(fileArg);
|
|
15980
16196
|
const parsed = readCarveReport(reportPath, (p) => readFileSync22(p, "utf8"));
|
|
15981
16197
|
if (!parsed.ok) {
|
|
15982
16198
|
process.stderr.write(`behold carve: ${parsed.refusal.error}
|
|
@@ -16006,8 +16222,8 @@ async function runDoctor(rest) {
|
|
|
16006
16222
|
}
|
|
16007
16223
|
}
|
|
16008
16224
|
const dir = dirArg ?? ".";
|
|
16009
|
-
if (!existsSync21(
|
|
16010
|
-
process.stderr.write(`behold doctor: no such directory: ${
|
|
16225
|
+
if (!existsSync21(resolve18(dir))) {
|
|
16226
|
+
process.stderr.write(`behold doctor: no such directory: ${resolve18(dir)}
|
|
16011
16227
|
`);
|
|
16012
16228
|
process.exit(2);
|
|
16013
16229
|
}
|
|
@@ -16106,7 +16322,7 @@ async function runDemo(rest) {
|
|
|
16106
16322
|
`);
|
|
16107
16323
|
process.exit(2);
|
|
16108
16324
|
}
|
|
16109
|
-
const target = dirArg ?
|
|
16325
|
+
const target = dirArg ? resolve18(dirArg) : demoTargetDir(entry);
|
|
16110
16326
|
const loaded = await loadDemo(entry, { pkgRoot: pkgRoot3, target, log: (line) => process.stdout.write(line + "\n") });
|
|
16111
16327
|
if (!loaded.ok) {
|
|
16112
16328
|
process.stderr.write(`behold demo ${entry.name}: ${loaded.error}
|
|
@@ -16138,7 +16354,7 @@ function spawnStep(cmd, args, cwd) {
|
|
|
16138
16354
|
});
|
|
16139
16355
|
}
|
|
16140
16356
|
async function serveCarveDemo(target, carve, port, live2 = false) {
|
|
16141
|
-
const at = (rel) =>
|
|
16357
|
+
const at = (rel) => resolve18(target, rel);
|
|
16142
16358
|
const project = at(carve.project);
|
|
16143
16359
|
const from = at(carve.from);
|
|
16144
16360
|
const state = carve.state ? at(carve.state) : void 0;
|
|
@@ -16259,7 +16475,7 @@ async function runPreview(rest) {
|
|
|
16259
16475
|
process.stderr.write("behold preview: --port must be a number\n");
|
|
16260
16476
|
process.exit(2);
|
|
16261
16477
|
}
|
|
16262
|
-
const projectDir =
|
|
16478
|
+
const projectDir = resolve18(dirArg ?? process.cwd());
|
|
16263
16479
|
if (!existsSync21(projectDir)) {
|
|
16264
16480
|
process.stderr.write(`behold preview: project not found at ${projectDir}
|
|
16265
16481
|
`);
|
|
@@ -16280,21 +16496,21 @@ async function runPreview(rest) {
|
|
|
16280
16496
|
await startServer({ projectDir, port, env: "local", previewMode: true });
|
|
16281
16497
|
}
|
|
16282
16498
|
async function runExportCmd(rest) {
|
|
16283
|
-
let outDir =
|
|
16499
|
+
let outDir = resolve18("behold-export");
|
|
16284
16500
|
let env;
|
|
16285
16501
|
let name;
|
|
16286
16502
|
let dirArg;
|
|
16287
16503
|
let emulator = false;
|
|
16288
16504
|
for (let i = 0; i < rest.length; i++) {
|
|
16289
16505
|
const a = rest[i];
|
|
16290
|
-
if (a === "--out") outDir =
|
|
16506
|
+
if (a === "--out") outDir = resolve18(rest[++i]);
|
|
16291
16507
|
else if (a === "--env") env = rest[++i];
|
|
16292
16508
|
else if (a === "--name") name = rest[++i];
|
|
16293
16509
|
else if (a === "--emulator") emulator = true;
|
|
16294
16510
|
else if (a === "-h" || a === "--help") return void process.stdout.write(USAGE);
|
|
16295
16511
|
else if (!a.startsWith("-")) dirArg = a;
|
|
16296
16512
|
}
|
|
16297
|
-
const projectDir =
|
|
16513
|
+
const projectDir = resolve18(dirArg ?? process.cwd());
|
|
16298
16514
|
if (emulator) {
|
|
16299
16515
|
injectEmulatorEnv(env);
|
|
16300
16516
|
env ??= "local";
|