gipity 1.0.424 → 1.0.425
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/api.js +5 -3
- package/dist/commands/add.js +6 -0
- package/dist/commands/bug.js +21 -1
- package/dist/commands/fn.js +21 -2
- package/dist/commands/page-eval.js +75 -5
- package/dist/commands/page-inspect.js +1 -1
- package/dist/commands/page-screenshot.js +27 -2
- package/dist/commands/page.js +1 -1
- package/dist/commands/sandbox.js +21 -0
- package/dist/commands/test.js +39 -3
- package/dist/commands/workflow.js +24 -16
- package/dist/index.js +235 -39
- package/dist/knowledge.js +1 -0
- package/dist/sync.js +17 -3
- package/dist/trace.js +69 -0
- package/dist/updater/shim.js +55 -4
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -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) {
|
|
@@ -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
|
|
|
@@ -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
|
-
|
|
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(
|
|
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(
|
|
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,7 @@ 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 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 () => {
|
|
10657
10723
|
const delayRaw = opts.postLoadDelay ?? opts.wait ?? "1000";
|
|
10658
10724
|
const postLoadDelayMs = delayRaw !== void 0 ? parseInt(String(delayRaw), 10) : void 0;
|
|
10659
10725
|
if (postLoadDelayMs !== void 0 && (!Number.isFinite(postLoadDelayMs) || postLoadDelayMs < 0)) {
|
|
@@ -10707,7 +10773,8 @@ var pageScreenshotCommand = new Command("screenshot").description("Screenshot a
|
|
|
10707
10773
|
title: meta.title,
|
|
10708
10774
|
final_url: meta.finalUrl,
|
|
10709
10775
|
status: meta.status,
|
|
10710
|
-
performance: meta.performance
|
|
10776
|
+
performance: meta.performance,
|
|
10777
|
+
...meta.auth ? { auth: meta.auth } : {}
|
|
10711
10778
|
},
|
|
10712
10779
|
screenshots: meta.screenshots.map((s, i) => ({
|
|
10713
10780
|
file: savedFiles[i],
|
|
@@ -10734,6 +10801,8 @@ var pageScreenshotCommand = new Command("screenshot").description("Screenshot a
|
|
|
10734
10801
|
if (meta.finalUrl) console.log(`${label("Web page URL")} ${meta.finalUrl}`);
|
|
10735
10802
|
if (meta.status != null) console.log(`${label("Web page status")} ${meta.status}`);
|
|
10736
10803
|
if (meta.performance) console.log(`${label("Web page perf")} ${fmtPerformance(meta.performance)}`);
|
|
10804
|
+
printAuthLine(meta.auth);
|
|
10805
|
+
printActionErrorLine(meta.actionError);
|
|
10737
10806
|
if (s.viewport.device) console.log(`${label("Emulated device")} ${s.viewport.device} ${muted("(touch events, mobile user-agent)")}`);
|
|
10738
10807
|
const sizePart = formatSize(s.screenshotSizeBytes) + (meta.full ? " (full page)" : "");
|
|
10739
10808
|
console.log(`${label("Screenshot size")} ${sizePart}`);
|
|
@@ -10746,6 +10815,8 @@ var pageScreenshotCommand = new Command("screenshot").description("Screenshot a
|
|
|
10746
10815
|
if (meta.finalUrl) console.log(`${label("Web page URL")} ${meta.finalUrl}`);
|
|
10747
10816
|
if (meta.status != null) console.log(`${label("Web page status")} ${meta.status}`);
|
|
10748
10817
|
if (meta.performance) console.log(`${label("Web page perf")} ${fmtPerformance(meta.performance)}`);
|
|
10818
|
+
printAuthLine(meta.auth);
|
|
10819
|
+
printActionErrorLine(meta.actionError);
|
|
10749
10820
|
for (let i = 0; i < meta.screenshots.length; i++) {
|
|
10750
10821
|
const s = meta.screenshots[i];
|
|
10751
10822
|
const dims = `${s.viewport.width}\xD7${s.viewport.height}${s.viewport.deviceScaleFactor > 1 ? ` @${s.viewport.deviceScaleFactor}x` : ""}`;
|
|
@@ -10794,7 +10865,7 @@ function shortUrl(url, truncate = true, maxLen = 100) {
|
|
|
10794
10865
|
const tailLen = Math.floor(keep / 2);
|
|
10795
10866
|
return result.slice(0, headLen) + "\u2026" + result.slice(-tailLen);
|
|
10796
10867
|
}
|
|
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) => {
|
|
10868
|
+
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
10869
|
if (opts.screenshot !== void 0) {
|
|
10799
10870
|
console.error(error("page inspect does not capture screenshots. Use page screenshot:"));
|
|
10800
10871
|
console.error(` gipity page screenshot ${url}${typeof opts.screenshot === "string" ? ` -o ${opts.screenshot}` : ""}`);
|
|
@@ -11345,6 +11416,11 @@ sandboxCommand.command("run [args...]").description("Run code").option("--langua
|
|
|
11345
11416
|
"--input <path>",
|
|
11346
11417
|
"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
11418
|
(v, prev) => [...prev ?? [], v]
|
|
11419
|
+
).option(
|
|
11420
|
+
"--no-sync-output <glob>",
|
|
11421
|
+
'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.',
|
|
11422
|
+
(v, prev) => [...prev, v],
|
|
11423
|
+
[]
|
|
11348
11424
|
).option("--json", "Output as JSON").addHelpText("after", `
|
|
11349
11425
|
By default the whole project is auto-mirrored into /work/ (up to 1 GB) -
|
|
11350
11426
|
so your code can reference project files by their relative path, and any
|
|
@@ -11416,6 +11492,11 @@ GCC/Rust).
|
|
|
11416
11492
|
}
|
|
11417
11493
|
}
|
|
11418
11494
|
const language = resolveLanguage({ langFromInterp, langOpt: opts.language, filePath, inlineCode });
|
|
11495
|
+
const noSyncOutput = opts.syncOutput ?? [];
|
|
11496
|
+
if (noSyncOutput.some((g) => !g.trim())) {
|
|
11497
|
+
console.error(error('--no-sync-output requires a non-empty glob (e.g. --no-sync-output "docs/preview*")'));
|
|
11498
|
+
process.exit(1);
|
|
11499
|
+
}
|
|
11419
11500
|
const { config } = await resolveProjectContext();
|
|
11420
11501
|
const timeout = parseInt(opts.timeout, 10);
|
|
11421
11502
|
const cwd = resolveRelativeCwd();
|
|
@@ -11436,7 +11517,11 @@ GCC/Rust).
|
|
|
11436
11517
|
language,
|
|
11437
11518
|
timeout: isNaN(timeout) ? 30 : timeout,
|
|
11438
11519
|
input_files: opts.input,
|
|
11439
|
-
cwd
|
|
11520
|
+
cwd,
|
|
11521
|
+
// The filter must run SERVER-side (in the output extractor): skipping
|
|
11522
|
+
// only in the CLI would still write the files into project storage and
|
|
11523
|
+
// make every later sync propose deleting them.
|
|
11524
|
+
noSyncOutput: noSyncOutput.length ? noSyncOutput : void 0
|
|
11440
11525
|
});
|
|
11441
11526
|
const res = opts.json ? await doRun() : await withSpinner("Running in sandbox\u2026", doRun, { done: null });
|
|
11442
11527
|
const pulledLocal = !!(res.data.outputFiles?.length && getConfigPath());
|
|
@@ -11461,6 +11546,10 @@ GCC/Rust).
|
|
|
11461
11546
|
Output files ${pulledLocal ? "synced to this directory" : "saved to project"}:`);
|
|
11462
11547
|
for (const f of res.data.outputFiles) console.log(`${f}`);
|
|
11463
11548
|
}
|
|
11549
|
+
if (res.data.skippedOutputFiles && res.data.skippedOutputFiles.length > 0) {
|
|
11550
|
+
console.log(dim("\nNot persisted (--no-sync-output):"));
|
|
11551
|
+
for (const f of res.data.skippedOutputFiles) console.log(dim(`${f}`));
|
|
11552
|
+
}
|
|
11464
11553
|
if (res.data.exitCode !== 0) {
|
|
11465
11554
|
process.exit(res.data.exitCode);
|
|
11466
11555
|
}
|
|
@@ -11858,6 +11947,21 @@ function formatRunLine(r) {
|
|
|
11858
11947
|
const statusColor = r.status === "completed" ? success : r.status === "failed" ? error : muted;
|
|
11859
11948
|
return `${muted(r.short_guid)} ${statusColor(r.status)} ${dur} ${runTokens(r)} tokens ${muted(fmtTime(r.started_at))}`;
|
|
11860
11949
|
}
|
|
11950
|
+
function printStepRuns(steps, emptyNote) {
|
|
11951
|
+
if (steps.length === 0) {
|
|
11952
|
+
console.log(` ${muted(emptyNote)}`);
|
|
11953
|
+
return;
|
|
11954
|
+
}
|
|
11955
|
+
for (const s of steps) {
|
|
11956
|
+
const statusColor = s.status === "completed" ? success : s.status === "failed" ? error : muted;
|
|
11957
|
+
const model = s.model_used ? ` ${muted(`[${s.model_used}]`)}` : "";
|
|
11958
|
+
console.log(` ${s.step_order}. ${statusColor(s.status)} ${s.tokens_used ?? 0} tokens${model}`);
|
|
11959
|
+
if (s.error_message) console.log(` ${error(s.error_message)}`);
|
|
11960
|
+
if (s.output_json !== null && s.output_json !== void 0) {
|
|
11961
|
+
console.log(JSON.stringify(s.output_json, null, 2).split("\n").map((l) => ` ${l}`).join("\n"));
|
|
11962
|
+
}
|
|
11963
|
+
}
|
|
11964
|
+
}
|
|
11861
11965
|
var TERMINAL_RUN_STATUSES = /* @__PURE__ */ new Set(["completed", "failed", "cancelled"]);
|
|
11862
11966
|
var sleep2 = (ms) => new Promise((resolve16) => setTimeout(resolve16, ms));
|
|
11863
11967
|
async function waitForRun(wfGuid, prevGuid, timeoutSec) {
|
|
@@ -11942,6 +12046,7 @@ workflowCommand.command("run <name>").description("Trigger a workflow (add --wai
|
|
|
11942
12046
|
} else {
|
|
11943
12047
|
console.log(formatRunLine(r));
|
|
11944
12048
|
if (r.error_message) console.log(` ${error(r.error_message)}`);
|
|
12049
|
+
printStepRuns(r.step_runs ?? [], "(no steps recorded)");
|
|
11945
12050
|
}
|
|
11946
12051
|
if (r.status !== "completed") process.exit(1);
|
|
11947
12052
|
}));
|
|
@@ -11956,21 +12061,7 @@ workflowCommand.command("runs <name> [runGuid]").description("List recent runs,
|
|
|
11956
12061
|
return;
|
|
11957
12062
|
}
|
|
11958
12063
|
console.log(formatRunLine(r));
|
|
11959
|
-
|
|
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
|
-
}
|
|
12064
|
+
printStepRuns(r.step_runs ?? [], "(no steps recorded)");
|
|
11974
12065
|
return;
|
|
11975
12066
|
}
|
|
11976
12067
|
const res = await get(`/workflows/${wf.short_guid}/runs`);
|
|
@@ -14231,6 +14322,9 @@ var addCommand = new Command("add").description("Add a template or kit").argumen
|
|
|
14231
14322
|
force: opts.force
|
|
14232
14323
|
};
|
|
14233
14324
|
}
|
|
14325
|
+
if (!opts.json && !process.stdout.isTTY) {
|
|
14326
|
+
console.error(muted("Installing (server writes files + generates favicons; first add for a title can take ~10s)..."));
|
|
14327
|
+
}
|
|
14234
14328
|
const doAdd = () => post(`/projects/${config.projectGuid}/add`, body);
|
|
14235
14329
|
const res = opts.json ? await doAdd() : await withSpinner("Installing\u2026", doAdd, { done: null });
|
|
14236
14330
|
const syncResult = await sync({ interactive: false, progress: opts.json ? void 0 : createProgressReporter() });
|
|
@@ -15015,7 +15109,7 @@ ${error(`\u2717 ${failed.length} of ${results.length} file(s) missing or wrong-t
|
|
|
15015
15109
|
}));
|
|
15016
15110
|
|
|
15017
15111
|
// src/commands/page.ts
|
|
15018
|
-
var pageCommand = new Command("page").description("Inspect web pages").addCommand(pageInspectCommand).addCommand(pageEvalCommand).addCommand(pageScreenshotCommand).addCommand(pageTestCommand).addCommand(pageFetchCommand);
|
|
15112
|
+
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
15113
|
pageCommand.action(() => {
|
|
15020
15114
|
pageCommand.help();
|
|
15021
15115
|
});
|
|
@@ -15142,14 +15236,25 @@ ${error(`error: ${log3.error_message}`)}`;
|
|
|
15142
15236
|
return line;
|
|
15143
15237
|
});
|
|
15144
15238
|
}));
|
|
15145
|
-
|
|
15239
|
+
async function callAnon(projectGuid, name, body) {
|
|
15240
|
+
let appToken;
|
|
15241
|
+
try {
|
|
15242
|
+
const minted = await publicPost("/api/token", { app: projectGuid });
|
|
15243
|
+
appToken = minted.data.token;
|
|
15244
|
+
} catch {
|
|
15245
|
+
}
|
|
15246
|
+
return publicPost(
|
|
15247
|
+
`/api/${projectGuid}/fn/${encodeURIComponent(name)}`,
|
|
15248
|
+
body,
|
|
15249
|
+
appToken ? { "X-App-Token": appToken } : void 0
|
|
15250
|
+
);
|
|
15251
|
+
}
|
|
15252
|
+
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
15253
|
const config = requireConfig();
|
|
15147
15254
|
const raw = bodyArg || opts.data || "{}";
|
|
15148
15255
|
const body = JSON.parse(raw);
|
|
15149
|
-
const
|
|
15150
|
-
|
|
15151
|
-
body
|
|
15152
|
-
);
|
|
15256
|
+
const path4 = `/api/${config.projectGuid}/fn/${encodeURIComponent(name)}`;
|
|
15257
|
+
const res = opts.anon ? await callAnon(config.projectGuid, name, body) : await post(path4, body);
|
|
15153
15258
|
if (opts.field) {
|
|
15154
15259
|
emitField(res.data, opts.field);
|
|
15155
15260
|
return;
|
|
@@ -15319,7 +15424,9 @@ so the team can triage it into a fix.
|
|
|
15319
15424
|
|
|
15320
15425
|
Categories: ${CATEGORIES.join(", ")}
|
|
15321
15426
|
Severity: ${SEVERITY_HINT}
|
|
15322
|
-
Never include PII or user data \u2014 describe the platform problem in the abstract
|
|
15427
|
+
Never include PII or user data \u2014 describe the platform problem in the abstract.
|
|
15428
|
+
Filed one by mistake? Withdraw it yourself with \`gipity bug retract <id>\`
|
|
15429
|
+
(works until a human picks it up) \u2014 don't file a second report asking for a close.`
|
|
15323
15430
|
);
|
|
15324
15431
|
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
15432
|
const category = String(opts.category).toLowerCase();
|
|
@@ -15340,6 +15447,24 @@ bugCommand.command("report").description("File a bug / friction report about the
|
|
|
15340
15447
|
}
|
|
15341
15448
|
console.log(success(`\u2713 Bug report filed (${res.data.report_guid}) \u2014 queued for triage.`));
|
|
15342
15449
|
}));
|
|
15450
|
+
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(
|
|
15451
|
+
"after",
|
|
15452
|
+
`
|
|
15453
|
+
Only works on your own reports, and only while they are still queued
|
|
15454
|
+
(status new/triaged). Once a report is filed/fixed/dismissed, a human
|
|
15455
|
+
owns closing it out. Find report ids with \`gipity bug list\`.`
|
|
15456
|
+
).action((id, opts) => run("Bug report", async () => {
|
|
15457
|
+
const { config } = await resolveProjectContext({ projectOverride: opts.project });
|
|
15458
|
+
const res = await post(
|
|
15459
|
+
`/api/${config.projectGuid}/services/bug-report/retract`,
|
|
15460
|
+
{ report_guid: id, reason: opts.reason }
|
|
15461
|
+
);
|
|
15462
|
+
if (opts.json) {
|
|
15463
|
+
console.log(JSON.stringify(res.data));
|
|
15464
|
+
return;
|
|
15465
|
+
}
|
|
15466
|
+
console.log(success(`\u2713 Bug report ${res.data.report_guid} retracted \u2014 it is out of the triage queue.`));
|
|
15467
|
+
}));
|
|
15343
15468
|
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
15469
|
const { config } = await resolveProjectContext({ projectOverride: opts.project });
|
|
15345
15470
|
const res = await get(
|
|
@@ -15597,7 +15722,7 @@ jobCommand.command("cancel <runGuid>").description("Cancel a queued or running j
|
|
|
15597
15722
|
}));
|
|
15598
15723
|
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
15724
|
const { existsSync: existsSync20, statSync: statSync9 } = await import("node:fs");
|
|
15600
|
-
const { resolve: resolve16, join:
|
|
15725
|
+
const { resolve: resolve16, join: join22 } = await import("node:path");
|
|
15601
15726
|
const { spawnCommand: spawnCommand2, spawnSyncCommand: spawnSyncCommand2 } = await Promise.resolve().then(() => (init_platform(), platform_exports));
|
|
15602
15727
|
const jobDir = resolve16(process.cwd(), "jobs", name);
|
|
15603
15728
|
if (!existsSync20(jobDir) || !statSync9(jobDir).isDirectory()) {
|
|
@@ -15621,7 +15746,7 @@ jobCommand.command("run-local <name>").description("Run the job in a local Docke
|
|
|
15621
15746
|
},
|
|
15622
15747
|
{ file: "main.sh", runtime: "bash", entry: "bash" }
|
|
15623
15748
|
];
|
|
15624
|
-
const picked = variants.find((v) => existsSync20(
|
|
15749
|
+
const picked = variants.find((v) => existsSync20(join22(jobDir, v.file)));
|
|
15625
15750
|
if (!picked) {
|
|
15626
15751
|
console.error(error(`No main.py / main.js / main.sh found under jobs/${name}/`));
|
|
15627
15752
|
process.exit(1);
|
|
@@ -15646,7 +15771,7 @@ jobCommand.command("run-local <name>").description("Run the job in a local Docke
|
|
|
15646
15771
|
"-v",
|
|
15647
15772
|
`${jobDir}:/work`
|
|
15648
15773
|
];
|
|
15649
|
-
const needsDeps = opts.deps !== false && picked.depsFile && picked.installCmd && existsSync20(
|
|
15774
|
+
const needsDeps = opts.deps !== false && picked.depsFile && picked.installCmd && existsSync20(join22(jobDir, picked.depsFile));
|
|
15650
15775
|
const handlerCmd = `${picked.entry} /work/${picked.file}`;
|
|
15651
15776
|
let shellCmd;
|
|
15652
15777
|
if (needsDeps && picked.installCmd) {
|
|
@@ -16308,6 +16433,16 @@ function statusIcon2(status) {
|
|
|
16308
16433
|
if (status === "skipped") return muted("\u2192");
|
|
16309
16434
|
return muted("?");
|
|
16310
16435
|
}
|
|
16436
|
+
function printSkippedCandidates(skipped) {
|
|
16437
|
+
if (skipped.length === 0) return;
|
|
16438
|
+
console.log("");
|
|
16439
|
+
console.log(muted(`Found ${skipped.length} test-looking file${skipped.length === 1 ? "" : "s"} that gipity test does not run:`));
|
|
16440
|
+
for (const s of skipped.slice(0, 10)) {
|
|
16441
|
+
console.log(muted(` ${s.path} (${s.reason})`));
|
|
16442
|
+
}
|
|
16443
|
+
if (skipped.length > 10) console.log(muted(` ... and ${skipped.length - 10} more`));
|
|
16444
|
+
console.log(muted('Run frontend/kit module tests in the sandbox: gipity sandbox run bash "node <file>"'));
|
|
16445
|
+
}
|
|
16311
16446
|
var LONG_RUN_MS = 6e4;
|
|
16312
16447
|
async function pollTestStatus(projectGuid, runGuid, opts) {
|
|
16313
16448
|
const startTime = Date.now();
|
|
@@ -16419,9 +16554,17 @@ var testCommand = new Command("test").description("Run tests").argument("[path]"
|
|
|
16419
16554
|
process.exit(1);
|
|
16420
16555
|
}
|
|
16421
16556
|
if (!filterPath && data.total === 0 && data.results.length === 0) {
|
|
16422
|
-
|
|
16557
|
+
const root = getProjectRoot();
|
|
16558
|
+
console.log(muted(`No tests ran - no tests/*.test.js files under the project root${root ? ` (${root})` : ""}.`));
|
|
16423
16559
|
console.log(muted("gipity test runs server-function tests only; a frontend-only app (no functions/) has nothing here to run."));
|
|
16424
16560
|
console.log(muted("Verify a frontend app by driving the live page: gipity page inspect <url> (or gipity page eval)."));
|
|
16561
|
+
try {
|
|
16562
|
+
const listRes = await get(
|
|
16563
|
+
`/projects/${config.projectGuid}/test/list`
|
|
16564
|
+
);
|
|
16565
|
+
printSkippedCandidates(listRes.data.skipped ?? []);
|
|
16566
|
+
} catch {
|
|
16567
|
+
}
|
|
16425
16568
|
return;
|
|
16426
16569
|
}
|
|
16427
16570
|
if (data.status === "failed" && data.errorMessage) {
|
|
@@ -16489,7 +16632,9 @@ testCommand.command("list").description("List the test files that would run \u20
|
|
|
16489
16632
|
return;
|
|
16490
16633
|
}
|
|
16491
16634
|
if (total === 0) {
|
|
16492
|
-
|
|
16635
|
+
const root = getProjectRoot();
|
|
16636
|
+
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/.`));
|
|
16637
|
+
if (!pathFilter) printSkippedCandidates(res.data.skipped ?? []);
|
|
16493
16638
|
return;
|
|
16494
16639
|
}
|
|
16495
16640
|
console.log(bold(`Test files${pathFilter ? ` (filter: ${pathFilter})` : ""}: ${total}`));
|
|
@@ -19694,7 +19839,58 @@ function collectRealFlags(argv2, program3) {
|
|
|
19694
19839
|
return flags;
|
|
19695
19840
|
}
|
|
19696
19841
|
|
|
19842
|
+
// src/trace.ts
|
|
19843
|
+
import { openSync as openSync4, writeSync, mkdirSync as mkdirSync12 } from "fs";
|
|
19844
|
+
import { join as join21 } from "path";
|
|
19845
|
+
import { homedir as homedir15 } from "os";
|
|
19846
|
+
var TRACE_KEY = /* @__PURE__ */ Symbol.for("gipity.traceOutput");
|
|
19847
|
+
function installOutputTrace(label2) {
|
|
19848
|
+
if (process.env.GIPITY_TRACE_OUTPUT !== "1") return;
|
|
19849
|
+
try {
|
|
19850
|
+
const g = globalThis;
|
|
19851
|
+
const existing = g[TRACE_KEY];
|
|
19852
|
+
if (existing) {
|
|
19853
|
+
existing.emit({ event: "reenter", label: label2 });
|
|
19854
|
+
return;
|
|
19855
|
+
}
|
|
19856
|
+
const dir = join21(process.env.GIPITY_DIR || join21(homedir15(), ".gipity"), "trace");
|
|
19857
|
+
mkdirSync12(dir, { recursive: true });
|
|
19858
|
+
const stamp = (/* @__PURE__ */ new Date()).toISOString().slice(0, 19).replace("T", "_").replace(/:/g, "-");
|
|
19859
|
+
const fd = openSync4(join21(dir, `${stamp}-pid${process.pid}.jsonl`), "a");
|
|
19860
|
+
const emit = (rec) => {
|
|
19861
|
+
try {
|
|
19862
|
+
writeSync(fd, JSON.stringify({ t: (/* @__PURE__ */ new Date()).toISOString(), ...rec }) + "\n");
|
|
19863
|
+
} catch {
|
|
19864
|
+
}
|
|
19865
|
+
};
|
|
19866
|
+
g[TRACE_KEY] = { emit };
|
|
19867
|
+
emit({
|
|
19868
|
+
event: "start",
|
|
19869
|
+
label: label2,
|
|
19870
|
+
pid: process.pid,
|
|
19871
|
+
ppid: process.ppid,
|
|
19872
|
+
argv: process.argv,
|
|
19873
|
+
cwd: process.cwd(),
|
|
19874
|
+
tty: { stdout: !!process.stdout.isTTY, stderr: !!process.stderr.isTTY }
|
|
19875
|
+
});
|
|
19876
|
+
for (const name of ["stdout", "stderr"]) {
|
|
19877
|
+
const stream = process[name];
|
|
19878
|
+
const original = stream.write.bind(stream);
|
|
19879
|
+
stream.write = ((chunk, encoding, cb) => {
|
|
19880
|
+
emit({
|
|
19881
|
+
stream: name,
|
|
19882
|
+
data: typeof chunk === "string" ? chunk : Buffer.from(chunk).toString("utf8")
|
|
19883
|
+
});
|
|
19884
|
+
return original(chunk, encoding, cb);
|
|
19885
|
+
});
|
|
19886
|
+
}
|
|
19887
|
+
process.on("exit", (code) => emit({ event: "exit", code }));
|
|
19888
|
+
} catch {
|
|
19889
|
+
}
|
|
19890
|
+
}
|
|
19891
|
+
|
|
19697
19892
|
// src/index.ts
|
|
19893
|
+
installOutputTrace("index");
|
|
19698
19894
|
var __dirname = dirname13(fileURLToPath3(import.meta.url));
|
|
19699
19895
|
var pkg = JSON.parse(readFileSync23(resolve15(__dirname, "../package.json"), "utf-8"));
|
|
19700
19896
|
function versionLabel() {
|
package/dist/knowledge.js
CHANGED
|
@@ -136,6 +136,7 @@ gipity bug report --category <cli|deploy|template|kit|db|docs|skill|service|sand
|
|
|
136
136
|
- **Never include PII or user data** (emails, names, secrets, tokens, prompt/file contents) — describe the platform problem in the abstract.
|
|
137
137
|
- File it for *platform* problems, not your own mistakes or the app's own bugs. One report per distinct problem.
|
|
138
138
|
- Reports go to a review queue for the team to triage into fixes; see what you've filed with \`gipity bug list\`.
|
|
139
|
+
- Filed one by mistake? Withdraw it yourself: \`gipity bug retract <report-id> [--reason "<why>"]\` — works while it's still queued (status new/triaged). Never file a second report asking a human to close the first.
|
|
139
140
|
|
|
140
141
|
## Tool output is complete and synchronous
|
|
141
142
|
|
package/dist/sync.js
CHANGED
|
@@ -29,7 +29,7 @@ import { get, del, downloadStream, ApiError } from './api.js';
|
|
|
29
29
|
import { requireConfig, shouldIgnore, getConfigPath } from './config.js';
|
|
30
30
|
import { formatSize, prompt, getAutoConfirm } from './utils.js';
|
|
31
31
|
import { uploadOneFile, hashFile, guessMime, transferToS3, uploadInitBatch, uploadCompleteBatch, UploadConflictError, UPLOAD_CONCURRENCY, UPLOAD_INIT_BATCH_SIZE, UPLOAD_MAX_BYTES, UPLOAD_MAX_PATH_CHARS, } from './upload.js';
|
|
32
|
-
import { DEFAULT_SYNC_IGNORE } from './setup.js';
|
|
32
|
+
import { DEFAULT_SYNC_IGNORE, SCRATCH_IGNORE } from './setup.js';
|
|
33
33
|
const CONFIG_FILE = '.gipity.json';
|
|
34
34
|
import * as tar from 'tar-stream';
|
|
35
35
|
// ─── Tunables ──────────────────────────────────────────────────
|
|
@@ -659,10 +659,24 @@ export function readGipityIgnore(root) {
|
|
|
659
659
|
/** The ignore list a sync/push actually runs with: the project config's
|
|
660
660
|
* `ignore` (falling back to DEFAULT_SYNC_IGNORE when empty, so an empty list
|
|
661
661
|
* never means "sync everything - node_modules, .git and all"), plus any
|
|
662
|
-
* `.gipityignore` patterns
|
|
662
|
+
* `.gipityignore` patterns, plus - unconditionally - the scratch namespaces.
|
|
663
|
+
* The ignore file itself never syncs.
|
|
664
|
+
*
|
|
665
|
+
* SCRATCH_IGNORE is unioned in even when the config has its own `ignore`
|
|
666
|
+
* list, because scratch dirs are a PLATFORM CONVENTION, not a user
|
|
667
|
+
* preference: they must mirror the server's `isEphemeralSandboxPath`
|
|
668
|
+
* denylist (see the comment on SCRATCH_IGNORE in setup.ts) so the same
|
|
669
|
+
* paths are throwaway everywhere - the sandbox refuses to persist them and
|
|
670
|
+
* sync/deploy refuses to upload them. `config.ignore` is frozen into
|
|
671
|
+
* .gipity.json at link time, so any project linked before a scratch dir was
|
|
672
|
+
* added to the defaults would otherwise keep syncing it forever (tmp/ did
|
|
673
|
+
* exactly this). Appended LAST so a stray `!tmp/` negation earlier in the
|
|
674
|
+
* list can't re-include scratch; deduped so the matcher cache in
|
|
675
|
+
* config.ts keys on one canonical pattern set. */
|
|
663
676
|
export function effectiveIgnore(root, configIgnore) {
|
|
664
677
|
const base = configIgnore && configIgnore.length ? configIgnore : DEFAULT_SYNC_IGNORE;
|
|
665
|
-
|
|
678
|
+
const merged = [...base, GIPITY_IGNORE_FILE, ...readGipityIgnore(root)];
|
|
679
|
+
return [...merged, ...SCRATCH_IGNORE.filter(p => !merged.includes(p))];
|
|
666
680
|
}
|
|
667
681
|
/** Fast, LOCAL-ONLY dirtiness probe: true when every local file matches its
|
|
668
682
|
* baseline entry by size+mtime and nothing was added or deleted since the
|