gipity 1.0.424 → 1.0.426

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/index.js CHANGED
@@ -3944,10 +3944,10 @@ function prompt(question) {
3944
3944
  return Promise.reject(new Error(`prompt() called without a TTY: ${question.trim()}`));
3945
3945
  }
3946
3946
  const rl = createInterface({ input: process.stdin, output: process.stdout });
3947
- return new Promise((resolve16) => {
3947
+ return new Promise((resolve17) => {
3948
3948
  rl.question(question, (answer) => {
3949
3949
  rl.close();
3950
- resolve16(answer.trim());
3950
+ resolve17(answer.trim());
3951
3951
  });
3952
3952
  });
3953
3953
  }
@@ -3977,7 +3977,7 @@ async function confirm(question, opts = {}) {
3977
3977
  const wasRaw = stdin.isRaw ?? false;
3978
3978
  stdin.setRawMode(true);
3979
3979
  stdin.resume();
3980
- return new Promise((resolve16) => {
3980
+ return new Promise((resolve17) => {
3981
3981
  stdin.once("data", (key) => {
3982
3982
  stdin.setRawMode(wasRaw);
3983
3983
  stdin.pause();
@@ -3992,12 +3992,12 @@ async function confirm(question, opts = {}) {
3992
3992
  else if (k === "n") answer = false;
3993
3993
  else answer = defaultYes;
3994
3994
  console.log(answer ? "y" : "n");
3995
- resolve16(answer);
3995
+ resolve17(answer);
3996
3996
  });
3997
3997
  });
3998
3998
  }
3999
3999
  function pickOne(label2, max, defaultIdx = 1) {
4000
- return new Promise((resolve16) => {
4000
+ return new Promise((resolve17) => {
4001
4001
  process.stdout.write(` ${bold(label2)} (1-${max}) [${bold(String(defaultIdx))}]: `);
4002
4002
  const { stdin } = process;
4003
4003
  const wasRaw = stdin.isRaw ?? false;
@@ -4013,15 +4013,15 @@ function pickOne(label2, max, defaultIdx = 1) {
4013
4013
  }
4014
4014
  if (ch === "\r" || ch === "\n") {
4015
4015
  console.log(String(defaultIdx));
4016
- return resolve16(defaultIdx);
4016
+ return resolve17(defaultIdx);
4017
4017
  }
4018
4018
  const n = parseInt(ch, 10);
4019
4019
  if (n >= 1 && n <= max) {
4020
4020
  console.log(String(n));
4021
- return resolve16(n);
4021
+ return resolve17(n);
4022
4022
  }
4023
4023
  console.log(String(defaultIdx));
4024
- resolve16(defaultIdx);
4024
+ resolve17(defaultIdx);
4025
4025
  });
4026
4026
  });
4027
4027
  }
@@ -5370,8 +5370,8 @@ var require_streamx = __commonJS({
5370
5370
  return this;
5371
5371
  },
5372
5372
  next() {
5373
- return new Promise(function(resolve16, reject) {
5374
- promiseResolve = resolve16;
5373
+ return new Promise(function(resolve17, reject) {
5374
+ promiseResolve = resolve17;
5375
5375
  promiseReject = reject;
5376
5376
  const data = stream.read();
5377
5377
  if (data !== null) ondata(data);
@@ -5401,11 +5401,11 @@ var require_streamx = __commonJS({
5401
5401
  }
5402
5402
  function destroy(err) {
5403
5403
  stream.destroy(err);
5404
- return new Promise((resolve16, reject) => {
5405
- if (stream._duplexState & DESTROYED) return resolve16({ value: void 0, done: true });
5404
+ return new Promise((resolve17, reject) => {
5405
+ if (stream._duplexState & DESTROYED) return resolve17({ value: void 0, done: true });
5406
5406
  stream.once("close", function() {
5407
5407
  if (err) reject(err);
5408
- else resolve16({ value: void 0, done: true });
5408
+ else resolve17({ value: void 0, done: true });
5409
5409
  });
5410
5410
  });
5411
5411
  }
@@ -5449,8 +5449,8 @@ var require_streamx = __commonJS({
5449
5449
  const writes = pending + (ws._duplexState & WRITE_WRITING ? 1 : 0);
5450
5450
  if (writes === 0) return Promise.resolve(true);
5451
5451
  if (state.drains === null) state.drains = [];
5452
- return new Promise((resolve16) => {
5453
- state.drains.push({ writes, resolve: resolve16 });
5452
+ return new Promise((resolve17) => {
5453
+ state.drains.push({ writes, resolve: resolve17 });
5454
5454
  });
5455
5455
  }
5456
5456
  write(data) {
@@ -5555,10 +5555,10 @@ var require_streamx = __commonJS({
5555
5555
  cb(null);
5556
5556
  }
5557
5557
  function pipelinePromise(...streams) {
5558
- return new Promise((resolve16, reject) => {
5558
+ return new Promise((resolve17, reject) => {
5559
5559
  return pipeline(...streams, (err) => {
5560
5560
  if (err) return reject(err);
5561
- resolve16();
5561
+ resolve17();
5562
5562
  });
5563
5563
  });
5564
5564
  }
@@ -6213,16 +6213,16 @@ var require_extract = __commonJS({
6213
6213
  entryCallback = null;
6214
6214
  cb(err);
6215
6215
  }
6216
- function onnext(resolve16, reject) {
6216
+ function onnext(resolve17, reject) {
6217
6217
  if (error2) {
6218
6218
  return reject(error2);
6219
6219
  }
6220
6220
  if (entryStream) {
6221
- resolve16({ value: entryStream, done: false });
6221
+ resolve17({ value: entryStream, done: false });
6222
6222
  entryStream = null;
6223
6223
  return;
6224
6224
  }
6225
- promiseResolve = resolve16;
6225
+ promiseResolve = resolve17;
6226
6226
  promiseReject = reject;
6227
6227
  consumeCallback(null);
6228
6228
  if (extract3._finished && promiseResolve) {
@@ -6250,11 +6250,11 @@ var require_extract = __commonJS({
6250
6250
  function destroy(err) {
6251
6251
  extract3.destroy(err);
6252
6252
  consumeCallback(err);
6253
- return new Promise((resolve16, reject) => {
6254
- if (extract3.destroyed) return resolve16({ value: void 0, done: true });
6253
+ return new Promise((resolve17, reject) => {
6254
+ if (extract3.destroyed) return resolve17({ value: void 0, done: true });
6255
6255
  extract3.once("close", function() {
6256
6256
  if (err) reject(err);
6257
- else resolve16({ value: void 0, done: true });
6257
+ else resolve17({ value: void 0, done: true });
6258
6258
  });
6259
6259
  });
6260
6260
  }
@@ -6705,7 +6705,7 @@ async function postForTarEntries(path4, body, retried = false) {
6705
6705
  if (!res.body) throw new ApiError(500, "EMPTY_RESPONSE", "Server returned no body");
6706
6706
  const extract3 = tar.extract();
6707
6707
  const entries = [];
6708
- const done = new Promise((resolve16, reject) => {
6708
+ const done = new Promise((resolve17, reject) => {
6709
6709
  extract3.on("entry", (header, stream, next) => {
6710
6710
  const chunks = [];
6711
6711
  stream.on("data", (c) => chunks.push(c));
@@ -6716,7 +6716,7 @@ async function postForTarEntries(path4, body, retried = false) {
6716
6716
  stream.on("error", reject);
6717
6717
  stream.resume();
6718
6718
  });
6719
- extract3.on("finish", () => resolve16());
6719
+ extract3.on("finish", () => resolve17());
6720
6720
  extract3.on("error", reject);
6721
6721
  });
6722
6722
  Readable.fromWeb(res.body).pipe(extract3);
@@ -6862,11 +6862,11 @@ async function getAccountSlug() {
6862
6862
  accountSlugCache = res.data.accountSlug;
6863
6863
  return accountSlugCache;
6864
6864
  }
6865
- async function publicPost(path4, body) {
6865
+ async function publicPost(path4, body, extraHeaders) {
6866
6866
  const url = `${baseUrl()}${path4}`;
6867
6867
  const res = await fetch(url, {
6868
6868
  method: "POST",
6869
- headers: { ...clientHeaders(), "Content-Type": "application/json" },
6869
+ headers: { ...clientHeaders(), "Content-Type": "application/json", ...extraHeaders },
6870
6870
  body: JSON.stringify(body)
6871
6871
  });
6872
6872
  if (!res.ok) {
@@ -7176,7 +7176,7 @@ init_config();
7176
7176
  init_utils();
7177
7177
  import { readFileSync as readFileSync23 } from "fs";
7178
7178
  import { fileURLToPath as fileURLToPath3 } from "url";
7179
- import { dirname as dirname13, resolve as resolve15 } from "path";
7179
+ import { dirname as dirname13, resolve as resolve16 } from "path";
7180
7180
 
7181
7181
  // src/helpers/output.ts
7182
7182
  var frameOpen = false;
@@ -7380,6 +7380,7 @@ gipity bug report --category <cli|deploy|template|kit|db|docs|skill|service|sand
7380
7380
  - **Never include PII or user data** (emails, names, secrets, tokens, prompt/file contents) \u2014 describe the platform problem in the abstract.
7381
7381
  - File it for *platform* problems, not your own mistakes or the app's own bugs. One report per distinct problem.
7382
7382
  - Reports go to a review queue for the team to triage into fixes; see what you've filed with \`gipity bug list\`.
7383
+ - Filed one by mistake? Withdraw it yourself: \`gipity bug retract <report-id> [--reason "<why>"]\` \u2014 works while it's still queued (status new/triaged). Never file a second report asking a human to close the first.
7383
7384
 
7384
7385
  ## Tool output is complete and synchronous
7385
7386
 
@@ -7615,25 +7616,25 @@ async function hashFile(path4) {
7615
7616
  const hash = createHash("sha256");
7616
7617
  let size = 0;
7617
7618
  const stream = createReadStream(path4);
7618
- await new Promise((resolve16, reject) => {
7619
+ await new Promise((resolve17, reject) => {
7619
7620
  stream.on("data", (chunk) => {
7620
7621
  const buf = chunk;
7621
7622
  hash.update(buf);
7622
7623
  size += buf.length;
7623
7624
  });
7624
- stream.on("end", () => resolve16());
7625
+ stream.on("end", () => resolve17());
7625
7626
  stream.on("error", reject);
7626
7627
  });
7627
7628
  return { sha256: hash.digest("hex"), size };
7628
7629
  }
7629
7630
  function readRange(path4, start, end) {
7630
- return new Promise((resolve16, reject) => {
7631
+ return new Promise((resolve17, reject) => {
7631
7632
  const chunks = [];
7632
7633
  const stream = createReadStream(path4, { start, end });
7633
7634
  stream.on("data", (c) => {
7634
7635
  chunks.push(c);
7635
7636
  });
7636
- stream.on("end", () => resolve16(Buffer.concat(chunks)));
7637
+ stream.on("end", () => resolve17(Buffer.concat(chunks)));
7637
7638
  stream.on("error", reject);
7638
7639
  });
7639
7640
  }
@@ -8325,7 +8326,7 @@ async function fetchRemote(projectGuid) {
8325
8326
  function extractTarToMap(stream, idleMs, onBytes, keep) {
8326
8327
  const extract3 = tar2.extract();
8327
8328
  const files = /* @__PURE__ */ new Map();
8328
- return new Promise((resolve16, reject) => {
8329
+ return new Promise((resolve17, reject) => {
8329
8330
  let settled = false;
8330
8331
  let idle;
8331
8332
  const arm = () => {
@@ -8361,7 +8362,7 @@ function extractTarToMap(stream, idleMs, onBytes, keep) {
8361
8362
  });
8362
8363
  entryStream.resume();
8363
8364
  });
8364
- extract3.on("finish", () => done(resolve16, files));
8365
+ extract3.on("finish", () => done(resolve17, files));
8365
8366
  extract3.on("error", (e) => done(reject, e));
8366
8367
  stream.on("error", (e) => done(reject, e));
8367
8368
  arm();
@@ -8585,7 +8586,8 @@ function readGipityIgnore(root) {
8585
8586
  }
8586
8587
  function effectiveIgnore(root, configIgnore) {
8587
8588
  const base = configIgnore && configIgnore.length ? configIgnore : DEFAULT_SYNC_IGNORE;
8588
- return [...base, GIPITY_IGNORE_FILE, ...readGipityIgnore(root)];
8589
+ const merged = [...base, GIPITY_IGNORE_FILE, ...readGipityIgnore(root)];
8590
+ return [...merged, ...SCRATCH_IGNORE.filter((p) => !merged.includes(p))];
8589
8591
  }
8590
8592
  function isLocalTreeClean() {
8591
8593
  try {
@@ -10335,6 +10337,7 @@ init_auth();
10335
10337
  import { readFileSync as readFileSync11 } from "node:fs";
10336
10338
  init_api();
10337
10339
  init_colors();
10340
+ init_auth();
10338
10341
  init_config();
10339
10342
 
10340
10343
  // src/page-fixtures.ts
@@ -10391,6 +10394,17 @@ function normalizeEvalResult(raw) {
10391
10394
  }
10392
10395
  return { result: raw, noValue: false };
10393
10396
  }
10397
+ var EXPR_ECHO_MAX_CHARS = 120;
10398
+ function summarizeExpr(expr) {
10399
+ const lines = expr.split("\n");
10400
+ const meaningful = lines.filter((l) => l.trim() !== "");
10401
+ const oneLine = meaningful.length <= 1;
10402
+ if (oneLine && expr.trim().length <= EXPR_ECHO_MAX_CHARS) return expr.trim();
10403
+ const first = (meaningful[0] ?? "").trim();
10404
+ const head = first.length > EXPR_ECHO_MAX_CHARS ? `${first.slice(0, EXPR_ECHO_MAX_CHARS - 1)}\u2026` : first;
10405
+ const shape = oneLine ? `(${expr.trim().length} chars)` : `(+${meaningful.length - 1} more ${meaningful.length - 1 === 1 ? "line" : "lines"}, ${expr.trim().length} chars)`;
10406
+ return `${head} ${shape}`;
10407
+ }
10394
10408
  var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
10395
10409
  var MAX_WAIT_MS = 3e4;
10396
10410
  function capWaitMs(rawWait, url) {
@@ -10435,12 +10449,15 @@ function evalExecTimeoutMessage(result) {
10435
10449
  return `the expression hit the ~${EVAL_EXEC_BUDGET_MS / 1e3}s in-page execution budget \u2014 the eval body (including its own await/setTimeout pauses) ran longer than that. This budget is the time the expression itself is allowed to run; it is separate from --wait, which only sleeps BEFORE the eval and cannot extend it. Split a long interactive check into several shorter 'page eval' calls (e.g. one per state to verify), keeping each body's in-page waits well under ${EVAL_EXEC_BUDGET_MS / 1e3}s.`;
10436
10450
  }
10437
10451
  var JS_DECOY_FLAGS = ["--js", "--javascript", "--script", "--code", "--expr", "--eval", "--exec"];
10438
- var pageEvalCommand = new Command("eval").description("Evaluate JS in a real browser on a page (DOM, computed styles, element rects; inline expr or --file script)").argument("<url>", "URL to load").argument("[expr]", "JavaScript to evaluate in page context (inline expression or statement body with return/await; result is JSON-serialized). Omit when using --file. Time budget: the body has ~20s to finish after page load - keep driver scripts within it.").option("--file <path>", "Read the script body from a file instead of the inline <expr> arg (mutually exclusive). Runs as an async function body, so top-level return/await work. Same ~20s post-load budget as <expr>.").option(
10452
+ var pageEvalCommand = new Command("eval").description("Evaluate JS in a real browser on a page (DOM, computed styles, element rects; inline expr or --file script). ONE client per call - to verify realtime/presence across concurrent clients use `page test --observe` instead").argument("<url>", "URL to load").argument("[expr]", "JavaScript to evaluate in page context (inline expression or statement body with return/await; result is JSON-serialized). Omit when using --file. Time budget: the body has ~20s to finish after page load - keep driver scripts within it.").option("--file <path>", "Read the script body from a file instead of the inline <expr> arg (mutually exclusive). Runs as an async function body, so top-level return/await work. Same ~20s post-load budget as <expr>.").option(
10439
10453
  "--fixture <path>",
10440
10454
  "Host a local file and expose it to the eval as `fixtureUrl` (and under `fixtures` by basename) to fetch in-page. For verifying a render/parse path against a real binary (an MP3, an image) - no size limit, auto-deleted after the run. Repeat for several files (single-value so it never swallows the inline <expr>).",
10441
10455
  (val, prev) => [...prev, val],
10442
10456
  []
10443
- ).option("--wait <ms>", "Sleep this many ms after DOMContentLoaded before evaluating (lets late async work settle; max 30000)", "500").option("--wait-for <selector>", "Wait until this CSS selector appears before evaluating (deterministic; replaces --wait)").option("--wait-timeout <ms>", "Max ms to wait for --wait-for before giving up", "5000").option("--auth", "Evaluate signed in as you (your Gipity account), so a page behind a Sign-in-with-Gipity login is reachable. Only works for apps using Sign in with Gipity, hosted on *.gipity.ai.").option("--json", "Output as JSON").action((url, exprArg, opts) => run("Page eval", async () => {
10457
+ ).option(
10458
+ "--reload <expr>",
10459
+ "After the first eval, reload the page IN PLACE (localStorage/sessionStorage/cookies preserved) and evaluate this second expression against the post-reload DOM. One command verifies persisted state survives a reload: seed/assert state with <expr>, then assert the restored UI here."
10460
+ ).option("--reload-file <path>", "Read the post-reload expression from a file instead of inline --reload (mutually exclusive)").option("--wait <ms>", "Sleep this many ms after DOMContentLoaded before evaluating (lets late async work settle; max 30000)", "500").option("--wait-for <selector>", "Wait until this CSS selector appears before evaluating (deterministic; replaces --wait)").option("--wait-timeout <ms>", "Max ms to wait for --wait-for before giving up", "5000").option("--auth", "Evaluate signed in as you (your Gipity account), so a page behind a Sign-in-with-Gipity login is reachable. Only works for apps using Sign in with Gipity, hosted on *.gipity.ai.").option("--json", "Output as JSON").action((url, exprArg, opts) => run("Page eval", async () => {
10444
10461
  const decoy = JS_DECOY_FLAGS.find((f) => opts[f.slice(2)] !== void 0);
10445
10462
  if (decoy) {
10446
10463
  pageEvalCommand.error(
@@ -10468,6 +10485,17 @@ var pageEvalCommand = new Command("eval").description("Evaluate JS in a real bro
10468
10485
  pageEvalCommand.error(`error: Cannot read file: ${opts.file}`);
10469
10486
  }
10470
10487
  }
10488
+ if (opts.reload !== void 0 && opts.reloadFile) {
10489
+ pageEvalCommand.error("error: Pass either --reload <expr> or --reload-file <path>, not both");
10490
+ }
10491
+ let reloadExpr = opts.reload;
10492
+ if (opts.reloadFile) {
10493
+ try {
10494
+ reloadExpr = readFileSync11(opts.reloadFile, "utf8");
10495
+ } catch {
10496
+ pageEvalCommand.error(`error: Cannot read file: ${opts.reloadFile}`);
10497
+ }
10498
+ }
10471
10499
  const waitMs = capWaitMs(opts.wait, url);
10472
10500
  const parsedTimeout = parseInt(opts.waitTimeout, 10);
10473
10501
  const waitForTimeoutMs = Number.isFinite(parsedTimeout) && parsedTimeout >= 0 ? parsedTimeout : 5e3;
@@ -10494,29 +10522,47 @@ return (${expr});`;
10494
10522
  url,
10495
10523
  expr: sentExpr,
10496
10524
  waitMs,
10525
+ reloadExpr,
10497
10526
  waitForSelector: opts.waitFor || void 0,
10498
10527
  waitForTimeoutMs: opts.waitFor ? waitForTimeoutMs : void 0,
10499
10528
  auth: opts.auth || void 0
10500
10529
  });
10501
- const d = await pollEvalResult(kickoff.data.evalJobId, waitMs);
10530
+ const d = await pollEvalResult(kickoff.data.evalJobId, reloadExpr !== void 0 ? waitMs * 2 : waitMs);
10502
10531
  const { result, noValue } = normalizeEvalResult(d.result);
10532
+ const reload = d.reloadResult !== void 0 ? normalizeEvalResult(d.reloadResult) : void 0;
10503
10533
  const execTimeout = evalExecTimeoutMessage(d.result);
10504
10534
  if (execTimeout) throw new Error(execTimeout);
10505
10535
  if (opts.json) {
10506
- console.log(JSON.stringify(noValue ? { ...d, result, hint: EVAL_NO_VALUE_HINT } : { ...d, result }));
10536
+ console.log(JSON.stringify({
10537
+ ...d,
10538
+ result,
10539
+ ...reload ? { reloadResult: reload.result } : {},
10540
+ ...noValue ? { hint: EVAL_NO_VALUE_HINT } : {}
10541
+ }));
10507
10542
  return;
10508
10543
  }
10509
10544
  console.log(`${brand("Eval")} ${bold(d.url || url)}`);
10510
10545
  if (d.navigationIncomplete) {
10511
10546
  console.log(`${warning("\u26A0 Navigation incomplete:")} ${d.note || "page did not reach full load"}`);
10512
10547
  }
10548
+ if (d.auth?.requested) {
10549
+ const who = getAuth()?.email;
10550
+ console.log(d.auth.established ? `${muted("Auth:")} ${success("session established")}${who ? muted(` as ${who}`) : ""} ${muted("(what the page renders with it is app-defined)")}` : `${warning("Auth: session NOT established")}${d.auth.detail ? ` \u2014 ${d.auth.detail}` : ""} ${muted("(this is the anonymous view)")}`);
10551
+ }
10513
10552
  if (hosted.length) console.log(`${muted("Fixtures:")} ${hosted.map((h) => h.name).join(", ")}`);
10514
- console.log(opts.file ? `${muted("Script:")} ${opts.file}` : `${muted("Expression:")} ${expr}`);
10553
+ console.log(opts.file ? `${muted("Script:")} ${opts.file}` : `${muted("Expression:")} ${summarizeExpr(expr)}`);
10515
10554
  console.log(`
10516
10555
  ${result.trim() ? result : muted("(empty result)")}`);
10517
10556
  if (noValue) console.log(muted(`
10518
10557
  ${EVAL_NO_VALUE_HINT}`));
10519
10558
  if (d.truncated) console.log(muted("\n(result truncated to fit context - narrow the expression for the full value)"));
10559
+ if (reload) {
10560
+ console.log(`
10561
+ ${bold("After reload")} ${muted("(page reloaded in place \u2014 storage preserved)")}`);
10562
+ console.log(reload.result.trim() ? reload.result : muted("(empty result)"));
10563
+ if (reload.noValue) console.log(muted(EVAL_NO_VALUE_HINT));
10564
+ if (d.reloadTruncated) console.log(muted("(reload result truncated to fit context - narrow the expression for the full value)"));
10565
+ }
10520
10566
  } finally {
10521
10567
  for (const h of hosted) {
10522
10568
  try {
@@ -10538,6 +10584,16 @@ Examples:
10538
10584
  # fetch-able 'fixtureUrl', runs the eval, then deletes the hosted copy:
10539
10585
  gipity page eval "https://dev.gipity.ai/me/app/" --fixture ./sample.mp3 \\
10540
10586
  "(async()=>{ const b = await fetch(fixtureUrl).then(r=>r.arrayBuffer()); return window.App.parseId3(b); })()"
10587
+ # Verify persisted state survives a reload (localStorage/sessionStorage kept):
10588
+ # run <expr>, reload the page in place, then run the --reload expression:
10589
+ gipity page eval "https://dev.gipity.ai/me/app/" \\
10590
+ "localStorage.setItem('todo','milk'); document.title" \\
10591
+ --reload "({ restored: localStorage.getItem('todo'), heading: document.querySelector('h1')?.textContent })"
10592
+
10593
+ Module resolution: dynamic import() specifiers starting with ./ or ../ resolve
10594
+ against the PAGE URL, so import('./packages/i18n/index.js') loads the app's own
10595
+ module without hand-building the deployed /account/project/ path. Absolute paths
10596
+ and full URLs pass through unchanged.
10541
10597
 
10542
10598
  The eval body runs under a ~20s in-page execution budget (its own await/setTimeout
10543
10599
  pauses count; --wait only sleeps BEFORE the eval and does not extend it). For a long
@@ -10553,6 +10609,7 @@ Testing realtime/shared state across clients?
10553
10609
  // src/commands/page-screenshot.ts
10554
10610
  init_api();
10555
10611
  init_config();
10612
+ init_auth();
10556
10613
  init_colors();
10557
10614
  init_utils();
10558
10615
  import { mkdirSync as mkdirSync6, writeFileSync as writeFileSync7 } from "fs";
@@ -10572,6 +10629,15 @@ function label(text) {
10572
10629
  function fmtMs(ms) {
10573
10630
  return ms >= 1e3 ? `${(ms / 1e3).toFixed(1)}s` : `${ms}ms`;
10574
10631
  }
10632
+ function printAuthLine(auth) {
10633
+ if (!auth?.requested) return;
10634
+ const who = getAuth()?.email;
10635
+ console.log(auth.established ? `${label("Auth")} ${success("session established")}${who ? muted(` as ${who}`) : ""} ${muted("(what the page renders with it is app-defined)")}` : `${warning("Auth: session NOT established")}${auth.detail ? ` \u2014 ${auth.detail}` : ""} ${muted("(this is the anonymous view)")}`);
10636
+ }
10637
+ function printActionErrorLine(actionError) {
10638
+ if (!actionError) return;
10639
+ console.log(`${warning("\u26A0 --action failed:")} ${actionError} ${muted("(this image shows the page BEFORE the action ran)")}`);
10640
+ }
10575
10641
  function fmtPerformance(p) {
10576
10642
  const parts = [
10577
10643
  `TTFB ${fmtMs(p.ttfb)}`,
@@ -10653,7 +10719,14 @@ function splitCsv(values) {
10653
10719
  function appendOption(value, previous = []) {
10654
10720
  return [...previous, value];
10655
10721
  }
10656
- var pageScreenshotCommand = new Command("screenshot").description("Screenshot a web page").argument("<url>", "URL to screenshot").option("--post-load-delay <ms>", "Delay after DOMContentLoaded before capture, in ms (default: 1000)").option("--action <js>", `Run JS in the page before capturing \u2014 e.g. click a button to enter a state ("document.getElementById('play').click()"). Runs after the post-load delay, then settles again before the shot.`).option("--full", "Capture the full scrollable page (default: viewport only). Scrolls the page through first so scroll-reveal/fade-in-on-scroll (IntersectionObserver) sections render into the shot instead of capturing blank.").option("-o, --output <file>", "Output path (single viewport only; default .gipity/screenshots/ss-<host>-<timestamp>.png)").option("--device <names>", `Device preset(s): ${Object.keys(DEVICE_PRESETS).join(", ")} (comma-separated or repeat flag). mobile/tablet emulate a real touch device \u2014 touch events, mobile user-agent, DPR \u2014 so touch-gated mobile UI actually renders.`, appendOption, []).option("--viewport <dims>", "Raw viewport(s): WxH or WxH@dpr (comma-separated or repeat flag)", appendOption, []).option("--no-reload-between", "Skip reload between viewports (faster, lower fidelity - only safe for static pages)").option("--fake-media", "Grant a synthetic microphone + camera and auto-accept the getUserMedia prompt, so voice/camera apps render headlessly (audio is a built-in tone, not real speech)").option("--auth", "Capture the page signed in as you (your Gipity account), so UI behind a Sign-in-with-Gipity login is shown. Only works for apps using Sign in with Gipity, hosted on *.gipity.ai.").option("--json", "Output JSON metadata instead of a friendly summary").addOption(new Option("--wait <ms>", "Alias for --post-load-delay").hideHelp()).addOption(new Option("--full-page", "Alias for --full").hideHelp()).action((url, opts) => run("Page screenshot", async () => {
10722
+ var ACTION_DECOY_FLAGS = ["--eval", "--js", "--javascript", "--script", "--code", "--exec"];
10723
+ var pageScreenshotCommand = new Command("screenshot").description("Screenshot a web page").argument("<url>", "URL to screenshot").option("--post-load-delay <ms>", "Delay after DOMContentLoaded before capture, in ms (default: 1000)").option("--action <js>", `Run JS in the page before capturing \u2014 e.g. click a button to enter a state ("document.getElementById('play').click()"). Runs as an async function body, so const/await and app-relative import('./\u2026') work. Runs after the post-load delay, then settles again before the shot. If it throws, the capture still happens and the failure is reported.`).option("--full", "Capture the full scrollable page (default: viewport only). Scrolls the page through first so scroll-reveal/fade-in-on-scroll (IntersectionObserver) sections render into the shot instead of capturing blank.").option("-o, --output <file>", "Output path (single viewport only; default .gipity/screenshots/ss-<host>-<timestamp>.png)").option("--device <names>", `Device preset(s): ${Object.keys(DEVICE_PRESETS).join(", ")} (comma-separated or repeat flag). mobile/tablet emulate a real touch device \u2014 touch events, mobile user-agent, DPR \u2014 so touch-gated mobile UI actually renders.`, appendOption, []).option("--viewport <dims>", "Raw viewport(s): WxH or WxH@dpr (comma-separated or repeat flag)", appendOption, []).option("--no-reload-between", "Skip reload between viewports (faster, lower fidelity - only safe for static pages)").option("--fake-media", "Grant a synthetic microphone + camera and auto-accept the getUserMedia prompt, so voice/camera apps render headlessly (audio is a built-in tone, not real speech)").option("--auth", "Capture the page signed in as you (your Gipity account), so UI behind a Sign-in-with-Gipity login is shown. Only works for apps using Sign in with Gipity, hosted on *.gipity.ai.").option("--json", "Output JSON metadata instead of a friendly summary").addOption(new Option("--wait <ms>", "Alias for --post-load-delay").hideHelp()).addOption(new Option("--full-page", "Alias for --full").hideHelp()).action((url, opts) => run("Page screenshot", async () => {
10724
+ const decoy = ACTION_DECOY_FLAGS.find((f) => opts[f.slice(2)] !== void 0);
10725
+ if (decoy) {
10726
+ pageScreenshotCommand.error(
10727
+ `error: ${decoy} is not a flag on screenshot \u2014 use --action "<js>" to run JavaScript in the page before the capture, e.g. gipity page screenshot "<url>" --action "document.getElementById('play').click()"`
10728
+ );
10729
+ }
10657
10730
  const delayRaw = opts.postLoadDelay ?? opts.wait ?? "1000";
10658
10731
  const postLoadDelayMs = delayRaw !== void 0 ? parseInt(String(delayRaw), 10) : void 0;
10659
10732
  if (postLoadDelayMs !== void 0 && (!Number.isFinite(postLoadDelayMs) || postLoadDelayMs < 0)) {
@@ -10707,7 +10780,8 @@ var pageScreenshotCommand = new Command("screenshot").description("Screenshot a
10707
10780
  title: meta.title,
10708
10781
  final_url: meta.finalUrl,
10709
10782
  status: meta.status,
10710
- performance: meta.performance
10783
+ performance: meta.performance,
10784
+ ...meta.auth ? { auth: meta.auth } : {}
10711
10785
  },
10712
10786
  screenshots: meta.screenshots.map((s, i) => ({
10713
10787
  file: savedFiles[i],
@@ -10734,6 +10808,8 @@ var pageScreenshotCommand = new Command("screenshot").description("Screenshot a
10734
10808
  if (meta.finalUrl) console.log(`${label("Web page URL")} ${meta.finalUrl}`);
10735
10809
  if (meta.status != null) console.log(`${label("Web page status")} ${meta.status}`);
10736
10810
  if (meta.performance) console.log(`${label("Web page perf")} ${fmtPerformance(meta.performance)}`);
10811
+ printAuthLine(meta.auth);
10812
+ printActionErrorLine(meta.actionError);
10737
10813
  if (s.viewport.device) console.log(`${label("Emulated device")} ${s.viewport.device} ${muted("(touch events, mobile user-agent)")}`);
10738
10814
  const sizePart = formatSize(s.screenshotSizeBytes) + (meta.full ? " (full page)" : "");
10739
10815
  console.log(`${label("Screenshot size")} ${sizePart}`);
@@ -10746,6 +10822,8 @@ var pageScreenshotCommand = new Command("screenshot").description("Screenshot a
10746
10822
  if (meta.finalUrl) console.log(`${label("Web page URL")} ${meta.finalUrl}`);
10747
10823
  if (meta.status != null) console.log(`${label("Web page status")} ${meta.status}`);
10748
10824
  if (meta.performance) console.log(`${label("Web page perf")} ${fmtPerformance(meta.performance)}`);
10825
+ printAuthLine(meta.auth);
10826
+ printActionErrorLine(meta.actionError);
10749
10827
  for (let i = 0; i < meta.screenshots.length; i++) {
10750
10828
  const s = meta.screenshots[i];
10751
10829
  const dims = `${s.viewport.width}\xD7${s.viewport.height}${s.viewport.deviceScaleFactor > 1 ? ` @${s.viewport.deviceScaleFactor}x` : ""}`;
@@ -10758,6 +10836,7 @@ ${brand("@ " + dims)}${deviceTag}`);
10758
10836
  console.log(`${label("Screenshot file")} ${success(savedFiles[i])}`);
10759
10837
  }
10760
10838
  }));
10839
+ for (const f of ACTION_DECOY_FLAGS) pageScreenshotCommand.addOption(new Option(`${f} <value>`).hideHelp());
10761
10840
  pageScreenshotCommand.addHelpText("after", `
10762
10841
  Examples:
10763
10842
  gipity page screenshot "https://dev.gipity.ai/me/app/"
@@ -10794,7 +10873,7 @@ function shortUrl(url, truncate = true, maxLen = 100) {
10794
10873
  const tailLen = Math.floor(keep / 2);
10795
10874
  return result.slice(0, headLen) + "\u2026" + result.slice(-tailLen);
10796
10875
  }
10797
- var pageInspectCommand = new Command("inspect").description("Inspect a web page (console, failed resources, timing, layout overflow)").argument("<url>", "URL to inspect").option("--wait <ms>", "Sleep this many ms after DOMContentLoaded before capturing (lets late async/LCP work settle; max 30000)", "500").option("--wait-for <selector>", "Wait until this CSS selector appears before capturing (deterministic; replaces --wait)").option("--wait-timeout <ms>", "Max ms to wait for --wait-for before giving up", "5000").option("--json", "Output as JSON").option("--no-reprobe", "Skip the automatic second probe that filters one-time transient console errors - roughly halves inspect time on a page with any console error, at the cost of possibly reporting a cold-load transient as real").option("--no-truncate", "Show full URLs instead of truncating long ones with middle-ellipsis").option("--all", "Include render-blocking, large resources, oversized images, overflow culprits, and LCP detail").option("--fake-media", "Grant a synthetic microphone + camera and auto-accept the getUserMedia prompt, so voice/camera apps run headlessly (audio is a built-in tone, not real speech)").option("--device <name>", `Inspect as a real touch device: ${TOUCH_DEVICES.join(", ")}. Emulates touch events, mobile user-agent and DPR, so a touch-gated mobile layout is what gets inspected.`).option("--auth", "Load the page signed in as you (your Gipity account), so pages behind a Sign-in-with-Gipity login are reachable. Only works for apps using Sign in with Gipity, hosted on *.gipity.ai.").addOption(new Option("--screenshot [path]", "Capture a screenshot").hideHelp()).action((url, opts) => {
10876
+ var pageInspectCommand = new Command("inspect").description("Inspect a web page (console, failed resources, timing, layout overflow). ONE passive load - to verify realtime/presence across concurrent clients use `page test --observe`").argument("<url>", "URL to inspect").option("--wait <ms>", "Sleep this many ms after DOMContentLoaded before capturing (lets late async/LCP work settle; max 30000)", "500").option("--wait-for <selector>", "Wait until this CSS selector appears before capturing (deterministic; replaces --wait)").option("--wait-timeout <ms>", "Max ms to wait for --wait-for before giving up", "5000").option("--json", "Output as JSON").option("--no-reprobe", "Skip the automatic second probe that filters one-time transient console errors - roughly halves inspect time on a page with any console error, at the cost of possibly reporting a cold-load transient as real").option("--no-truncate", "Show full URLs instead of truncating long ones with middle-ellipsis").option("--all", "Include render-blocking, large resources, oversized images, overflow culprits, and LCP detail").option("--fake-media", "Grant a synthetic microphone + camera and auto-accept the getUserMedia prompt, so voice/camera apps run headlessly (audio is a built-in tone, not real speech)").option("--device <name>", `Inspect as a real touch device: ${TOUCH_DEVICES.join(", ")}. Emulates touch events, mobile user-agent and DPR, so a touch-gated mobile layout is what gets inspected.`).option("--auth", "Load the page signed in as you (your Gipity account), so pages behind a Sign-in-with-Gipity login are reachable. Only works for apps using Sign in with Gipity, hosted on *.gipity.ai.").addOption(new Option("--screenshot [path]", "Capture a screenshot").hideHelp()).action((url, opts) => {
10798
10877
  if (opts.screenshot !== void 0) {
10799
10878
  console.error(error("page inspect does not capture screenshots. Use page screenshot:"));
10800
10879
  console.error(` gipity page screenshot ${url}${typeof opts.screenshot === "string" ? ` -o ${opts.screenshot}` : ""}`);
@@ -11223,7 +11302,7 @@ memoryCommand.command("delete <topic>").description("Delete a topic").option("--
11223
11302
  init_api();
11224
11303
  init_config();
11225
11304
  import { readFileSync as readFileSync12, existsSync as existsSync9, statSync as statSync6 } from "fs";
11226
- import { dirname as dirname8, extname as extname4, relative as relative3 } from "path";
11305
+ import { dirname as dirname8, extname as extname4, relative as relative3, resolve as resolve11 } from "path";
11227
11306
  init_colors();
11228
11307
  var LANG_MAP = {
11229
11308
  js: "javascript",
@@ -11345,6 +11424,11 @@ sandboxCommand.command("run [args...]").description("Run code").option("--langua
11345
11424
  "--input <path>",
11346
11425
  "Narrow to specific project files instead of auto-mirroring the whole tree (repeatable). Use this only for >1 GB projects or when you want surgical control.",
11347
11426
  (v, prev) => [...prev ?? [], v]
11427
+ ).option(
11428
+ "--no-sync-output <glob>",
11429
+ 'Do not persist run outputs matching this glob back to the project (repeatable). For byproducts you want to inspect once but never keep, e.g. --no-sync-output "docs/preview*". Supports *, **, and dir/ prefixes.',
11430
+ (v, prev) => [...prev, v],
11431
+ []
11348
11432
  ).option("--json", "Output as JSON").addHelpText("after", `
11349
11433
  By default the whole project is auto-mirrored into /work/ (up to 1 GB) -
11350
11434
  so your code can reference project files by their relative path, and any
@@ -11416,6 +11500,11 @@ GCC/Rust).
11416
11500
  }
11417
11501
  }
11418
11502
  const language = resolveLanguage({ langFromInterp, langOpt: opts.language, filePath, inlineCode });
11503
+ const noSyncOutput = opts.syncOutput ?? [];
11504
+ if (noSyncOutput.some((g) => !g.trim())) {
11505
+ console.error(error('--no-sync-output requires a non-empty glob (e.g. --no-sync-output "docs/preview*")'));
11506
+ process.exit(1);
11507
+ }
11419
11508
  const { config } = await resolveProjectContext();
11420
11509
  const timeout = parseInt(opts.timeout, 10);
11421
11510
  const cwd = resolveRelativeCwd();
@@ -11436,7 +11525,11 @@ GCC/Rust).
11436
11525
  language,
11437
11526
  timeout: isNaN(timeout) ? 30 : timeout,
11438
11527
  input_files: opts.input,
11439
- cwd
11528
+ cwd,
11529
+ // The filter must run SERVER-side (in the output extractor): skipping
11530
+ // only in the CLI would still write the files into project storage and
11531
+ // make every later sync propose deleting them.
11532
+ noSyncOutput: noSyncOutput.length ? noSyncOutput : void 0
11440
11533
  });
11441
11534
  const res = opts.json ? await doRun() : await withSpinner("Running in sandbox\u2026", doRun, { done: null });
11442
11535
  const pulledLocal = !!(res.data.outputFiles?.length && getConfigPath());
@@ -11456,10 +11549,27 @@ GCC/Rust).
11456
11549
  if (res.data.stdout) console.log(res.data.stdout);
11457
11550
  if (res.data.stderr) console.error(res.data.stderr);
11458
11551
  if (res.data.timedOut) console.error(`[Timed out after ${res.data.durationMs}ms]`);
11552
+ if (res.data.stdoutTruncated || res.data.stderrTruncated) {
11553
+ console.error(dim("Note: output truncated at 256 KB. Write large results to a file instead of printing them."));
11554
+ }
11459
11555
  if (res.data.outputFiles && res.data.outputFiles.length > 0) {
11460
- console.log(`
11461
- Output files ${pulledLocal ? "synced to this directory" : "saved to project"}:`);
11462
- for (const f of res.data.outputFiles) console.log(`${f}`);
11556
+ const projectRoot = getProjectRoot();
11557
+ const landed = (f) => pulledLocal && !!projectRoot && existsSync9(resolve11(projectRoot, f));
11558
+ const onDisk = res.data.outputFiles.filter(landed);
11559
+ const notOnDisk = res.data.outputFiles.filter((f) => !landed(f));
11560
+ if (onDisk.length > 0) {
11561
+ console.log("\nOutput files synced to this directory:");
11562
+ for (const f of onDisk) console.log(`${f}`);
11563
+ }
11564
+ if (notOnDisk.length > 0) {
11565
+ console.log("\nOutput files saved to project:");
11566
+ for (const f of notOnDisk) console.log(`${f}`);
11567
+ if (pulledLocal) console.log(dim("Not pulled locally - run 'gipity sync' to fetch them."));
11568
+ }
11569
+ }
11570
+ if (res.data.skippedOutputFiles && res.data.skippedOutputFiles.length > 0) {
11571
+ console.log(dim("\nNot persisted (--no-sync-output):"));
11572
+ for (const f of res.data.skippedOutputFiles) console.log(dim(`${f}`));
11463
11573
  }
11464
11574
  if (res.data.exitCode !== 0) {
11465
11575
  process.exit(res.data.exitCode);
@@ -11858,8 +11968,24 @@ function formatRunLine(r) {
11858
11968
  const statusColor = r.status === "completed" ? success : r.status === "failed" ? error : muted;
11859
11969
  return `${muted(r.short_guid)} ${statusColor(r.status)} ${dur} ${runTokens(r)} tokens ${muted(fmtTime(r.started_at))}`;
11860
11970
  }
11971
+ function printStepRuns(steps, emptyNote) {
11972
+ if (steps.length === 0) {
11973
+ console.log(` ${muted(emptyNote)}`);
11974
+ return;
11975
+ }
11976
+ for (const s of steps) {
11977
+ const statusColor = s.status === "completed" ? success : s.status === "failed" ? error : muted;
11978
+ const model = s.model_used ? ` ${muted(`[${s.model_used}]`)}` : "";
11979
+ const name = s.step_name ? `${bold(s.step_name)} ` : "";
11980
+ console.log(` ${s.step_order}. ${name}${statusColor(s.status)} ${s.tokens_used ?? 0} tokens${model}`);
11981
+ if (s.error_message) console.log(` ${error(s.error_message)}`);
11982
+ if (s.output_json !== null && s.output_json !== void 0) {
11983
+ console.log(JSON.stringify(s.output_json, null, 2).split("\n").map((l) => ` ${l}`).join("\n"));
11984
+ }
11985
+ }
11986
+ }
11861
11987
  var TERMINAL_RUN_STATUSES = /* @__PURE__ */ new Set(["completed", "failed", "cancelled"]);
11862
- var sleep2 = (ms) => new Promise((resolve16) => setTimeout(resolve16, ms));
11988
+ var sleep2 = (ms) => new Promise((resolve17) => setTimeout(resolve17, ms));
11863
11989
  async function waitForRun(wfGuid, prevGuid, timeoutSec) {
11864
11990
  const deadline = Date.now() + timeoutSec * 1e3;
11865
11991
  let runGuid;
@@ -11942,6 +12068,7 @@ workflowCommand.command("run <name>").description("Trigger a workflow (add --wai
11942
12068
  } else {
11943
12069
  console.log(formatRunLine(r));
11944
12070
  if (r.error_message) console.log(` ${error(r.error_message)}`);
12071
+ printStepRuns(r.step_runs ?? [], "(no steps recorded)");
11945
12072
  }
11946
12073
  if (r.status !== "completed") process.exit(1);
11947
12074
  }));
@@ -11956,21 +12083,7 @@ workflowCommand.command("runs <name> [runGuid]").description("List recent runs,
11956
12083
  return;
11957
12084
  }
11958
12085
  console.log(formatRunLine(r));
11959
- const steps = r.step_runs ?? [];
11960
- if (steps.length === 0) {
11961
- console.log(" (no steps recorded)");
11962
- return;
11963
- }
11964
- for (const s of steps) {
11965
- const statusColor = s.status === "completed" ? success : s.status === "failed" ? error : muted;
11966
- const model = s.model_used ? ` ${muted(`[${s.model_used}]`)}` : "";
11967
- console.log(` ${s.step_order}. ${statusColor(s.status)} ${s.tokens_used ?? 0} tokens${model}`);
11968
- if (s.error_message) console.log(` ${error(s.error_message)}`);
11969
- if (s.output_json !== null && s.output_json !== void 0) {
11970
- const pretty = JSON.stringify(s.output_json, null, 2).split("\n").map((l) => ` ${l}`).join("\n");
11971
- console.log(pretty);
11972
- }
11973
- }
12086
+ printStepRuns(r.step_runs ?? [], "(no steps recorded)");
11974
12087
  return;
11975
12088
  }
11976
12089
  const res = await get(`/workflows/${wf.short_guid}/runs`);
@@ -12465,7 +12578,7 @@ storageCommand.command("retention").description("View or adjust your file versio
12465
12578
  init_platform();
12466
12579
  init_auth();
12467
12580
  init_api();
12468
- import { join as join14, dirname as dirname10, resolve as resolve12, basename as basename4, sep as sep4 } from "path";
12581
+ import { join as join14, dirname as dirname10, resolve as resolve13, basename as basename4, sep as sep4 } from "path";
12469
12582
  import { existsSync as existsSync11, mkdirSync as mkdirSync9, readFileSync as readFileSync15, writeFileSync as writeFileSync9, renameSync as renameSync2, readdirSync as readdirSync5, statSync as statSync7 } from "fs";
12470
12583
  import { homedir as homedir10 } from "os";
12471
12584
  import { fileURLToPath as fileURLToPath2 } from "url";
@@ -12780,7 +12893,7 @@ function windowsTaskPlan(cliPath) {
12780
12893
  init_platform();
12781
12894
  init_api();
12782
12895
  import { hostname as hostname3, userInfo, platform as osPlatform2 } from "os";
12783
- import { resolve as resolve11, dirname as dirname9 } from "path";
12896
+ import { resolve as resolve12, dirname as dirname9 } from "path";
12784
12897
  import { mkdirSync as mkdirSync8, writeFileSync as writeFileSync8 } from "fs";
12785
12898
 
12786
12899
  // src/relay/machine-id.ts
@@ -12842,7 +12955,7 @@ function friendlyDeviceName() {
12842
12955
  return host || `My ${kind}`;
12843
12956
  }
12844
12957
  function resolveCliPath() {
12845
- return resolve11(process.argv[1] ?? "gipity");
12958
+ return resolve12(process.argv[1] ?? "gipity");
12846
12959
  }
12847
12960
  async function pairDevice(opts = {}) {
12848
12961
  const existing = getDevice();
@@ -13558,7 +13671,7 @@ var claudeCommand = new Command("claude").description("Set up and run Claude Cod
13558
13671
  let existing = getConfig();
13559
13672
  let forceAdoptCwd = false;
13560
13673
  if (existing && !opts.newProject && !opts.project && !nonInteractive) {
13561
- const cwdHasConfig = existsSync11(resolve12(process.cwd(), ".gipity.json"));
13674
+ const cwdHasConfig = existsSync11(resolve13(process.cwd(), ".gipity.json"));
13562
13675
  if (!cwdHasConfig && isLikelyEmpty(process.cwd())) {
13563
13676
  const ancestorRoot = dirname10(getConfigPath());
13564
13677
  console.log(` ${bold("You are inside an existing Gipity project.")}
@@ -14231,6 +14344,10 @@ var addCommand = new Command("add").description("Add a template or kit").argumen
14231
14344
  force: opts.force
14232
14345
  };
14233
14346
  }
14347
+ if (!opts.json && !process.stdout.isTTY) {
14348
+ const isKit = KITS.some((k) => k.key === name);
14349
+ console.error(muted(isKit ? "Installing kit (server-side install pipeline)..." : "Installing (server writes files + generates favicons; first add for a title can take ~10s)..."));
14350
+ }
14234
14351
  const doAdd = () => post(`/projects/${config.projectGuid}/add`, body);
14235
14352
  const res = opts.json ? await doAdd() : await withSpinner("Installing\u2026", doAdd, { done: null });
14236
14353
  const syncResult = await sync({ interactive: false, progress: opts.json ? void 0 : createProgressReporter() });
@@ -14551,7 +14668,7 @@ logsCommand.command("fn <name>").description("Show function logs").option("--lim
14551
14668
  for (const log3 of res.data) {
14552
14669
  const time = new Date(log3.created_at).toLocaleTimeString("en-US", { hour12: false, hour: "2-digit", minute: "2-digit" });
14553
14670
  const dur = log3.duration_ms !== null ? `${log3.duration_ms}ms`.padEnd(8) : "".padEnd(8);
14554
- const statusColor = log3.status === "success" ? success : log3.status === "error" ? error : warning;
14671
+ const statusColor = log3.status === "ok" ? success : log3.status === "error" ? error : warning;
14555
14672
  const status = statusColor(log3.status.padEnd(8));
14556
14673
  const trigger = muted(log3.trigger_type.padEnd(8));
14557
14674
  const err = log3.error_message ? ` ${error(`"${log3.error_message}"`)}` : "";
@@ -15015,7 +15132,7 @@ ${error(`\u2717 ${failed.length} of ${results.length} file(s) missing or wrong-t
15015
15132
  }));
15016
15133
 
15017
15134
  // src/commands/page.ts
15018
- var pageCommand = new Command("page").description("Inspect web pages").addCommand(pageInspectCommand).addCommand(pageEvalCommand).addCommand(pageScreenshotCommand).addCommand(pageTestCommand).addCommand(pageFetchCommand);
15135
+ var pageCommand = new Command("page").description("Inspect, drive, and test web pages in a real browser (page test = N concurrent clients for realtime/presence verification)").addCommand(pageInspectCommand).addCommand(pageEvalCommand).addCommand(pageScreenshotCommand).addCommand(pageTestCommand).addCommand(pageFetchCommand);
15019
15136
  pageCommand.action(() => {
15020
15137
  pageCommand.help();
15021
15138
  });
@@ -15142,14 +15259,25 @@ ${error(`error: ${log3.error_message}`)}`;
15142
15259
  return line;
15143
15260
  });
15144
15261
  }));
15145
- fnCommand.command("call <name> [body]").description("Call a function").option("--data <json>", "JSON request body").option("--field <path>", "Print only this field of the result (dot path, e.g. items.0.short_guid)").option("--json", "Output as JSON").action((name, bodyArg, opts) => run("Call", async () => {
15262
+ async function callAnon(projectGuid, name, body) {
15263
+ let appToken;
15264
+ try {
15265
+ const minted = await publicPost("/api/token", { app: projectGuid });
15266
+ appToken = minted.data.token;
15267
+ } catch {
15268
+ }
15269
+ return publicPost(
15270
+ `/api/${projectGuid}/fn/${encodeURIComponent(name)}`,
15271
+ body,
15272
+ appToken ? { "X-App-Token": appToken } : void 0
15273
+ );
15274
+ }
15275
+ fnCommand.command("call <name> [body]").description("Call a function").option("--data <json>", "JSON request body").option("--anon", "Call as an anonymous visitor (the public path a signed-out user hits) instead of as your signed-in account").option("--field <path>", "Print only this field of the result (dot path, e.g. items.0.short_guid)").option("--json", "Output as JSON").action((name, bodyArg, opts) => run("Call", async () => {
15146
15276
  const config = requireConfig();
15147
15277
  const raw = bodyArg || opts.data || "{}";
15148
15278
  const body = JSON.parse(raw);
15149
- const res = await post(
15150
- `/api/${config.projectGuid}/fn/${encodeURIComponent(name)}`,
15151
- body
15152
- );
15279
+ const path4 = `/api/${config.projectGuid}/fn/${encodeURIComponent(name)}`;
15280
+ const res = opts.anon ? await callAnon(config.projectGuid, name, body) : await post(path4, body);
15153
15281
  if (opts.field) {
15154
15282
  emitField(res.data, opts.field);
15155
15283
  return;
@@ -15319,7 +15447,9 @@ so the team can triage it into a fix.
15319
15447
 
15320
15448
  Categories: ${CATEGORIES.join(", ")}
15321
15449
  Severity: ${SEVERITY_HINT}
15322
- Never include PII or user data \u2014 describe the platform problem in the abstract.`
15450
+ Never include PII or user data \u2014 describe the platform problem in the abstract.
15451
+ Filed one by mistake? Withdraw it yourself with \`gipity bug retract <id>\`
15452
+ (works until a human picks it up) \u2014 don't file a second report asking for a close.`
15323
15453
  );
15324
15454
  bugCommand.command("report").description("File a bug / friction report about the Gipity platform").requiredOption("--category <cat>", `One of: ${CATEGORIES.join(", ")}`).requiredOption("--severity <sev>", SEVERITY_HINT).requiredOption("--summary <text>", "One line, 7 words max. No PII/user data.").option("--detail <text>", "Succinct: what you did, what failed, the workaround. No PII/user data.").option("--project <guid-or-slug>", "Attribute to a specific project instead of cwd / Home").option("--json", "Output raw JSON").action((opts) => run("Bug report", async () => {
15325
15455
  const category = String(opts.category).toLowerCase();
@@ -15340,6 +15470,24 @@ bugCommand.command("report").description("File a bug / friction report about the
15340
15470
  }
15341
15471
  console.log(success(`\u2713 Bug report filed (${res.data.report_guid}) \u2014 queued for triage.`));
15342
15472
  }));
15473
+ bugCommand.command("retract <id>").description("Withdraw a bug report you filed (e.g. it was your own mistake)").option("--reason <text>", "Short note for triage on why you are retracting").option("--project <guid-or-slug>", "Resolve auth against a specific project instead of cwd / Home").option("--json", "Output raw JSON").addHelpText(
15474
+ "after",
15475
+ `
15476
+ Only works on your own reports, and only while they are still queued
15477
+ (status new/triaged). Once a report is filed/fixed/dismissed, a human
15478
+ owns closing it out. Find report ids with \`gipity bug list\`.`
15479
+ ).action((id, opts) => run("Bug report", async () => {
15480
+ const { config } = await resolveProjectContext({ projectOverride: opts.project });
15481
+ const res = await post(
15482
+ `/api/${config.projectGuid}/services/bug-report/retract`,
15483
+ { report_guid: id, reason: opts.reason }
15484
+ );
15485
+ if (opts.json) {
15486
+ console.log(JSON.stringify(res.data));
15487
+ return;
15488
+ }
15489
+ console.log(success(`\u2713 Bug report ${res.data.report_guid} retracted \u2014 it is out of the triage queue.`));
15490
+ }));
15343
15491
  bugCommand.command("list").description("List the bug / friction reports you have filed").option("--project <guid-or-slug>", "Resolve auth against a specific project instead of cwd / Home").option("--json", "Output raw JSON").action((opts) => run("Bug report", async () => {
15344
15492
  const { config } = await resolveProjectContext({ projectOverride: opts.project });
15345
15493
  const res = await get(
@@ -15597,9 +15745,9 @@ jobCommand.command("cancel <runGuid>").description("Cancel a queued or running j
15597
15745
  }));
15598
15746
  jobCommand.command("run-local <name>").description("Run the job in a local Docker container against your local filesystem (no platform round-trip)").option("--image <image>", "Docker image to run inside", "easyclaw/sandbox:latest").option("--no-deps", "Skip pip/npm install even if a deps file is present").action(async (name, opts) => {
15599
15747
  const { existsSync: existsSync20, statSync: statSync9 } = await import("node:fs");
15600
- const { resolve: resolve16, join: join21 } = await import("node:path");
15748
+ const { resolve: resolve17, join: join22 } = await import("node:path");
15601
15749
  const { spawnCommand: spawnCommand2, spawnSyncCommand: spawnSyncCommand2 } = await Promise.resolve().then(() => (init_platform(), platform_exports));
15602
- const jobDir = resolve16(process.cwd(), "jobs", name);
15750
+ const jobDir = resolve17(process.cwd(), "jobs", name);
15603
15751
  if (!existsSync20(jobDir) || !statSync9(jobDir).isDirectory()) {
15604
15752
  console.error(error(`jobs/${name}/ not found in current directory`));
15605
15753
  process.exit(1);
@@ -15621,7 +15769,7 @@ jobCommand.command("run-local <name>").description("Run the job in a local Docke
15621
15769
  },
15622
15770
  { file: "main.sh", runtime: "bash", entry: "bash" }
15623
15771
  ];
15624
- const picked = variants.find((v) => existsSync20(join21(jobDir, v.file)));
15772
+ const picked = variants.find((v) => existsSync20(join22(jobDir, v.file)));
15625
15773
  if (!picked) {
15626
15774
  console.error(error(`No main.py / main.js / main.sh found under jobs/${name}/`));
15627
15775
  process.exit(1);
@@ -15646,7 +15794,7 @@ jobCommand.command("run-local <name>").description("Run the job in a local Docke
15646
15794
  "-v",
15647
15795
  `${jobDir}:/work`
15648
15796
  ];
15649
- const needsDeps = opts.deps !== false && picked.depsFile && picked.installCmd && existsSync20(join21(jobDir, picked.depsFile));
15797
+ const needsDeps = opts.deps !== false && picked.depsFile && picked.installCmd && existsSync20(join22(jobDir, picked.depsFile));
15650
15798
  const handlerCmd = `${picked.entry} /work/${picked.file}`;
15651
15799
  let shellCmd;
15652
15800
  if (needsDeps && picked.installCmd) {
@@ -16211,14 +16359,14 @@ var domainCommand = new Command("domain").description("Manage custom domains").a
16211
16359
 
16212
16360
  // src/commands/realtime.ts
16213
16361
  import { existsSync as existsSync13 } from "fs";
16214
- import { dirname as dirname12, resolve as resolve13 } from "path";
16362
+ import { dirname as dirname12, resolve as resolve14 } from "path";
16215
16363
  init_api();
16216
16364
  init_config();
16217
16365
  init_colors();
16218
16366
  function hasDeployManifest() {
16219
16367
  const cfgPath = getConfigPath();
16220
16368
  if (!cfgPath) return false;
16221
- return existsSync13(resolve13(dirname12(cfgPath), "gipity.yaml"));
16369
+ return existsSync13(resolve14(dirname12(cfgPath), "gipity.yaml"));
16222
16370
  }
16223
16371
  var roomCommand = new Command("room").description("Manage realtime rooms").argument("[action]", "list | create | delete | info", "list").argument("[name]", "room name (for create | delete | info)").option("--type <type>", "room type for create: state | relay", "state").option("--auth <level>", "auth level for create: public | user", "public").option("--max-clients <n>", "max clients for create (1-200)").option("--json", "Output as JSON").action((action, name, opts) => run("Realtime room", async () => {
16224
16372
  const config = requireConfig();
@@ -16308,6 +16456,16 @@ function statusIcon2(status) {
16308
16456
  if (status === "skipped") return muted("\u2192");
16309
16457
  return muted("?");
16310
16458
  }
16459
+ function printSkippedCandidates(skipped) {
16460
+ if (skipped.length === 0) return;
16461
+ console.log("");
16462
+ console.log(muted(`Found ${skipped.length} test-looking file${skipped.length === 1 ? "" : "s"} that gipity test does not run:`));
16463
+ for (const s of skipped.slice(0, 10)) {
16464
+ console.log(muted(` ${s.path} (${s.reason})`));
16465
+ }
16466
+ if (skipped.length > 10) console.log(muted(` ... and ${skipped.length - 10} more`));
16467
+ console.log(muted('Run frontend/kit module tests in the sandbox: gipity sandbox run bash "node <file>"'));
16468
+ }
16311
16469
  var LONG_RUN_MS = 6e4;
16312
16470
  async function pollTestStatus(projectGuid, runGuid, opts) {
16313
16471
  const startTime = Date.now();
@@ -16384,7 +16542,7 @@ async function pollTestStatus(projectGuid, runGuid, opts) {
16384
16542
  }
16385
16543
  }
16386
16544
  }
16387
- await new Promise((resolve16) => setTimeout(resolve16, getPollInterval()));
16545
+ await new Promise((resolve17) => setTimeout(resolve17, getPollInterval()));
16388
16546
  }
16389
16547
  }
16390
16548
  var testCommand = new Command("test").description("Run tests").argument("[path]", 'Test path filter (e.g. "api", "e2e/portal")').option("--filter <path>", "Alias for the positional test path filter").option("--timeout <ms>", "Per-test timeout in ms", "30000").option("--retry <n>", "Retry failed tests N times", "0").option("--no-sync", "Skip sync-up before tests").option("--json", "Output as JSON").action((pathFilter, opts) => run("Test", async () => {
@@ -16419,9 +16577,17 @@ var testCommand = new Command("test").description("Run tests").argument("[path]"
16419
16577
  process.exit(1);
16420
16578
  }
16421
16579
  if (!filterPath && data.total === 0 && data.results.length === 0) {
16422
- console.log(muted("No tests ran - this app has no tests/*.test.js files."));
16580
+ const root = getProjectRoot();
16581
+ console.log(muted(`No tests ran - no tests/*.test.js files under the project root${root ? ` (${root})` : ""}.`));
16423
16582
  console.log(muted("gipity test runs server-function tests only; a frontend-only app (no functions/) has nothing here to run."));
16424
16583
  console.log(muted("Verify a frontend app by driving the live page: gipity page inspect <url> (or gipity page eval)."));
16584
+ try {
16585
+ const listRes = await get(
16586
+ `/projects/${config.projectGuid}/test/list`
16587
+ );
16588
+ printSkippedCandidates(listRes.data.skipped ?? []);
16589
+ } catch {
16590
+ }
16425
16591
  return;
16426
16592
  }
16427
16593
  if (data.status === "failed" && data.errorMessage) {
@@ -16489,7 +16655,9 @@ testCommand.command("list").description("List the test files that would run \u20
16489
16655
  return;
16490
16656
  }
16491
16657
  if (total === 0) {
16492
- console.log(muted(pathFilter ? `No test files matched filter: ${pathFilter}` : "No test files found. Add *.test.js files under tests/."));
16658
+ const root = getProjectRoot();
16659
+ console.log(muted(pathFilter ? `No test files matched filter: ${pathFilter}` : `No test files found under tests/ at the project root${root ? ` (${root})` : ""}. Add *.test.js files under tests/.`));
16660
+ if (!pathFilter) printSkippedCandidates(res.data.skipped ?? []);
16493
16661
  return;
16494
16662
  }
16495
16663
  console.log(bold(`Test files${pathFilter ? ` (filter: ${pathFilter})` : ""}: ${total}`));
@@ -17459,13 +17627,13 @@ function parseVersion(out) {
17459
17627
  return m ? m[0] : void 0;
17460
17628
  }
17461
17629
  function runProbe(cmd, args) {
17462
- return new Promise((resolve16) => {
17630
+ return new Promise((resolve17) => {
17463
17631
  let out = "";
17464
17632
  let settled = false;
17465
17633
  const done = (s) => {
17466
17634
  if (!settled) {
17467
17635
  settled = true;
17468
- resolve16(s);
17636
+ resolve17(s);
17469
17637
  }
17470
17638
  };
17471
17639
  let child;
@@ -17852,9 +18020,9 @@ async function dispatchLoop(ctx, opts) {
17852
18020
  while (inflight.size >= MAX_CONCURRENT_DISPATCHES && !ctx.abort.signal.aborted) {
17853
18021
  await Promise.race([
17854
18022
  ...inflight,
17855
- new Promise((resolve16) => {
17856
- if (ctx.abort.signal.aborted) return resolve16();
17857
- ctx.abort.signal.addEventListener("abort", () => resolve16(), { once: true });
18023
+ new Promise((resolve17) => {
18024
+ if (ctx.abort.signal.aborted) return resolve17();
18025
+ ctx.abort.signal.addEventListener("abort", () => resolve17(), { once: true });
17858
18026
  })
17859
18027
  ]);
17860
18028
  }
@@ -18339,7 +18507,7 @@ async function killRunningForConv(convGuid) {
18339
18507
  }
18340
18508
  }
18341
18509
  const graceTimers = [];
18342
- const escalate = new Promise((resolve16) => {
18510
+ const escalate = new Promise((resolve17) => {
18343
18511
  const t = setTimeout(() => {
18344
18512
  for (const e of matches) {
18345
18513
  log2("warn", "previous dispatch ignored SIGTERM - escalating to SIGKILL", { conv: convGuid });
@@ -18348,7 +18516,7 @@ async function killRunningForConv(convGuid) {
18348
18516
  } catch {
18349
18517
  }
18350
18518
  }
18351
- resolve16();
18519
+ resolve17();
18352
18520
  }, KILL_GRACE_MS);
18353
18521
  graceTimers.push(t);
18354
18522
  });
@@ -18358,7 +18526,7 @@ async function killRunningForConv(convGuid) {
18358
18526
  }
18359
18527
  async function spawnSync2(cwd, timeoutMs, onSpawn) {
18360
18528
  const cmd = process.env.GIPITY_RELAY_CLAUDE_CMD || resolveCommand("gipity");
18361
- return new Promise((resolve16, reject) => {
18529
+ return new Promise((resolve17, reject) => {
18362
18530
  const child = spawnCommand(cmd, ["sync", "--json"], {
18363
18531
  cwd,
18364
18532
  env: childEnv(),
@@ -18398,7 +18566,7 @@ async function spawnSync2(cwd, timeoutMs, onSpawn) {
18398
18566
  child.on("exit", (code) => finish(() => {
18399
18567
  if (code === 0) {
18400
18568
  log2("info", "sync done", { cwd, stdoutLen });
18401
- resolve16();
18569
+ resolve17();
18402
18570
  } else {
18403
18571
  reject(new Error(`gipity sync exited ${code}${stderrBuf ? `: ${stderrBuf.trim().slice(0, 300)}` : ""}`));
18404
18572
  }
@@ -18409,7 +18577,7 @@ async function spawnGipityClaude(args, cwd, d, queue, meta) {
18409
18577
  const cmd = process.env.GIPITY_RELAY_CLAUDE_CMD || resolveCommand("gipity");
18410
18578
  const fullArgs = [...args, "--output-format", "stream-json", "--verbose", "--include-partial-messages"];
18411
18579
  const env = childEnv({ GIPITY_CONVERSATION_GUID: d.conversation_guid, GIPITY_CAPTURE: "off" });
18412
- return new Promise((resolve16, reject) => {
18580
+ return new Promise((resolve17, reject) => {
18413
18581
  const child = spawnCommand(cmd, fullArgs, { cwd, env, stdio: ["ignore", "pipe", "pipe"] });
18414
18582
  let resolveExited = () => {
18415
18583
  };
@@ -18575,7 +18743,7 @@ async function spawnGipityClaude(args, cwd, d, queue, meta) {
18575
18743
  void postProgress(d.conversation_guid, buildProgressPayload(false));
18576
18744
  if (ownQueue) await q.close();
18577
18745
  cleanup();
18578
- resolve16({
18746
+ resolve17({
18579
18747
  exitCode: code ?? 1,
18580
18748
  killed,
18581
18749
  runtimeLimit,
@@ -18589,10 +18757,10 @@ async function runDispatchSync(d, cwd) {
18589
18757
  let killed = false;
18590
18758
  try {
18591
18759
  await spawnSync2(cwd, PROJECT_SYNC_TIMEOUT_MS, (child) => {
18592
- const exited = new Promise((resolve16) => {
18760
+ const exited = new Promise((resolve17) => {
18593
18761
  child.once("exit", (_code, signal) => {
18594
18762
  if (signal === "SIGTERM") killed = true;
18595
- resolve16();
18763
+ resolve17();
18596
18764
  });
18597
18765
  });
18598
18766
  running.set(d.short_guid, { child, convGuid: d.conversation_guid, exited });
@@ -18616,18 +18784,18 @@ function killDispatch(shortGuid) {
18616
18784
  }
18617
18785
  }
18618
18786
  function sleep4(ms, signal) {
18619
- return new Promise((resolve16) => {
18620
- if (signal?.aborted) return resolve16();
18787
+ return new Promise((resolve17) => {
18788
+ if (signal?.aborted) return resolve17();
18621
18789
  let onAbort = null;
18622
18790
  const t = setTimeout(() => {
18623
18791
  if (onAbort && signal) signal.removeEventListener("abort", onAbort);
18624
- resolve16();
18792
+ resolve17();
18625
18793
  }, ms);
18626
18794
  if (signal) {
18627
18795
  onAbort = () => {
18628
18796
  clearTimeout(t);
18629
18797
  signal.removeEventListener("abort", onAbort);
18630
- resolve16();
18798
+ resolve17();
18631
18799
  };
18632
18800
  signal.addEventListener("abort", onAbort, { once: true });
18633
18801
  }
@@ -19020,7 +19188,7 @@ init_utils();
19020
19188
  init_colors();
19021
19189
  import { existsSync as existsSync19, rmSync, unlinkSync as unlinkSync6, readFileSync as readFileSync21, writeFileSync as writeFileSync13 } from "fs";
19022
19190
  import { homedir as homedir14, platform as osPlatform4 } from "os";
19023
- import { join as join20, resolve as resolve14 } from "path";
19191
+ import { join as join20, resolve as resolve15 } from "path";
19024
19192
  function removeGipityPluginConfig() {
19025
19193
  const settingsPath = join20(homedir14(), ".claude", "settings.json");
19026
19194
  if (!existsSync19(settingsPath)) return false;
@@ -19075,7 +19243,7 @@ function tildify2(p) {
19075
19243
  return p === home || p.startsWith(home + "/") ? "~" + p.slice(home.length) : p;
19076
19244
  }
19077
19245
  function resolveCliPath2() {
19078
- return resolve14(process.argv[1] ?? "gipity");
19246
+ return resolve15(process.argv[1] ?? "gipity");
19079
19247
  }
19080
19248
  async function stopDaemon() {
19081
19249
  if (!isDaemonRunning()) return;
@@ -19694,13 +19862,64 @@ function collectRealFlags(argv2, program3) {
19694
19862
  return flags;
19695
19863
  }
19696
19864
 
19865
+ // src/trace.ts
19866
+ import { openSync as openSync4, writeSync, mkdirSync as mkdirSync12 } from "fs";
19867
+ import { join as join21 } from "path";
19868
+ import { homedir as homedir15 } from "os";
19869
+ var TRACE_KEY = /* @__PURE__ */ Symbol.for("gipity.traceOutput");
19870
+ function installOutputTrace(label2) {
19871
+ if (process.env.GIPITY_TRACE_OUTPUT !== "1") return;
19872
+ try {
19873
+ const g = globalThis;
19874
+ const existing = g[TRACE_KEY];
19875
+ if (existing) {
19876
+ existing.emit({ event: "reenter", label: label2 });
19877
+ return;
19878
+ }
19879
+ const dir = join21(process.env.GIPITY_DIR || join21(homedir15(), ".gipity"), "trace");
19880
+ mkdirSync12(dir, { recursive: true });
19881
+ const stamp = (/* @__PURE__ */ new Date()).toISOString().slice(0, 19).replace("T", "_").replace(/:/g, "-");
19882
+ const fd = openSync4(join21(dir, `${stamp}-pid${process.pid}.jsonl`), "a");
19883
+ const emit = (rec) => {
19884
+ try {
19885
+ writeSync(fd, JSON.stringify({ t: (/* @__PURE__ */ new Date()).toISOString(), ...rec }) + "\n");
19886
+ } catch {
19887
+ }
19888
+ };
19889
+ g[TRACE_KEY] = { emit };
19890
+ emit({
19891
+ event: "start",
19892
+ label: label2,
19893
+ pid: process.pid,
19894
+ ppid: process.ppid,
19895
+ argv: process.argv,
19896
+ cwd: process.cwd(),
19897
+ tty: { stdout: !!process.stdout.isTTY, stderr: !!process.stderr.isTTY }
19898
+ });
19899
+ for (const name of ["stdout", "stderr"]) {
19900
+ const stream = process[name];
19901
+ const original = stream.write.bind(stream);
19902
+ stream.write = ((chunk, encoding, cb) => {
19903
+ emit({
19904
+ stream: name,
19905
+ data: typeof chunk === "string" ? chunk : Buffer.from(chunk).toString("utf8")
19906
+ });
19907
+ return original(chunk, encoding, cb);
19908
+ });
19909
+ }
19910
+ process.on("exit", (code) => emit({ event: "exit", code }));
19911
+ } catch {
19912
+ }
19913
+ }
19914
+
19697
19915
  // src/index.ts
19916
+ installOutputTrace("index");
19698
19917
  var __dirname = dirname13(fileURLToPath3(import.meta.url));
19699
- var pkg = JSON.parse(readFileSync23(resolve15(__dirname, "../package.json"), "utf-8"));
19918
+ var pkg = JSON.parse(readFileSync23(resolve16(__dirname, "../package.json"), "utf-8"));
19700
19919
  function versionLabel() {
19701
19920
  const base = `v${pkg.version}`;
19702
19921
  try {
19703
- const info2 = JSON.parse(readFileSync23(resolve15(__dirname, "build-info.json"), "utf-8"));
19922
+ const info2 = JSON.parse(readFileSync23(resolve16(__dirname, "build-info.json"), "utf-8"));
19704
19923
  if (info2?.sha) return `${base} (dev ${info2.sha}${info2.dirty ? ", modified" : ""})`;
19705
19924
  } catch {
19706
19925
  }