gipity 1.0.365 → 1.0.379

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.
Files changed (56) hide show
  1. package/dist/banner.js +3 -1
  2. package/dist/commands/add.js +2 -2
  3. package/dist/commands/agent.js +3 -5
  4. package/dist/commands/approval.js +3 -3
  5. package/dist/commands/audit.js +2 -2
  6. package/dist/commands/chat.js +4 -4
  7. package/dist/commands/claude.js +26 -78
  8. package/dist/commands/credits.js +1 -1
  9. package/dist/commands/db.js +7 -6
  10. package/dist/commands/deploy.js +10 -8
  11. package/dist/commands/doctor.js +11 -13
  12. package/dist/commands/domain.js +18 -15
  13. package/dist/commands/email.js +0 -4
  14. package/dist/commands/fn.js +2 -2
  15. package/dist/commands/generate.js +57 -5
  16. package/dist/commands/job.js +6 -6
  17. package/dist/commands/location.js +7 -7
  18. package/dist/commands/login.js +2 -16
  19. package/dist/commands/logout.js +2 -3
  20. package/dist/commands/logs.js +1 -1
  21. package/dist/commands/page-eval.js +22 -7
  22. package/dist/commands/page-fetch.js +136 -0
  23. package/dist/commands/page-inspect.js +29 -27
  24. package/dist/commands/page-screenshot.js +22 -19
  25. package/dist/commands/page-test.js +196 -26
  26. package/dist/commands/page.js +6 -3
  27. package/dist/commands/plan.js +4 -4
  28. package/dist/commands/project.js +2 -1
  29. package/dist/commands/push.js +2 -6
  30. package/dist/commands/realtime.js +7 -9
  31. package/dist/commands/relay-install.js +18 -21
  32. package/dist/commands/relay.js +29 -31
  33. package/dist/commands/sandbox.js +48 -8
  34. package/dist/commands/service.js +1 -3
  35. package/dist/commands/status.js +5 -3
  36. package/dist/commands/sync.js +4 -1
  37. package/dist/commands/test.js +7 -13
  38. package/dist/commands/text.js +10 -10
  39. package/dist/commands/uninstall.js +20 -42
  40. package/dist/commands/update.js +0 -2
  41. package/dist/commands/upload.js +4 -4
  42. package/dist/commands/workflow.js +1 -2
  43. package/dist/config.js +11 -0
  44. package/dist/flag-aliases.js +11 -3
  45. package/dist/helpers/output.js +45 -7
  46. package/dist/index.js +4 -0
  47. package/dist/knowledge.js +23 -8
  48. package/dist/progress.js +60 -0
  49. package/dist/project-setup.js +5 -1
  50. package/dist/prompts.js +0 -8
  51. package/dist/provider-docs.js +7 -7
  52. package/dist/setup.js +34 -10
  53. package/dist/sync.js +16 -6
  54. package/dist/updater/shim.js +18 -4
  55. package/dist/upload.js +6 -0
  56. package/package.json +5 -4
@@ -28,13 +28,13 @@ Gemini-specific options:
28
28
  Examples:
29
29
  gipity generate image "a cat wearing a top hat"
30
30
  gipity generate image "landscape sunset" --provider gemini --aspect-ratio 16:9 --image-size 2K
