gipity 1.0.365 → 1.0.374

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 (52) 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 +8 -9
  8. package/dist/commands/credits.js +1 -1
  9. package/dist/commands/db.js +7 -6
  10. package/dist/commands/deploy.js +5 -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 +6 -4
  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 +17 -18
  25. package/dist/commands/page-test.js +8 -8
  26. package/dist/commands/page.js +6 -3
  27. package/dist/commands/plan.js +4 -4
  28. package/dist/commands/push.js +2 -6
  29. package/dist/commands/realtime.js +7 -9
  30. package/dist/commands/relay-install.js +18 -21
  31. package/dist/commands/relay.js +29 -31
  32. package/dist/commands/sandbox.js +16 -3
  33. package/dist/commands/service.js +1 -3
  34. package/dist/commands/status.js +2 -2
  35. package/dist/commands/sync.js +4 -1
  36. package/dist/commands/test.js +7 -13
  37. package/dist/commands/text.js +10 -10
  38. package/dist/commands/uninstall.js +20 -42
  39. package/dist/commands/update.js +0 -2
  40. package/dist/commands/upload.js +4 -4
  41. package/dist/commands/workflow.js +1 -2
  42. package/dist/helpers/output.js +45 -7
  43. package/dist/index.js +4 -0
  44. package/dist/knowledge.js +19 -4
  45. package/dist/progress.js +60 -0
  46. package/dist/project-setup.js +5 -1
  47. package/dist/provider-docs.js +7 -7
  48. package/dist/setup.js +20 -7
  49. package/dist/sync.js +16 -6
  50. package/dist/updater/shim.js +18 -4
  51. package/dist/upload.js +6 -0
  52. package/package.json +5 -4
@@ -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,6 +1,6 @@
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
@@ -60,11 +60,13 @@ export const pageEvalCommand = new Command('eval')
60
60
  console.log(JSON.stringify(d));
61
61
  return;
62
62
  }
63
- console.log(`\n${brand('Eval')} ${bold(d.url || url)}`);
64
- console.log(` ${muted('Expression:')} ${expr}`);
63
+ console.log(`${brand('Eval')} ${bold(d.url || url)}`);
64
+ if (d.navigationIncomplete) {
65
+ console.log(`${warning('⚠ Navigation incomplete:')} ${d.note || 'page did not reach full load'}`);
66
+ }
67
+ console.log(`${muted('Expression:')} ${expr}`);
65
68
  console.log(`\n${d.result || muted('(empty result)')}`);
66
69
  if (d.truncated)
67
70
  console.log(muted('\n(result truncated to fit context - narrow the expression for the full value)'));
68
- console.log('');
69
71
  }));
70
72
  //# 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
@@ -183,41 +183,40 @@ export const pageScreenshotCommand = new Command('screenshot')
183
183
  }
