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.
- package/dist/banner.js +3 -1
- package/dist/commands/add.js +2 -2
- package/dist/commands/agent.js +3 -5
- package/dist/commands/approval.js +3 -3
- package/dist/commands/audit.js +2 -2
- package/dist/commands/chat.js +4 -4
- package/dist/commands/claude.js +26 -78
- package/dist/commands/credits.js +1 -1
- package/dist/commands/db.js +7 -6
- package/dist/commands/deploy.js +10 -8
- package/dist/commands/doctor.js +11 -13
- package/dist/commands/domain.js +18 -15
- package/dist/commands/email.js +0 -4
- package/dist/commands/fn.js +2 -2
- package/dist/commands/generate.js +57 -5
- package/dist/commands/job.js +6 -6
- package/dist/commands/location.js +7 -7
- package/dist/commands/login.js +2 -16
- package/dist/commands/logout.js +2 -3
- package/dist/commands/logs.js +1 -1
- package/dist/commands/page-eval.js +22 -7
- package/dist/commands/page-fetch.js +136 -0
- package/dist/commands/page-inspect.js +29 -27
- package/dist/commands/page-screenshot.js +22 -19
- package/dist/commands/page-test.js +196 -26
- package/dist/commands/page.js +6 -3
- package/dist/commands/plan.js +4 -4
- package/dist/commands/project.js +2 -1
- package/dist/commands/push.js +2 -6
- package/dist/commands/realtime.js +7 -9
- package/dist/commands/relay-install.js +18 -21
- package/dist/commands/relay.js +29 -31
- package/dist/commands/sandbox.js +48 -8
- package/dist/commands/service.js +1 -3
- package/dist/commands/status.js +5 -3
- package/dist/commands/sync.js +4 -1
- package/dist/commands/test.js +7 -13
- package/dist/commands/text.js +10 -10
- package/dist/commands/uninstall.js +20 -42
- package/dist/commands/update.js +0 -2
- package/dist/commands/upload.js +4 -4
- package/dist/commands/workflow.js +1 -2
- package/dist/config.js +11 -0
- package/dist/flag-aliases.js +11 -3
- package/dist/helpers/output.js +45 -7
- package/dist/index.js +4 -0
- package/dist/knowledge.js +23 -8
- package/dist/progress.js +60 -0
- package/dist/project-setup.js +5 -1
- package/dist/prompts.js +0 -8
- package/dist/provider-docs.js +7 -7
- package/dist/setup.js +34 -10
- package/dist/sync.js +16 -6
- package/dist/updater/shim.js +18 -4
- package/dist/upload.js +6 -0
- 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-
|
|
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-
|
|
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
|
-
|
|
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
|
|
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
|
package/dist/commands/job.js
CHANGED
|
@@ -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
|
|
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(`
|
|
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(`
|
|
62
|
+
console.log(`duration: ${r.duration_ms}ms`);
|
|
63
63
|
if (r.error)
|
|
64
|
-
console.log(
|
|
64
|
+
console.log(`${clrError('error:')} ${r.error}`);
|
|
65
65
|
if (r.output)
|
|
66
|
-
console.log(`
|
|
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
|
|
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(
|
|
15
|
+
lines.push(place);
|
|
16
16
|
if (r.lat != null && r.lon != null)
|
|
17
|
-
lines.push(`
|
|
17
|
+
lines.push(`Coordinates: ${r.lat}, ${r.lon}`);
|
|
18
18
|
if (r.ip)
|
|
19
|
-
lines.push(`
|
|
19
|
+
lines.push(`IP: ${r.ip}`);
|
|
20
20
|
if (r.timezone)
|
|
21
|
-
lines.push(`
|
|
21
|
+
lines.push(`Timezone: ${r.timezone}`);
|
|
22
22
|
if (r.accuracy != null)
|
|
23
|
-
lines.push(`
|
|
24
|
-
lines.push(`
|
|
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('
|
|
74
|
+
console.log('No location data.');
|
|
75
75
|
return;
|
|
76
76
|
}
|
|
77
77
|
console.log(formatLocation(res.data));
|
package/dist/commands/login.js
CHANGED
|
@@ -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
|
package/dist/commands/logout.js
CHANGED
|
@@ -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
|
package/dist/commands/logs.js
CHANGED
|
@@ -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(
|
|
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
|
-
|
|
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() +
|
|
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(
|
|
64
|
-
|
|
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(
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
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
|
|
67
|
-
console.log(
|
|
68
|
-
console.log(
|
|
69
|
-
console.log(
|
|
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(`
|
|
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
|
|
78
|
+
console.log(`\n${bold('Console')} ${muted(`(${b.console.length})`)}:`);
|
|
76
79
|
for (const line of b.console) {
|
|
77
|
-
console.log(
|
|
80
|
+
console.log(`${warning(line)}`);
|
|
78
81
|
}
|
|
79
82
|
}
|
|
80
83
|
else {
|
|
81
|
-
console.log(`\n
|
|
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
|
|
103
|
+
console.log(`\n${clrError(`Failed resources (${failed.length}):`)}`);
|
|
101
104
|
for (const r of failed) {
|
|
102
|
-
console.log(
|
|
105
|
+
console.log(`${clrError(r)}`);
|
|
103
106
|
}
|
|
104
107
|
}
|
|
105
108
|
if (rootFaviconMissing) {
|
|
106
|
-
console.log(`\n
|
|
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
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
|
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
|
|
133
|
+
console.log(`\n${warning(`Render-blocking (${b.renderBlocking.length}):`)}`);
|
|
131
134
|
for (const r of b.renderBlocking) {
|
|
132
|
-
console.log(
|
|
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
|
|
140
|
+
console.log(`\n${warning(`Large resources >100KB (${b.largeResources.length}):`)}`);
|
|
138
141
|
for (const r of b.largeResources) {
|
|
139
|
-
console.log(
|
|
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
|
|
147
|
+
console.log(`\n${warning(`Oversized images (${b.oversizedImages.length}):`)}`);
|
|
145
148
|
for (const img of b.oversizedImages) {
|
|
146
|
-
console.log(
|
|
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(
|
|
190
|
+
console.log(`${brand('Screenshot')} ${bold(url)}`);
|
|
187
191
|
if (meta.title)
|
|
188
|
-
console.log(
|
|
192
|
+
console.log(`${label('Web page title')} ${meta.title}`);
|
|
189
193
|
if (meta.finalUrl)
|
|
190
|
-
console.log(
|
|
194
|
+
console.log(`${label('Web page URL')} ${meta.finalUrl}`);
|
|
191
195
|
if (meta.status != null)
|
|
192
|
-
console.log(
|
|
196
|
+
console.log(`${label('Web page status')} ${meta.status}`);
|
|
193
197
|
if (meta.performance)
|
|
194
|
-
console.log(
|
|
198
|
+
console.log(`${label('Web page perf')} ${fmtPerformance(meta.performance)}`);
|
|
195
199
|
const sizePart = formatSize(s.screenshotSizeBytes) + (meta.full ? ' (full page)' : '');
|
|
196
|
-
console.log(
|
|
200
|
+
console.log(`${label('Screenshot size')} ${sizePart}`);
|
|
197
201
|
if (s.width && s.height)
|
|
198
|
-
console.log(
|
|
199
|
-
console.log(
|
|
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(
|
|
206
|
+
console.log(`${brand('Loading')} ${bold(url)} ${muted(`once → ${meta.screenshots.length} viewports`)}`);
|
|
203
207
|
if (meta.title)
|
|
204
|
-
console.log(
|
|
208
|
+
console.log(`${label('Web page title')} ${meta.title}`);
|
|
205
209
|
if (meta.finalUrl)
|
|
206
|
-
console.log(
|
|
210
|
+
console.log(`${label('Web page URL')} ${meta.finalUrl}`);
|
|
207
211
|
if (meta.status != null)
|
|
208
|
-
console.log(
|
|
212
|
+
console.log(`${label('Web page status')} ${meta.status}`);
|
|
209
213
|
if (meta.performance)
|
|
210
|
-
console.log(
|
|
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
|
|
218
|
+
console.log(`\n${brand('@ ' + dims)}`);
|
|
215
219
|
const sizePart = formatSize(s.screenshotSizeBytes) + (meta.full ? ' (full page)' : '');
|
|
216
|
-
console.log(
|
|
220
|
+
console.log(`${label('Screenshot size')} ${sizePart}`);
|
|
217
221
|
if (s.width && s.height)
|
|
218
|
-
console.log(
|
|
219
|
-
console.log(
|
|
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
|