31
- gipity generate image "product photo" --provider openai --model gpt-image-1 --size 1536x1024 --quality high
31
+ gipity generate image "product photo" --provider openai --model gpt-image-2 --size 1536x1024 --quality high
32
32
  gipity generate image "abstract art" --provider bfl --model flux-2-pro -o art.png`)
33
33
  .argument('<prompt>', 'Text description of the image to generate')
34
34
  .option('--provider <provider>', 'Image provider: openai, bfl, or gemini (default: bfl)')
35
35
  .option('--model <model>', 'Model ID (see provider list above)')
36
36
  .option('--size <size>', 'Dimensions as WxH, e.g. "1024x1024" (OpenAI/BFL)')
37
- .option('--quality <quality>', 'Quality: low|medium|high|auto (gpt-image-1), standard|hd (dall-e-3)')
37
+ .option('--quality <quality>', 'Quality: low|medium|high|auto (gpt-image-2)')
38
38
  .option('--aspect-ratio <ratio>', 'Aspect ratio (Gemini only): 1:1, 16:9, 9:16, 4:3, 3:4, 3:2, 2:3, 4:5, 5:4, 21:9')
39
39
  .option('--image-size <size>', 'Output resolution (Gemini only): 512, 1K, 2K, 4K')
40
40
  .option('-o, --output <file>', 'Output path (default ./generated.png). For an image your app ships, write it into the source tree so it deploys, e.g. -o src/assets/images/hero.png; the cwd default is fine for one-off generation.')
@@ -96,7 +96,8 @@ Examples:
96
96
  .action(async (prompt, opts) => {
97
97
  try {
98
98
  const { config } = await resolveProjectContext();
99
- console.log(info('Generating video (this may take 30-120 seconds)...'));
99
+ if (!opts.json)
100
+ console.log(info('Generating video (this may take 30-120 seconds)...')); // keep --json stdout pure JSON
100
101
  const result = await post(`/projects/${config.projectGuid}/generate/video`, {
101
102
  prompt,
102
103
  model: opts.model,
@@ -178,10 +179,61 @@ Examples:
178
179
  process.exit(1);
179
180
  }
180
181
  });
182
+ // ── MUSIC ──────────────────────────────────────────────────────────────
183
+ const musicCommand = new Command('music')
184
+ .description(`Generate music from a text prompt using AI.
185
+
186
+ The model is chosen from the platform's music catalog. Omit --model to use the
187
+ default; pass --model <id> only if you want a specific one (see the catalog with
188
+ \`gipity service call music/models --get\`).
189
+
190
+ Tips:
191
+ - Describe genre, mood, instruments, and tempo (e.g. "upbeat lo-fi hip hop with mellow piano")
192
+ - Music is instrumental by default; pass --vocals to allow singing
193
+ - Longer clips cost more; the max length depends on the model
194
+
195
+ Examples:
196
+ gipity generate music "chill lo-fi beat for studying"
197
+ gipity generate music "epic orchestral battle theme" --duration 60 -o src/assets/audio/theme.mp3
198
+ gipity generate music "indie pop chorus" --vocals --duration 20`)
199
+ .argument('<prompt>', 'Text description of the music to generate')
200
+ .option('--duration <seconds>', 'Clip length in seconds (default 30; max depends on the model)', (v) => parseInt(v, 10))
201
+ .option('--model <model>', 'Music model id (default: platform default)')
202
+ .option('--vocals', 'Allow vocals (default: instrumental only)')
203
+ .option('-o, --output <file>', 'Output path (default ./music.mp3). For audio your app ships, write it into the source tree so it deploys, e.g. -o src/assets/audio/theme.mp3; the cwd default is fine for one-off generation.')
204
+ .option('--json', 'Output as JSON')
205
+ .action(async (prompt, opts) => {
206
+ try {
207
+ const { config } = await resolveProjectContext();
208
+ if (!opts.json)
209
+ console.log(info('Generating music...')); // keep --json stdout pure JSON
210
+ const result = await post(`/projects/${config.projectGuid}/generate/music`, {
211
+ prompt,
212
+ duration_seconds: opts.duration,
213
+ model: opts.model,
214
+ instrumental: !opts.vocals,
215
+ });
216
+ const filename = opts.output || 'music.mp3';
217
+ const savedPath = await downloadFile(result.url, filename);
218
+ if (opts.json) {
219
+ console.log(JSON.stringify({ ...result, saved: savedPath }));
220
+ }
221
+ else {
222
+ const sizeKb = Math.round(result.size_bytes / 1024);
223
+ console.log(`${muted(`Generated with ${result.model} (${sizeKb}KB)`)}`);
224
+ console.log(success(`Saved to ${savedPath}`));
225
+ }
226
+ }
227
+ catch (err) {
228
+ console.error(clrError(`Music generation failed: ${err.message}`));
229
+ process.exit(1);
230
+ }
231
+ });
181
232
  // ── PARENT COMMAND ─────────────────────────────────────────────────────
182
233
  export const generateCommand = new Command('generate')
183
- .description('Generate images, video, or speech')
234
+ .description('Generate images, video, speech, or music')
184
235
  .addCommand(imageCommand)
185
236
  .addCommand(videoCommand)
186
- .addCommand(speechCommand);
237
+ .addCommand(speechCommand)
238
+ .addCommand(musicCommand);
187
239
  //# sourceMappingURL=generate.js.map
@@ -17,7 +17,7 @@ jobCommand
17
17
  if (j.on_complete)
18
18
  tags.push(muted(`→ ${j.on_complete}`));
19
19
  const line = `${bold(j.name)} ${tags.join(' ')}`;
20
- return j.description ? `${line}\n ${muted(j.description)}` : line;
20
+ return j.description ? `${line}\n${muted(j.description)}` : line;
21
21
  });
22
22
  }));
23
23
  jobCommand
@@ -57,13 +57,13 @@ jobCommand
57
57
  const statusColor = r.status === 'success' ? success : r.status === 'failed' ? clrError : muted;
58
58
  console.log(`${statusColor(r.status)} ${muted(r.guid)}`);
59
59
  if (r.progress_pct != null)
60
- console.log(` progress: ${Math.round(r.progress_pct * 100)}%${r.progress_message ? ` (${r.progress_message})` : ''}`);
60
+ console.log(`progress: ${Math.round(r.progress_pct * 100)}%${r.progress_message ? ` (${r.progress_message})` : ''}`);
61
61
  if (r.duration_ms != null)
62
- console.log(` duration: ${r.duration_ms}ms`);
62
+ console.log(`duration: ${r.duration_ms}ms`);
63
63
  if (r.error)
64
- console.log(` ${clrError('error:')} ${r.error}`);
64
+ console.log(`${clrError('error:')} ${r.error}`);
65
65
  if (r.output)
66
- console.log(` output: ${JSON.stringify(r.output)}`);
66
+ console.log(`output: ${JSON.stringify(r.output)}`);
67
67
  }));
