@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/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 resolve18;
121
+ let resolve19;
122
122
  let reject;
123
123
  const promise = new Promise((yes, no) => {
124
- resolve18 = yes;
124
+ resolve19 = yes;
125
125
  reject = no;
126
126
  });
127
127
  job = {
128
128
  key,
129
129
  run: run3,
130
130
  promise,
131
- resolve: resolve18,
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((resolve18, reject) => {
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 resolve18(value);
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 resolve18(selector, signal) {
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 resolve18(selector, signal);
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 resolve18({ apiVersion: ref.apiVersion, kind: ref.kind }, signal);
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((resolve19) => {
1172
- const finish = () => resolve19({ done: true, value: void 0 });
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((resolve19) => {
1200
- const timer = setTimeout(resolve19, reopenDelayMs);
1199
+ await new Promise((resolve20) => {
1200
+ const timer = setTimeout(resolve20, reopenDelayMs);
1201
1201
  controller.signal.addEventListener("abort", () => {
1202
1202
  clearTimeout(timer);
1203
- resolve19(void 0);
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: resolve18,
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 resolve17, dirname as dirname10, join as join29 } from "node:path";
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 resolve15, sep as sep7 } from "node:path";
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((resolve18) => {
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
- resolve18({ code: 127, out: "" });
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", () => resolve18({ code: 127, out }));
4701
- proc.on("close", (code) => resolve18({ code: code ?? 1, out }));
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
- // chant#2040: the manifests a member holds name the files its carved
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 = joinCarvedSources(await memberSource(dir, srcOpts), (await carveStatesFor(dir)).values(), dir).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
- dropped.push({ name, reason: firstLine2(err2) });
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((resolve18) => {
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
- resolve18({ code: 127, out: "" });
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", () => resolve18({ code: 127, out }));
11517
- proc.on("close", (code) => resolve18({ code: code ?? 1, out }));
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 resolve13 } from "node:path";
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 = resolve13(dir);
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: resolve13(root, m.dir) }));
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 resolve14, sep as sep6 } from "node:path";
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(resolve14(file));
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(resolve14(named), "workbench") : []]) {
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 : resolve14(entry.catalogDir ?? process.cwd(), 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 = resolve14(cwd, "behold-demo");
13533
+ const legacy = resolve15(cwd, "behold-demo");
13378
13534
  if (entry.name === "writes" && existsSync17(legacy)) return legacy;
13379
- return resolve14(cwd, "behold-demos", entry.name);
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() ? resolve15(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() ? resolve15(body.dir.trim()) : cfg.projectDir;
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 env = optsFromQuery(new URL(c.req.url)).env ?? cfg.env;
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 = resolve15(cfg.projectDir, plan);
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(resolve15(d) + sep7));
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 server = serve({ fetch: app.fetch, port: cfg.port }, (info) => {
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 resolve16, sep as sep8 } from "node:path";
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 = resolve16(dir); ; at = dirname8(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 = resolve16(rootArg);
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) => resolve16(root, m.dir));
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 (resolve16(plan.layoutFrom) === resolve16(target)) layout = { skipped: "the saved layout is already in the root" };
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) => resolve17(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 = resolve17(fileArg);
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(resolve17(dir))) {
16010
- process.stderr.write(`behold doctor: no such directory: ${resolve17(dir)}
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 ? resolve17(dirArg) : demoTargetDir(entry);
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) => resolve17(target, 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 = resolve17(dirArg ?? process.cwd());
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 = resolve17("behold-export");
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 = resolve17(rest[++i]);
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 = resolve17(dirArg ?? process.cwd());
16533
+ const projectDir = resolve18(dirArg ?? process.cwd());
16298
16534
  if (emulator) {
16299
16535
  injectEmulatorEnv(env);
16300
16536
  env ??= "local";