gipity 1.0.420 → 1.0.424
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/commands/add.js +8 -3
- package/dist/commands/claude.js +20 -2
- package/dist/commands/deploy.js +34 -6
- package/dist/commands/page-eval.js +12 -2
- package/dist/commands/page-inspect.js +200 -185
- package/dist/commands/push.js +12 -7
- package/dist/commands/sandbox.js +69 -16
- package/dist/commands/storage.js +14 -1
- package/dist/commands/test.js +13 -0
- package/dist/helpers/sync.js +8 -1
- package/dist/index.js +19930 -386
- package/dist/knowledge.js +7 -6
- package/dist/sync.js +69 -27
- package/dist/updater/check.js +159 -84
- package/dist/updater/shim.js +153 -71
- package/package.json +5 -3
package/dist/commands/add.js
CHANGED
|
@@ -5,7 +5,7 @@ import { Command } from 'commander';
|
|
|
5
5
|
import { post } from '../api.js';
|
|
6
6
|
import { requireConfig } from '../config.js';
|
|
7
7
|
import { sync } from '../sync.js';
|
|
8
|
-
import { success, muted, bold, warning } from '../colors.js';
|
|
8
|
+
import { success, muted, bold, warning, error as clrError } from '../colors.js';
|
|
9
9
|
import { run } from '../helpers/index.js';
|
|
10
10
|
import { createProgressReporter, withSpinner } from '../progress.js';
|
|
11
11
|
const STARTERS = [
|
|
@@ -164,9 +164,14 @@ export const addCommand = new Command('add')
|
|
|
164
164
|
}
|
|
165
165
|
return;
|
|
166
166
|
}
|
|
167
|
-
// No name
|
|
167
|
+
// No name is a usage error, not a help request: error + help on STDERR and
|
|
168
|
+
// a nonzero exit. (It used to outputHelp() to stdout and exit 0, which an
|
|
169
|
+
// agent piping through `tail` reads as a successful add that printed
|
|
170
|
+
// help-shaped output.)
|
|
168
171
|
if (!name) {
|
|
169
|
-
|
|
172
|
+
console.error(clrError('Missing template or kit name. Usage: gipity add <name> (see the catalog below, or `gipity add --list`)'));
|
|
173
|
+
command.outputHelp({ error: true });
|
|
174
|
+
process.exitCode = 1;
|
|
170
175
|
return;
|
|
171
176
|
}
|
|
172
177
|
const config = requireConfig();
|
package/dist/commands/claude.js
CHANGED
|
@@ -20,7 +20,25 @@ import { printBanner } from '../banner.js';
|
|
|
20
20
|
import { scanForAdoption, isLikelyEmpty, canAdoptCwd, formatCwdLabel, formatBytes, adoptCurrentDir, ADOPT_THRESHOLDS, } from '../adopt-cwd.js';
|
|
21
21
|
import { isClaudeInstalled, ensureClaudeInstalled, CLAUDE_PACKAGE } from '../claude-setup.js';
|
|
22
22
|
const __clDir = dirname(fileURLToPath(import.meta.url));
|
|
23
|
-
|
|
23
|
+
// Walk up to the nearest gipity package.json instead of a hardcoded '../..':
|
|
24
|
+
// the per-module tsc layout puts this file at dist/commands/, the bundled CLI
|
|
25
|
+
// at dist/, so a fixed depth breaks one of the two layouts.
|
|
26
|
+
function readOwnPkg(fromDir) {
|
|
27
|
+
for (let d = fromDir;; d = dirname(d)) {
|
|
28
|
+
const p = join(d, 'package.json');
|
|
29
|
+
if (existsSync(p)) {
|
|
30
|
+
try {
|
|
31
|
+
const pkg = JSON.parse(readFileSync(p, 'utf-8'));
|
|
32
|
+
if (pkg.name === 'gipity')
|
|
33
|
+
return pkg;
|
|
34
|
+
}
|
|
35
|
+
catch { /* keep walking */ }
|
|
36
|
+
}
|
|
37
|
+
if (dirname(d) === d)
|
|
38
|
+
return {};
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
const __clPkg = readOwnPkg(__clDir);
|
|
24
42
|
/** Report a sync run to the user. Beyond the applied-changes line, this SURFACES
|
|
25
43
|
* sync.errors - a download that came back incomplete (truncated bulk tar, a file
|
|
26
44
|
* that couldn't be refetched) lands here, so we never print "ready" over a
|
|
@@ -292,7 +310,7 @@ export const claudeCommand = new Command('claude')
|
|
|
292
310
|
process.exit(1);
|
|
293
311
|
}
|
|
294
312
|
if (!nonInteractive) {
|
|
295
|
-
printBanner({ version: __clPkg.version, email: auth?.email, cwd: process.cwd() });
|
|
313
|
+
printBanner({ version: __clPkg.version ?? 'unknown', email: auth?.email, cwd: process.cwd() });
|
|
296
314
|
}
|
|
297
315
|
// A GIPITY_TOKEN env token authenticates every request on its own, so the
|
|
298
316
|
// saved session's expiry is irrelevant when one is set — never re-login or
|
package/dist/commands/deploy.js
CHANGED
|
@@ -5,6 +5,7 @@ import { formatSize } from '../utils.js';
|
|
|
5
5
|
import { success, error as clrError, warning, muted, bold, brand } from '../colors.js';
|
|
6
6
|
import { run, syncBeforeAction } from '../helpers/index.js';
|
|
7
7
|
import { withSpinner } from '../progress.js';
|
|
8
|
+
import { inspectPage } from './page-inspect.js';
|
|
8
9
|
// ── Status icons ───────────────────────────────────────────────────────
|
|
9
10
|
function statusIcon(status) {
|
|
10
11
|
if (status === 'ok')
|
|
@@ -26,6 +27,7 @@ export const deployCommand = new Command('deploy')
|
|
|
26
27
|
.option('--optimize', 'Force Vite build optimization on (default for prod; use this to optimize a dev deploy too)')
|
|
27
28
|
.option('--no-optimize', 'Skip build optimization and upload files as-is - the escape hatch for plain-HTML apps whose <script src> tags are not type="module"')
|
|
28
29
|
.option('--json', 'Output as JSON')
|
|
30
|
+
.option('--inspect [path]', 'After a successful deploy, run `page inspect` on the deployed URL (or URL + path) in the same command - one build-loop step instead of two. With --json, emits two JSON lines: deploy, then inspect.')
|
|
29
31
|
.action((target, opts) => run('Deploy', async () => {
|
|
30
32
|
if (target !== 'dev' && target !== 'prod') {
|
|
31
33
|
console.error(clrError('Target must be "dev" or "prod"'));
|
|
@@ -44,8 +46,32 @@ export const deployCommand = new Command('deploy')
|
|
|
44
46
|
? await doDeploy()
|
|
45
47
|
: await withSpinner(`Deploying to ${target}…`, doDeploy, { done: null });
|
|
46
48
|
const d = res.data;
|
|
49
|
+
// Deploy + verify in one command: after a successful deploy, run the
|
|
50
|
+
// same inspect pipeline `page inspect` uses against the live URL. An
|
|
51
|
+
// inspect failure is reported but never turns a successful deploy into
|
|
52
|
+
// a nonzero exit - the deploy DID land.
|
|
53
|
+
const inspectAfter = async () => {
|
|
54
|
+
if (!opts.inspect)
|
|
55
|
+
return;
|
|
56
|
+
if (!d.url) {
|
|
57
|
+
console.error(warning('--inspect skipped: this deploy produced no URL (e.g. --only database)'));
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
const url = typeof opts.inspect === 'string' ? new URL(opts.inspect, d.url).toString() : d.url;
|
|
61
|
+
if (!opts.json)
|
|
62
|
+
console.log('');
|
|
63
|
+
try {
|
|
64
|
+
await inspectPage(url, { json: opts.json });
|
|
65
|
+
}
|
|
66
|
+
catch (err) {
|
|
67
|
+
console.error(warning(`Inspect failed (deploy itself succeeded): ${err?.message ?? err}`));
|
|
68
|
+
}
|
|
69
|
+
};
|
|
47
70
|
if (opts.json) {
|
|
48
71
|
console.log(JSON.stringify(d));
|
|
72
|
+
const failedJson = d.phases?.some(p => p.status === 'failed');
|
|
73
|
+
if (!failedJson)
|
|
74
|
+
await inspectAfter();
|
|
49
75
|
return;
|
|
50
76
|
}
|
|
51
77
|
// Format output
|
|
@@ -54,7 +80,8 @@ export const deployCommand = new Command('deploy')
|
|
|
54
80
|
console.log(muted('─'.repeat(40)));
|
|
55
81
|
if (d.phases && d.phases.length > 0) {
|
|
56
82
|
for (const phase of d.phases) {
|
|
57
|
-
|
|
83
|
+
const ms = phase.elapsedMs != null ? muted(` (${phase.elapsedMs}ms)`) : '';
|
|
84
|
+
console.log(`${statusIcon(phase.status)} ${bold(phase.name)}: ${phase.summary}${ms}`);
|
|
58
85
|
}
|
|
59
86
|
}
|
|
60
87
|
else {
|
|
@@ -80,11 +107,11 @@ export const deployCommand = new Command('deploy')
|
|
|
80
107
|
const failedPhases = d.phases?.filter(p => p.status === 'failed') ?? [];
|
|
81
108
|
if (failedPhases.length > 0) {
|
|
82
109
|
// The database phase can fail on the account-wide database cap, whose
|
|
83
|
-
// server message ("Maximum of N databases reached. Drop one
|
|
84
|
-
// names no command. The droppable databases live in OTHER
|
|
85
|
-
// the default project-scoped `gipity db list` shows nothing
|
|
86
|
-
// caller straight at the account-wide list + drop path so
|
|
87
|
-
// dead-end (or reach for raw DB access) to free a slot.
|
|
110
|
+
// server message ("Maximum of N databases reached (you have N). Drop one
|
|
111
|
+
// first.") names no command. The droppable databases live in OTHER
|
|
112
|
+
// projects, so the default project-scoped `gipity db list` shows nothing
|
|
113
|
+
// — point the caller straight at the account-wide list + drop path so
|
|
114
|
+
// they don't dead-end (or reach for raw DB access) to free a slot.
|
|
88
115
|
if (failedPhases.some(p => /databases? reached|database (cap|limit)/i.test(p.summary))) {
|
|
89
116
|
console.log('');
|
|
90
117
|
console.log(muted('Free a slot under the account database cap:'));
|
|
@@ -101,6 +128,7 @@ export const deployCommand = new Command('deploy')
|
|
|
101
128
|
// has to reconstruct the URL convention or guess a subdomain.
|
|
102
129
|
if (d.url)
|
|
103
130
|
console.log(`${muted('Live:')} ${brand(d.url)}`);
|
|
131
|
+
await inspectAfter();
|
|
104
132
|
}
|
|
105
133
|
}));
|
|
106
134
|
//# sourceMappingURL=deploy.js.map
|
|
@@ -142,8 +142,8 @@ const JS_DECOY_FLAGS = ['--js', '--javascript', '--script', '--code', '--expr',
|
|
|
142
142
|
export const pageEvalCommand = new Command('eval')
|
|
143
143
|
.description('Evaluate JS in a real browser on a page (DOM, computed styles, element rects; inline expr or --file script)')
|
|
144
144
|
.argument('<url>', 'URL to load')
|
|
145
|
-
.argument('[expr]', 'JavaScript to evaluate in page context (inline expression or statement body with return/await; result is JSON-serialized). Omit when using --file.')
|
|
146
|
-
.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.')
|
|
145
|
+
.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.')
|
|
146
|
+
.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>.')
|
|
147
147
|
.option('--fixture <path>', '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>).', (val, prev) => [...prev, val], [])
|
|
148
148
|
.option('--wait <ms>', 'Sleep this many ms after DOMContentLoaded before evaluating (lets late async work settle; max 30000)', '500')
|
|
149
149
|
.option('--wait-for <selector>', 'Wait until this CSS selector appears before evaluating (deterministic; replaces --wait)')
|
|
@@ -167,6 +167,16 @@ export const pageEvalCommand = new Command('eval')
|
|
|
167
167
|
if (exprArg === undefined && !opts.file) {
|
|
168
168
|
pageEvalCommand.error('error: Provide an inline <expr> arg or --file <path>');
|
|
169
169
|
}
|
|
170
|
+
// Catch a swapped <url>/<expr> locally: the server's bare "Invalid URL"
|
|
171
|
+
// names neither the bad argument nor the expected order, so it reads like
|
|
172
|
+
// the page failed to evaluate rather than like a mis-invocation.
|
|
173
|
+
if (!/^https?:\/\//i.test(url)) {
|
|
174
|
+
const flatUrl = url.replace(/\s+/g, ' ').trim();
|
|
175
|
+
const shownUrl = flatUrl.length > 60 ? `${flatUrl.slice(0, 57)}...` : flatUrl;
|
|
176
|
+
pageEvalCommand.error(exprArg !== undefined && /^https?:\/\//i.test(exprArg)
|
|
177
|
+
? `error: arguments are swapped — the URL is the FIRST positional: gipity page eval "${exprArg}" '<expr>'`
|
|
178
|
+
: `error: <url> must be an absolute http(s) URL (got: "${shownUrl}") — usage: gipity page eval <url> [expr]`);
|
|
179
|
+
}
|
|
170
180
|
let expr = exprArg;
|
|
171
181
|
if (opts.file) {
|
|
172
182
|
try {
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { Command, Option } from 'commander';
|
|
2
2
|
import { post } from '../api.js';
|
|
3
3
|
import { formatSize } from '../utils.js';
|
|
4
|
-
import { brand, bold, error as clrError, warning, muted, info } from '../colors.js';
|
|
4
|
+
import { brand, bold, error as clrError, warning, muted, info, success } from '../colors.js';
|
|
5
5
|
import { run } from '../helpers/index.js';
|
|
6
|
+
import { getAuth } from '../auth.js';
|
|
6
7
|
import { capWaitMs } from './page-eval.js';
|
|
7
8
|
import { TOUCH_DEVICES as INSPECT_DEVICES, resolveTouchDevice } from './page-screenshot.js';
|
|
8
9
|
/** A console line is an error-level entry (page error or console.error). */
|
|
@@ -37,6 +38,7 @@ export const pageInspectCommand = new Command('inspect')
|
|
|
37
38
|
.option('--wait-for <selector>', 'Wait until this CSS selector appears before capturing (deterministic; replaces --wait)')
|
|
38
39
|
.option('--wait-timeout <ms>', 'Max ms to wait for --wait-for before giving up', '5000')
|
|
39
40
|
.option('--json', 'Output as JSON')
|
|
41
|
+
.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')
|
|
40
42
|
.option('--no-truncate', 'Show full URLs instead of truncating long ones with middle-ellipsis')
|
|
41
43
|
.option('--all', 'Include render-blocking, large resources, oversized images, overflow culprits, and LCP detail')
|
|
42
44
|
.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)')
|
|
@@ -51,206 +53,219 @@ export const pageInspectCommand = new Command('inspect')
|
|
|
51
53
|
console.error(` gipity page screenshot ${url}${typeof opts.screenshot === 'string' ? ` -o ${opts.screenshot}` : ''}`);
|
|
52
54
|
process.exit(1);
|
|
53
55
|
}
|
|
54
|
-
return run('Page inspect',
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
if (resourceEchoesToDrop > 0) {
|
|
97
|
-
const isHttpStatusResourceError = (l) => /^error:\s*Failed to load resource: the server responded with a status of \d{3}/i.test(l);
|
|
98
|
-
b.console = (b.console || []).filter((l) => {
|
|
99
|
-
if (resourceEchoesToDrop > 0 && isHttpStatusResourceError(l)) {
|
|
100
|
-
resourceEchoesToDrop--;
|
|
101
|
-
return false;
|
|
102
|
-
}
|
|
103
|
-
return true;
|
|
104
|
-
});
|
|
56
|
+
return run('Page inspect', () => inspectPage(url, opts));
|
|
57
|
+
});
|
|
58
|
+
/** Run the full inspect pipeline (probe, transient-error re-probe, noise
|
|
59
|
+
* filtering) against a URL and print the report. Shared by `page inspect`
|
|
60
|
+
* and `deploy --inspect`, so a build agent can deploy + verify in ONE
|
|
61
|
+
* command instead of two round trips through the model. */
|
|
62
|
+
export async function inspectPage(url, opts = {}) {
|
|
63
|
+
const waitMs = capWaitMs(opts.wait ?? '500', url);
|
|
64
|
+
const parsedTimeout = parseInt(opts.waitTimeout ?? '5000', 10);
|
|
65
|
+
const waitForTimeoutMs = Number.isFinite(parsedTimeout) && parsedTimeout >= 0 ? parsedTimeout : 5000;
|
|
66
|
+
const truncate = opts.truncate !== false;
|
|
67
|
+
const showAll = opts.all === true;
|
|
68
|
+
const inspectBody = {
|
|
69
|
+
url, waitMs,
|
|
70
|
+
waitForSelector: opts.waitFor || undefined,
|
|
71
|
+
waitForTimeoutMs: opts.waitFor ? waitForTimeoutMs : undefined,
|
|
72
|
+
fakeMedia: opts.fakeMedia || undefined,
|
|
73
|
+
device: opts.device ? resolveTouchDevice(opts.device) : undefined,
|
|
74
|
+
auth: opts.auth || undefined,
|
|
75
|
+
};
|
|
76
|
+
const res = await post(`/tools/browser/inspect`, inspectBody);
|
|
77
|
+
const b = res.data;
|
|
78
|
+
// ── Move resource-load failures out of the console, where they're noise ──
|
|
79
|
+
// A sub-resource that returns an HTTP 4xx/5xx is reported twice: once in
|
|
80
|
+
// `failedResources` (CDP network layer — carries the full URL, method, and
|
|
81
|
+
// status) and once as a generic, URL-less console echo ("Failed to load
|
|
82
|
+
// resource: the server responded with a status of 404 ()"). The echo names
|
|
83
|
+
// nothing, so it can't be triaged on its own and only duplicates — worse —
|
|
84
|
+
// what `failedResources` already says with a URL. Drop one echo per failed
|
|
85
|
+
// resource we actually have, so every attributed failure (a real app 404 AND
|
|
86
|
+
// the platform's own injected-SDK telemetry-beacon 404, which 404s on
|
|
87
|
+
// essentially every deployed page) is surfaced exactly once, under Failed
|
|
88
|
+
// resources, with its URL — never as a bare, unattributable console line.
|
|
89
|
+
// Count BEFORE stripping the platform log endpoints below, so their echoes go
|
|
90
|
+
// too. Any SURPLUS echo (a failure the network drain missed, leaving
|
|
91
|
+
// failedResources short) is KEPT — a real problem is never hidden just because
|
|
92
|
+
// we couldn't name it.
|
|
93
|
+
const isPlatformLog = (entry) => {
|
|
94
|
+
const urlPart = entry.replace(/\s*\([^)]*\)\s*$/, '');
|
|
95
|
+
try {
|
|
96
|
+
const u = new URL(urlPart);
|
|
97
|
+
return /(^|\.)gipity\.ai$/.test(u.hostname) && /\/log\/(traffic|error)$/.test(u.pathname);
|
|
105
98
|
}
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
// from the failed-resource list entirely (its console echo is already gone,
|
|
109
|
-
// dropped above with every other attributed failure).
|
|
110
|
-
b.failedResources = (b.failedResources || []).filter((r) => !isPlatformLog(r));
|
|
111
|
-
// Pull message-less cross-origin "Script error." lines out first. They carry
|
|
112
|
-
// no source/stack, so they're never actionable as app-code defects, and on a
|
|
113
|
-
// Gipity-deployed page the platform's own injected SDK is itself a
|
|
114
|
-
// cross-origin script — so these are reported separately (not as app console
|
|
115
|
-
// errors, and not folded into the re-probe count) instead of misleading the
|
|
116
|
-
// agent into chasing its own code.
|
|
117
|
-
const crossOriginErrors = (b.console || []).filter(isMessagelessCrossOrigin);
|
|
118
|
-
b.console = (b.console || []).filter((l) => !isMessagelessCrossOrigin(l));
|
|
119
|
-
// Self-verify the remaining console errors before flagging them. A
|
|
120
|
-
// freshly-deployed page's first hit can throw a one-time, non-reproducible
|
|
121
|
-
// error from an asset still propagating — and reporting it as a real defect
|
|
122
|
-
// sends agents chasing a phantom. So when the first probe reports error-level
|
|
123
|
-
// console lines, re-probe once (the sticky session is now warm) and keep only
|
|
124
|
-
// the errors that recur; errors seen on a single probe are surfaced
|
|
125
|
-
// separately as transient noise.
|
|
126
|
-
let transientErrors = [];
|
|
127
|
-
if ((b.console || []).some(isErrorLine)) {
|
|
128
|
-
try {
|
|
129
|
-
const verify = await post(`/tools/browser/inspect`, inspectBody);
|
|
130
|
-
const recurring = new Set((verify.data.console || []).filter(isErrorLine));
|
|
131
|
-
transientErrors = (b.console || []).filter((l) => isErrorLine(l) && !recurring.has(l));
|
|
132
|
-
b.console = (b.console || []).filter((l) => !isErrorLine(l) || recurring.has(l));
|
|
133
|
-
}
|
|
134
|
-
catch {
|
|
135
|
-
// Re-probe failed (timeout / browser error) — report the first probe's
|
|
136
|
-
// console as-is rather than hiding anything.
|
|
137
|
-
}
|
|
99
|
+
catch {
|
|
100
|
+
return false;
|
|
138
101
|
}
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
102
|
+
};
|
|
103
|
+
let resourceEchoesToDrop = (b.failedResources || []).length;
|
|
104
|
+
if (resourceEchoesToDrop > 0) {
|
|
105
|
+
const isHttpStatusResourceError = (l) => /^error:\s*Failed to load resource: the server responded with a status of \d{3}/i.test(l);
|
|
106
|
+
b.console = (b.console || []).filter((l) => {
|
|
107
|
+
if (resourceEchoesToDrop > 0 && isHttpStatusResourceError(l)) {
|
|
108
|
+
resourceEchoesToDrop--;
|
|
109
|
+
return false;
|
|
110
|
+
}
|
|
111
|
+
return true;
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
// The injected analytics SDK POSTs to `/log/{traffic,error}` on the Gipity
|
|
115
|
+
// host; a failure there is platform infrastructure, not the app's, so strip it
|
|
116
|
+
// from the failed-resource list entirely (its console echo is already gone,
|
|
117
|
+
// dropped above with every other attributed failure).
|
|
118
|
+
b.failedResources = (b.failedResources || []).filter((r) => !isPlatformLog(r));
|
|
119
|
+
// Pull message-less cross-origin "Script error." lines out first. They carry
|
|
120
|
+
// no source/stack, so they're never actionable as app-code defects, and on a
|
|
121
|
+
// Gipity-deployed page the platform's own injected SDK is itself a
|
|
122
|
+
// cross-origin script — so these are reported separately (not as app console
|
|
123
|
+
// errors, and not folded into the re-probe count) instead of misleading the
|
|
124
|
+
// agent into chasing its own code.
|
|
125
|
+
const crossOriginErrors = (b.console || []).filter(isMessagelessCrossOrigin);
|
|
126
|
+
b.console = (b.console || []).filter((l) => !isMessagelessCrossOrigin(l));
|
|
127
|
+
// Self-verify the remaining console errors before flagging them. A
|
|
128
|
+
// freshly-deployed page's first hit can throw a one-time, non-reproducible
|
|
129
|
+
// error from an asset still propagating — and reporting it as a real defect
|
|
130
|
+
// sends agents chasing a phantom. So when the first probe reports error-level
|
|
131
|
+
// console lines, re-probe once (the sticky session is now warm) and keep only
|
|
132
|
+
// the errors that recur; errors seen on a single probe are surfaced
|
|
133
|
+
// separately as transient noise.
|
|
134
|
+
let transientErrors = [];
|
|
135
|
+
if (opts.reprobe !== false && (b.console || []).some(isErrorLine)) {
|
|
136
|
+
try {
|
|
137
|
+
const verify = await post(`/tools/browser/inspect`, inspectBody);
|
|
138
|
+
const recurring = new Set((verify.data.console || []).filter(isErrorLine));
|
|
139
|
+
transientErrors = (b.console || []).filter((l) => isErrorLine(l) && !recurring.has(l));
|
|
140
|
+
b.console = (b.console || []).filter((l) => !isErrorLine(l) || recurring.has(l));
|
|
146
141
|
}
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
if (b.navigationIncomplete) {
|
|
151
|
-
console.log(`${warning('⚠ Navigation incomplete:')} ${b.note || 'page did not reach full load'}`);
|
|
142
|
+
catch {
|
|
143
|
+
// Re-probe failed (timeout / browser error) — report the first probe's
|
|
144
|
+
// console as-is rather than hiding anything.
|
|
152
145
|
}
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
console.log(
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
146
|
+
}
|
|
147
|
+
if (opts.json) {
|
|
148
|
+
console.log(JSON.stringify({
|
|
149
|
+
...b,
|
|
150
|
+
...(transientErrors.length ? { transientConsole: transientErrors } : {}),
|
|
151
|
+
...(crossOriginErrors.length ? { crossOriginConsole: crossOriginErrors } : {}),
|
|
152
|
+
}));
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
const timing = b.timing || { ttfb: 0, domReady: 0, load: 0 };
|
|
156
|
+
// ── Page Info ──
|
|
157
|
+
console.log(`${brand('Inspecting')} ${bold(b.url || url)}`);
|
|
158
|
+
if (b.navigationIncomplete) {
|
|
159
|
+
console.log(`${warning('⚠ Navigation incomplete:')} ${b.note || 'page did not reach full load'}`);
|
|
160
|
+
}
|
|
161
|
+
// Auth state: without this line an agent can't distinguish "signed-in render"
|
|
162
|
+
// from "--auth silently no-op'd and I'm looking at the anonymous page".
|
|
163
|
+
if (b.auth?.requested) {
|
|
164
|
+
const who = getAuth()?.email;
|
|
165
|
+
console.log(b.auth.established
|
|
166
|
+
? `${muted('Auth:')} ${success('session established')}${who ? muted(` as ${who}`) : ''} ${muted('(what the page renders with it is app-defined)')}`
|
|
167
|
+
: `${warning('Auth: session NOT established')}${b.auth.detail ? ` — ${b.auth.detail}` : ''} ${muted('(this is the anonymous view)')}`);
|
|
168
|
+
}
|
|
169
|
+
console.log(`${muted('Title:')} ${b.title || '(none)'}`);
|
|
170
|
+
console.log(`${muted('Elements:')} ${b.elementCount || 0}`);
|
|
171
|
+
console.log(`${muted('Page weight:')} ${info(formatSize(b.totalBytes || 0))}`);
|
|
172
|
+
// ── Timing ──
|
|
173
|
+
console.log(`\n${bold('Timing:')}`);
|
|
174
|
+
console.log(`${muted('TTFB:')} ${timing.ttfb}ms`);
|
|
175
|
+
console.log(`${muted('DOM ready:')} ${timing.domReady}ms`);
|
|
176
|
+
console.log(`${muted('Load:')} ${timing.load}ms`);
|
|
177
|
+
if (showAll && b.lcp) {
|
|
178
|
+
console.log(`LCP: ${b.lcp.time}ms (${b.lcp.element}${b.lcp.url ? ' ' + shortUrl(b.lcp.url, truncate) : ''})`);
|
|
179
|
+
}
|
|
180
|
+
// ── Console ──
|
|
181
|
+
if (b.console?.length > 0) {
|
|
182
|
+
console.log(`\n${bold('Console')} ${muted(`(${b.console.length})`)}:`);
|
|
183
|
+
for (const line of b.console) {
|
|
184
|
+
console.log(`${warning(line)}`);
|
|
163
185
|
}
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
186
|
+
}
|
|
187
|
+
else {
|
|
188
|
+
console.log(`\n${bold('Console:')} ${muted('(clean)')}`);
|
|
189
|
+
}
|
|
190
|
+
// ── Transient console errors (seen on first probe, gone on re-probe) ──
|
|
191
|
+
if (transientErrors.length > 0) {
|
|
192
|
+
console.log(`\n${bold('Transient console errors')} ${muted(`(${transientErrors.length}, not reproduced on re-probe)`)}:`);
|
|
193
|
+
for (const line of transientErrors) {
|
|
194
|
+
console.log(muted(line));
|
|
170
195
|
}
|
|
171
|
-
|
|
172
|
-
|
|
196
|
+
console.log(muted('One-time cold-load artifact (first hit of freshly-deployed assets) — not reproducible, not in your app code. Ignore unless it recurs.'));
|
|
197
|
+
}
|
|
198
|
+
// ── Cross-origin console errors (message-less; source hidden by the browser) ──
|
|
199
|
+
if (crossOriginErrors.length > 0) {
|
|
200
|
+
console.log(`\n${bold('Cross-origin console errors')} ${muted(`(${crossOriginErrors.length}, source hidden by the browser)`)}:`);
|
|
201
|
+
console.log(muted("Message-less — the throwing <script> lacks CORS, so the browser hides its source and there's no own-code stack to chase. Gipity's injected SDK is itself cross-origin, so if your app loads no third-party CDN scripts these are platform noise — ignore them. If your app DOES load a third-party <script>, add crossorigin=\"anonymous\" to that tag to surface the real error."));
|
|
202
|
+
}
|
|
203
|
+
// ── Failed Resources ──
|
|
204
|
+
// Browsers auto-request /favicon.ico at the site root for every page, so a
|
|
205
|
+
// 404 there isn't a resource the page actually links — it's noise on any
|
|
206
|
+
// app served under a subpath. Split that implicit request out of the failure
|
|
207
|
+
// list into a harmless note rather than flagging it as an error.
|
|
208
|
+
const isImplicitFavicon = (entry) => {
|
|
209
|
+
const urlPart = entry.replace(/\s*\([^)]*\)\s*$/, '');
|
|
210
|
+
try {
|
|
211
|
+
return new URL(urlPart).pathname === '/favicon.ico';
|
|
173
212
|
}
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
console.log(`\n${bold('Transient console errors')} ${muted(`(${transientErrors.length}, not reproduced on re-probe)`)}:`);
|
|
177
|
-
for (const line of transientErrors) {
|
|
178
|
-
console.log(muted(line));
|
|
179
|
-
}
|
|
180
|
-
console.log(muted('One-time cold-load artifact (first hit of freshly-deployed assets) — not reproducible, not in your app code. Ignore unless it recurs.'));
|
|
213
|
+
catch {
|
|
214
|
+
return false;
|
|
181
215
|
}
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
216
|
+
};
|
|
217
|
+
const failed = (b.failedResources || []).filter((r) => !isImplicitFavicon(r));
|
|
218
|
+
const rootFaviconMissing = (b.failedResources || []).some(isImplicitFavicon);
|
|
219
|
+
if (failed.length > 0) {
|
|
220
|
+
console.log(`\n${clrError(`Failed resources (${failed.length}):`)}`);
|
|
221
|
+
for (const r of failed) {
|
|
222
|
+
console.log(`${clrError(r)}`);
|
|
186
223
|
}
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
224
|
+
}
|
|
225
|
+
if (rootFaviconMissing) {
|
|
226
|
+
console.log(`\n${muted('No root /favicon.ico (browsers request this automatically; harmless for app pages served under a subpath)')}`);
|
|
227
|
+
}
|
|
228
|
+
// ── Layout (horizontal overflow) ──
|
|
229
|
+
if (b.overflow) {
|
|
230
|
+
if (b.overflow.overflowX) {
|
|
231
|
+
console.log(`\n${clrError(`Horizontal overflow: +${b.overflow.amount}px`)} ${muted(`(content ${b.overflow.scrollWidth}px vs viewport ${b.overflow.clientWidth}px)`)}`);
|
|
232
|
+
if (showAll && b.overflow.culprits.length > 0) {
|
|
233
|
+
console.log(`${muted('Overflowing elements:')}`);
|
|
234
|
+
for (const c of b.overflow.culprits) {
|
|
235
|
+
const sel = c.cls ? `${c.tag}.${c.cls.split(/\s+/)[0]}` : c.tag;
|
|
236
|
+
console.log(`${sel} ${muted(`(right ${c.right}px, width ${c.width}px)`)}`);
|
|
237
|
+
}
|
|
199
238
|
}
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
const rootFaviconMissing = (b.failedResources || []).some(isImplicitFavicon);
|
|
203
|
-
if (failed.length > 0) {
|
|
204
|
-
console.log(`\n${clrError(`Failed resources (${failed.length}):`)}`);
|
|
205
|
-
for (const r of failed) {
|
|
206
|
-
console.log(`${clrError(r)}`);
|
|
239
|
+
else if (b.overflow.culprits.length > 0) {
|
|
240
|
+
console.log(`${muted(`${b.overflow.culprits.length} overflowing element(s) - use --all to list`)}`);
|
|
207
241
|
}
|
|
208
242
|
}
|
|
209
|
-
|
|
210
|
-
console.log(`\n${
|
|
243
|
+
else {
|
|
244
|
+
console.log(`\n${bold('Layout:')} ${muted('no horizontal overflow')}`);
|
|
211
245
|
}
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
const sel = c.cls ? `${c.tag}.${c.cls.split(/\s+/)[0]}` : c.tag;
|
|
220
|
-
console.log(`${sel} ${muted(`(right ${c.right}px, width ${c.width}px)`)}`);
|
|
221
|
-
}
|
|
222
|
-
}
|
|
223
|
-
else if (b.overflow.culprits.length > 0) {
|
|
224
|
-
console.log(`${muted(`${b.overflow.culprits.length} overflowing element(s) - use --all to list`)}`);
|
|
225
|
-
}
|
|
226
|
-
}
|
|
227
|
-
else {
|
|
228
|
-
console.log(`\n${bold('Layout:')} ${muted('no horizontal overflow')}`);
|
|
246
|
+
}
|
|
247
|
+
if (showAll) {
|
|
248
|
+
// ── Render Blocking ──
|
|
249
|
+
if (b.renderBlocking?.length > 0) {
|
|
250
|
+
console.log(`\n${warning(`Render-blocking (${b.renderBlocking.length}):`)}`);
|
|
251
|
+
for (const r of b.renderBlocking) {
|
|
252
|
+
console.log(`${shortUrl(r, truncate)}`);
|
|
229
253
|
}
|
|
230
254
|
}
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
console.log(`${shortUrl(r, truncate)}`);
|
|
237
|
-
}
|
|
238
|
-
}
|
|
239
|
-
// ── Large Resources ──
|
|
240
|
-
if (b.largeResources?.length > 0) {
|
|
241
|
-
console.log(`\n${warning(`Large resources >100KB (${b.largeResources.length}):`)}`);
|
|
242
|
-
for (const r of b.largeResources) {
|
|
243
|
-
console.log(`${info(formatSize(r.size).padEnd(10))} ${muted(r.type.padEnd(8))} ${shortUrl(r.url, truncate)}`);
|
|
244
|
-
}
|
|
255
|
+
// ── Large Resources ──
|
|
256
|
+
if (b.largeResources?.length > 0) {
|
|
257
|
+
console.log(`\n${warning(`Large resources >100KB (${b.largeResources.length}):`)}`);
|
|
258
|
+
for (const r of b.largeResources) {
|
|
259
|
+
console.log(`${info(formatSize(r.size).padEnd(10))} ${muted(r.type.padEnd(8))} ${shortUrl(r.url, truncate)}`);
|
|
245
260
|
}
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
}
|
|
261
|
+
}
|
|
262
|
+
// ── Oversized Images ──
|
|
263
|
+
if (b.oversizedImages?.length > 0) {
|
|
264
|
+
console.log(`\n${warning(`Oversized images (${b.oversizedImages.length}):`)}`);
|
|
265
|
+
for (const img of b.oversizedImages) {
|
|
266
|
+
console.log(`${img.natural} served, ${img.displayed} displayed - ${shortUrl(img.src, truncate)}`);
|
|
252
267
|
}
|
|
253
268
|
}
|
|
254
|
-
}
|
|
255
|
-
}
|
|
269
|
+
}
|
|
270
|
+
}
|
|
256
271
|
//# sourceMappingURL=page-inspect.js.map
|