68
68
  jobCommand
69
69
  .command('runs <name>')
@@ -78,7 +78,7 @@ jobCommand
78
78
  const dur = r.duration_ms != null ? `${r.duration_ms}ms` : '?';
79
79
  const ts = new Date(r.created_at).toLocaleString();
80
80
  const line = `${statusColor(r.status)} ${dur} ${muted(r.trigger_type)} ${muted(r.guid)} ${muted(ts)}`;
81
- return r.error ? `${line}\n ${clrError(`error: ${r.error}`)}` : line;
81
+ return r.error ? `${line}\n${clrError(`error: ${r.error}`)}` : line;
82
82
  });
83
83
  }));
84
84
  jobCommand
@@ -12,16 +12,16 @@ function formatLocation(r) {
12
12
  const lines = [];
13
13
  const place = [r.city, r.region, r.country].filter(Boolean).join(', ');
14
14
  if (place)
15
- lines.push(` ${place}`);
15
+ lines.push(place);
16
16
  if (r.lat != null && r.lon != null)
17
- lines.push(` Coordinates: ${r.lat}, ${r.lon}`);
17
+ lines.push(`Coordinates: ${r.lat}, ${r.lon}`);
18
18
  if (r.ip)
19
- lines.push(` IP: ${r.ip}`);
19
+ lines.push(`IP: ${r.ip}`);
20
20
  if (r.timezone)
21
- lines.push(` Timezone: ${r.timezone}`);
21
+ lines.push(`Timezone: ${r.timezone}`);
22
22
  if (r.accuracy != null)
23
- lines.push(` Accuracy: ${Math.round(r.accuracy)}m`);
24
- lines.push(` Source: ${r.source}`);
23
+ lines.push(`Accuracy: ${Math.round(r.accuracy)}m`);
24
+ lines.push(`Source: ${r.source}`);
25
25
  return lines.join('\n');
26
26
  }
