gipity 1.0.356 → 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 (60) hide show
  1. package/dist/banner.js +3 -1
  2. package/dist/capture/sources/claude-code.js +76 -11
  3. package/dist/commands/add.js +45 -28
  4. package/dist/commands/agent.js +3 -5
  5. package/dist/commands/approval.js +3 -3
  6. package/dist/commands/audit.js +2 -2
  7. package/dist/commands/chat.js +4 -4
  8. package/dist/commands/claude.js +8 -9
  9. package/dist/commands/credits.js +1 -1
  10. package/dist/commands/db.js +7 -6
  11. package/dist/commands/deploy.js +5 -8
  12. package/dist/commands/doctor.js +11 -13
  13. package/dist/commands/domain.js +18 -15
  14. package/dist/commands/email.js +0 -4
  15. package/dist/commands/fn.js +2 -2
  16. package/dist/commands/generate.js +73 -18
  17. package/dist/commands/job.js +6 -6
  18. package/dist/commands/location.js +7 -7
  19. package/dist/commands/login.js +2 -16
  20. package/dist/commands/logout.js +2 -3
  21. package/dist/commands/logs.js +1 -1
  22. package/dist/commands/page-eval.js +48 -7
  23. package/dist/commands/page-fetch.js +136 -0
  24. package/dist/commands/page-inspect.js +59 -29
  25. package/dist/commands/page-screenshot.js +51 -41
  26. package/dist/commands/page-test.js +86 -0
  27. package/dist/commands/page.js +8 -3
  28. package/dist/commands/plan.js +4 -4
  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 +16 -3
  34. package/dist/commands/service.js +54 -0
  35. package/dist/commands/skill.js +2 -1
  36. package/dist/commands/status.js +2 -2
  37. package/dist/commands/sync.js +4 -1
  38. package/dist/commands/test.js +7 -13
  39. package/dist/commands/text.js +148 -0
  40. package/dist/commands/uninstall.js +20 -42
  41. package/dist/commands/update.js +0 -2
  42. package/dist/commands/upload.js +4 -4
  43. package/dist/commands/workflow.js +11 -16
  44. package/dist/config.js +8 -1
  45. package/dist/help-skills.js +1 -0
  46. package/dist/helpers/output.js +52 -8
  47. package/dist/helpers/text-analysis.js +200 -0
  48. package/dist/hooks/capture-runner.js +32 -8
  49. package/dist/index.js +35 -2
  50. package/dist/knowledge.js +32 -7
  51. package/dist/progress.js +60 -0
  52. package/dist/project-setup.js +5 -1
  53. package/dist/provider-docs.js +7 -7
  54. package/dist/relay/daemon.js +11 -1
  55. package/dist/relay/stream-json.js +45 -8
  56. package/dist/setup.js +38 -11
  57. package/dist/sync.js +30 -8
  58. package/dist/updater/shim.js +18 -4
  59. package/dist/upload.js +6 -0
  60. package/package.json +5 -4
@@ -1,7 +1,7 @@
1
1
  import { Command } from 'commander';
2
2
  import { get, post, del } from '../api.js';
3
3
  import { getConfig, requireConfig } from '../config.js';
4
- import { error as clrError, success, muted } from '../colors.js';
4
+ import { error as clrError, success, muted, brand } from '../colors.js';
5
5
  import { run, printList } from '../helpers/index.js';
6
6
  export const domainCommand = new Command('domain')
7
7
  .description('Manage custom domains')
@@ -21,7 +21,7 @@ export const domainCommand = new Command('domain')
21
21
  console.log(JSON.stringify(res.data));
22
22
  return;
23
23
  }
24
- console.log(`Domains: ${count}/${limit}\n`);
24
+ console.log(`${muted('Domains:')} ${brand(`${count}/${limit}`)}\n`);
25
25
  if (domains.length === 0) {
26
26
  console.log('No custom domains.');
27
27
  return;
@@ -34,18 +34,21 @@ export const domainCommand = new Command('domain')
34
34
  grouped.set(key, []);
35
35
  grouped.get(key).push(d);
36
36
  }
