austere-axi 0.6.0 → 0.7.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.mjs CHANGED
@@ -618,7 +618,7 @@ var init_paths = __esm({
618
618
  HERE = path.dirname(fileURLToPath(import.meta.url));
619
619
  BUNDLED = true;
620
620
  ASSET_DIR = BUNDLED ? HERE : path.resolve(HERE, "../../dist");
621
- VERSION = true ? "0.6.0" : packageVersion();
621
+ VERSION = true ? "0.7.0" : packageVersion();
622
622
  CLI_ENTRY = BUNDLED ? fileURLToPath(import.meta.url) : path.resolve(HERE, "main.ts");
623
623
  securedDir = null;
624
624
  }
@@ -1953,8 +1953,8 @@ __export(install_exports, {
1953
1953
  removePack: () => removePack,
1954
1954
  untar: () => untar
1955
1955
  });
1956
- import fs10 from "node:fs";
1957
- import path9 from "node:path";
1956
+ import fs11 from "node:fs";
1957
+ import path10 from "node:path";
1958
1958
  import zlib from "node:zlib";
1959
1959
  import crypto4 from "node:crypto";
1960
1960
  function registryUrl() {
@@ -2091,15 +2091,15 @@ function readTarball(tgz, label) {
2091
2091
  return { packJs, catalog, packageJson: files.get("package.json") || null };
2092
2092
  }
2093
2093
  function fromTarball(file3) {
2094
- const abs = path9.resolve(file3.replace(/^~(?=[\\/])/, process.env.HOME || ""));
2095
- if (!fs10.existsSync(abs)) throw new Error(`no such file: ${abs}`);
2096
- return { ...readTarball(fs10.readFileSync(abs), path9.basename(abs)), source: "tarball", resolved: abs };
2094
+ const abs = path10.resolve(file3.replace(/^~(?=[\\/])/, process.env.HOME || ""));
2095
+ if (!fs11.existsSync(abs)) throw new Error(`no such file: ${abs}`);
2096
+ return { ...readTarball(fs11.readFileSync(abs), path10.basename(abs)), source: "tarball", resolved: abs };
2097
2097
  }
2098
2098
  function fromFolder(dir) {
2099
- const abs = path9.resolve(dir.replace(/^~(?=[\\/])/, process.env.HOME || ""));
2100
- if (!fs10.existsSync(abs)) throw new Error(`no such folder: ${abs}`);
2101
- if (fs10.statSync(abs).isFile()) return fromTarball(abs);
2102
- const read = (rel) => fs10.existsSync(path9.join(abs, rel)) ? fs10.readFileSync(path9.join(abs, rel)) : null;
2099
+ const abs = path10.resolve(dir.replace(/^~(?=[\\/])/, process.env.HOME || ""));
2100
+ if (!fs11.existsSync(abs)) throw new Error(`no such folder: ${abs}`);
2101
+ if (fs11.statSync(abs).isFile()) return fromTarball(abs);
2102
+ const read = (rel) => fs11.existsSync(path10.join(abs, rel)) ? fs11.readFileSync(path10.join(abs, rel)) : null;
2103
2103
  const packJs = read("dist/pack.js");
2104
2104
  const catalog = read("dist/catalog.json");
2105
2105
  if (!packJs || !catalog) throw new Error(`${abs} has no dist/pack.js and dist/catalog.json: run \`austere-axi components build ${dir}\` first`);
@@ -2139,10 +2139,10 @@ async function addPack(spec, { trust = false } = {}) {
2139
2139
  }
2140
2140
  const got = local ? fromFolder(spec) : await fromRegistry(spec);
2141
2141
  if (got.source === "tarball" && !trust) {
2142
- const peek = parseCatalog(got.catalog, path9.basename(got.resolved));
2142
+ const peek = parseCatalog(got.catalog, path10.basename(got.resolved));
2143
2143
  throw Object.assign(
2144
2144
  new Error(
2145
- `${path9.basename(got.resolved)} (${peek.pack}) comes from a file, not the registry: its code would run inside your review pages. Rerun with --trust once you have looked at it.`
2145
+ `${path10.basename(got.resolved)} (${peek.pack}) comes from a file, not the registry: its code would run inside your review pages. Rerun with --trust once you have looked at it.`
2146
2146
  ),
2147
2147
  { code: "untrusted" }
2148
2148
  );
@@ -2167,14 +2167,14 @@ async function addPack(spec, { trust = false } = {}) {
2167
2167
  }
2168
2168
  const lock = readLock();
2169
2169
  const previous = lock.packs[cat.pack];
2170
- const dir = path9.join(packsDir(), packDirName(cat.pack, cat.version));
2171
- fs10.rmSync(dir, { recursive: true, force: true });
2172
- fs10.mkdirSync(dir, { recursive: true });
2173
- fs10.writeFileSync(path9.join(dir, "pack.js"), got.packJs);
2174
- fs10.writeFileSync(path9.join(dir, "catalog.json"), got.catalog);
2175
- if (got.packageJson) fs10.writeFileSync(path9.join(dir, "package.json"), got.packageJson);
2170
+ const dir = path10.join(packsDir(), packDirName(cat.pack, cat.version));
2171
+ fs11.rmSync(dir, { recursive: true, force: true });
2172
+ fs11.mkdirSync(dir, { recursive: true });
2173
+ fs11.writeFileSync(path10.join(dir, "pack.js"), got.packJs);
2174
+ fs11.writeFileSync(path10.join(dir, "catalog.json"), got.catalog);
2175
+ if (got.packageJson) fs11.writeFileSync(path10.join(dir, "package.json"), got.packageJson);
2176
2176
  if (previous && previous.version !== cat.version) {
2177
- fs10.rmSync(path9.join(packsDir(), packDirName(cat.pack, previous.version)), { recursive: true, force: true });
2177
+ fs11.rmSync(path10.join(packsDir(), packDirName(cat.pack, previous.version)), { recursive: true, force: true });
2178
2178
  }
2179
2179
  lock.packs[cat.pack] = {
2180
2180
  version: cat.version,
@@ -2194,7 +2194,7 @@ function removePack(name) {
2194
2194
  if (!entry) return null;
2195
2195
  delete lock.packs[name];
2196
2196
  writeLock(lock);
2197
- fs10.rmSync(path9.join(packsDir(), packDirName(name, entry.version)), { recursive: true, force: true });
2197
+ fs11.rmSync(path10.join(packsDir(), packDirName(name, entry.version)), { recursive: true, force: true });
2198
2198
  return { name, version: entry.version };
2199
2199
  }
2200
2200
  var MAX_TARBALL, MAX_PACK_JS;
@@ -2215,8 +2215,8 @@ __export(pack_dev_exports, {
2215
2215
  buildPackDir: () => buildPackDir,
2216
2216
  newPack: () => newPack
2217
2217
  });
2218
- import fs11 from "node:fs";
2219
- import path10 from "node:path";
2218
+ import fs12 from "node:fs";
2219
+ import path11 from "node:path";
2220
2220
  import { createRequire } from "node:module";
2221
2221
  import { pathToFileURL } from "node:url";
2222
2222
  function packageJson(name) {
@@ -2253,9 +2253,9 @@ Publish with \`npm publish\` after \`npm run build\`. Others install it with
2253
2253
  }
2254
2254
  function newPack(dir, { name } = {}) {
2255
2255
  if (!dir) throw new Error("new needs a folder: austere-axi components new austere-pack-milestones");
2256
- const abs = path10.resolve(dir);
2257
- if (fs11.existsSync(abs) && fs11.readdirSync(abs).length) throw new Error(`${abs} is not empty`);
2258
- const pkgName = name || path10.basename(abs).toLowerCase().replace(/[^a-z0-9._-]+/g, "-");
2256
+ const abs = path11.resolve(dir);
2257
+ if (fs12.existsSync(abs) && fs12.readdirSync(abs).length) throw new Error(`${abs} is not empty`);
2258
+ const pkgName = name || path11.basename(abs).toLowerCase().replace(/[^a-z0-9._-]+/g, "-");
2259
2259
  if (!isValidPackName(pkgName)) throw new Error(`${JSON.stringify(pkgName)} is not an npm package name; pass --name`);
2260
2260
  const files = {
2261
2261
  "package.json": JSON.stringify(packageJson(pkgName), null, 2) + "\n",
@@ -2264,13 +2264,13 @@ function newPack(dir, { name } = {}) {
2264
2264
  ...TEMPLATE
2265
2265
  };
2266
2266
  for (const [rel, text] of Object.entries(files)) {
2267
- fs11.mkdirSync(path10.dirname(path10.join(abs, rel)), { recursive: true });
2268
- fs11.writeFileSync(path10.join(abs, rel), text);
2267
+ fs12.mkdirSync(path11.dirname(path11.join(abs, rel)), { recursive: true });
2268
+ fs12.writeFileSync(path11.join(abs, rel), text);
2269
2269
  }
2270
2270
  return { dir: abs, name: pkgName, files: Object.keys(files) };
2271
2271
  }
2272
2272
  async function findEsbuild(dir) {
2273
- for (const base of [path10.join(dir, "package.json"), path10.join(process.cwd(), "package.json"), import.meta.url]) {
2273
+ for (const base of [path11.join(dir, "package.json"), path11.join(process.cwd(), "package.json"), import.meta.url]) {
2274
2274
  try {
2275
2275
  const resolved = createRequire(base).resolve("esbuild");
2276
2276
  const esbuild = await import(pathToFileURL(resolved).href);
@@ -2281,8 +2281,8 @@ async function findEsbuild(dir) {
2281
2281
  return null;
2282
2282
  }
2283
2283
  async function buildPackDir(dir = ".") {
2284
- const abs = path10.resolve(dir);
2285
- if (!fs11.existsSync(path10.join(abs, "package.json"))) throw new Error(`${abs} has no package.json (\`austere-axi components new <dir>\` makes a pack)`);
2284
+ const abs = path11.resolve(dir);
2285
+ if (!fs12.existsSync(path11.join(abs, "package.json"))) throw new Error(`${abs} has no package.json (\`austere-axi components new <dir>\` makes a pack)`);
2286
2286
  const esbuild = await findEsbuild(abs);
2287
2287
  if (!esbuild) throw new Error(`components build needs esbuild: run \`npm install -D esbuild\` in ${abs}`);
2288
2288
  process.env.OPENUI_TELEMETRY_DISABLED ||= "1";
@@ -2406,7 +2406,7 @@ export default austereComponent({
2406
2406
 
2407
2407
  // src/cli/net.ts
2408
2408
  import { execFile } from "node:child_process";
2409
- import fs12 from "node:fs";
2409
+ import fs13 from "node:fs";
2410
2410
  function run(bin, args) {
2411
2411
  return new Promise((resolve) => {
2412
2412
  execFile(bin, args, { timeout: 2500, windowsHide: true }, (err, stdout) => resolve(err ? null : stdout));
@@ -2417,7 +2417,7 @@ async function detectTailscale({ fresh = false } = {}) {
2417
2417
  if (!fresh && Date.now() - cache.at < 3e4) return cache.value;
2418
2418
  let value = null;
2419
2419
  for (const bin of CANDIDATES) {
2420
- if (bin.startsWith("/") && !fs12.existsSync(bin)) continue;
2420
+ if (bin.startsWith("/") && !fs13.existsSync(bin)) continue;
2421
2421
  const out = await run(bin, ["status", "--json"]);
2422
2422
  if (!out) continue;
2423
2423
  try {
@@ -2514,8 +2514,8 @@ var init_items = __esm({
2514
2514
  });
2515
2515
 
2516
2516
  // src/cli/hub-page.ts
2517
- import os2 from "node:os";
2518
- import path11 from "node:path";
2517
+ import os3 from "node:os";
2518
+ import path12 from "node:path";
2519
2519
  function day(ms) {
2520
2520
  const d = new Date(ms);
2521
2521
  const today = /* @__PURE__ */ new Date();
@@ -2529,8 +2529,8 @@ function time3(ms) {
2529
2529
  return new Date(ms).toLocaleTimeString("en", { hour: "2-digit", minute: "2-digit", hour12: false });
2530
2530
  }
2531
2531
  function tilde(p) {
2532
- const home = os2.homedir();
2533
- return p === home || p.startsWith(home + path11.sep) ? "~" + p.slice(home.length) : p;
2532
+ const home = os3.homedir();
2533
+ return p === home || p.startsWith(home + path12.sep) ? "~" + p.slice(home.length) : p;
2534
2534
  }
2535
2535
  function counts(a) {
2536
2536
  const waiting = a.open + a.received;
@@ -2566,13 +2566,13 @@ function hubPage(rows, { version: version2, q: q2, project, stats }) {
2566
2566
  const body = [...days.entries()].map(
2567
2567
  ([d, items]) => `<h3>${escapeHtml(d)}</h3><ul>${items.map(
2568
2568
  (a) => `<li><a href="/hub/open/${a.id}" title="${escapeHtml(a.file)}">
2569
- <strong>${escapeHtml(a.title || path11.basename(a.file))}</strong>
2570
- <span class="meta">${escapeHtml(path11.relative(proj, a.file) || path11.basename(a.file))} \xB7 ${time3(a.updated_at)} \xB7 ${state(a)}${counts(a) ? ` \xB7 ${counts(a)}` : ""}</span>
2569
+ <strong>${escapeHtml(a.title || path12.basename(a.file))}</strong>
2570
+ <span class="meta">${escapeHtml(path12.relative(proj, a.file) || path12.basename(a.file))} \xB7 ${time3(a.updated_at)} \xB7 ${state(a)}${counts(a) ? ` \xB7 ${counts(a)}` : ""}</span>
2571
2571
  </a></li>`
2572
2572
  ).join("")}</ul>`
2573
2573
  ).join("");
2574
2574
  const unanswered = list.reduce((n, a) => n + a.open + a.received, 0);
2575
- return `<section><h2><a href="/?project=${encodeURIComponent(proj)}">${escapeHtml(path11.basename(proj) || proj)}</a> <small title="${escapeHtml(proj)}">${escapeHtml(
2575
+ return `<section><h2><a href="/?project=${encodeURIComponent(proj)}">${escapeHtml(path12.basename(proj) || proj)}</a> <small title="${escapeHtml(proj)}">${escapeHtml(
2576
2576
  shortPath(proj)
2577
2577
  )}</small>${unanswered ? ` <b class="open">${unanswered} unanswered</b>` : ""}</h2>${body}</section>`;
2578
2578
  }).join("");
@@ -2595,7 +2595,7 @@ function notesPage(rows, { version: version2, q: q2, project, status, author = "
2595
2595
  const when = `${day(n.at)} ${time3(n.at)}`;
2596
2596
  return `<li class="n"><a href="/hub/open/${escapeHtml(n.artifact_id)}" title="${escapeHtml(n.file)}">
2597
2597
  <p class="note">${escapeHtml(n.text || "(no text)")}</p>
2598
- <span class="note-t">${n.author ? `<b class="by">${escapeHtml(n.author)}</b> \xB7 ` : ""}${n.target ? `on ${escapeHtml(n.target)} \xB7 ` : ""}<strong>${escapeHtml(n.title || path11.basename(n.file))}</strong> \xB7 ${escapeHtml(path11.basename(n.project) || n.project)} \xB7 ${escapeHtml(when)} \xB7 <span class="st ${waiting ? "waiting" : "done"}">${STATUS_LABEL[n.status] || escapeHtml(n.status)}</span></span>
2598
+ <span class="note-t">${n.author ? `<b class="by">${escapeHtml(n.author)}</b> \xB7 ` : ""}${n.target ? `on ${escapeHtml(n.target)} \xB7 ` : ""}<strong>${escapeHtml(n.title || path12.basename(n.file))}</strong> \xB7 ${escapeHtml(path12.basename(n.project) || n.project)} \xB7 ${escapeHtml(when)} \xB7 <span class="st ${waiting ? "waiting" : "done"}">${STATUS_LABEL[n.status] || escapeHtml(n.status)}</span></span>
2599
2599
  </a></li>`;
2600
2600
  }).join("");
2601
2601
  const opt = (v, label) => `<option value="${v}"${status === v ? " selected" : ""}>${label}</option>`;
@@ -2650,9 +2650,9 @@ __export(server_exports, {
2650
2650
  persistEnv: () => persistEnv,
2651
2651
  runServer: () => runServer
2652
2652
  });
2653
- import fs13 from "node:fs";
2654
- import http2 from "node:http";
2655
- import path12 from "node:path";
2653
+ import fs14 from "node:fs";
2654
+ import http3 from "node:http";
2655
+ import path13 from "node:path";
2656
2656
  import crypto5 from "node:crypto";
2657
2657
  function persistEnv() {
2658
2658
  return /^(1|true|yes)$/i.test(process.env.AUSTERE_AXI_PERSIST || "");
@@ -2667,7 +2667,7 @@ async function runServer({
2667
2667
  const line = `${(/* @__PURE__ */ new Date()).toISOString()} ${a.join(" ")}`;
2668
2668
  if (verbose) process.stderr.write(line + "\n");
2669
2669
  try {
2670
- fs13.appendFileSync(path12.join(stateDir(), "server.log"), line + "\n", { mode: 384 });
2670
+ fs14.appendFileSync(path13.join(stateDir(), "server.log"), line + "\n", { mode: 384 });
2671
2671
  } catch {
2672
2672
  }
2673
2673
  };
@@ -2692,6 +2692,7 @@ async function runServer({
2692
2692
  const graceTimers = /* @__PURE__ */ new Map();
2693
2693
  const byeAt = /* @__PURE__ */ new Map();
2694
2694
  const browserSeen = /* @__PURE__ */ new Set();
2695
+ const renderReported = /* @__PURE__ */ new Map();
2695
2696
  let tailnet = null;
2696
2697
  const linkBase = linkUrl();
2697
2698
  let lastActivity = Date.now();
@@ -2848,7 +2849,7 @@ data: ${JSON.stringify({ state: "revoked" })}
2848
2849
  }
2849
2850
  function readFileSafe(file3) {
2850
2851
  try {
2851
- return fs13.readFileSync(file3, "utf8");
2852
+ return fs14.readFileSync(file3, "utf8");
2852
2853
  } catch {
2853
2854
  return null;
2854
2855
  }
@@ -2860,14 +2861,14 @@ data: ${JSON.stringify({ state: "revoked" })}
2860
2861
  const w = { html, program: art?.program ?? null, shell: art?.shell ?? null, timer: void 0 };
2861
2862
  watchers.set(s.id, w);
2862
2863
  if (art?.title) s.title = art.title;
2863
- fs13.watchFile(s.file, { interval: 300 }, () => {
2864
+ fs14.watchFile(s.file, { interval: 300 }, () => {
2864
2865
  clearTimeout(w.timer);
2865
2866
  w.timer = setTimeout(() => onFileChange(s, w), 120);
2866
2867
  });
2867
2868
  }
2868
2869
  function unwatch(id) {
2869
2870
  const s = store.get(id);
2870
- if (s) fs13.unwatchFile(s.file);
2871
+ if (s) fs14.unwatchFile(s.file);
2871
2872
  watchers.delete(id);
2872
2873
  }
2873
2874
  function onFileChange(s, w) {
@@ -3044,7 +3045,7 @@ data: ${JSON.stringify({ state: "revoked" })}
3044
3045
  const file3 = typeof body.file === "string" ? body.file : "";
3045
3046
  if (!file3) return send(res, 400, { error: "file is required" });
3046
3047
  if (route === "share") {
3047
- if (!fs13.existsSync(file3)) return send(res, 404, { error: `no such file: ${file3}` });
3048
+ if (!fs14.existsSync(file3)) return send(res, 404, { error: `no such file: ${file3}` });
3048
3049
  await ensureTailnetListener();
3049
3050
  if (!remoteBase()) {
3050
3051
  return send(res, 409, {
@@ -3064,7 +3065,7 @@ data: ${JSON.stringify({ state: "revoked" })}
3064
3065
  return send(res, 200, { id: inviteId(inv), name: inv.name, auto: inv.auto, link: inviteLink(inv), file: inv.file });
3065
3066
  }
3066
3067
  if (route === "open") {
3067
- if (!fs13.existsSync(file3)) return send(res, 404, { error: `no such file: ${file3}` });
3068
+ if (!fs14.existsSync(file3)) return send(res, 404, { error: `no such file: ${file3}` });
3068
3069
  await ensureTailnetListener();
3069
3070
  const s2 = store.ensure(file3);
3070
3071
  if (s2.closedAt && !body.reopen) {
@@ -3197,7 +3198,7 @@ data: ${JSON.stringify({ state: "revoked" })}
3197
3198
  }
3198
3199
  const file3 = installedPackFile(spec.name, spec.version);
3199
3200
  if (!file3) return send(res, 404, `pack ${spec.name}@${spec.version} is not installed (austere-axi components add ${spec.name}@${spec.version})`);
3200
- fs13.readFile(file3, (err, buf) => {
3201
+ fs14.readFile(file3, (err, buf) => {
3201
3202
  if (err) return send(res, 404, "not found");
3202
3203
  const actual = `sha384-${crypto5.createHash("sha384").update(buf).digest("base64")}`;
3203
3204
  const locked = readLock().packs[spec.name]?.integrity;
@@ -3221,13 +3222,13 @@ data: ${JSON.stringify({ state: "revoked" })}
3221
3222
  }
3222
3223
  function serveAsset(res, rel, url2) {
3223
3224
  if (rel.startsWith("installed-packs/")) return serveInstalledPack(res, rel, url2);
3224
- const clean = path12.normalize(rel).replace(/^(\.\.[/\\])+/, "");
3225
- const file3 = path12.join(ASSET_DIR, clean);
3226
- if (!file3.startsWith(ASSET_DIR + path12.sep)) return send(res, 404, "not found");
3227
- fs13.stat(file3, (err, st2) => {
3225
+ const clean = path13.normalize(rel).replace(/^(\.\.[/\\])+/, "");
3226
+ const file3 = path13.join(ASSET_DIR, clean);
3227
+ if (!file3.startsWith(ASSET_DIR + path13.sep)) return send(res, 404, "not found");
3228
+ fs14.stat(file3, (err, st2) => {
3228
3229
  if (err || !st2.isFile()) return send(res, 404, "not found");
3229
3230
  streamFile(res, file3, {
3230
- "content-type": MIME[path12.extname(file3)] || "application/octet-stream",
3231
+ "content-type": MIME[path13.extname(file3)] || "application/octet-stream",
3231
3232
  "content-length": st2.size,
3232
3233
  "cache-control": "no-cache",
3233
3234
  "x-content-type-options": "nosniff"
@@ -3236,7 +3237,7 @@ data: ${JSON.stringify({ state: "revoked" })}
3236
3237
  }
3237
3238
  const ARTIFACT_CSP = "sandbox allow-scripts allow-popups allow-popups-to-escape-sandbox allow-forms";
3238
3239
  function streamFile(res, file3, headers) {
3239
- const rs = fs13.createReadStream(file3);
3240
+ const rs = fs14.createReadStream(file3);
3240
3241
  rs.once("open", () => {
3241
3242
  res.writeHead(200, headers);
3242
3243
  rs.pipe(res);
@@ -3249,8 +3250,8 @@ data: ${JSON.stringify({ state: "revoked" })}
3249
3250
  res.once("close", () => rs.destroy());
3250
3251
  }
3251
3252
  function serveArtifact(req, res, s, rest) {
3252
- const dir = path12.dirname(s.file);
3253
- const base = path12.basename(s.file);
3253
+ const dir = path13.dirname(s.file);
3254
+ const base = path13.basename(s.file);
3254
3255
  if (rest === base || rest === "") {
3255
3256
  const html = readFileSafe(s.file);
3256
3257
  if (html == null) return send(res, 404, "artifact file is missing");
@@ -3264,19 +3265,19 @@ data: ${JSON.stringify({ state: "revoked" })}
3264
3265
  return send(res, 400, "bad path");
3265
3266
  }
3266
3267
  if (decoded.includes("\0")) return send(res, 404, "not found");
3267
- const file3 = path12.resolve(dir, decoded);
3268
- if (!file3.startsWith(dir + path12.sep) || decoded.split(/[/\\]/).some((p) => p.startsWith("."))) {
3268
+ const file3 = path13.resolve(dir, decoded);
3269
+ if (!file3.startsWith(dir + path13.sep) || decoded.split(/[/\\]/).some((p) => p.startsWith("."))) {
3269
3270
  return send(res, 404, "not found");
3270
3271
  }
3271
- fs13.realpath(dir, (e1, realDir) => {
3272
+ fs14.realpath(dir, (e1, realDir) => {
3272
3273
  if (e1) return send(res, 404, "not found");
3273
- fs13.realpath(file3, (e2, real) => {
3274
- if (e2 || !real.startsWith(realDir + path12.sep)) return send(res, 404, "not found");
3275
- if (path12.relative(realDir, real).split(path12.sep).some((p) => p.startsWith("."))) return send(res, 404, "not found");
3276
- fs13.stat(real, (err, st2) => {
3274
+ fs14.realpath(file3, (e2, real) => {
3275
+ if (e2 || !real.startsWith(realDir + path13.sep)) return send(res, 404, "not found");
3276
+ if (path13.relative(realDir, real).split(path13.sep).some((p) => p.startsWith("."))) return send(res, 404, "not found");
3277
+ fs14.stat(real, (err, st2) => {
3277
3278
  if (err || !st2.isFile() || st2.size > 64 * 1024 * 1024) return send(res, 404, "not found");
3278
3279
  streamFile(res, real, {
3279
- "content-type": MIME[path12.extname(real).toLowerCase()] || "application/octet-stream",
3280
+ "content-type": MIME[path13.extname(real).toLowerCase()] || "application/octet-stream",
3280
3281
  "content-length": st2.size,
3281
3282
  "cache-control": "no-cache",
3282
3283
  "x-content-type-options": "nosniff",
@@ -3292,8 +3293,8 @@ data: ${JSON.stringify({ state: "revoked" })}
3292
3293
  title: s.title,
3293
3294
  file: viewer.role === "owner" ? s.file : "",
3294
3295
  viewer: { role: viewer.role, name: viewer.name, auto: viewer.role === "reviewer" ? viewer.auto : void 0 },
3295
- name: path12.basename(s.file),
3296
- artifactUrl: `/s/${s.id}/a/${encodeURIComponent(path12.basename(s.file))}`,
3296
+ name: path13.basename(s.file),
3297
+ artifactUrl: `/s/${s.id}/a/${encodeURIComponent(path13.basename(s.file))}`,
3297
3298
  version: VERSION
3298
3299
  };
3299
3300
  return `<!doctype html>
@@ -3327,7 +3328,7 @@ data: ${JSON.stringify({ state: "revoked" })}
3327
3328
  function hubOpen(res, id) {
3328
3329
  const a = /^[a-f0-9]{12}$/.test(id) ? hub.artifact(id) : null;
3329
3330
  if (!a) return send(res, 404, "not in the hub");
3330
- if (!fs13.existsSync(a.file)) return send(res, 410, `${a.file} no longer exists`);
3331
+ if (!fs14.existsSync(a.file)) return send(res, 410, `${a.file} no longer exists`);
3331
3332
  const existed = store.byFile(a.file);
3332
3333
  const s = store.ensure(a.file);
3333
3334
  if (!existed) store.touch(s);
@@ -3503,6 +3504,30 @@ data: ${JSON.stringify(snapshot)}
3503
3504
  broadcastPresence(s);
3504
3505
  return send(res, 200, { ok: true, entry });
3505
3506
  }
3507
+ if (sub === "/render-errors" && req.method === "POST") {
3508
+ const body = await readBody(req);
3509
+ if (s.closedAt || body.revision !== s.revision) return send(res, 200, { ok: true, count: 0, stale: true });
3510
+ if (thirdPartyPacks(watchers.get(s.id)?.html).length) return send(res, 409, { error: "this page uses third-party packs; queue render errors by hand" });
3511
+ let seen = renderReported.get(s.id);
3512
+ if (!seen || seen.revision !== s.revision) renderReported.set(s.id, seen = { revision: s.revision, keys: /* @__PURE__ */ new Set() });
3513
+ const items = [];
3514
+ for (const e of Array.isArray(body.errors) ? body.errors.slice(0, MAX_RENDER_ERRORS) : []) {
3515
+ if (!e || typeof e !== "object" || typeof e.message !== "string" || !e.message) continue;
3516
+ const statement = typeof e.statement === "string" ? e.statement.slice(0, 200) : "";
3517
+ const message = e.message.slice(0, 1e3);
3518
+ const key3 = `${statement}
3519
+ ${message}`;
3520
+ if (seen.keys.has(key3)) continue;
3521
+ seen.keys.add(key3);
3522
+ items.push({ kind: "render-error", statement: statement || void 0, target: statement || void 0, message, note: "Reported when the page rendered." });
3523
+ }
3524
+ if (!items.length) return send(res, 200, { ok: true, count: 0 });
3525
+ const entry = store.addUserBatch(s, items, { author: RENDER_CHECK_AUTHOR });
3526
+ broadcastTranscript(s, [entry]);
3527
+ wake(s);
3528
+ broadcastPresence(s);
3529
+ return send(res, 200, { ok: true, count: items.length });
3530
+ }
3506
3531
  if ((sub === "/release" || sub === "/discard") && req.method === "POST") {
3507
3532
  const body = await readBody(req);
3508
3533
  const ids = Array.isArray(body.ids) ? body.ids.map(String) : sub === "/discard" && body.all ? null : [];
@@ -3543,7 +3568,7 @@ data: ${JSON.stringify(snapshot)}
3543
3568
  if (html == null) return send(res, 404, "artifact file is missing");
3544
3569
  try {
3545
3570
  const out = exportHtml(html);
3546
- const name = path12.basename(s.file).replace(/\.html?$/i, "") + ".export.html";
3571
+ const name = path13.basename(s.file).replace(/\.html?$/i, "") + ".export.html";
3547
3572
  return send(res, 200, out.html, {
3548
3573
  "content-type": "text/html; charset=utf-8",
3549
3574
  "content-disposition": `attachment; filename="${name.replace(/"/g, "")}"`,
@@ -3558,7 +3583,7 @@ data: ${JSON.stringify(snapshot)}
3558
3583
  function listen(host) {
3559
3584
  return new Promise((resolve) => {
3560
3585
  if (listeners.has(host)) return resolve(true);
3561
- const server = http2.createServer((req, res) => {
3586
+ const server = http3.createServer((req, res) => {
3562
3587
  handle(req, res).catch((err) => {
3563
3588
  log("error", req.method, String(req.url).split("?")[0], String(err?.stack || err));
3564
3589
  if (!res.headersSent) send(res, err.status || 500, { error: err.status ? String(err.message) : "internal error" });
@@ -3635,7 +3660,7 @@ data: ${JSON.stringify({ state: "stopped", reason })}
3635
3660
  log("started", `v${VERSION}`, `pid=${process.pid}`);
3636
3661
  return { port: port2, shutdown };
3637
3662
  }
3638
- var MIME, SHORT_GRACE_MS, LOGIN_TTL_MS, OWNER, REVIEWER_KINDS, PAGE, SSE_MAX_BUFFER, SSE_STALL_MS, SSE_HARD_MAX, SSE_MAX_PER_REVIEWER, MAX_HELD_PER_REVIEWER, SSE_MAX_PER_SESSION;
3663
+ var MIME, SHORT_GRACE_MS, LOGIN_TTL_MS, OWNER, REVIEWER_KINDS, RENDER_CHECK_AUTHOR, MAX_RENDER_ERRORS, PAGE, SSE_MAX_BUFFER, SSE_STALL_MS, SSE_HARD_MAX, SSE_MAX_PER_REVIEWER, MAX_HELD_PER_REVIEWER, SSE_MAX_PER_SESSION;
3639
3664
  var init_server = __esm({
3640
3665
  "src/cli/server.ts"() {
3641
3666
  "use strict";
@@ -3681,6 +3706,8 @@ var init_server = __esm({
3681
3706
  LOGIN_TTL_MS = 6e4;
3682
3707
  OWNER = Object.freeze({ role: "owner", name: "" });
3683
3708
  REVIEWER_KINDS = /* @__PURE__ */ new Set(["annotation", "message"]);
3709
+ RENDER_CHECK_AUTHOR = "Render check";
3710
+ MAX_RENDER_ERRORS = 20;
3684
3711
  PAGE = 200;
3685
3712
  SSE_MAX_BUFFER = 1 << 20;
3686
3713
  SSE_STALL_MS = 5e3;
@@ -3696,12 +3723,12 @@ var mcp_exports = {};
3696
3723
  __export(mcp_exports, {
3697
3724
  runMcp: () => runMcp
3698
3725
  });
3699
- import { spawn as spawn2 } from "node:child_process";
3700
- import path13 from "node:path";
3726
+ import { spawn as spawn3 } from "node:child_process";
3727
+ import path14 from "node:path";
3701
3728
  import readline from "node:readline";
3702
3729
  function runCli({ args, stdin }, signal) {
3703
3730
  return new Promise((resolve) => {
3704
- const child = spawn2(process.execPath, [CLI_ENTRY, ...args], {
3731
+ const child = spawn3(process.execPath, [CLI_ENTRY, ...args], {
3705
3732
  env: process.env,
3706
3733
  stdio: ["pipe", "pipe", "ignore"],
3707
3734
  windowsHide: true
@@ -3740,7 +3767,7 @@ async function callTool(name, a, signal) {
3740
3767
  const bad = validate2(tool, a);
3741
3768
  if (bad) return { content: [{ type: "text", text: `status: error
3742
3769
  error: ${bad}` }], isError: true };
3743
- if (typeof a.file === "string") a = { ...a, file: path13.resolve(a.file) };
3770
+ if (typeof a.file === "string") a = { ...a, file: path14.resolve(a.file) };
3744
3771
  const args = a;
3745
3772
  let text = "";
3746
3773
  let isError = false;
@@ -3842,7 +3869,7 @@ var init_mcp = __esm({
3842
3869
  isRecord2 = (v) => !!v && typeof v === "object";
3843
3870
  PROTOCOLS = ["2025-11-25", "2025-06-18", "2024-11-05"];
3844
3871
  POLL_TIMEOUT_MS = 5e4;
3845
- INSTRUCTIONS = `austere-axi turns plans, comparisons, decisions, code reviews and reports into review pages the user annotates in the browser. Call \`spec\` once per session for the workflow, the file skeleton and the component reference. Results are the CLI's output: where a \`next:\` hint names an \`austere-axi <command>\`, call the tool of that name instead. Pass absolute file paths.`;
3872
+ INSTRUCTIONS = `austere-axi turns plans, comparisons, decisions, code reviews and reports into review pages the user annotates in the browser. Call \`spec\` once per session for the workflow, the file skeleton and the component reference. Results are the CLI's output: where a \`next:\` hint names an \`austere-axi <command>\`, call the tool of that name instead. Pass absolute file paths. The reviewer judges how a page looks: do not open, screenshot or inspect it yourself unless the user asks.`;
3846
3873
  file2 = { type: "string", description: "Path of the .html page. Use an absolute path; a relative one resolves against the server's working directory." };
3847
3874
  TOOLS = {
3848
3875
  spec: {
@@ -3857,9 +3884,13 @@ var init_mcp = __esm({
3857
3884
  check: {
3858
3885
  title: "Validate a page",
3859
3886
  description: "Validate the page's OpenUI Lang program without a browser: syntax, component names, required props, unresolved references, unreachable statements.",
3860
- properties: { file: file2, fix: { type: "boolean", description: 'Write the <meta name="austere-packs"> tag for the packs the page uses.' } },
3887
+ properties: {
3888
+ file: file2,
3889
+ fix: { type: "boolean", description: 'Write the <meta name="austere-packs"> tag for the packs the page uses.' },
3890
+ render: { type: "boolean", description: "Also render the page in headless Chrome and report render errors and sideways scrolling as text. Only when the user asks to verify the page." }
3891
+ },
3861
3892
  required: ["file"],
3862
- run: (a) => [{ args: ["check", ...a.fix ? ["--fix"] : [], "--", a.file] }]
3893
+ run: (a) => [{ args: ["check", ...a.fix ? ["--fix"] : [], ...a.render ? ["--render"] : [], "--", a.file] }]
3863
3894
  },
3864
3895
  open: {
3865
3896
  title: "Validate and open the review",
@@ -3921,8 +3952,8 @@ var init_mcp = __esm({
3921
3952
  });
3922
3953
 
3923
3954
  // src/cli/main.ts
3924
- import fs14 from "node:fs";
3925
- import path14 from "node:path";
3955
+ import fs15 from "node:fs";
3956
+ import path15 from "node:path";
3926
3957
 
3927
3958
  // node_modules/@openuidev/lang-core/dist/shared-Dzgc28Wo.mjs
3928
3959
  var TELEMETRY_REQUEST_TIMEOUT_MS = 2e3;
@@ -4783,10 +4814,10 @@ function mergeDefs(...defs) {
4783
4814
  function cloneDef(schema) {
4784
4815
  return mergeDefs(schema._zod.def);
4785
4816
  }
4786
- function getElementAtPath(obj, path15) {
4787
- if (!path15)
4817
+ function getElementAtPath(obj, path16) {
4818
+ if (!path16)
4788
4819
  return obj;
4789
- return path15.reduce((acc, key2) => acc?.[key2], obj);
4820
+ return path16.reduce((acc, key2) => acc?.[key2], obj);
4790
4821
  }
4791
4822
  function promiseAllObject(promisesObj) {
4792
4823
  const keys = Object.keys(promisesObj);
@@ -5126,11 +5157,11 @@ function explicitlyAborted(x, startIndex = 0) {
5126
5157
  }
5127
5158
  return false;
5128
5159
  }
5129
- function prefixIssues(path15, issues) {
5160
+ function prefixIssues(path16, issues) {
5130
5161
  return issues.map((iss) => {
5131
5162
  var _a3;
5132
5163
  (_a3 = iss).path ?? (_a3.path = []);
5133
- iss.path.unshift(path15);
5164
+ iss.path.unshift(path16);
5134
5165
  return iss;
5135
5166
  });
5136
5167
  }
@@ -5592,16 +5623,16 @@ function flattenError(error62, mapper = (issue2) => issue2.message) {
5592
5623
  }
5593
5624
  function formatError(error62, mapper = (issue2) => issue2.message) {
5594
5625
  const fieldErrors = { _errors: [] };
5595
- const processError = (error63, path15 = []) => {
5626
+ const processError = (error63, path16 = []) => {
5596
5627
  for (const issue2 of error63.issues) {
5597
5628
  if (issue2.code === "invalid_union" && issue2.errors.length) {
5598
- issue2.errors.map((issues) => processError({ issues }, [...path15, ...issue2.path]));
5629
+ issue2.errors.map((issues) => processError({ issues }, [...path16, ...issue2.path]));
5599
5630
  } else if (issue2.code === "invalid_key") {
5600
- processError({ issues: issue2.issues }, [...path15, ...issue2.path]);
5631
+ processError({ issues: issue2.issues }, [...path16, ...issue2.path]);
5601
5632
  } else if (issue2.code === "invalid_element") {
5602
- processError({ issues: issue2.issues }, [...path15, ...issue2.path]);
5633
+ processError({ issues: issue2.issues }, [...path16, ...issue2.path]);
5603
5634
  } else {
5604
- const fullpath = [...path15, ...issue2.path];
5635
+ const fullpath = [...path16, ...issue2.path];
5605
5636
  if (fullpath.length === 0) {
5606
5637
  fieldErrors._errors.push(mapper(issue2));
5607
5638
  } else {
@@ -5640,17 +5671,17 @@ function formatError(error62, mapper = (issue2) => issue2.message) {
5640
5671
  }
5641
5672
  function treeifyError(error62, mapper = (issue2) => issue2.message) {
5642
5673
  const result = { errors: [] };
5643
- const processError = (error63, path15 = []) => {
5674
+ const processError = (error63, path16 = []) => {
5644
5675
  var _a3;
5645
5676
  for (const issue2 of error63.issues) {
5646
5677
  if (issue2.code === "invalid_union" && issue2.errors.length) {
5647
- issue2.errors.map((issues) => processError({ issues }, [...path15, ...issue2.path]));
5678
+ issue2.errors.map((issues) => processError({ issues }, [...path16, ...issue2.path]));
5648
5679
  } else if (issue2.code === "invalid_key") {
5649
- processError({ issues: issue2.issues }, [...path15, ...issue2.path]);
5680
+ processError({ issues: issue2.issues }, [...path16, ...issue2.path]);
5650
5681
  } else if (issue2.code === "invalid_element") {
5651
- processError({ issues: issue2.issues }, [...path15, ...issue2.path]);
5682
+ processError({ issues: issue2.issues }, [...path16, ...issue2.path]);
5652
5683
  } else {
5653
- const fullpath = [...path15, ...issue2.path];
5684
+ const fullpath = [...path16, ...issue2.path];
5654
5685
  if (fullpath.length === 0) {
5655
5686
  result.errors.push(mapper(issue2));
5656
5687
  continue;
@@ -5689,8 +5720,8 @@ function treeifyError(error62, mapper = (issue2) => issue2.message) {
5689
5720
  }
5690
5721
  function toDotPath(_path) {
5691
5722
  const segs = [];
5692
- const path15 = _path.map((seg) => typeof seg === "object" ? seg.key : seg);
5693
- for (const seg of path15) {
5723
+ const path16 = _path.map((seg) => typeof seg === "object" ? seg.key : seg);
5724
+ for (const seg of path16) {
5694
5725
  if (typeof seg === "number")
5695
5726
  segs.push(`[${seg}]`);
5696
5727
  else if (typeof seg === "symbol")
@@ -22828,13 +22859,13 @@ function resolveRef(ref, ctx) {
22828
22859
  if (!ref.startsWith("#")) {
22829
22860
  throw new Error("External $ref is not supported, only local refs (#/...) are allowed");
22830
22861
  }
22831
- const path15 = ref.slice(1).split("/").filter(Boolean);
22832
- if (path15.length === 0) {
22862
+ const path16 = ref.slice(1).split("/").filter(Boolean);
22863
+ if (path16.length === 0) {
22833
22864
  return ctx.rootSchema;
22834
22865
  }
22835
22866
  const defsKey = ctx.version === "draft-2020-12" ? "$defs" : "definitions";
22836
- if (path15[0] === defsKey) {
22837
- const key2 = path15[1] === void 0 ? void 0 : decodeJSONPointerSegment(path15[1]);
22867
+ if (path16[0] === defsKey) {
22868
+ const key2 = path16[1] === void 0 ? void 0 : decodeJSONPointerSegment(path16[1]);
22838
22869
  if (!key2 || !ctx.defs[key2]) {
22839
22870
  throw new Error(`Reference not found: ${ref}`);
22840
22871
  }
@@ -23716,11 +23747,11 @@ function defineAction(config2) {
23716
23747
  ref: actionRef(config2.name, data)
23717
23748
  };
23718
23749
  }
23719
- function resolveField(obj, path15) {
23720
- if (!path15 || obj == null) return void 0;
23721
- if (!path15.includes(".")) return obj[path15];
23750
+ function resolveField(obj, path16) {
23751
+ if (!path16 || obj == null) return void 0;
23752
+ if (!path16.includes(".")) return obj[path16];
23722
23753
  let cur = obj;
23723
- for (const p of path15.split(".")) {
23754
+ for (const p of path16.split(".")) {
23724
23755
  if (cur == null) return void 0;
23725
23756
  cur = cur[p];
23726
23757
  }
@@ -25096,14 +25127,14 @@ function checkTypeMismatch(value, info) {
25096
25127
  };
25097
25128
  return null;
25098
25129
  }
25099
- function validationMessage(component, path15, issue2) {
25130
+ function validationMessage(component, path16, issue2) {
25100
25131
  switch (issue2.code) {
25101
25132
  case "type-mismatch":
25102
- return `field "${path15}" expects ${issue2.expected} but got ${issue2.actual}`;
25133
+ return `field "${path16}" expects ${issue2.expected} but got ${issue2.actual}`;
25103
25134
  case "missing-required":
25104
- return `missing required field "${path15}"${issue2.signature ? ` \u2014 signature: ${issue2.signature}` : ""}`;
25135
+ return `missing required field "${path16}"${issue2.signature ? ` \u2014 signature: ${issue2.signature}` : ""}`;
25105
25136
  case "null-required":
25106
- return `required field "${path15}" cannot be null${issue2.signature ? ` \u2014 signature: ${issue2.signature}` : ""}`;
25137
+ return `required field "${path16}" cannot be null${issue2.signature ? ` \u2014 signature: ${issue2.signature}` : ""}`;
25107
25138
  case "unknown-component":
25108
25139
  return `Unknown component "${component}" \u2014 not found in catalog or builtins${issue2.available?.length ? `. Available components: ${issue2.available.join(", ")}` : ""}`;
25109
25140
  case "inline-reserved":
@@ -25118,12 +25149,12 @@ function validationMessage(component, path15, issue2) {
25118
25149
  return component ? `No root statement, and no statement calls the root component ${component}` : "No root statement";
25119
25150
  }
25120
25151
  }
25121
- function pushValidationIssue(ctx, component, path15, issue2) {
25152
+ function pushValidationIssue(ctx, component, path16, issue2) {
25122
25153
  ctx.errors.push({
25123
25154
  code: issue2.code,
25124
25155
  component,
25125
- path: path15,
25126
- message: validationMessage(component, path15, issue2),
25156
+ path: path16,
25157
+ message: validationMessage(component, path16, issue2),
25127
25158
  statementId: ctx.currentStatementId,
25128
25159
  ...issue2.code === "no-root" && issue2.reason === "fallback" && { severity: "warning" }
25129
25160
  });
@@ -25133,10 +25164,10 @@ function resolveInvalidValue(required2, defaultValue) {
25133
25164
  if (defaultValue !== void 0) return defaultValue;
25134
25165
  return required2 ? INVALID2 : void 0;
25135
25166
  }
25136
- function validateElementPosition(element, s, component, path15, ctx) {
25167
+ function validateElementPosition(element, s, component, path16, ctx) {
25137
25168
  if (isCompositeSchema(s)) return false;
25138
25169
  if (typeof s["type"] === "string" || Array.isArray(s["enum"]) || "const" in s) {
25139
- pushValidationIssue(ctx, component, path15, {
25170
+ pushValidationIssue(ctx, component, path16, {
25140
25171
  code: "type-mismatch",
25141
25172
  expected: typeof s["type"] === "string" ? s["type"] : "a literal value",
25142
25173
  actual: `component "${element.typeName}"`
@@ -25152,9 +25183,9 @@ function isOnlyComponentSlot(s, ctx) {
25152
25183
  const names = slotRefNames(s);
25153
25184
  return names.length > 0 && names.every((n) => n !== void 0 && ctx.cat?.has(n));
25154
25185
  }
25155
- function validateObjectValue(value, s, component, path15, ctx) {
25186
+ function validateObjectValue(value, s, component, path16, ctx) {
25156
25187
  if (typeof value !== "object" || Array.isArray(value)) {
25157
- pushValidationIssue(ctx, component, path15, {
25188
+ pushValidationIssue(ctx, component, path16, {
25158
25189
  code: "type-mismatch",
25159
25190
  expected: "object",
25160
25191
  actual: jsType(value)
@@ -25174,14 +25205,14 @@ function validateObjectValue(value, s, component, path15, ctx) {
25174
25205
  (out ??= { ...obj })[key2] = fallback;
25175
25206
  continue;
25176
25207
  }
25177
- pushValidationIssue(ctx, component, `${path15}/${key2}`, { code: present ? "null-required" : "missing-required" });
25208
+ pushValidationIssue(ctx, component, `${path16}/${key2}`, { code: present ? "null-required" : "missing-required" });
25178
25209
  invalid = true;
25179
25210
  }
25180
25211
  }
25181
25212
  for (const key2 of Object.keys(props)) {
25182
25213
  if (!(key2 in obj)) continue;
25183
25214
  const sub = props[key2];
25184
- let next = validateSchemaValue(obj[key2], sub, component, `${path15}/${key2}`, ctx);
25215
+ let next = validateSchemaValue(obj[key2], sub, component, `${path16}/${key2}`, ctx);
25185
25216
  if (next === INVALID2) {
25186
25217
  next = resolveInvalidValue(required2.includes(key2), getSchemaDefaultValue(sub));
25187
25218
  if (next === INVALID2) {
@@ -25196,9 +25227,9 @@ function validateObjectValue(value, s, component, path15, ctx) {
25196
25227
  }
25197
25228
  return invalid ? INVALID2 : out ?? obj;
25198
25229
  }
25199
- function validateArrayValue(value, s, component, path15, ctx) {
25230
+ function validateArrayValue(value, s, component, path16, ctx) {
25200
25231
  if (!Array.isArray(value)) {
25201
- pushValidationIssue(ctx, component, path15, {
25232
+ pushValidationIssue(ctx, component, path16, {
25202
25233
  code: "type-mismatch",
25203
25234
  expected: "array",
25204
25235
  actual: jsType(value)
@@ -25209,19 +25240,19 @@ function validateArrayValue(value, s, component, path15, ctx) {
25209
25240
  if (!items || typeof items !== "object" || Array.isArray(items)) return value;
25210
25241
  let out;
25211
25242
  for (let i = 0; i < value.length; i++) {
25212
- const next = validateSchemaValue(value[i], items, component, `${path15}/${i}`, ctx);
25243
+ const next = validateSchemaValue(value[i], items, component, `${path16}/${i}`, ctx);
25213
25244
  if (next !== value[i] && !out) out = value.slice(0, i);
25214
25245
  if (out && next !== INVALID2) out.push(next);
25215
25246
  }
25216
25247
  return out ?? value;
25217
25248
  }
25218
- function validateLeafValue(value, s, component, path15, ctx) {
25249
+ function validateLeafValue(value, s, component, path16, ctx) {
25219
25250
  const leaf = getScalarTypeInfo(s);
25220
25251
  if (leaf.expectedType == null && leaf.enumValues == null) return false;
25221
25252
  if (leaf.enumValues && ctx.partial) return false;
25222
25253
  const mismatch = checkTypeMismatch(value, leaf);
25223
25254
  if (mismatch) {
25224
- pushValidationIssue(ctx, component, path15, {
25255
+ pushValidationIssue(ctx, component, path16, {
25225
25256
  code: "type-mismatch",
25226
25257
  ...mismatch
25227
25258
  });
@@ -25229,15 +25260,15 @@ function validateLeafValue(value, s, component, path15, ctx) {
25229
25260
  }
25230
25261
  return false;
25231
25262
  }
25232
- function validateSchemaValue(value, schema, component, path15, ctx) {
25263
+ function validateSchemaValue(value, schema, component, path16, ctx) {
25233
25264
  if (!schema || typeof schema !== "object" || Array.isArray(schema)) return value;
25234
25265
  const s = schema;
25235
25266
  if (value == null) return value;
25236
25267
  if (isASTNode(value)) return value;
25237
- if (isElementNode(value)) return validateElementPosition(value, s, component, path15, ctx) ? INVALID2 : value;
25268
+ if (isElementNode(value)) return validateElementPosition(value, s, component, path16, ctx) ? INVALID2 : value;
25238
25269
  if (isCompositeSchema(s)) {
25239
25270
  if (!isOnlyComponentSlot(s, ctx)) return value;
25240
- pushValidationIssue(ctx, component, path15, {
25271
+ pushValidationIssue(ctx, component, path16, {
25241
25272
  code: "type-mismatch",
25242
25273
  expected: slotRefNames(s).join(" | "),
25243
25274
  actual: Array.isArray(value) ? "array" : typeof value === "object" ? "plain object" : typeof value
@@ -25245,9 +25276,9 @@ function validateSchemaValue(value, schema, component, path15, ctx) {
25245
25276
  return INVALID2;
25246
25277
  }
25247
25278
  const type = s["type"];
25248
- if (type === "object") return validateObjectValue(value, s, component, path15, ctx);
25249
- if (type === "array") return validateArrayValue(value, s, component, path15, ctx);
25250
- return validateLeafValue(value, s, component, path15, ctx) ? INVALID2 : value;
25279
+ if (type === "object") return validateObjectValue(value, s, component, path16, ctx);
25280
+ if (type === "array") return validateArrayValue(value, s, component, path16, ctx);
25281
+ return validateLeafValue(value, s, component, path16, ctx) ? INVALID2 : value;
25251
25282
  }
25252
25283
  function containsDynamicValue(v) {
25253
25284
  if (v == null || typeof v !== "object") return false;
@@ -28873,11 +28904,11 @@ function routeOrthogonal({ boxes, edges: input2, xLines = [], yLines = [], avoid
28873
28904
  const blocked = solid.some(([id, r]) => id !== e.from && id !== e.to && seg.x0 < r.x + r.width && r.x < seg.x1 && seg.y0 < r.y + r.height && r.y < seg.y1);
28874
28905
  if (!blocked) {
28875
28906
  routes.set(e.id, [a, b]);
28876
- const path16 = [];
28907
+ const path17 = [];
28877
28908
  const [i0, j0, i1, j1] = [ix(a.x), iy(a.y), ix(b.x), iy(b.y)];
28878
- if (j0 === j1) for (let i = Math.min(i0, i1); i <= Math.max(i0, i1); i++) path16.push([i, j0]);
28879
- else for (let j = Math.min(j0, j1); j <= Math.max(j0, j1); j++) path16.push([i0, j]);
28880
- if (path16.every((pt2) => pt2[0] !== void 0 && pt2[1] !== void 0)) mark(path16);
28909
+ if (j0 === j1) for (let i = Math.min(i0, i1); i <= Math.max(i0, i1); i++) path17.push([i, j0]);
28910
+ else for (let j = Math.min(j0, j1); j <= Math.max(j0, j1); j++) path17.push([i0, j]);
28911
+ if (path17.every((pt2) => pt2[0] !== void 0 && pt2[1] !== void 0)) mark(path17);
28881
28912
  continue;
28882
28913
  }
28883
28914
  }
@@ -28887,12 +28918,12 @@ function routeOrthogonal({ boxes, edges: input2, xLines = [], yLines = [], avoid
28887
28918
  const sj = iy(st2.y);
28888
28919
  const ti = ix(tt2.x);
28889
28920
  const tj = iy(tt2.y);
28890
- let path15 = null;
28921
+ let path16 = null;
28891
28922
  if (si !== void 0 && sj !== void 0 && ti !== void 0 && tj !== void 0) {
28892
- path15 = search(si, sj, dirOf(e.fromSide), ti, tj, reverse[dirOf(e.toSide)]);
28923
+ path16 = search(si, sj, dirOf(e.fromSide), ti, tj, reverse[dirOf(e.toSide)]);
28893
28924
  }
28894
- const pts = path15 ? path15.map(([i, j]) => ({ x: xs[i], y: ys[j] })) : elbow(st2, tt2, e.fromSide);
28895
- if (path15) mark(path15);
28925
+ const pts = path16 ? path16.map(([i, j]) => ({ x: xs[i], y: ys[j] })) : elbow(st2, tt2, e.fromSide);
28926
+ if (path16) mark(path16);
28896
28927
  routes.set(e.id, simplify([e.fromPort, ...pts, e.toPort]));
28897
28928
  }
28898
28929
  return routes;
@@ -28961,10 +28992,10 @@ function routeOrthogonal({ boxes, edges: input2, xLines = [], yLines = [], avoid
28961
28992
  }
28962
28993
  return out.reverse();
28963
28994
  }
28964
- function mark(path15) {
28965
- for (let k = 1; k < path15.length; k++) {
28966
- const [i0, j0] = path15[k - 1];
28967
- const [i1, j1] = path15[k];
28995
+ function mark(path16) {
28996
+ for (let k = 1; k < path16.length; k++) {
28997
+ const [i0, j0] = path16[k - 1];
28998
+ const [i1, j1] = path16[k];
28968
28999
  if (j0 === j1) {
28969
29000
  hUse[j0 * nx + Math.min(i0, i1)]++;
28970
29001
  hPass[j0 * nx + i0]++;
@@ -29669,6 +29700,280 @@ init_lock();
29669
29700
  init_sessions();
29670
29701
  init_export();
29671
29702
 
29703
+ // src/cli/render-check.ts
29704
+ init_export();
29705
+ import { spawn as spawn2 } from "node:child_process";
29706
+ import fs10 from "node:fs";
29707
+ import http2 from "node:http";
29708
+ import os2 from "node:os";
29709
+ import path9 from "node:path";
29710
+ var WIDTHS = [1280, 390];
29711
+ var LAUNCH_TIMEOUT_MS = 15e3;
29712
+ var RENDER_TIMEOUT_MS = 3e4;
29713
+ var FRAME_SANDBOX = "allow-scripts";
29714
+ function findChrome() {
29715
+ const fromEnv = process.env.CHROME_PATH;
29716
+ if (fromEnv) return fs10.existsSync(fromEnv) ? fromEnv : null;
29717
+ const candidates = [];
29718
+ if (process.platform === "darwin") {
29719
+ for (const p of ["Google Chrome.app/Contents/MacOS/Google Chrome", "Chromium.app/Contents/MacOS/Chromium", "Microsoft Edge.app/Contents/MacOS/Microsoft Edge"]) {
29720
+ candidates.push(path9.join("/Applications", p));
29721
+ }
29722
+ } else if (process.platform === "win32") {
29723
+ const roots = [process.env.PROGRAMFILES, process.env["PROGRAMFILES(X86)"], process.env.LOCALAPPDATA].filter((x) => typeof x === "string");
29724
+ for (const r of roots) candidates.push(path9.join(r, "Google/Chrome/Application/chrome.exe"), path9.join(r, "Microsoft/Edge/Application/msedge.exe"));
29725
+ } else {
29726
+ const dirs = (process.env.PATH || "").split(path9.delimiter).filter(Boolean);
29727
+ for (const name of ["google-chrome", "google-chrome-stable", "chromium", "chromium-browser", "microsoft-edge"]) {
29728
+ for (const d of dirs) candidates.push(path9.join(d, name));
29729
+ }
29730
+ }
29731
+ return candidates.find((c) => fs10.existsSync(c)) ?? null;
29732
+ }
29733
+ var MEASURE = `<script>(() => {
29734
+ const exceptions = [];
29735
+ addEventListener("error", (e) => exceptions.push(String((e.error && e.error.message) || e.message || "error")));
29736
+ addEventListener("unhandledrejection", (e) => exceptions.push(String((e.reason && e.reason.message) || e.reason)));
29737
+ const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
29738
+ addEventListener("message", async (e) => {
29739
+ const d = e.data;
29740
+ if (e.source !== parent || !d || d.austereMeasure !== 1) return;
29741
+ for (let i = 0; i < 100; i++) {
29742
+ const pending = [...document.querySelectorAll(".austere-mermaid")].filter((m) => !m.querySelector("svg, .austere-render-error"));
29743
+ if (!pending.length) break;
29744
+ await sleep(100);
29745
+ }
29746
+ await sleep(200);
29747
+ const shown = [...document.querySelectorAll(".austere-render-error")].map((el) => ({
29748
+ statement: (el.closest("[data-oui-id]") && el.closest("[data-oui-id]").getAttribute("data-oui-id")) || "",
29749
+ error: el.textContent.trim().slice(0, 300),
29750
+ }));
29751
+ const se = document.scrollingElement;
29752
+ let past = null;
29753
+ if (se.scrollWidth > se.clientWidth + 1) {
29754
+ const vw = se.clientWidth;
29755
+ // Past the right edge and not inside a box that clips or scrolls it (a wide table in its own scroller is fine).
29756
+ const sticksOut = (el) => {
29757
+ if (el.getBoundingClientRect().right <= vw + 1) return false;
29758
+ for (let a = el.parentElement; a && a !== document.body; a = a.parentElement) {
29759
+ if (getComputedStyle(a).overflowX !== "visible" && a.getBoundingClientRect().right <= vw + 1) return false;
29760
+ }
29761
+ return true;
29762
+ };
29763
+ // Statement wrappers have no box of their own (display: contents), so find the innermost
29764
+ // elements that stick out and name the statement around each.
29765
+ const out = [...document.body.querySelectorAll("*")].slice(0, 20000).filter(sticksOut);
29766
+ const outSet = new Set(out);
29767
+ const leaves = out.filter((el) => ![...el.children].some((c) => outSet.has(c)));
29768
+ const ids = leaves.map((el) => el.closest("[data-oui-id]") && el.closest("[data-oui-id]").getAttribute("data-oui-id")).filter(Boolean);
29769
+ past = [...new Set(ids)].slice(0, 8);
29770
+ }
29771
+ const statements = new Set([...document.querySelectorAll("[data-oui-id]")].map((el) => el.getAttribute("data-oui-id"))).size;
29772
+ parent.postMessage({ austereMeasured: 1, id: d.id, shown, exceptions: exceptions.slice(0, 20), past, statements }, "*");
29773
+ });
29774
+ })();</script>`;
29775
+ var HARNESS = `<!doctype html><html><head><meta charset="utf-8"></head><body style="margin:0">
29776
+ <iframe id="f" sandbox="${FRAME_SANDBOX}" src="/page.html" style="border:0;width:${WIDTHS[0]}px;height:900px"></iframe>
29777
+ <script>
29778
+ const f = document.getElementById("f");
29779
+ const errors = [];
29780
+ const answers = new Map();
29781
+ let ready = false;
29782
+ addEventListener("message", (e) => {
29783
+ const d = e.data;
29784
+ if (e.source !== f.contentWindow || !d) return;
29785
+ if (d.austereMeasured === 1) return answers.get(d.id)?.(d);
29786
+ if (d.austere !== 1) return;
29787
+ if (d.type === "hello") f.contentWindow.postMessage({ austere: 1, type: "init", mode: "explore", scrollY: 0 }, "*");
29788
+ else if (d.type === "ready") ready = true;
29789
+ else if (d.type === "errors") for (const x of d.errors || []) errors.push({ statement: String(x.statementId || ""), error: String(x.message || "") });
29790
+ else if (d.type === "runtime-error" && d.error) errors.push({ statement: String(d.error.statementId || ""), error: String(d.error.message || "") });
29791
+ });
29792
+ const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
29793
+ let next = 0;
29794
+ const measure = () => new Promise((resolve) => {
29795
+ const id = ++next;
29796
+ answers.set(id, resolve);
29797
+ f.contentWindow.postMessage({ austereMeasure: 1, id }, "*");
29798
+ });
29799
+ window.__austereReport = async (widths) => {
29800
+ for (let i = 0; i < 150 && !ready; i++) await sleep(100);
29801
+ const overflow = [];
29802
+ let last = null;
29803
+ for (const w of widths) {
29804
+ f.style.width = w + "px";
29805
+ await sleep(300);
29806
+ last = await measure();
29807
+ if (last.past) overflow.push({ width: w, statements: last.past });
29808
+ }
29809
+ const all = [...errors];
29810
+ for (const s of last.shown) if (!all.some((x) => x.statement === s.statement)) all.push(s);
29811
+ for (const x of last.exceptions) if (!all.some((y) => y.error === x)) all.push({ statement: "", error: x.slice(0, 300) });
29812
+ return { rendered: ready, statements: last.statements, errors: all, overflow };
29813
+ };
29814
+ </script></body></html>`;
29815
+ function connect(url2) {
29816
+ const ws = new WebSocket(url2);
29817
+ let next = 0;
29818
+ const pending = /* @__PURE__ */ new Map();
29819
+ const listeners = [];
29820
+ ws.onmessage = (ev) => {
29821
+ const m = JSON.parse(String(ev.data));
29822
+ const p = m.id != null ? pending.get(m.id) : void 0;
29823
+ if (p && m.id != null) {
29824
+ pending.delete(m.id);
29825
+ if (m.error) p.reject(new Error(m.error.message));
29826
+ else p.resolve(m.result);
29827
+ } else for (const fn2 of listeners) fn2(m);
29828
+ };
29829
+ const opened = new Promise((resolve, reject) => {
29830
+ ws.onopen = () => resolve();
29831
+ ws.onerror = () => reject(new Error("could not connect to Chrome's DevTools endpoint"));
29832
+ });
29833
+ return {
29834
+ opened,
29835
+ send(method, params = {}, sessionId2) {
29836
+ const id = ++next;
29837
+ ws.send(JSON.stringify({ id, method, params, sessionId: sessionId2 }));
29838
+ return new Promise((resolve, reject) => pending.set(id, { resolve: (v) => resolve(v), reject }));
29839
+ },
29840
+ on(fn2) {
29841
+ listeners.push(fn2);
29842
+ },
29843
+ close() {
29844
+ ws.close();
29845
+ }
29846
+ };
29847
+ }
29848
+ function launch(bin, profile) {
29849
+ const args = [
29850
+ "--headless=new",
29851
+ "--remote-debugging-port=0",
29852
+ `--user-data-dir=${profile}`,
29853
+ "--no-first-run",
29854
+ "--no-default-browser-check",
29855
+ "--disable-gpu",
29856
+ "--disable-extensions",
29857
+ "--disable-background-networking",
29858
+ "--disable-component-update",
29859
+ "--disable-breakpad",
29860
+ "--disable-sync",
29861
+ "--disable-dev-shm-usage",
29862
+ // Without these, Chrome waits about 25 s for a desktop keyring (Linux) or keychain (macOS) that may never answer.
29863
+ "--password-store=basic",
29864
+ "--use-mock-keychain",
29865
+ "--mute-audio",
29866
+ "--window-size=1400,1000"
29867
+ ];
29868
+ if (process.getuid?.() === 0) args.push("--no-sandbox");
29869
+ args.push("about:blank");
29870
+ const child = spawn2(bin, args, { stdio: ["ignore", "ignore", "pipe"], windowsHide: true });
29871
+ const exited = new Promise((resolve) => {
29872
+ child.once("exit", () => resolve());
29873
+ child.once("error", () => resolve());
29874
+ });
29875
+ const endpoint = new Promise((resolve, reject) => {
29876
+ let err = "";
29877
+ const timer = setTimeout(() => reject(new Error(`Chrome did not start within ${LAUNCH_TIMEOUT_MS / 1e3} s`)), LAUNCH_TIMEOUT_MS);
29878
+ child.stderr?.on("data", (c) => {
29879
+ err += c;
29880
+ const m = /DevTools listening on (ws:\/\/\S+)/.exec(err);
29881
+ if (m?.[1]) {
29882
+ clearTimeout(timer);
29883
+ resolve(m[1]);
29884
+ }
29885
+ });
29886
+ child.once("error", (e) => {
29887
+ clearTimeout(timer);
29888
+ reject(e);
29889
+ });
29890
+ child.once("exit", (code) => {
29891
+ clearTimeout(timer);
29892
+ reject(new Error(`Chrome exited (${code}): ${err.trim().split("\n").pop() || "no output"}`));
29893
+ });
29894
+ });
29895
+ return { child, endpoint, exited };
29896
+ }
29897
+ function serve(page) {
29898
+ const server = http2.createServer((req, res) => {
29899
+ if (req.url === "/") {
29900
+ res.writeHead(200, { "content-type": "text/html; charset=utf-8", "cache-control": "no-store" }).end(HARNESS);
29901
+ } else if (req.url === "/page.html") {
29902
+ res.writeHead(200, { "content-type": "text/html; charset=utf-8", "cache-control": "no-store", "content-security-policy": `sandbox ${FRAME_SANDBOX}` }).end(page);
29903
+ } else res.writeHead(404).end();
29904
+ });
29905
+ return new Promise((resolve, reject) => {
29906
+ server.on("error", reject);
29907
+ server.listen(0, "127.0.0.1", () => {
29908
+ const addr = server.address();
29909
+ resolve({ url: `http://127.0.0.1:${addr.port}/`, close: () => server.close() });
29910
+ });
29911
+ });
29912
+ }
29913
+ function withMeasure(html) {
29914
+ const at2 = html.toLowerCase().lastIndexOf("</body>");
29915
+ return at2 < 0 ? html + MEASURE : html.slice(0, at2) + MEASURE + html.slice(at2);
29916
+ }
29917
+ async function renderCheck(html) {
29918
+ const bin = findChrome();
29919
+ if (!bin) {
29920
+ const reason = process.env.CHROME_PATH ? `CHROME_PATH does not exist: ${process.env.CHROME_PATH}` : "no Chrome, Chromium or Edge found; install one or set CHROME_PATH";
29921
+ return { status: "unavailable", reason };
29922
+ }
29923
+ const { html: page, notices } = exportHtml(html);
29924
+ const profile = fs10.mkdtempSync(path9.join(os2.tmpdir(), "austere-render-"));
29925
+ const site = await serve(withMeasure(page));
29926
+ const { child, endpoint, exited } = launch(bin, profile);
29927
+ let cdp = null;
29928
+ let timer;
29929
+ try {
29930
+ const run2 = (async () => {
29931
+ const conn = connect(await endpoint);
29932
+ cdp = conn;
29933
+ await conn.opened;
29934
+ const { targetId } = await conn.send("Target.createTarget", { url: "about:blank" });
29935
+ const { sessionId: sessionId2 } = await conn.send("Target.attachToTarget", { targetId, flatten: true });
29936
+ await conn.send("Page.enable", {}, sessionId2);
29937
+ const loaded = new Promise((resolve) => conn.on((m) => m.method === "Page.loadEventFired" && m.sessionId === sessionId2 && resolve()));
29938
+ await conn.send("Page.navigate", { url: site.url }, sessionId2);
29939
+ await loaded;
29940
+ const r = await conn.send(
29941
+ "Runtime.evaluate",
29942
+ { expression: `window.__austereReport(${JSON.stringify(WIDTHS)})`, awaitPromise: true, returnByValue: true },
29943
+ sessionId2
29944
+ );
29945
+ if (r.exceptionDetails) throw new Error(`render report failed: ${r.exceptionDetails.text}`);
29946
+ const report = r.result.value;
29947
+ if (!report.rendered && !report.errors.length) report.errors.push({ statement: "", error: "the page did not finish rendering" });
29948
+ return { status: "ok", statements: report.statements, errors: report.errors, overflow: report.overflow, notices };
29949
+ })();
29950
+ const timeout = new Promise((_, reject) => {
29951
+ timer = setTimeout(() => reject(new Error(`rendering took longer than ${RENDER_TIMEOUT_MS / 1e3} s`)), RENDER_TIMEOUT_MS);
29952
+ });
29953
+ return await Promise.race([run2, timeout]);
29954
+ } catch (err) {
29955
+ return { status: "unavailable", reason: err instanceof Error ? err.message : String(err) };
29956
+ } finally {
29957
+ clearTimeout(timer);
29958
+ const conn = cdp;
29959
+ conn?.send("Browser.close").catch(() => {
29960
+ });
29961
+ let grace;
29962
+ await Promise.race([exited, new Promise((r) => grace = setTimeout(r, 3e3))]);
29963
+ clearTimeout(grace);
29964
+ conn?.close();
29965
+ if (child.exitCode == null && child.pid != null) child.kill();
29966
+ site.close();
29967
+ let killGrace;
29968
+ await Promise.race([exited, new Promise((r) => killGrace = setTimeout(r, 3e3))]);
29969
+ clearTimeout(killGrace);
29970
+ try {
29971
+ fs10.rmSync(profile, { recursive: true, force: true, maxRetries: 3, retryDelay: 100 });
29972
+ } catch {
29973
+ }
29974
+ }
29975
+ }
29976
+
29672
29977
  // src/cli/output.ts
29673
29978
  var PLAIN = /^[A-Za-z0-9_./@:+-][A-Za-z0-9 _./@:+(),=<>*#%?!'-]*$/;
29674
29979
  var json2 = (v) => JSON.stringify(v).replace(/[\u0085\u2028\u2029]/g, (c) => `\\u${c.charCodeAt(0).toString(16).padStart(4, "0")}`);
@@ -29902,85 +30207,69 @@ WORKFLOW
29902
30207
  intro = Card([CardHeader("Title", "Subtitle"), introText])
29903
30208
  introText = MarkDownRenderer("...")
29904
30209
  </script></body></html>
29905
- The CDN tag lets the file open on its own (needs network); review and \`export\` work offline.
29906
- 3. ${BIN2} <file.html> validate + open for review (fix any errors it lists).
29907
- With --no-open the session still exists; it prints the url
29908
- (and a phone url when Tailscale is up) for the user. A browser
29909
- the CLI did not open signs in once with a link from \`${BIN2} login\`.
29910
- 4. ${BIN2} poll <file.html> wait for feedback. Leave it running in the foreground;
29911
- it returns when the reviewer presses Send. If your tool call
29912
- times out first, run the same poll again: nothing is lost.
29913
- 5. Apply feedback by editing ONLY the named statements (keep names stable). The page
29914
- live-updates and highlights what changed. Then reply, passing the poll's cursor:
30210
+ Arguments are positional and typed: follow each signature in spec exactly (order and
30211
+ type; an array stays an array). Pass null to skip an optional one.
30212
+ 3. ${BIN2} <file.html> validate + open for review; fix any errors it lists.
30213
+ --no-open: print the url for the user instead.
30214
+ 4. ${BIN2} poll <file.html> wait for feedback (foreground; if the call times out, poll again).
30215
+ 5. Revise with patch only: send just the changed statements; do not rewrite or re-read
30216
+ the file (each note carries its statement's line and source):
30217
+ ${BIN2} patch <file.html> <<'EOF'
30218
+ risks = Table([...]) same name replaces, new name adds, \`name = null\` deletes
30219
+ EOF
30220
+ If a changed value appears in several statements, patch each of them.
30221
+ Then acknowledge and reply in one step:
29915
30222
  ${BIN2} poll <file.html> --cursor <n> --agent-reply "one line: what changed"
29916
- Feedback stays unread until you acknowledge it (--cursor, or any reply); a poll
29917
- without either returns it again, so feedback lost on the way is not lost.
29918
- 6. On status: ended, stop polling and continue in chat. Never reopen uninvited. Notes
29919
- the reviewer adds later are kept, and the next poll returns them.
30223
+ 6. On status: ended, stop polling and continue in chat. Never reopen uninvited.
29920
30224
 
29921
- FEEDBACK (what poll returns)
29922
- status: feedback | ended | browser_disconnected | interrupted | replaced, plus a next: hint.
29923
- cursor: the number to acknowledge this feedback with.
29924
- items, each one of:
29925
- annotation: <statement> (<Component>) line + source of that statement in the file,
29926
- within (parent statements), selection | excerpt, detail (table row/column, or
29927
- part: the exact piece clicked, e.g. a diff file:+line or a diagram node), note
29928
- action: <@ToAssistant message> form: {<formName>: {<field>: value}}, context
29929
- message: <free text from the composer>
29930
- render_error: <statement> a render failure the reviewer asked you to fix
30225
+ FEEDBACK (poll)
30226
+ status: feedback | ended | browser_disconnected | interrupted | replaced, plus next:
30227
+ cursor: pass it back to acknowledge; unacknowledged feedback is returned again.
30228
+ items: annotation: <statement> (<Component>) with line, source, note
30229
+ action: <@ToAssistant message> with form values | message: <free text>
30230
+ render_error: <statement> sent by the open review page on its own
29931
30231
 
29932
30232
  CHECK verifies syntax, component names, required props, unresolved references and
29933
- statements unreachable from root. Mermaid syntax is only checked when it renders; failures
29934
- show in the reviewer's panel and can come back as render_error. CHECK does not judge how
29935
- a page looks: open it for that. It does warn when an Architecture diagram will not fit the
30233
+ statements unreachable from root. It warns when an Architecture diagram will not fit the
29936
30234
  page (it would scroll sideways).
29937
30235
 
29938
- WHEN TO USE
29939
- Plans, comparisons, decisions, reviews, reports: anything easier to grasp as a page.
29940
- Not for a few sentences of prose.
30236
+ LOOKS
30237
+ The reviewer judges how the page looks. Do not open, screenshot or inspect the page
30238
+ yourself (browser tools, curl, headless runs) unless the user asks: check ok + open is
30239
+ done. Render failures come back through poll. When the user asks you to verify the
30240
+ page, run \`${BIN2} check --render <file>\` (headless Chrome, text report, no image).
29941
30241
 
29942
- COMMANDS (\`${BIN2} <command> --help\` for one command's flags and an example)
29943
- ${BIN2} sessions + command list
29944
- ${BIN2} <file> open/resume a review [--no-open] [--reopen]
29945
- ${BIN2} poll <file> long-poll for feedback [--cursor n] [--agent-reply "..."] [--agent-reply-file f|-]
29946
- --cursor n acknowledges feedback up to n; --after n returns
29947
- everything after n again. One poll per session; --takeover
29948
- replaces a stuck one
29949
- ${BIN2} reply <file> post a reply without polling --agent-reply "..." | --agent-reply-file f|- [--cursor n]
29950
- ${BIN2} end <file> stop as the agent; the reviewer can still add notes for later
29951
- ${BIN2} close <file> close the review for the owner: no more notes until reopen
29952
- ${BIN2} reopen <file> reopen an ended or closed review
29953
- ${BIN2} login [file] single-use link that signs a browser in as you (SSH, --no-open,
29954
- another browser), to the file's review or the session list
29955
- ${BIN2} feedback <file> feedback no agent has answered yet (sent after polling stopped,
29956
- or in an earlier session) [--all] [--peek]; answer with reply
29957
- ${BIN2} check <file> validate the program (no browser) [--fix: declare the packs it uses]
29958
- ${BIN2} patch <file> merge statements from stdin into the program: same name
29959
- replaces, new name adds, \`name = null\` deletes;
29960
- reports added statements dropped as unreferenced
29961
- ${BIN2} spec [group|pack...] component reference (groups: Layout Content Data Charts Forms Buttons)
29962
- ${BIN2} components [group] every component, including ones spec leaves out
29963
- ${BIN2} components search <words> | show <Name...>
29964
- ${BIN2} components add <npm-package|folder|file.tgz> [--trust] | remove <package>
29965
- packs add components; a page that uses one declares it
29966
- (check --fix writes the <meta name="austere-packs"> tag)
29967
- ${BIN2} components new <dir> | build [dir] write your own pack
29968
- ${BIN2} export <file> single self-contained HTML [--out path]
29969
- ${BIN2} hub on|off|status|list|notes|open the owner's home page: every artifact by project/date, every note
29970
- ${BIN2} share <file> --as "<name>" [--auto] invite link for a teammate on your tailnet. Their notes
29971
- wait for the owner to send them on; --auto skips the wait
29972
- ${BIN2} share list [--links] | revoke <name|id>
29973
- ${BIN2} stop stop the background server
29974
- ${BIN2} server [--persist] run the server in the foreground; --persist: never idle out
29975
- (behind a reverse proxy, under systemd)
29976
- ${BIN2} mcp MCP server on stdio: spec check open poll reply patch end as tools
30242
+ COMMANDS
30243
+ ${BIN2} check <file> validate without a browser: syntax, components, props,
30244
+ references [--fix: declare packs] [--render]
30245
+ ${BIN2} reply <file> reply without polling --agent-reply "..." [--cursor n]
30246
+ ${BIN2} end <file> stop as the agent; later notes wait for the next poll
30247
+ ${BIN2} feedback <file> notes no agent has answered yet [--all] [--peek]
30248
+ ${BIN2} login [file] single-use sign-in link for a browser the CLI did not open
30249
+ ${BIN2} components search <words> | show <Name...> components spec leaves out
30250
+ ${BIN2} <command> --help one command's flags and an example
30251
+ ${BIN2} help all every command, notes and environment
30252
+ `;
30253
+ function helpAll() {
30254
+ const rows = Object.entries(COMMANDS).map(([, c]) => ` ${BIN2} ${c.usage}
30255
+ ${c.summary}`);
30256
+ return `
30257
+ ALL COMMANDS
30258
+ ${rows.join("\n")}
30259
+
30260
+ NOTES
30261
+ The CDN script tag lets the file open on its own (needs network); review and export work
30262
+ offline. A browser the CLI did not open gets 403 until it signs in with \`${BIN2} login\`.
30263
+ Mermaid syntax is only checked when it renders. check warns when an Architecture diagram
30264
+ will not fit the page. check --render needs Chrome, Chromium or Edge (or CHROME_PATH).
29977
30265
 
29978
30266
  ENV
29979
30267
  AUSTERE_AXI_PORT (4388) AUSTERE_AXI_STATE_DIR (~/.austere-axi) AUSTERE_AXI_NO_OPEN=1
29980
30268
  AUSTERE_AXI_HOST AUSTERE_AXI_NO_TAILSCALE=1 AUSTERE_AXI_DISCONNECT_GRACE_MS (300000)
29981
30269
  AUSTERE_AXI_IDLE_TIMEOUT_MS (1800000) AUSTERE_AXI_PERSIST=1 AUSTERE_AXI_ALLOWED_HOSTS AUSTERE_AXI_LINK_URL
29982
- AUSTERE_AXI_NPM_REGISTRY
30270
+ AUSTERE_AXI_NPM_REGISTRY CHROME_PATH (check --render)
29983
30271
  `;
30272
+ }
29984
30273
  var COMMANDS = {
29985
30274
  open: {
29986
30275
  usage: "<file> [--no-open] [--reopen]",
@@ -30028,9 +30317,12 @@ var COMMANDS = {
30028
30317
  example: "feedback plan.html"
30029
30318
  },
30030
30319
  check: {
30031
- usage: "check <file> [--fix]",
30320
+ usage: "check <file> [--fix] [--render]",
30032
30321
  summary: "validate the program without a browser: syntax, components, props, references, reachability",
30033
- flags: { fix: 'declare the packs the page uses (writes the <meta name="austere-packs"> tag)' },
30322
+ flags: {
30323
+ fix: 'declare the packs the page uses (writes the <meta name="austere-packs"> tag)',
30324
+ render: "also render it in headless Chrome and report render errors and sideways scrolling as text (needs Chrome or CHROME_PATH); only when the user asks"
30325
+ },
30034
30326
  example: "check plan.html"
30035
30327
  },
30036
30328
  patch: {
@@ -30135,7 +30427,7 @@ function needFile(file3, cmd) {
30135
30427
  return null;
30136
30428
  }
30137
30429
  const abs = canonicalFile(file3);
30138
- if (!fs14.existsSync(abs)) {
30430
+ if (!fs15.existsSync(abs)) {
30139
30431
  fail(`no such file: ${abs}`);
30140
30432
  return null;
30141
30433
  }
@@ -30152,7 +30444,7 @@ function readStdin() {
30152
30444
  async function replyText(flags) {
30153
30445
  if (typeof flags["agent-reply-file"] === "string") {
30154
30446
  const f = flags["agent-reply-file"];
30155
- return (f === "-" ? await readStdin() : fs14.readFileSync(f, "utf8")).trim();
30447
+ return (f === "-" ? await readStdin() : fs15.readFileSync(f, "utf8")).trim();
30156
30448
  }
30157
30449
  if (typeof flags["agent-reply"] === "string") return flags["agent-reply"].trim();
30158
30450
  return "";
@@ -30191,7 +30483,7 @@ async function cmdHome() {
30191
30483
  async function cmdOpen(file3, flags) {
30192
30484
  const abs = needFile(file3, "open");
30193
30485
  if (!abs || file3 === void 0) return;
30194
- const html = fs14.readFileSync(abs, "utf8");
30486
+ const html = fs15.readFileSync(abs, "utf8");
30195
30487
  const check2 = checkHtml(html, { file: quote(file3) });
30196
30488
  await ensureServer();
30197
30489
  const r = await request("POST", "/api/open", { file: abs, reopen: !!flags.reopen });
@@ -30230,7 +30522,7 @@ function quote(file3) {
30230
30522
  function describeItems(abs, items) {
30231
30523
  let lines = /* @__PURE__ */ new Map();
30232
30524
  try {
30233
- lines = statementLines(fs14.readFileSync(abs, "utf8"));
30525
+ lines = statementLines(fs15.readFileSync(abs, "utf8"));
30234
30526
  } catch {
30235
30527
  }
30236
30528
  const at2 = locator(lines);
@@ -30247,14 +30539,14 @@ function describeItems(abs, items) {
30247
30539
  }
30248
30540
  function thirdPartyOf(abs) {
30249
30541
  try {
30250
- return pagePacks(fs14.readFileSync(abs, "utf8")).thirdParty.map((p) => `${p.name}@${p.version}`);
30542
+ return pagePacks(fs15.readFileSync(abs, "utf8")).thirdParty.map((p) => `${p.name}@${p.version}`);
30251
30543
  } catch {
30252
30544
  return [];
30253
30545
  }
30254
30546
  }
30255
30547
  var NEXT = {
30256
- feedback: (f, cursor) => `Apply it by editing the named statements in place (keep names stable; the page live-updates and highlights changes). Run \`${BIN2} check ${f}\` if you restructured, then \`${BIN2} poll ${f} --cursor ${cursor} --agent-reply "<one line: what changed>"\`.`,
30257
- feedbackEnded: (f, cursor) => `The reviewer sent this and ended the review. Apply it, then \`${BIN2} reply ${f} --cursor ${cursor} --agent-reply "<what changed>"\` and summarize in chat. Do not poll or reopen.`,
30548
+ feedback: (f, cursor) => `Revise with \`${BIN2} patch ${f}\`: send only the changed statements on stdin (same name replaces, new name adds); do not rewrite or re-read the file. Then \`${BIN2} poll ${f} --cursor ${cursor} --agent-reply "<one line: what changed>"\`.`,
30549
+ feedbackEnded: (f, cursor) => `The reviewer sent this and ended the review. Apply it with \`${BIN2} patch ${f}\` (changed statements only), then \`${BIN2} reply ${f} --cursor ${cursor} --agent-reply "<what changed>"\` and summarize in chat. Do not poll or reopen.`,
30258
30550
  ended: (f) => `The review ended. Stop polling and continue in chat. Notes the reviewer adds later are kept for the next poll. Reopen only if the user asks (\`${BIN2} ${f} --reopen\`).`,
30259
30551
  browser_disconnected: (f) => `Every review tab has been closed for a while. Stop polling and continue in chat; do not end the review. Notes the reviewer adds later are kept, and \`${BIN2} poll ${f}\` returns them.`,
30260
30552
  replaced: () => "Another poll took over this session. Stop polling here.",
@@ -30273,7 +30565,7 @@ async function cmdPoll(file3, flags) {
30273
30565
  if (!reply && (flags["agent-reply"] !== void 0 || flags["agent-reply-file"] !== void 0)) {
30274
30566
  return fail("the agent reply is empty");
30275
30567
  }
30276
- process.stderr.write(`[${BIN2}] waiting for feedback on ${path14.basename(abs)} (interrupting is safe: feedback stays queued)
30568
+ process.stderr.write(`[${BIN2}] waiting for feedback on ${path15.basename(abs)} (interrupting is safe: feedback stays queued)
30277
30569
  `);
30278
30570
  const ticker = process.stderr.isTTY ? setInterval(() => process.stderr.write(`[${BIN2}] still waiting\u2026
30279
30571
  `), 6e4) : null;
@@ -30370,13 +30662,13 @@ function fixPacks(abs, html) {
30370
30662
  return { html, changes };
30371
30663
  }
30372
30664
  const out = setPacksMeta(html, next);
30373
- if (out !== html) fs14.writeFileSync(abs, out);
30665
+ if (out !== html) fs15.writeFileSync(abs, out);
30374
30666
  return { html: out, changes: changes.length ? changes : ["updated integrity hashes"] };
30375
30667
  }
30376
30668
  async function cmdCheck(file3, flags = {}) {
30377
30669
  const abs = needFile(file3, "check");
30378
30670
  if (!abs || file3 === void 0) return;
30379
- let html = fs14.readFileSync(abs, "utf8");
30671
+ let html = fs15.readFileSync(abs, "utf8");
30380
30672
  let fixed = [];
30381
30673
  if (flags.fix) ({ html, changes: fixed } = fixPacks(abs, html));
30382
30674
  const result = checkHtml(html, { file: quote(file3) });
@@ -30385,9 +30677,30 @@ async function cmdCheck(file3, flags = {}) {
30385
30677
  if (fixed.length) out.fixed = fixed;
30386
30678
  if (result.errors.length) out.errors = result.errors;
30387
30679
  if (result.warnings.length) out.warnings = result.warnings;
30388
- if (result.ok && result.hasMermaid) out.note = "Mermaid sources are checked only when they render in the review page.";
30680
+ if (flags.render) {
30681
+ if (!result.ok) out.render = "skipped: fix the errors above first";
30682
+ else {
30683
+ const render = await renderOut(html);
30684
+ out.render = render;
30685
+ if (render.errors) out.status = "errors";
30686
+ else if (render.status === "unavailable") out.status = "render_unavailable";
30687
+ }
30688
+ } else if (result.ok && result.hasMermaid) {
30689
+ out.note = "Mermaid sources are checked when the review page renders them; failures come back through poll.";
30690
+ }
30389
30691
  print(out);
30390
- if (!result.ok) process.exitCode = 1;
30692
+ if (out.status !== "ok") process.exitCode = 1;
30693
+ }
30694
+ async function renderOut(html) {
30695
+ const r = await renderCheck(html);
30696
+ if (r.status === "unavailable") return { status: "unavailable", reason: r.reason };
30697
+ const out = { status: r.errors.length ? "errors" : "clean", widths: "1280, 390", statements: r.statements };
30698
+ if (r.errors.length) out.errors = r.errors.map((e) => ({ statement: e.statement || "(page)", message: e.error }));
30699
+ if (r.overflow.length) {
30700
+ out.scrolls_sideways = r.overflow.map((o) => ({ width: o.width, statements: o.statements.length ? o.statements.join(", ") : "(not one statement)" }));
30701
+ }
30702
+ if (r.notices.length) out.notices = r.notices;
30703
+ return out;
30391
30704
  }
30392
30705
  async function cmdPatch(file3) {
30393
30706
  const abs = needFile(file3, "patch");
@@ -30395,7 +30708,7 @@ async function cmdPatch(file3) {
30395
30708
  if (process.stdin.isTTY) return usage(`pipe the changed statements on stdin: ${BIN2} patch ${quote(file3)} <<'EOF' ... EOF`, "patch");
30396
30709
  const patch = await readStdin();
30397
30710
  if (!patch.trim()) return fail("stdin was empty");
30398
- const html = fs14.readFileSync(abs, "utf8");
30711
+ const html = fs15.readFileSync(abs, "utf8");
30399
30712
  const art = readArtifact(html);
30400
30713
  if (art.program == null) return fail('no <script type="text/openui"> block to patch');
30401
30714
  const merged = mergeStatements(art.program.trim(), patch.trim(), "root");
@@ -30403,7 +30716,7 @@ async function cmdPatch(file3) {
30403
30716
  const patched = new Set(splitStatements(patch).filter((s) => !/=\s*null\s*;?$/.test(s.text)).map((s) => s.id));
30404
30717
  const dropped = [...patched].filter((id) => !kept.has(id));
30405
30718
  const next = replaceProgram(html, merged);
30406
- fs14.writeFileSync(abs, next);
30719
+ fs15.writeFileSync(abs, next);
30407
30720
  const result = checkHtml(next, { file: quote(file3) });
30408
30721
  await registerWithHub(abs, result.title, sessionId(abs));
30409
30722
  const out = { status: "patched", file: abs };
@@ -30418,7 +30731,7 @@ function packSpec(p) {
30418
30731
  return lines.join("\n") + "\n";
30419
30732
  }
30420
30733
  function cmdSpec(groups) {
30421
- let text = fs14.readFileSync(assetPath("prompt.md"), "utf8");
30734
+ let text = fs15.readFileSync(assetPath("prompt.md"), "utf8");
30422
30735
  const packs = installedPacks();
30423
30736
  const packArgs = groups.filter((g) => packs.some((p) => p.name === g));
30424
30737
  if (packArgs.length) {
@@ -30452,9 +30765,9 @@ ${trimmed.join("\n")}
30452
30765
  function cmdExport(file3, flags) {
30453
30766
  const abs = needFile(file3, "export");
30454
30767
  if (!abs) return;
30455
- const out = typeof flags.out === "string" ? path14.resolve(flags.out) : abs.replace(/\.html?$/i, "") + ".export.html";
30456
- const { html, notices } = exportHtml(fs14.readFileSync(abs, "utf8"));
30457
- fs14.writeFileSync(out, html);
30768
+ const out = typeof flags.out === "string" ? path15.resolve(flags.out) : abs.replace(/\.html?$/i, "") + ".export.html";
30769
+ const { html, notices } = exportHtml(fs15.readFileSync(abs, "utf8"));
30770
+ fs15.writeFileSync(out, html);
30458
30771
  print({ status: "exported", file: out, bytes: html.length, notices: notices.length ? notices : void 0 });
30459
30772
  }
30460
30773
  function printResult(out) {
@@ -30514,7 +30827,7 @@ async function cmdComponents(sub, args, flags) {
30514
30827
  pack: r.name,
30515
30828
  dir: r.dir,
30516
30829
  files: r.files,
30517
- next: `cd ${quote(path14.relative(process.cwd(), r.dir) || ".")} && npm install && npm run build, then \`${BIN2} components add .\``
30830
+ next: `cd ${quote(path15.relative(process.cwd(), r.dir) || ".")} && npm install && npm run build, then \`${BIN2} components add .\``
30518
30831
  });
30519
30832
  } catch (err) {
30520
30833
  return fail(messageOf(err));
@@ -30596,7 +30909,7 @@ async function cmdFeedback(file3, flags) {
30596
30909
  file: abs,
30597
30910
  review: status,
30598
30911
  items,
30599
- next: `Apply what still applies (edit the named statements; names are stable), then \`${BIN2} reply ${f} --agent-reply "<what changed>"\`, which marks it addressed. To continue live with the reviewer: \`${BIN2} ${f}\`${status === "ended" ? " --reopen, if the user asks" : ""}, then poll.`
30912
+ next: `Apply what still applies with \`${BIN2} patch ${f}\` (changed statements only; names are stable), then \`${BIN2} reply ${f} --agent-reply "<what changed>"\`, which marks it addressed. To continue live with the reviewer: \`${BIN2} ${f}\`${status === "ended" ? " --reopen, if the user asks" : ""}, then poll.`
30600
30913
  });
30601
30914
  }
30602
30915
  async function cmdHub(sub, args, flags) {
@@ -30625,7 +30938,7 @@ async function cmdHub(sub, args, flags) {
30625
30938
  server: h ? `running on ${port()}${h.persist ? " (persist)" : ""}` : "not running"
30626
30939
  };
30627
30940
  if (hubEnabled() && h) out.url = url2;
30628
- if (fs14.existsSync(hubDbFile())) {
30941
+ if (fs15.existsSync(hubDbFile())) {
30629
30942
  try {
30630
30943
  const hub = await Hub.open();
30631
30944
  try {
@@ -30641,8 +30954,8 @@ async function cmdHub(sub, args, flags) {
30641
30954
  return print(out);
30642
30955
  }
30643
30956
  case "list": {
30644
- if (!fs14.existsSync(hubDbFile())) return print({ artifacts: "none", next: `\`${BIN2} hub on\` starts indexing.` });
30645
- const project = typeof flags.project === "string" ? path14.resolve(flags.project) : "";
30957
+ if (!fs15.existsSync(hubDbFile())) return print({ artifacts: "none", next: `\`${BIN2} hub on\` starts indexing.` });
30958
+ const project = typeof flags.project === "string" ? path15.resolve(flags.project) : "";
30646
30959
  let hub;
30647
30960
  try {
30648
30961
  hub = await Hub.open();
@@ -30667,8 +30980,8 @@ async function cmdHub(sub, args, flags) {
30667
30980
  });
30668
30981
  }
30669
30982
  case "notes": {
30670
- if (!fs14.existsSync(hubDbFile())) return print({ notes: "none", next: `\`${BIN2} hub on\` starts indexing.` });
30671
- const project = typeof flags.project === "string" ? path14.resolve(flags.project) : "";
30983
+ if (!fs15.existsSync(hubDbFile())) return print({ notes: "none", next: `\`${BIN2} hub on\` starts indexing.` });
30984
+ const project = typeof flags.project === "string" ? path15.resolve(flags.project) : "";
30672
30985
  let hub;
30673
30986
  try {
30674
30987
  hub = await Hub.open();
@@ -30688,7 +31001,7 @@ async function cmdHub(sub, args, flags) {
30688
31001
  text: r.text || void 0,
30689
31002
  target: r.target || void 0,
30690
31003
  status: r.status === "addressed" ? "answered" : r.status === "held" ? "held" : "unanswered",
30691
- page: r.title || path14.basename(r.file),
31004
+ page: r.title || path15.basename(r.file),
30692
31005
  file: r.file,
30693
31006
  sent: shortDate(r.at)
30694
31007
  })),
@@ -30812,7 +31125,7 @@ async function main() {
30812
31125
  let name = null;
30813
31126
  if (cmd !== void 0) {
30814
31127
  if (Object.hasOwn(COMMANDS, cmd)) name = cmd;
30815
- else if (/\.html?$/i.test(cmd) || fs14.existsSync(cmd)) name = "open";
31128
+ else if (/\.html?$/i.test(cmd) || fs15.existsSync(cmd)) name = "open";
30816
31129
  else if (flags.help) return process.stdout.write(HELP);
30817
31130
  else return usage(`unknown command: ${cmd}. One of: ${Object.keys(COMMANDS).join(" ")}, or a page file`);
30818
31131
  }
@@ -30868,7 +31181,8 @@ async function main() {
30868
31181
  return cmdOpen(rest[0], flags);
30869
31182
  case "help":
30870
31183
  if (!rest[0]) return process.stdout.write(HELP);
30871
- if (!Object.hasOwn(COMMANDS, rest[0])) return usage(`no command named ${rest[0]}. One of: ${Object.keys(COMMANDS).join(" ")}`, "help");
31184
+ if (rest[0] === "all") return process.stdout.write(HELP + helpAll());
31185
+ if (!Object.hasOwn(COMMANDS, rest[0])) return usage(`no command named ${rest[0]}. One of: all ${Object.keys(COMMANDS).join(" ")}`, "help");
30872
31186
  return commandHelp(rest[0]);
30873
31187
  default:
30874
31188
  return cmdOpen(cmd, flags);