27
27
  /**
@@ -71,7 +71,7 @@ export const locationCommand = new Command('location')
71
71
  return;
72
72
  }
73
73
  if (!res.data) {
74
- console.log(' No location data.');
74
+ console.log('No location data.');
75
75
  return;
76
76
  }
77
77
  console.log(formatLocation(res.data));
@@ -2,7 +2,7 @@ import { Command } from 'commander';
2
2
  import { saveAuth, getAuth } from '../auth.js';
3
3
  import { publicPost } from '../api.js';
4
4
  import { prompt, decodeJwtExp } from '../utils.js';
5
- import { success, error as clrError } from '../colors.js';
5
+ import { success, error as clrError, muted } from '../colors.js';
6
6
  export const loginCommand = new Command('login')
7
7
  .description('Log in or sign up')
8
8
  .option('--email <email>', 'Email address')
@@ -19,37 +19,27 @@ export const loginCommand = new Command('login')
19
19
  // Email only → send code and exit (non-interactive step 1)
20
20
  if (email && !code) {
21
21
  await publicPost('/auth/login', { email });
22
- console.log('');
23
22
  console.log('Check your email for a 6-digit code.');
24
- console.log(`Then run: gipity login --email ${email} --code <code>`);
25
- console.log('');
23
+ console.log(muted(`Then run: gipity login --email ${email} --code <code>`));
26
24
  return;
27
25
  }
28
26
  // Fully interactive flow
29
- console.log('');
30
27
  console.log('Enter your email to log in or create an account.');
31
- console.log('');
32
28
  const existing = getAuth();
33
29
  email = await prompt(existing ? `Email [${existing.email}]: ` : 'Email: ');
34
30
  if (!email && existing)
35
31
  email = existing.email;
36
32
  if (!email) {
37
- console.log('');
38
33
  console.error(clrError('Email required.'));
39
- console.log('');
40
34
  process.exit(1);
41
35
  }
42
36
  await publicPost('/auth/login', { email });
43
- console.log('');
44
37
  console.log('Check your email for a 6-digit code.');
45
- console.log('');
46
38
  code = await prompt('Code: ');
47
39
  await verify(email, code);
48
40
  }
49
41
  catch (err) {
50
- console.log('');
51
42
  console.error(clrError(`Login failed: ${err.message}`));
52
- console.log('');
53
43
  process.exit(1);
54
44
  }
55
45
  });
@@ -57,9 +47,7 @@ async function verify(email, code) {
57
47
  const res = await publicPost('/auth/verify', { email, code });
58
48
  const exp = decodeJwtExp(res.accessToken);
59
49
  if (!exp) {
60
- console.log('');
61
50
  console.error(clrError('Invalid token received.'));
62
- console.log('');
63
51
  process.exit(1);
64
52
  }
65
53
  const expiresAt = new Date(exp * 1000).toISOString();
@@ -69,8 +57,6 @@ async function verify(email, code) {
69
57
  email,
70
58
  expiresAt,
71
59
  });
72
- console.log('');
73
60
  console.log(success(`Logged in (${email}).`));
74
- console.log('');
75
61
  }
76
62
  //# sourceMappingURL=login.js.map
@@ -1,17 +1,16 @@
1
1
  import { Command } from 'commander';
2
2
  import { getAuth, clearAuth } from '../auth.js';
3
+ import { success } from '../colors.js';
3
4
  export const logoutCommand = new Command('logout')
4
5
  .description('Log out')
5
6
  .action(() => {
6
7
  const auth = getAuth();
7
- console.log('');
8
8
  if (!auth) {
9
9
  console.log('Not logged in.');
10
10
  }
11
11
  else {
12
12
  clearAuth();
13
- console.log(`Logged out (${auth.email}).`);
13
+ console.log(success(`Logged out (${auth.email}).`));
14
14
  }
15
- console.log('');
16
15
  });
17
16
  //# sourceMappingURL=logout.js.map
@@ -30,7 +30,7 @@ logsCommand
30
30
  const status = statusColor(log.status.padEnd(8));
31
31
  const trigger = muted(log.trigger_type.padEnd(8));
32
32
  const err = log.error_message ? ` ${clrError(`"${log.error_message}"`)}` : '';
33
- console.log(` ${muted(time)} ${status} ${dur} ${trigger}${err}`);
33
+ console.log(`${muted(time)} ${status} ${dur} ${trigger}${err}`);
34
34
  }
35
35
  }));
36
36
  logsCommand
@@ -1,15 +1,17 @@
1
1
  import { Command } from 'commander';
2
2
  import { post, get, ApiError } from '../api.js';
3
- import { brand, bold, muted } from '../colors.js';
3
+ import { brand, bold, muted, warning } from '../colors.js';
4
4
  import { run } from '../helpers/index.js';
5
5
  const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
6
6
  /** Poll the async eval job until it finishes. Eval runs server-side as a
7
7
  * short-lived job (so a long --wait can't trip the gateway idle timeout);
8
- * we submit, then poll the result out of the job store. */
9
- async function pollEvalResult(evalJobId, waitMs) {
8
+ * we submit, then poll the result out of the job store. `expectedWorkMs` is
9
+ * the time the server-side work is expected to take (settle + any in-page
10
+ * awaits); the client budget is that plus 60s of headroom. */
11
+ export async function pollEvalResult(evalJobId, expectedWorkMs) {
10
12
  // Generous client budget: the server work is bounded by --wait plus browser
11
13
  // open/settle overhead; give it that plus headroom before giving up.
12
- const deadline = Date.now() + waitMs + 60_000;
14
+ const deadline = Date.now() + expectedWorkMs + 60_000;
13
15
  let missCount = 0;
14
16
  while (Date.now() < deadline) {
15
17
  let rec;
@@ -60,11 +62,24 @@ export const pageEvalCommand = new Command('eval')
60
62
  console.log(JSON.stringify(d));
61
63
  return;
62
64
  }
63
- console.log(`\n${brand('Eval')} ${bold(d.url || url)}`);
64
- console.log(` ${muted('Expression:')} ${expr}`);
65
+ console.log(`${brand('Eval')} ${bold(d.url || url)}`);
66
+ if (d.navigationIncomplete) {
67
+ console.log(`${warning('⚠ Navigation incomplete:')} ${d.note || 'page did not reach full load'}`);
68
+ }
69
+ console.log(`${muted('Expression:')} ${expr}`);
65
70
  console.log(`\n${d.result || muted('(empty result)')}`);
66
71
  if (d.truncated)
67
72
  console.log(muted('\n(result truncated to fit context - narrow the expression for the full value)'));
68
- console.log('');
69
73
  }));
74
+ // Each `page eval` call runs to completion before the next starts, so two evals
75
+ // fired back-to-back never coexist in time - they CANNOT test whether two live
76
+ // clients see each other (presence, shared state). For that, use the genuinely-
77
+ // concurrent `page test --observe` instead, which overlaps N clients and reports
78
+ // whether they actually ran together.
79
+ pageEvalCommand.addHelpText('after', `
80
+ Testing realtime/shared state across clients?
81
+ Separate 'page eval' calls run sequentially (one finishes before the next
82
+ starts), so they never overlap and will each see only themselves - a false
83
+ negative. Use 'gipity page test <url> --observe <expr>' for genuinely
84
+ concurrent clients with overlap verification.`);
70
85
  //# sourceMappingURL=page-eval.js.map