37
+ let first = true;
37
38
  for (const [label, doms] of grouped) {
38
- console.log(label);
39
+ if (!first)
40
+ console.log('');
41
+ first = false;
42
+ console.log(brand(label));
39
43
  for (const d of doms) {
40
- console.log(` ${d.domain} ${muted(d.status)} ${muted(`[${d.shortGuid}]`)}`);
44
+ console.log(`${d.domain} ${muted(d.status)} ${muted(`[${d.shortGuid}]`)}`);
41
45
  }
42
- console.log();
43
46
  }
44
47
  }
45
48
  else {
46
49
  const config = requireConfig();
47
50
  const res = await get(`/projects/${config.projectGuid}/domains`);
48
- printList(res.data, opts, 'No custom domains configured.', d => ` ${d.domain} ${muted(d.status)} ${muted(`[${d.short_guid}]`)}`);
51
+ printList(res.data, opts, 'No custom domains configured.', d => `${d.domain} ${muted(d.status)} ${muted(`[${d.short_guid}]`)}`);
49
52
  }
50
53
  break;
51
54
  }
@@ -64,15 +67,15 @@ export const domainCommand = new Command('domain')
64
67
  console.log(success(`Domain "${data.domain.domain}" added.`));
65
68
  console.log('');
66
69
  console.log('Add this DNS record:');
67
- console.log(` Type: ${data.instructions.type}`);
68
- console.log(` Name: ${data.instructions.name}`);
69
- console.log(` Target: ${data.instructions.target}`);
70
+ console.log(`${muted('Type:'.padEnd(8))}${data.instructions.type}`);
71
+ console.log(`${muted('Name:'.padEnd(8))}${data.instructions.name}`);
72
+ console.log(`${muted('Target:'.padEnd(8))}${data.instructions.target}`);
70
73
  if (data.instructions.note) {
71
74
  console.log('');
72
75
  console.log(data.instructions.note);
73
76
  }
74
77
  console.log('');
75
- console.log(`Then run: gipity domain verify ${data.domain.short_guid}`);
78
+ console.log(muted(`Then run: gipity domain verify ${data.domain.short_guid}`));
76
79
  }
77
80
  break;
78
81
  }
@@ -121,11 +124,11 @@ export const domainCommand = new Command('domain')
121
124
  default:
122
125
  console.log('Usage: gipity domain [list|add|verify|remove]');
123
126
  console.log('');
124
- console.log(' gipity domain list List project domains');
125
- console.log(' gipity domain list --all List all domains across projects');
126
- console.log(' gipity domain add <domain.com> Add a custom domain (requires project)');
127
- console.log(' gipity domain verify <guid> Verify DNS and activate (requires project)');
128
- console.log(' gipity domain remove <guid> Remove a custom domain');
127
+ console.log(`${brand('gipity domain list')} ${muted('List project domains')}`);
128
+ console.log(`${brand('gipity domain list --all')} ${muted('List all domains across projects')}`);
129
+ console.log(`${brand('gipity domain add <domain.com>')} ${muted('Add a custom domain (requires project)')}`);
130
+ console.log(`${brand('gipity domain verify <guid>')} ${muted('Verify DNS and activate (requires project)')}`);
131
+ console.log(`${brand('gipity domain remove <guid>')} ${muted('Remove a custom domain')}`);
129
132
  }
130
133
  }));
131
134
  //# sourceMappingURL=domain.js.map
@@ -41,15 +41,11 @@ export const emailCommand = new Command('email')
41
41
  const recap = res.data.to.join(', ')
42
42
  + (res.data.cc.length ? `, cc: ${res.data.cc.join(', ')}` : '')
43
43
  + (res.data.bcc.length ? `, bcc: ${res.data.bcc.length}` : '');
44
- console.log('');
45
44
  console.log(success(`Email sent to ${recap}: ${res.data.subject}`));
46
- console.log('');
47
45
  }
48
46
  }
49
47
  catch (err) {
50
- console.log('');
51
48
  console.error(clrError(`Email failed: ${err.message}`));
52
- console.log('');
53
49
  process.exit(1);
54
50
  }
55
51
  });
@@ -14,7 +14,7 @@ fnCommand
14
14
  const res = await get(`/projects/${config.projectGuid}/functions`);