184
184
  if (meta.screenshots.length === 1) {
185
185
  const s = meta.screenshots[0];
186
- console.log(`\n${brand('Screenshot')} ${bold(url)}`);
186
+ console.log(`${brand('Screenshot')} ${bold(url)}`);
187
187
  if (meta.title)
188
- console.log(` ${label('Web page title')} ${meta.title}`);
188
+ console.log(`${label('Web page title')} ${meta.title}`);
189
189
  if (meta.finalUrl)
190
- console.log(` ${label('Web page URL')} ${meta.finalUrl}`);
190
+ console.log(`${label('Web page URL')} ${meta.finalUrl}`);
191
191
  if (meta.status != null)
192
- console.log(` ${label('Web page status')} ${meta.status}`);
192
+ console.log(`${label('Web page status')} ${meta.status}`);
193
193
  if (meta.performance)
194
- console.log(` ${label('Web page perf')} ${fmtPerformance(meta.performance)}`);
194
+ console.log(`${label('Web page perf')} ${fmtPerformance(meta.performance)}`);
195
195
  const sizePart = formatSize(s.screenshotSizeBytes) + (meta.full ? ' (full page)' : '');
196
- console.log(` ${label('Screenshot size')} ${sizePart}`);
196
+ console.log(`${label('Screenshot size')} ${sizePart}`);
197
197
  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`);
198
+ console.log(`${label('Screenshot dims')} ${s.width} × ${s.height}`);
199
+ console.log(`${label('Screenshot file')} ${success(savedFiles[0])}`);
200
200
  return;
201
201
  }
202
- console.log(`\n${brand('Loading')} ${bold(url)} ${muted(`once → ${meta.screenshots.length} viewports`)}`);
202
+ console.log(`${brand('Loading')} ${bold(url)} ${muted(`once → ${meta.screenshots.length} viewports`)}`);
203
203
  if (meta.title)
204
- console.log(` ${label('Web page title')} ${meta.title}`);
204
+ console.log(`${label('Web page title')} ${meta.title}`);
205
205
  if (meta.finalUrl)
206
- console.log(` ${label('Web page URL')} ${meta.finalUrl}`);
206
+ console.log(`${label('Web page URL')} ${meta.finalUrl}`);
207
207
  if (meta.status != null)
208
- console.log(` ${label('Web page status')} ${meta.status}`);
208
+ console.log(`${label('Web page status')} ${meta.status}`);
209
209
  if (meta.performance)
210
- console.log(` ${label('Web page perf')} ${fmtPerformance(meta.performance)}`);
210
+ console.log(`${label('Web page perf')} ${fmtPerformance(meta.performance)}`);
211
211
  for (let i = 0; i < meta.screenshots.length; i++) {
212
212
  const s = meta.screenshots[i];
213
213
  const dims = `${s.viewport.width}×${s.viewport.height}${s.viewport.deviceScaleFactor > 1 ? ` @${s.viewport.deviceScaleFactor}x` : ''}`;
214
- console.log(`\n ${brand('@ ' + dims)}`);
214
+ console.log(`\n${brand('@ ' + dims)}`);
215
215
  const sizePart = formatSize(s.screenshotSizeBytes) + (meta.full ? ' (full page)' : '');
216
- console.log(` ${label('Screenshot size')} ${sizePart}`);
216
+ console.log(`${label('Screenshot size')} ${sizePart}`);
217
217
  if (s.width && s.height)
218
- console.log(` ${label('Screenshot dims')} ${s.width} × ${s.height}`);
219
- console.log(` ${label('Screenshot file')} ${success(savedFiles[i])}`);
218
+ console.log(`${label('Screenshot dims')} ${s.width} × ${s.height}`);
219
+ console.log(`${label('Screenshot file')} ${success(savedFiles[i])}`);
220
220
  }
221
- console.log('');
222
221
  }));
223
222
  //# sourceMappingURL=page-screenshot.js.map
@@ -36,15 +36,15 @@ export const pageTestCommand = new Command('test')
36
36
  const stagger = Math.max(0, parseInt(opts.stagger, 10) || 0);
37
37
  const wait = Math.min(30000, Math.max(2000, parseInt(opts.wait, 10) || 24000));
38
38
  if (!opts.json) {
39
- console.log(`\n${brand('Page test')} ${bold(url)}`);
40
- console.log(` ${muted(`${clients} client(s), stagger ${stagger}s, ${wait}ms open each`)}\n`);
39
+ console.log(`${brand('Page test')} ${bold(url)}`);
40
+ console.log(`${muted(`${clients} client(s), stagger ${stagger}s, ${wait}ms open each`)}`);
41
41
  }
42
42
  const runs = [];
43
43
  for (let i = 0; i < clients; i++) {
44
44
  runs.push((async () => {
45
45
  await sleep(i * stagger * 1000);
46
46
  if (!opts.json)
47
- console.log(` ${muted(`client ${i}${i === 0 ? ' (first)' : ''} starting`)}`);
47
+ console.log(`${muted(`client ${i}${i === 0 ? ' (first)' : ''} starting`)}`);
48
48
  return inspectClient(url, wait, i);
49
49
  })());
50
50
  }
@@ -65,21 +65,21 @@ export const pageTestCommand = new Command('test')
65
65
  for (const r of results) {
66
66
  console.log(`\n${bold(`=== client ${r.i}${r.i === 0 ? ' (first)' : ''} ===`)}`);
67
67
  if (r.error) {
68
- console.log(` ${clrError(`page inspect failed: ${r.error}`)}`);
68
+ console.log(`${clrError(`page inspect failed: ${r.error}`)}`);
69
69
  continue;
70
70
  }
71
71
  if (r.lines.length === 0) {
72
- console.log(` ${muted('(no console output)')}`);
72
+ console.log(`${muted('(no console output)')}`);
73
73
  continue;
74
74
  }
75
75
  for (const line of r.lines) {
76
76
  const bad = BAD.test(line);
77
- console.log(` ${bad ? warning('⚠ ' + line) : ' ' + line}`);
77
+ console.log(`${bad ? warning('⚠ ' + line) : ' ' + line}`);
78
78
  }
79
79
  }
80
80
  console.log(problems === 0
81
- ? `\n${success('✓ no error/crash lines across all clients')}\n`
82
- : `\n${clrError(`⚠ ${problems} error/crash line(s) flagged above`)}\n`);
81
+ ? `\n${success('✓ no error/crash lines across all clients')}`
82
+ : `\n${clrError(`⚠ ${problems} error/crash line(s) flagged above`)}`);
83
83
  if (problems > 0)
84
84
  process.exitCode = 1;
85
85
  }));
@@ -3,16 +3,19 @@ import { pageInspectCommand } from './page-inspect.js';
3
3
  import { pageScreenshotCommand } from './page-screenshot.js';
4
4
  import { pageEvalCommand } from './page-eval.js';
5
5
  import { pageTestCommand } from './page-test.js';
6
+ import { pageFetchCommand } from './page-fetch.js';
6
7
  // Parent namespace grouping the page/browser diagnostics under one command:
7
- // gipity page inspect | eval | screenshot | test
8
+ // gipity page inspect | eval | screenshot | test | fetch
8
9
  // Each subcommand is canonical for its capability; the namespace keeps the
9
10
  // top-level surface lean and makes the siblings discoverable via `page --help`.
11
+ // `inspect` is the rendered DOM (browser); `fetch` is the raw asset (plain HTTP).
10
12
  export const pageCommand = new Command('page')
11
- .description('Inspect, evaluate, screenshot, and multi-client test web pages (page inspect | eval | screenshot | test)')
13
+ .description('Inspect, evaluate, screenshot, multi-client test, and verify raw files of web pages (page inspect | eval | screenshot | test | fetch)')
12
14
  .addCommand(pageInspectCommand)
13
15
  .addCommand(pageEvalCommand)
14
16
  .addCommand(pageScreenshotCommand)
15
- .addCommand(pageTestCommand);
17
+ .addCommand(pageTestCommand)
18
+ .addCommand(pageFetchCommand);
16
19
  // No subcommand → show help instead of commander's terse error.
17
20
  pageCommand.action(() => {
18
21
  pageCommand.help();
@@ -22,7 +22,7 @@ function formatLimit(key, value) {
22
22
  }
23
23
  return value.toLocaleString();
24
24
  }
25
- function renderLimits(limits, indent = ' ') {
25
+ function renderLimits(limits, indent = '') {
26
26
  for (const key of Object.keys(LIMIT_LABELS)) {
27
27
  const value = limits[key];
28
28
  if (value !== undefined) {
@@ -48,14 +48,14 @@ export const planCommand = new Command('plan')
48
48
  const price = plan.monthlyPriceUsd > 0 ? ` $${plan.monthlyPriceUsd}/mo` : '';
49
49
  console.log(`Plan: ${brand(plan.displayName)} (${plan.tier})${price}`);
50
50
  if (plan.monthlyCredits > 0) {
51
- console.log(` ${plan.monthlyCredits.toLocaleString()} credits/mo, ${plan.creditExpiryDays}-day expiry`);
51
+ console.log(`${plan.monthlyCredits.toLocaleString()} credits/mo, ${plan.creditExpiryDays}-day expiry`);
52
52
  }
53
53
  }
54
54
  else {
55
55
  console.log(`Plan: ${tier} (no matching plan row)`);
56
56
  }
57
57
  if (planAppliedAt) {
58
- console.log(` ${dim('Applied: ' + new Date(planAppliedAt).toLocaleDateString())}`);
58
+ console.log(`${dim('Applied: ' + new Date(planAppliedAt).toLocaleDateString())}`);
59
59
  }
60
60
  console.log('\nLimits:');
61
61
  renderLimits(limits);
@@ -85,7 +85,7 @@ planCommand
85
85
  ? ` - ${plan.monthlyCredits.toLocaleString()} credits/mo (${plan.creditExpiryDays}-day expiry)`
86
86
  : '';
87
87
  console.log(`${marker}${plan.displayName} (${plan.tier})${price}${credits}`);
88
- renderLimits(plan.limits, ' ');
88
+ renderLimits(plan.limits);
89
89
  console.log('');
90
90
  }
91
91
  console.log(dim('(* = your current plan)'));
@@ -1,7 +1,7 @@
1
1
  import { Command } from 'commander';
2
2
  import { resolve } from 'path';
3
3
  import { pushFile } from '../sync.js';
4
- import { error as clrError } from '../colors.js';
4
+ import { error as clrError, success } from '../colors.js';
5
5
  export const pushCommand = new Command('push')
6
6
  .description('Push a file')
7
7
  .argument('<file>', 'File path to push')
@@ -22,16 +22,12 @@ export const pushCommand = new Command('push')
22
22
  }
23
23
  await pushFile(fullPath);
24
24
  if (!opts.quiet) {
25
- console.log('');
26
- console.log(`Pushed ${file}`);
27
- console.log('');
25
+ console.log(success(`Pushed ${file}`));
28
26
  }
29
27
  }
30
28
  catch (err) {
31
29
  if (!opts.quiet) {
32
- console.log('');
33
30
  console.error(clrError(`Push failed: ${err.message}`));
34
- console.log('');
35
31
  }
36
32
  process.exit(1);
37
33
  }
@@ -32,7 +32,7 @@ const roomCommand = new Command('room')
32
32
  switch (sub) {
33
33
  case 'list': {
34
34
  const res = await get(base);
35
- printList(res.data, opts, 'No realtime rooms. Create one: gipity realtime room create <name>', r => ` ${bold(r.name)} ${muted(`${r.room_type} · ${r.auth_level} · max ${r.max_clients}`)}`);
35
+ printList(res.data, opts, 'No realtime rooms. Create one: gipity realtime room create <name>', r => `${bold(r.name)} ${muted(`${r.room_type} · ${r.auth_level} · max ${r.max_clients}`)}`);
36
36
  break;
37
37
  }
38
38
  case 'create': {
@@ -74,7 +74,7 @@ const roomCommand = new Command('room')
74
74
  console.log(JSON.stringify({ success: true }));
75
75
  }
76
76
  else {
77
- console.log(`Deleted room '${name}'. Active instances drain as clients disconnect.`);
77
+ console.log(success(`Deleted room '${name}'.`) + ' Active instances drain as clients disconnect.');
78
78
  }
79
79
  break;
80
80
  }
@@ -89,13 +89,11 @@ const roomCommand = new Command('room')
89
89
  }
90
90
  else {
91
91
  const { room, live } = res.data;
92
- console.log('');
93
- console.log(` ${bold(room.name)}`);
94
- console.log(` Type: ${room.room_type}`);
95
- console.log(` Auth: ${room.auth_level}`);
96
- console.log(` Max clients: ${room.max_clients}`);
97
- console.log(` Live: ${live ? `${live.instances} instance(s), ${live.clients} client(s)` : muted('Colyseus server unreachable')}`);
98
- console.log('');
92
+ console.log(`${bold(room.name)}`);
93
+ console.log(`Type: ${room.room_type}`);
94
+ console.log(`Auth: ${room.auth_level}`);
95
+ console.log(`Max clients: ${room.max_clients}`);
96
+ console.log(`Live: ${live ? `${live.instances} instance(s), ${live.clients} client(s)` : muted('Colyseus server unreachable')}`);
99
97
  }
100
98
  break;
101
99
  }