@@ -0,0 +1,136 @@
1
+ import { Command } from 'commander';
2
+ import { createHash } from 'crypto';
3
+ import { brand, bold, muted, success, warning, error as clrError } from '../colors.js';
4
+ import { formatSize } from '../utils.js';
5
+ import { run } from '../helpers/index.js';
6
+ function expectedType(path) {
7
+ const p = path.toLowerCase().split('?')[0].split('#')[0];
8
+ if (p.endsWith('.json'))
9
+ return 'json';
10
+ if (p.endsWith('.xml'))
11
+ return 'xml';
12
+ if (p.endsWith('.html') || p.endsWith('.htm'))
13
+ return 'html';
14
+ if (p.endsWith('.txt') || p.endsWith('.md') || p.endsWith('.markdown'))
15
+ return 'text';
16
+ return null;
17
+ }
18
+ function contentTypeMatches(expect, ct) {
19
+ if (!expect || !ct)
20
+ return true; // no expectation, or host sent no type → don't flag
21
+ const c = ct.toLowerCase();
22
+ switch (expect) {
23
+ case 'json': return c.includes('application/json');
24
+ case 'xml': return c.includes('xml');
25
+ case 'html': return c.includes('text/html');
26
+ case 'text': return c.includes('text/plain') || c.includes('text/markdown');
27
+ }
28
+ }
29
+ function looksLikeHtml(body) {
30
+ const head = body.slice(0, 300).toLowerCase();
31
+ return /<!doctype html|<html[\s>]/.test(head);
32
+ }
33
+ function sha256(s) {
34
+ return createHash('sha256').update(s).digest('hex');
35
+ }
36
+ function baseWithSlash(url) {
37
+ return url.endsWith('/') ? url : url + '/';
38
+ }
39
+ /** Resolve a file path against the app base, keeping the app's subpath. */
40
+ function resolveUrl(base, path) {
41
+ return new URL(path.replace(/^\/+/, ''), baseWithSlash(base)).toString();
42
+ }
43
+ async function fetchRaw(url) {
44
+ try {
45
+ const res = await fetch(url, { redirect: 'follow' });
46
+ const body = await res.text();
47
+ return { status: res.status, contentType: res.headers.get('content-type'), body };
48
+ }
49
+ catch {
50
+ return null; // DNS failure, connection refused, etc.
51
+ }
52
+ }
53
+ /** Hit a guaranteed-absent path to learn whether the host serves a catch-all shell. */
54
+ async function probeShell(base) {
55
+ const rand = Math.random().toString(36).slice(2) + Date.now().toString(36);
56
+ const r = await fetchRaw(resolveUrl(base, `__gipity_probe_${rand}.notreal`));
57
+ if (!r)
58
+ return { served: false, status: 0, sha256: null, isHtml: false };
59
+ return {
60
+ served: r.status >= 200 && r.status < 300,
61
+ status: r.status,
62
+ sha256: sha256(r.body),
63
+ isHtml: looksLikeHtml(r.body),
64
+ };
65
+ }
66
+ function classify(path, r, shell) {
67
+ if (!r)
68
+ return { status: null, contentType: null, bytes: 0, verdict: 'MISSING', detail: 'fetch failed (host unreachable)' };
69
+ const bytes = Buffer.byteLength(r.body);
70
+ const base = { status: r.status, contentType: r.contentType, bytes };
71
+ // Honest 404/5xx - the file simply isn't there.
72
+ if (r.status >= 400)
73
+ return { ...base, verdict: 'MISSING', detail: `HTTP ${r.status}` };
74
+ const expect = expectedType(path);
75
+ // 1) Served the catch-all shell verbatim → 200, but the file isn't deployed.
76
+ if (shell.served && shell.sha256 && sha256(r.body) === shell.sha256) {
77
+ return { ...base, verdict: 'MISSING', detail: `HTTP ${r.status} but served the SPA shell (file not deployed)` };
78
+ }
79
+ // 2) Asked for a non-HTML asset but got an HTML body → a per-path shell variant.
80
+ if (expect && expect !== 'html' && looksLikeHtml(r.body)) {
81
+ return { ...base, verdict: 'MISSING', detail: `HTTP ${r.status} but got HTML where ${expect} expected (SPA shell)` };
82
+ }
83
+ // 3) Present and real, but the content-type header disagrees with the extension.
84
+ if (expect && !contentTypeMatches(expect, r.contentType)) {
85
+ return { ...base, verdict: 'WRONG-TYPE', detail: `expected ${expect}, served as ${r.contentType ?? '(none)'}` };
86
+ }
87
+ return { ...base, verdict: 'OK', detail: r.contentType ?? '' };
88
+ }
89
+ const TAG = {
90
+ 'OK': success('OK '),
91
+ 'MISSING': clrError('MISSING '),
92
+ 'WRONG-TYPE': warning('WRONG-TYPE'),
93
+ };
94
+ export const pageFetchCommand = new Command('fetch')
95
+ .description('Verify deployed non-rendered files (llms.txt, AGENTS.md, robots.txt, served JSON…) really exist - catches the static-host trap where a missing file returns 200 with the SPA shell instead of a 404')
96
+ .argument('<url>', 'Deployed app base URL; file paths resolve relative to it')
97
+ .argument('<paths...>', 'One or more file paths to verify, e.g. llms.txt AGENTS.md robots.txt')
98
+ .option('--json', 'Output as JSON')
99
+ .action((url, paths, opts) => run('Page fetch', async () => {
100
+ const shell = await probeShell(url);
101
+ const results = [];
102
+ for (const p of paths) {
103
+ const target = resolveUrl(url, p);
104
+ const c = classify(p, await fetchRaw(target), shell);
105
+ results.push({ path: p, url: target, ...c });
106
+ }
107
+ const failed = results.filter((r) => r.verdict !== 'OK');
108
+ if (opts.json) {
109
+ console.log(JSON.stringify({
110
+ base: baseWithSlash(url),
111
+ shellFallback: shell.served,
112
+ ok: failed.length === 0,
113
+ failed: failed.length,
114
+ files: results,
115
+ }));
116
+ if (failed.length)
117
+ process.exitCode = 1;
118
+ return;
119
+ }
120
+ console.log(`${brand('Page fetch')} ${bold(baseWithSlash(url))}`);
121
+ if (shell.served) {
122
+ console.log(`${muted('Host serves a catch-all shell for unknown paths - a bare 200 is not proof; verifying bodies.')}`);
123
+ }
124
+ const pad = Math.min(48, Math.max(...results.map((r) => r.path.length)));
125
+ for (const r of results) {
126
+ const size = r.verdict === 'OK' ? muted(formatSize(r.bytes).padStart(9)) : ' '.repeat(9);
127
+ const detail = r.detail && r.verdict !== 'OK' ? muted(` ${r.detail}`) : '';
128
+ console.log(`${TAG[r.verdict]} ${r.path.padEnd(pad)} ${size}${detail}`);
129
+ }
130
+ console.log(failed.length === 0
131
+ ? `\n${success(`✓ all ${results.length} file(s) present with the right content-type`)}`
132
+ : `\n${clrError(`✗ ${failed.length} of ${results.length} file(s) missing or wrong-type`)}`);
133
+ if (failed.length)
134
+ process.exitCode = 1;
135
+ }));
136
+ //# sourceMappingURL=page-fetch.js.map
@@ -58,27 +58,30 @@ export const pageInspectCommand = new Command('inspect')
58
58
  }