15
15
  printList(res.data, opts, 'No functions defined.', f => {
16
16
  const line = `${bold(f.name)} ${muted(`v${f.version}`)} ${muted(f.auth_level)} ${muted(`timeout=${f.timeout_ms}ms`)}`;
17
- return f.description ? `${line}\n ${muted(f.description)}` : line;
17
+ return f.description ? `${line}\n${muted(f.description)}` : line;
18
18
  });
19
19
  }));
20
20
  fnCommand
@@ -30,7 +30,7 @@ fnCommand
30
30
  const ts = new Date(log.created_at).toLocaleString();
31
31
  const statusColor = log.status === 'success' ? success : log.status === 'error' ? clrError : muted;
32
32
  const line = `${statusColor(log.status)} ${dur} ${muted(log.trigger_type || 'http')} ${muted(ts)}`;
33
- return log.error_message ? `${line}\n ${clrError(`error: ${log.error_message}`)}` : line;
33
+ return log.error_message ? `${line}\n${clrError(`error: ${log.error_message}`)}` : line;
34
34
  });
35
35
  }));
36
36
  fnCommand
@@ -2,15 +2,18 @@ import { Command } from 'commander';
2
2
  import { post } from '../api.js';
3
3
  import { resolveProjectContext } from '../config.js';
4
4
  import { writeFileSync } from 'fs';
5
+ import { resolve as resolvePath } from 'path';
5
6
  import { error as clrError, success, muted, info } from '../colors.js';
6
7
  import { IMAGE_MODELS_DOC, IMAGE_GEMINI_ASPECT_RATIOS, IMAGE_GEMINI_SIZES, VIDEO_MODELS_DOC, TTS_PROVIDER_DESCRIPTIONS } from '../provider-docs.js';
7
- /** Download a URL and save to a local file */
8
+ /** Download a URL and save to a local file. Returns the absolute path written,
9
+ * so callers can report exactly where the file landed. */
8
10
  async function downloadFile(url, filename) {
9
11
  const res = await fetch(url);
10
12
  if (!res.ok)
11
13
  throw new Error(`Download failed: ${res.status}`);
12
14
  const buffer = Buffer.from(await res.arrayBuffer());
13
15
  writeFileSync(filename, buffer);
16
+ return resolvePath(filename);
14
17
  }
15
18
  // ── IMAGE ──────────────────────────────────────────────────────────────
16
19
  const imageCommand = new Command('image')
@@ -25,16 +28,16 @@ Gemini-specific options:
25
28
  Examples:
26
29
  gipity generate image "a cat wearing a top hat"
27
30
  gipity generate image "landscape sunset" --provider gemini --aspect-ratio 16:9 --image-size 2K
