gipity 1.0.422 → 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.
@@ -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 = show the full help (usage + options + catalog), same as --help.
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
- command.outputHelp();
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();
@@ -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
- const __clPkg = JSON.parse(readFileSync(resolve(__clDir, '../../package.json'), 'utf-8'));
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
@@ -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
- console.log(`${statusIcon(phase.status)} ${bold(phase.name)}: ${phase.summary}`);
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 {
@@ -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', async () => {
55
- const waitMs = capWaitMs(opts.wait, url);
56
- const parsedTimeout = parseInt(opts.waitTimeout, 10);
57
- const waitForTimeoutMs = Number.isFinite(parsedTimeout) && parsedTimeout >= 0 ? parsedTimeout : 5000;
58
- const truncate = opts.truncate !== false;
59
- const showAll = opts.all === true;
60
- const inspectBody = {
61
- url, waitMs,
62
- waitForSelector: opts.waitFor || undefined,
63
- waitForTimeoutMs: opts.waitFor ? waitForTimeoutMs : undefined,
64
- fakeMedia: opts.fakeMedia || undefined,
65
- device: opts.device ? resolveTouchDevice(opts.device) : undefined,
66
- auth: opts.auth || undefined,
67
- };
68
- const res = await post(`/tools/browser/inspect`, inspectBody);
69
- const b = res.data;
70
- // ── Move resource-load failures out of the console, where they're noise ──
71
- // A sub-resource that returns an HTTP 4xx/5xx is reported twice: once in
72
- // `failedResources` (CDP network layer — carries the full URL, method, and
73
- // status) and once as a generic, URL-less console echo ("Failed to load
74
- // resource: the server responded with a status of 404 ()"). The echo names
75
- // nothing, so it can't be triaged on its own and only duplicates — worse —
76
- // what `failedResources` already says with a URL. Drop one echo per failed
77
- // resource we actually have, so every attributed failure (a real app 404 AND
78
- // the platform's own injected-SDK telemetry-beacon 404, which 404s on
79
- // essentially every deployed page) is surfaced exactly once, under Failed
80
- // resources, with its URL never as a bare, unattributable console line.
81
- // Count BEFORE stripping the platform log endpoints below, so their echoes go
82
- // too. Any SURPLUS echo (a failure the network drain missed, leaving
83
- // failedResources short) is KEPT a real problem is never hidden just because
84
- // we couldn't name it.
85
- const isPlatformLog = (entry) => {
86
- const urlPart = entry.replace(/\s*\([^)]*\)\s*$/, '');
87
- try {
88
- const u = new URL(urlPart);
89
- return /(^|\.)gipity\.ai$/.test(u.hostname) && /\/log\/(traffic|error)$/.test(u.pathname);
90
- }
91
- catch {
92
- return false;
93
- }
94
- };
95
- let resourceEchoesToDrop = (b.failedResources || []).length;
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
- // The injected analytics SDK POSTs to `/log/{traffic,error}` on the Gipity
107
- // host; a failure there is platform infrastructure, not the app's, so strip it
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
- if (opts.json) {
140
- console.log(JSON.stringify({
141
- ...b,
142
- ...(transientErrors.length ? { transientConsole: transientErrors } : {}),
143
- ...(crossOriginErrors.length ? { crossOriginConsole: crossOriginErrors } : {}),
144
- }));
145
- return;
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
- const timing = b.timing || { ttfb: 0, domReady: 0, load: 0 };
148
- // ── Page Info ──
149
- console.log(`${brand('Inspecting')} ${bold(b.url || url)}`);
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
- console.log(`${muted('Title:')} ${b.title || '(none)'}`);
154
- console.log(`${muted('Elements:')} ${b.elementCount || 0}`);
155
- console.log(`${muted('Page weight:')} ${info(formatSize(b.totalBytes || 0))}`);
156
- // ── Timing ──
157
- console.log(`\n${bold('Timing:')}`);
158
- console.log(`${muted('TTFB:')} ${timing.ttfb}ms`);
159
- console.log(`${muted('DOM ready:')} ${timing.domReady}ms`);
160
- console.log(`${muted('Load:')} ${timing.load}ms`);
161
- if (showAll && b.lcp) {
162
- console.log(`LCP: ${b.lcp.time}ms (${b.lcp.element}${b.lcp.url ? ' ' + shortUrl(b.lcp.url, truncate) : ''})`);
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
- // ── Console ──
165
- if (b.console?.length > 0) {
166
- console.log(`\n${bold('Console')} ${muted(`(${b.console.length})`)}:`);
167
- for (const line of b.console) {
168
- console.log(`${warning(line)}`);
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
- else {
172
- console.log(`\n${bold('Console:')} ${muted('(clean)')}`);
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
- // ── Transient console errors (seen on first probe, gone on re-probe) ──
175
- if (transientErrors.length > 0) {
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
- // ── Cross-origin console errors (message-less; source hidden by the browser) ──
183
- if (crossOriginErrors.length > 0) {
184
- console.log(`\n${bold('Cross-origin console errors')} ${muted(`(${crossOriginErrors.length}, source hidden by the browser)`)}:`);
185
- 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."));
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
- // ── Failed Resources ──
188
- // Browsers auto-request /favicon.ico at the site root for every page, so a
189
- // 404 there isn't a resource the page actually links it's noise on any
190
- // app served under a subpath. Split that implicit request out of the failure
191
- // list into a harmless note rather than flagging it as an error.
192
- const isImplicitFavicon = (entry) => {
193
- const urlPart = entry.replace(/\s*\([^)]*\)\s*$/, '');
194
- try {
195
- return new URL(urlPart).pathname === '/favicon.ico';
196
- }
197
- catch {
198
- return false;
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
- const failed = (b.failedResources || []).filter((r) => !isImplicitFavicon(r));
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
- if (rootFaviconMissing) {
210
- console.log(`\n${muted('No root /favicon.ico (browsers request this automatically; harmless for app pages served under a subpath)')}`);
243
+ else {
244
+ console.log(`\n${bold('Layout:')} ${muted('no horizontal overflow')}`);
211
245
  }
212
- // ── Layout (horizontal overflow) ──
213
- if (b.overflow) {
214
- if (b.overflow.overflowX) {
215
- console.log(`\n${clrError(`Horizontal overflow: +${b.overflow.amount}px`)} ${muted(`(content ${b.overflow.scrollWidth}px vs viewport ${b.overflow.clientWidth}px)`)}`);
216
- if (showAll && b.overflow.culprits.length > 0) {
217
- console.log(`${muted('Overflowing elements:')}`);
218
- for (const c of b.overflow.culprits) {
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
- if (showAll) {
232
- // ── Render Blocking ──
233
- if (b.renderBlocking?.length > 0) {
234
- console.log(`\n${warning(`Render-blocking (${b.renderBlocking.length}):`)}`);
235
- for (const r of b.renderBlocking) {
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
- // ── Oversized Images ──
247
- if (b.oversizedImages?.length > 0) {
248
- console.log(`\n${warning(`Oversized images (${b.oversizedImages.length}):`)}`);
249
- for (const img of b.oversizedImages) {
250
- console.log(`${img.natural} served, ${img.displayed} displayed - ${shortUrl(img.src, truncate)}`);
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