59
59
  const timing = b.timing || { ttfb: 0, domReady: 0, load: 0 };
60
60
  // ── Page Info ──
61
- console.log(`\n${brand('Inspecting')} ${bold(b.url || url)}`);
62
- console.log(` ${muted('Title:')} ${b.title || '(none)'}`);
63
- console.log(` ${muted('Elements:')} ${b.elementCount || 0}`);
64
- console.log(` ${muted('Page weight:')} ${info(formatSize(b.totalBytes || 0))}`);
61
+ console.log(`${brand('Inspecting')} ${bold(b.url || url)}`);
62
+ if (b.navigationIncomplete) {
63
+ console.log(`${warning('⚠ Navigation incomplete:')} ${b.note || 'page did not reach full load'}`);
64
+ }
65
+ console.log(`${muted('Title:')} ${b.title || '(none)'}`);
66
+ console.log(`${muted('Elements:')} ${b.elementCount || 0}`);
67
+ console.log(`${muted('Page weight:')} ${info(formatSize(b.totalBytes || 0))}`);
65
68
  // ── Timing ──
66
- console.log(`\n ${bold('Timing:')}`);
67
- console.log(` ${muted('TTFB:')} ${timing.ttfb}ms`);
68
- console.log(` ${muted('DOM ready:')} ${timing.domReady}ms`);
69
- console.log(` ${muted('Load:')} ${timing.load}ms`);
69
+ console.log(`\n${bold('Timing:')}`);
70
+ console.log(`${muted('TTFB:')} ${timing.ttfb}ms`);
71
+ console.log(`${muted('DOM ready:')} ${timing.domReady}ms`);
72
+ console.log(`${muted('Load:')} ${timing.load}ms`);
70
73
  if (showAll && b.lcp) {
71
- console.log(` LCP: ${b.lcp.time}ms (${b.lcp.element}${b.lcp.url ? ' ' + shortUrl(b.lcp.url, truncate) : ''})`);
74
+ console.log(`LCP: ${b.lcp.time}ms (${b.lcp.element}${b.lcp.url ? ' ' + shortUrl(b.lcp.url, truncate) : ''})`);
72
75
  }
73
76
  // ── Console ──
74
77
  if (b.console?.length > 0) {
75
- console.log(`\n ${bold('Console')} ${muted(`(${b.console.length})`)}:`);
78
+ console.log(`\n${bold('Console')} ${muted(`(${b.console.length})`)}:`);
76
79
  for (const line of b.console) {
77
- console.log(` ${warning(line)}`);
80
+ console.log(`${warning(line)}`);
78
81
  }
79
82
  }
80
83
  else {
81
- console.log(`\n ${bold('Console:')} ${muted('(clean)')}`);
84
+ console.log(`\n${bold('Console:')} ${muted('(clean)')}`);
82
85
  }
83
86
  // ── Failed Resources ──
84
87
  // Browsers auto-request /favicon.ico at the site root for every page, so a
@@ -97,57 +100,56 @@ export const pageInspectCommand = new Command('inspect')
97
100
  const failed = (b.failedResources || []).filter((r) => !isImplicitFavicon(r));
98
101
  const rootFaviconMissing = (b.failedResources || []).some(isImplicitFavicon);