28
- 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
29
32
  gipity generate image "abstract art" --provider bfl --model flux-2-pro -o art.png`)
30
33
  .argument('<prompt>', 'Text description of the image to generate')
31
34
  .option('--provider <provider>', 'Image provider: openai, bfl, or gemini (default: bfl)')
32
35
  .option('--model <model>', 'Model ID (see provider list above)')
33
36
  .option('--size <size>', 'Dimensions as WxH, e.g. "1024x1024" (OpenAI/BFL)')
34
- .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)')
35
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')
36
39
  .option('--image-size <size>', 'Output resolution (Gemini only): 512, 1K, 2K, 4K')
37
- .option('-o, --output <file>', 'Output filename (default: generated.png)')
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.')
38
41
  .option('--json', 'Output as JSON')
39
42
  .action(async (prompt, opts) => {
40
43
  try {
@@ -50,14 +53,14 @@ Examples:
50
53
  });
51
54
  const ext = result.content_type.includes('png') ? 'png' : 'jpg';
52
55
  const filename = opts.output || `generated.${ext}`;
53
- await downloadFile(result.url, filename);
56
+ const savedPath = await downloadFile(result.url, filename);
54
57
  if (opts.json) {
55
- console.log(JSON.stringify({ ...result, saved: filename }));
58
+ console.log(JSON.stringify({ ...result, saved: savedPath }));
56
59
  }
57
60
  else {
58
61
  const sizeKb = Math.round(result.size_bytes / 1024);
59
62
  console.log(`${muted(`Generated with ${result.provider}/${result.model} (${sizeKb}KB)`)}`);
60
- console.log(success(`Saved to ${filename}`));
63
+ console.log(success(`Saved to ${savedPath}`));
61
64
  }
62
65
  }
63
66
  catch (err) {
@@ -88,12 +91,13 @@ Examples:
88
91
  .option('--model <model>', 'Veo model: veo-3.1-generate-preview (quality), veo-3.1-fast-generate-preview (speed), veo-3.1-lite-generate-preview (budget)')
89
92
  .option('--aspect <ratio>', 'Aspect ratio: 16:9 (landscape), 9:16 (portrait), 1:1 (square)')
90
93
  .option('--resolution <res>', 'Video resolution: 720p, 1080p, 4k')
91
- .option('-o, --output <file>', 'Output filename (default: generated.mp4)')
94
+ .option('-o, --output <file>', 'Output path (default ./generated.mp4). For a clip your app ships, write it into the source tree so it deploys, e.g. -o src/assets/video/clip.mp4; the cwd default is fine for one-off generation.')
92
95
  .option('--json', 'Output as JSON')
93
96
  .action(async (prompt, opts) => {
94
97
  try {
95
98
  const { config } = await resolveProjectContext();
96
- 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
97
101
  const result = await post(`/projects/${config.projectGuid}/generate/video`, {
98
102
  prompt,
99
103
  model: opts.model,
@@ -101,14 +105,14 @@ Examples:
101
105
  resolution: opts.resolution,
102
106
  });
103
107
  const filename = opts.output || 'generated.mp4';
104
- await downloadFile(result.url, filename);
108
+ const savedPath = await downloadFile(result.url, filename);
105
109
  if (opts.json) {
106
- console.log(JSON.stringify({ ...result, saved: filename }));
110
+ console.log(JSON.stringify({ ...result, saved: savedPath }));
107
111
  }
108
112
  else {
109
113
  const sizeKb = Math.round(result.size_bytes / 1024);
110
114
  console.log(`${muted(`Generated with ${result.provider}/${result.model} (${sizeKb}KB)`)}`);
111
- console.log(success(`Saved to ${filename}`));
115
+ console.log(success(`Saved to ${savedPath}`));
112
116
  }
113
117
  }
114
118
  catch (err) {
@@ -137,7 +141,7 @@ Examples:
137
141
  .option('--voice <voice>', 'Voice ID or name (provider-specific)')
138
142
  .option('--language <code>', 'BCP-47 language code, e.g. ja-JP, es-ES (Gemini only, 60+ languages)')
139
143
  .option('--speakers <json>', 'Multi-speaker config as JSON array (Gemini only, up to 2 speakers)')
140
- .option('-o, --output <file>', 'Output filename (default: speech.mp3)')
144
+ .option('-o, --output <file>', 'Output path (default ./speech.mp3). For audio your app ships, write it into the source tree so it deploys, e.g. -o src/assets/sounds/intro.mp3; the cwd default is fine for one-off generation.')
141
145
  .option('--json', 'Output as JSON')
142
146
  .action(async (text, opts) => {
143
147
  try {
@@ -160,14 +164,14 @@ Examples:
160
164
  speakers,
161
165
  });
162
166
  const filename = opts.output || 'speech.mp3';
163
- await downloadFile(result.url, filename);
167
+ const savedPath = await downloadFile(result.url, filename);
164
168
  if (opts.json) {
165
- console.log(JSON.stringify({ ...result, saved: filename }));
169
+ console.log(JSON.stringify({ ...result, saved: savedPath }));
166
170
  }
167
171
  else {
168
172
  const sizeKb = Math.round(result.size_bytes / 1024);
169
173
  console.log(`${muted(`Generated with ${result.provider} (${sizeKb}KB)`)}`);
170
- console.log(success(`Saved to ${filename}`));
174
+ console.log(success(`Saved to ${savedPath}`));
171
175
  }
172
176
  }
173
177
  catch (err) {
@@ -175,10 +179,61 @@ Examples:
175
179
  process.exit(1);
176
180
  }
177
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
+ });
178
232
  // ── PARENT COMMAND ─────────────────────────────────────────────────────
179
233
  export const generateCommand = new Command('generate')
180
- .description('Generate images, video, or speech')
234
+ .description('Generate images, video, speech, or music')
181
235
  .addCommand(imageCommand)
182
236
  .addCommand(videoCommand)
183
- .addCommand(speechCommand);
237
+ .addCommand(speechCommand)
238
+ .addCommand(musicCommand);
184
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,7 +1,38 @@
1
1
  import { Command } from 'commander';
2
- import { post } from '../api.js';
3
- import { brand, bold, muted } from '../colors.js';
2
+ import { post, get, ApiError } from '../api.js';
3
+ import { brand, bold, muted, warning } from '../colors.js';
4
4
  import { run } from '../helpers/index.js';
5
+ const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
6
+ /** Poll the async eval job until it finishes. Eval runs server-side as a
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) {
10
+ // Generous client budget: the server work is bounded by --wait plus browser
11
+ // open/settle overhead; give it that plus headroom before giving up.
12
+ const deadline = Date.now() + waitMs + 60_000;
13
+ let missCount = 0;
14
+ while (Date.now() < deadline) {
15
+ let rec;
16
+ try {
17
+ rec = (await get(`/tools/browser/eval/${evalJobId}`)).data;
18
+ }
19
+ catch (err) {
20
+ // A 404 right after submit can happen if the record hasn't landed yet;
21
+ // tolerate a few, then treat a persistent 404 as the job being gone.
22
+ if (err instanceof ApiError && err.statusCode === 404 && missCount++ < 3) {
23
+ await sleep(500);
24
+ continue;
25
+ }
26
+ throw err;
27
+ }
28
+ if (rec.status === 'done')
29
+ return rec;
30
+ if (rec.status === 'error')
31
+ throw new ApiError(rec.httpStatus, rec.code, rec.reason);
32
+ await sleep(1000);
33
+ }
34
+ throw new ApiError(504, 'EVAL_TIMEOUT', 'Eval did not finish in time; narrow the expression or lower --wait');
35
+ }
5
36
  // The long-tail escape hatch alongside `page inspect`'s fixed bundle: when the
6
37
  // curated metrics don't cover what you need (computed styles, element rects,
7
38
  // visibility, z-index stacks), eval an expression in page context and get the
@@ -11,21 +42,31 @@ export const pageEvalCommand = new Command('eval')
11
42
  .argument('<url>', 'URL to load')
12
43
  .argument('<expr>', 'JavaScript expression to evaluate in page context (result is JSON-serialized)')
13
44
  .option('--wait <ms>', 'Sleep this many ms after DOMContentLoaded before evaluating (lets late async work settle)', '500')
45
+ .option('--wait-for <selector>', 'Wait until this CSS selector appears before evaluating (deterministic; replaces --wait)')
46
+ .option('--wait-timeout <ms>', 'Max ms to wait for --wait-for before giving up', '5000')
14
47
  .option('--json', 'Output as JSON')
15
48
  .action((url, expr, opts) => run('Page eval', async () => {
16
49
  const parsedWait = parseInt(opts.wait, 10);
17
50
  const waitMs = Number.isFinite(parsedWait) && parsedWait >= 0 ? parsedWait : 500;
18
- const res = await post('/tools/browser/eval', { url, expr, waitMs });
19
- const d = res.data;
51
+ const parsedTimeout = parseInt(opts.waitTimeout, 10);
52
+ const waitForTimeoutMs = Number.isFinite(parsedTimeout) && parsedTimeout >= 0 ? parsedTimeout : 5000;
53
+ const kickoff = await post('/tools/browser/eval', {
54
+ url, expr, waitMs,
55
+ waitForSelector: opts.waitFor || undefined,
56
+ waitForTimeoutMs: opts.waitFor ? waitForTimeoutMs : undefined,
57
+ });
58
+ const d = await pollEvalResult(kickoff.data.evalJobId, waitMs);
20
59
  if (opts.json) {
21
60
  console.log(JSON.stringify(d));
22
61
  return;
23
62
  }
24
- console.log(`\n${brand('Eval')} ${bold(d.url || url)}`);
25
- 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}`);
26
68
  console.log(`\n${d.result || muted('(empty result)')}`);
27
69
  if (d.truncated)
28
70
  console.log(muted('\n(result truncated to fit context - narrow the expression for the full value)'));
29
- console.log('');
30
71
  }));
31
72
  //# sourceMappingURL=page-eval.js.map