99
102
  if (failed.length > 0) {
100
- console.log(`\n ${clrError(`Failed resources (${failed.length}):`)}`);
103
+ console.log(`\n${clrError(`Failed resources (${failed.length}):`)}`);
101
104
  for (const r of failed) {
102
- console.log(` ${clrError(r)}`);
105
+ console.log(`${clrError(r)}`);
103
106
  }
104
107
  }
105
108
  if (rootFaviconMissing) {
106
- console.log(`\n ${muted('No root /favicon.ico (browsers request this automatically; harmless for app pages served under a subpath)')}`);
109
+ console.log(`\n${muted('No root /favicon.ico (browsers request this automatically; harmless for app pages served under a subpath)')}`);
107
110
  }
108
111
  // ── Layout (horizontal overflow) ──
109
112
  if (b.overflow) {
110
113
  if (b.overflow.overflowX) {
111
- console.log(`\n ${clrError(`Horizontal overflow: +${b.overflow.amount}px`)} ${muted(`(content ${b.overflow.scrollWidth}px vs viewport ${b.overflow.clientWidth}px)`)}`);
114
+ console.log(`\n${clrError(`Horizontal overflow: +${b.overflow.amount}px`)} ${muted(`(content ${b.overflow.scrollWidth}px vs viewport ${b.overflow.clientWidth}px)`)}`);
112
115
  if (showAll && b.overflow.culprits.length > 0) {
113
- console.log(` ${muted('Overflowing elements:')}`);
116
+ console.log(`${muted('Overflowing elements:')}`);
114
117
  for (const c of b.overflow.culprits) {
115
118
  const sel = c.cls ? `${c.tag}.${c.cls.split(/\s+/)[0]}` : c.tag;
116
- console.log(` ${sel} ${muted(`(right ${c.right}px, width ${c.width}px)`)}`);
119
+ console.log(`${sel} ${muted(`(right ${c.right}px, width ${c.width}px)`)}`);
117
120
  }
118
121
  }
119
122
  else if (b.overflow.culprits.length > 0) {
120
- console.log(` ${muted(`${b.overflow.culprits.length} overflowing element(s) - use --all to list`)}`);
123
+ console.log(`${muted(`${b.overflow.culprits.length} overflowing element(s) - use --all to list`)}`);
121
124
  }
122
125
  }
123
126
  else {
124
- console.log(`\n ${bold('Layout:')} ${muted('no horizontal overflow')}`);
127
+ console.log(`\n${bold('Layout:')} ${muted('no horizontal overflow')}`);
125
128
  }
126
129
  }
127
130
  if (showAll) {
128
131
  // ── Render Blocking ──
129
132
  if (b.renderBlocking?.length > 0) {
130
- console.log(`\n ${warning(`Render-blocking (${b.renderBlocking.length}):`)}`);
133
+ console.log(`\n${warning(`Render-blocking (${b.renderBlocking.length}):`)}`);
131
134
  for (const r of b.renderBlocking) {
132
- console.log(` ${shortUrl(r, truncate)}`);
135
+ console.log(`${shortUrl(r, truncate)}`);
133
136
  }
134
137
  }
135
138
  // ── Large Resources ──
136
139
  if (b.largeResources?.length > 0) {
137
- console.log(`\n ${warning(`Large resources >100KB (${b.largeResources.length}):`)}`);
140
+ console.log(`\n${warning(`Large resources >100KB (${b.largeResources.length}):`)}`);
138
141
  for (const r of b.largeResources) {
139
- console.log(` ${info(formatSize(r.size).padEnd(10))} ${muted(r.type.padEnd(8))} ${shortUrl(r.url, truncate)}`);
142
+ console.log(`${info(formatSize(r.size).padEnd(10))} ${muted(r.type.padEnd(8))} ${shortUrl(r.url, truncate)}`);
140
143
  }
141
144
  }
142
145
  // ── Oversized Images ──
143
146
  if (b.oversizedImages?.length > 0) {
144
- console.log(`\n ${warning(`Oversized images (${b.oversizedImages.length}):`)}`);
147
+ console.log(`\n${warning(`Oversized images (${b.oversizedImages.length}):`)}`);
145
148
  for (const img of b.oversizedImages) {
146
- console.log(` ${img.natural} served, ${img.displayed} displayed - ${shortUrl(img.src, truncate)}`);
149
+ console.log(`${img.natural} served, ${img.displayed} displayed - ${shortUrl(img.src, truncate)}`);
147
150
  }
148
151
  }
149
152
  }
150
- console.log('');
151
153
  });
152
154
  });
153
155
  //# sourceMappingURL=page-inspect.js.map
@@ -105,6 +105,10 @@ export const pageScreenshotCommand = new Command('screenshot')
105
105
  .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)')
106
106
  .option('--json', 'Output JSON metadata instead of a friendly summary')
107
107
  .addOption(new Option('--wait <ms>', 'Alias for --post-load-delay').hideHelp())
108
+ // `--full-page` is the Puppeteer/Playwright name for this (their `fullPage`),
109
+ // so agents reach for it by reflex. Accept it as a hidden alias for `--full`
110
+ // rather than reject it as an unknown option and send them on a --help detour.
111
+ .addOption(new Option('--full-page', 'Alias for --full').hideHelp())
108
112
  .action((url, opts) => run('Page screenshot', async () => {
109
113
  const delayRaw = opts.postLoadDelay ?? opts.wait;
110
114
  const postLoadDelayMs = delayRaw !== undefined ? parseInt(String(delayRaw), 10) : undefined;
@@ -126,7 +130,7 @@ export const pageScreenshotCommand = new Command('screenshot')
126
130
  const body = {
127
131
  url,
128
132
  postLoadDelayMs,
129
- full: !!opts.full,
133
+ full: !!(opts.full || opts.fullPage),
130
134
  reloadBetween: opts.reloadBetween !== false,
131
135
  ...(userSpecifiedViewports ? { viewports: customViewports } : {}),
132
136
  ...(opts.fakeMedia ? { fakeMedia: true } : {}),
@@ -183,41 +187,40 @@ export const pageScreenshotCommand = new Command('screenshot')
183
187
  }
184
188
  if (meta.screenshots.length === 1) {
185
189
  const s = meta.screenshots[0];
186
- console.log(`\n${brand('Screenshot')} ${bold(url)}`);
190
+ console.log(`${brand('Screenshot')} ${bold(url)}`);
187
191
  if (meta.title)
188
- console.log(` ${label('Web page title')} ${meta.title}`);
192
+ console.log(`${label('Web page title')} ${meta.title}`);
189
193
  if (meta.finalUrl)
190
- console.log(` ${label('Web page URL')} ${meta.finalUrl}`);
194
+ console.log(`${label('Web page URL')} ${meta.finalUrl}`);
191
195
  if (meta.status != null)
192
- console.log(` ${label('Web page status')} ${meta.status}`);
196
+ console.log(`${label('Web page status')} ${meta.status}`);
193
197
  if (meta.performance)
194
- console.log(` ${label('Web page perf')} ${fmtPerformance(meta.performance)}`);
198
+ console.log(`${label('Web page perf')} ${fmtPerformance(meta.performance)}`);
195
199
  const sizePart = formatSize(s.screenshotSizeBytes) + (meta.full ? ' (full page)' : '');
196
- console.log(` ${label('Screenshot size')} ${sizePart}`);
200
+ console.log(`${label('Screenshot size')} ${sizePart}`);
197
201
  if (s.width && s.height)
198
- console.log(` ${label('Screenshot dims')} ${s.width} × ${s.height}`);
199
- console.log(` ${label('Screenshot file')} ${success(savedFiles[0])}\n`);
202
+ console.log(`${label('Screenshot dims')} ${s.width} × ${s.height}`);
203
+ console.log(`${label('Screenshot file')} ${success(savedFiles[0])}`);
200
204
  return;
201
205
  }
202
- console.log(`\n${brand('Loading')} ${bold(url)} ${muted(`once → ${meta.screenshots.length} viewports`)}`);
206
+ console.log(`${brand('Loading')} ${bold(url)} ${muted(`once → ${meta.screenshots.length} viewports`)}`);
203
207
  if (meta.title)
204
- console.log(` ${label('Web page title')} ${meta.title}`);
208
+ console.log(`${label('Web page title')} ${meta.title}`);
205
209
  if (meta.finalUrl)
206
- console.log(` ${label('Web page URL')} ${meta.finalUrl}`);
210
+ console.log(`${label('Web page URL')} ${meta.finalUrl}`);
207
211
  if (meta.status != null)
208
- console.log(` ${label('Web page status')} ${meta.status}`);
212
+ console.log(`${label('Web page status')} ${meta.status}`);
209
213
  if (meta.performance)
210
- console.log(` ${label('Web page perf')} ${fmtPerformance(meta.performance)}`);
214
+ console.log(`${label('Web page perf')} ${fmtPerformance(meta.performance)}`);
211
215
  for (let i = 0; i < meta.screenshots.length; i++) {
212
216
  const s = meta.screenshots[i];
213
217
  const dims = `${s.viewport.width}×${s.viewport.height}${s.viewport.deviceScaleFactor > 1 ? ` @${s.viewport.deviceScaleFactor}x` : ''}`;
214
- console.log(`\n ${brand('@ ' + dims)}`);
218
+ console.log(`\n${brand('@ ' + dims)}`);
215
219
  const sizePart = formatSize(s.screenshotSizeBytes) + (meta.full ? ' (full page)' : '');
216
- console.log(` ${label('Screenshot size')} ${sizePart}`);
220
+ console.log(`${label('Screenshot size')} ${sizePart}`);
217
221
  if (s.width && s.height)
218
- console.log(` ${label('Screenshot dims')} ${s.width} × ${s.height}`);
219
- console.log(` ${label('Screenshot file')} ${success(savedFiles[i])}`);
222
+ console.log(`${label('Screenshot dims')} ${s.width} × ${s.height}`);
223
+ console.log(`${label('Screenshot file')} ${success(savedFiles[i])}`);
220
224
  }
221
- console.log('');
222
225
  }));
223
226
  //# sourceMappingURL=page-screenshot.js.map