badgr-cli 1.1.1 → 1.1.3

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 (73) hide show
  1. package/LICENSE +207 -0
  2. package/README.md +13 -6
  3. package/package.json +44 -2
  4. package/src/api.js +16 -0
  5. package/src/badgr.js +26 -10
  6. package/src/commands/batch.js +11 -0
  7. package/src/commands/billing.js +3 -3
  8. package/src/commands/comfyui.js +31 -15
  9. package/src/commands/connect.js +4 -1
  10. package/src/commands/diagnose.js +493 -0
  11. package/src/commands/embed.js +13 -10
  12. package/src/commands/job.js +246 -0
  13. package/src/commands/launch.js +152 -16
  14. package/src/commands/login.js +75 -20
  15. package/src/commands/run.js +74 -16
  16. package/src/commands/sbatch.js +6 -1
  17. package/src/commands/serve.js +44 -30
  18. package/src/commands/train.js +8 -12
  19. package/src/commands/transcribe.js +13 -10
  20. package/src/credentials.js +33 -0
  21. package/src/envFlag.js +10 -0
  22. package/src/fallback.js +13 -2
  23. package/src/onboarding.js +8 -1
  24. package/src/progress.js +48 -0
  25. package/src/commands/task.js +0 -25
  26. package/tests/agent-images.test.js +0 -17
  27. package/tests/api.test.js +0 -168
  28. package/tests/artifactDownload.test.js +0 -113
  29. package/tests/artifacts.test.js +0 -168
  30. package/tests/batch.test.js +0 -641
  31. package/tests/browser.test.js +0 -51
  32. package/tests/capacity.test.js +0 -68
  33. package/tests/commands.test.js +0 -417
  34. package/tests/config.test.js +0 -96
  35. package/tests/connect.test.js +0 -83
  36. package/tests/detect.test.js +0 -191
  37. package/tests/down.test.js +0 -150
  38. package/tests/errors.test.js +0 -130
  39. package/tests/fallback-timeout.test.js +0 -41
  40. package/tests/fanout.test.js +0 -124
  41. package/tests/gpu-doctor-classifiers.test.js +0 -402
  42. package/tests/gpu-doctor-doctor.test.js +0 -304
  43. package/tests/gpu-doctor-probe-cache.test.js +0 -110
  44. package/tests/gpu-doctor-probes.test.js +0 -257
  45. package/tests/heartbeat.test.js +0 -70
  46. package/tests/job-progress-poll.test.js +0 -136
  47. package/tests/launch-command-argv.test.js +0 -93
  48. package/tests/launch-readiness.test.js +0 -403
  49. package/tests/launch.test.js +0 -440
  50. package/tests/onboarding.test.js +0 -134
  51. package/tests/productized-dry-run.test.js +0 -141
  52. package/tests/productized-runners.test.js +0 -237
  53. package/tests/pull.test.js +0 -266
  54. package/tests/rerun.test.js +0 -94
  55. package/tests/restart.test.js +0 -88
  56. package/tests/router.test.js +0 -98
  57. package/tests/run-lifecycle.test.js +0 -1054
  58. package/tests/sbatch.test.js +0 -190
  59. package/tests/secrets.test.js +0 -16
  60. package/tests/serve-apps.test.js +0 -189
  61. package/tests/serve-lifecycle.test.js +0 -931
  62. package/tests/slurm.test.js +0 -77
  63. package/tests/spec.test.js +0 -201
  64. package/tests/status.test.js +0 -73
  65. package/tests/store.test.js +0 -187
  66. package/tests/task.test.js +0 -109
  67. package/tests/template.test.js +0 -556
  68. package/tests/train-lora-dataset.test.js +0 -176
  69. package/tests/upload.test.js +0 -79
  70. package/tests/workload-rerun.test.js +0 -56
  71. package/tests/workload-spec.test.js +0 -180
  72. package/tests/workload-templates.test.js +0 -865
  73. package/tests/workload-workspace-paths.test.js +0 -46
@@ -79,5 +79,8 @@ export async function connectCommand(args, chalk, deps = {}) {
79
79
  setCredential(provider, key);
80
80
  console.log(chalk.green(`\n ✓ ${alreadyConnected ? 'Updated' : 'Connected'} ${provider}`));
81
81
  console.log(chalk.dim(` Stored in ${CREDENTIALS_FILE} (owner-only file permissions)`));
82
- console.log(chalk.dim(` Used automatically by: badgr launch ${provider === 'anthropic' ? 'claude' : provider === 'openai' ? 'codex' : provider} "<task>"\n`));
82
+ const usageHint = provider === 'anthropic' ? 'badgr launch claude "<task>"'
83
+ : provider === 'openai' ? 'badgr launch codex "<task>"'
84
+ : `badgr launch cline/claude/codex --provider ${provider} --model <model-id> "<task>"`;
85
+ console.log(chalk.dim(` Used automatically by: ${usageHint}\n`));
83
86
  }
@@ -0,0 +1,493 @@
1
+ import fs from 'fs';
2
+ import { callApi } from '../api.js';
3
+ import { ensureLoggedIn } from '../onboarding.js';
4
+
5
+ const DIAGNOSE_HELP = `
6
+ Paste anything. Badgr detects the input, diagnoses it for free, then
7
+ shows either missing information, a verified template, or a capped
8
+ smoke-test plan. No GPU launches without explicit --approve.
9
+
10
+ Usage:
11
+ badgr diagnose "<input>"
12
+ badgr diagnose "<input>" --approve
13
+ badgr diagnose "<input>" --json
14
+ badgr diagnose <case_id_or_url> --approve Resume an existing case (e.g.
15
+ one shared via a case link)
16
+
17
+ Input (auto-detected):
18
+ GitHub issue URL https://github.com/org/repo/issues/N
19
+ GitHub repo URL https://github.com/org/repo
20
+ Docker image owner/image:tag or image:tag
21
+ Local log / file ./error.log or vllm.log
22
+ ComfyUI workflow workflow.json (JSON file)
23
+ Raw conversation paste Discord / Slack / support thread text
24
+ Existing case repro_xxxxxxxx or https://aibadgr.com/repro/repro_xxxxxxxx
25
+
26
+ Flags:
27
+ --approve Approve the capped smoke test after diagnosis (opens
28
+ browser sign-in automatically if not logged in)
29
+ --docker <image> Force Docker-image intake (override auto-detect)
30
+ --repo <url> Force repository intake (override auto-detect)
31
+ --comfyui <path> Force ComfyUI workflow intake (override auto-detect)
32
+ --github <url> Include a GitHub issue URL found inside pasted text
33
+ as additional context. Opt-in only, never fetched
34
+ automatically (see "Additional context found" below)
35
+ --json Machine-readable JSON output
36
+ --help, -h Show this help
37
+
38
+ Safety: nothing runs from AI-extracted data without --approve.
39
+ No GPU launches without explicit approval and a credit check.
40
+
41
+ Full interactive flow: https://aibadgr.com/run-issue`;
42
+
43
+ function detectInput(raw, flags) {
44
+ if (flags.docker) {
45
+ return {
46
+ kind: 'docker_image',
47
+ label: `Docker image: ${flags.docker}`,
48
+ body: { docker_image: flags.docker },
49
+ };
50
+ }
51
+ if (flags.repo) {
52
+ return {
53
+ kind: 'repo_url',
54
+ label: `GitHub repository: ${flags.repo}`,
55
+ body: { repo_url: flags.repo },
56
+ };
57
+ }
58
+ if (flags.comfyui) {
59
+ if (!fs.existsSync(flags.comfyui)) throw new Error(`File not found: ${flags.comfyui}`);
60
+ return {
61
+ kind: 'comfyui_workflow',
62
+ label: `ComfyUI workflow: ${flags.comfyui}`,
63
+ body: { workflow_json: fs.readFileSync(flags.comfyui, 'utf8') },
64
+ };
65
+ }
66
+
67
+ if (!raw) return null;
68
+
69
+ // A case_id (bare, or embedded in a /repro/<id> or /run-issue?case_id=<id>
70
+ // link — the two shapes an admin-prepared or self-created case gets
71
+ // shared as) resumes that existing case instead of diagnosing new input.
72
+ const bareCaseMatch = /^repro_[a-zA-Z0-9]+$/.exec(raw.trim());
73
+ const urlCaseMatch = /\/repro\/(repro_[a-zA-Z0-9]+)/.exec(raw) || /[?&]case_id=(repro_[a-zA-Z0-9]+)/.exec(raw);
74
+ const caseId = bareCaseMatch?.[0] || urlCaseMatch?.[1];
75
+ if (caseId) {
76
+ return { kind: 'existing_case', label: `Existing case: ${caseId}`, caseId };
77
+ }
78
+
79
+ if (/^https?:\/\/(www\.)?github\.com\/[^/]+\/[^/]+\/issues\/\d/.test(raw)) {
80
+ return {
81
+ kind: 'github_issue',
82
+ label: `GitHub issue: ${raw}`,
83
+ body: { github_url: raw },
84
+ };
85
+ }
86
+
87
+ if (/^https?:\/\/(www\.)?github\.com\//.test(raw)) {
88
+ return {
89
+ kind: 'repo_url',
90
+ label: `GitHub repository: ${raw}`,
91
+ body: { repo_url: raw },
92
+ };
93
+ }
94
+
95
+ if (fs.existsSync(raw)) {
96
+ const content = fs.readFileSync(raw, 'utf8');
97
+ if (raw.endsWith('.json') || raw.endsWith('.JSON')) {
98
+ let parsed;
99
+ try { parsed = JSON.parse(content); } catch { /* not valid JSON */ }
100
+ if (parsed && typeof parsed === 'object' && (parsed.nodes || parsed['1'] || parsed['0'])) {
101
+ return {
102
+ kind: 'comfyui_workflow',
103
+ label: `ComfyUI workflow: ${raw}`,
104
+ body: { workflow_json: content },
105
+ };
106
+ }
107
+ }
108
+ return {
109
+ kind: 'log_file',
110
+ label: `Log / error file: ${raw}`,
111
+ body: { text: content.slice(0, 50_000) },
112
+ };
113
+ }
114
+
115
+ if (
116
+ !raw.startsWith('http') &&
117
+ /^[a-z0-9][a-z0-9._\-]*(?:\/[a-z0-9._\-]+)*(?::[a-zA-Z0-9._\-]+)?$/.test(raw) &&
118
+ raw.length < 200
119
+ ) {
120
+ return {
121
+ kind: 'docker_image',
122
+ label: `Docker image: ${raw}`,
123
+ body: { docker_image: raw },
124
+ };
125
+ }
126
+
127
+ return {
128
+ kind: 'text',
129
+ label: 'Text / conversation',
130
+ body: { text: raw.slice(0, 50_000) },
131
+ };
132
+ }
133
+
134
+ function _printResult(result, chalk) {
135
+ const { extraction: e, static_incompatibilities, multi_gpu_detected, large_download, github_issue, detected_github_url } = result;
136
+
137
+ if (github_issue) {
138
+ console.log(` ${chalk.bold('Issue:')} ${github_issue.title} ${chalk.dim(`[${github_issue.state}]`)}`);
139
+ }
140
+
141
+ // The conversation stays primary — a GitHub URL found inside pasted text
142
+ // is only ever offered here, never fetched automatically.
143
+ if (detected_github_url) {
144
+ console.log();
145
+ console.log(` ${chalk.bold('Additional context found:')} ${detected_github_url}`);
146
+ console.log(` ${chalk.dim(`Include it: badgr diagnose "<input>" --github ${detected_github_url}`)}`);
147
+ }
148
+
149
+ const wt = e.workload_type && e.workload_type !== 'unknown' ? e.workload_type : null;
150
+ const conf = e.confidence ? ` ${chalk.dim(`(${Math.round(e.confidence * 100)}% confidence)`)}` : '';
151
+ if (wt) console.log(` ${chalk.bold('Workload:')} ${wt}${conf}`);
152
+ if (e.summary) console.log(` ${chalk.bold('Summary:')} ${e.summary}`);
153
+ console.log();
154
+
155
+ if (e.docker_images?.length) {
156
+ console.log(` ${chalk.bold('Image:')} ${e.docker_images[0]}${e.docker_images.length > 1 ? chalk.dim(` +${e.docker_images.length - 1} more`) : ''}`);
157
+ }
158
+ if (e.models?.length) {
159
+ console.log(` ${chalk.bold('Model:')} ${e.models[0]}${e.models.length > 1 ? chalk.dim(` +${e.models.length - 1} more`) : ''}`);
160
+ }
161
+ if (e.commands?.length) {
162
+ console.log(` ${chalk.bold('Command:')} ${e.commands[0]}`);
163
+ }
164
+ if (e.errors?.length) {
165
+ console.log(` ${chalk.bold('Errors:')}`);
166
+ for (const err of e.errors.slice(0, 3)) console.log(` ${chalk.red('✗')} ${err}`);
167
+ }
168
+
169
+ if (static_incompatibilities?.length) {
170
+ console.log();
171
+ console.log(` ${chalk.bold('Diagnosis:')} ${chalk.yellow('Static incompatibility — no GPU needed')}`);
172
+ for (const inc of static_incompatibilities) console.log(` ${chalk.yellow('!')} ${inc}`);
173
+ } else if (e.missing_information?.length) {
174
+ console.log();
175
+ console.log(` ${chalk.bold('Diagnosis:')} Missing information`);
176
+ for (const m of e.missing_information) console.log(` ${chalk.dim('?')} ${m}`);
177
+ } else if (wt) {
178
+ console.log();
179
+ console.log(` ${chalk.bold('Diagnosis:')} Workload detected — ready for capped test`);
180
+ }
181
+
182
+ if (multi_gpu_detected) {
183
+ console.log(` ${chalk.yellow('!')} Multi-GPU detected: ${multi_gpu_detected}`);
184
+ console.log(` ${chalk.dim('Capped test will use a single GPU only.')}`);
185
+ }
186
+ if (large_download) {
187
+ console.log(` ${chalk.yellow('!')} Large model download: ~${large_download.estimated_gb} GB`);
188
+ console.log(` ${chalk.dim('Funding approval required before test launches.')}`);
189
+ }
190
+ if (result.secret_types_detected?.length) {
191
+ console.log(` ${chalk.dim(`Secrets redacted before analysis: ${result.secret_types_detected.join(', ')}`)}`);
192
+ }
193
+ if (!result.ai_available) {
194
+ console.log(` ${chalk.dim('(AI extraction unavailable — pattern matching only)')}`);
195
+ }
196
+
197
+ console.log();
198
+ if (static_incompatibilities?.length) {
199
+ console.log(` ${chalk.bold('Recommended action:')} Fix the incompatibility listed above — no GPU test needed`);
200
+ } else if (e.missing_information?.length) {
201
+ console.log(` ${chalk.bold('Recommended action:')} Provide the missing details above, then re-run`);
202
+ } else {
203
+ console.log(` ${chalk.bold('Recommended action:')} Run ${chalk.cyan('badgr diagnose "<input>" --approve')} to launch a capped test`);
204
+ }
205
+ }
206
+
207
+ async function _doApprove(config, result, chalk) {
208
+ // Diagnosis and case creation stay anonymous and free — login is only
209
+ // required once we know a real GPU test needs approving (below).
210
+ const e = result.extraction;
211
+ const confirmBody = {
212
+ workload_type: e.workload_type || 'generic',
213
+ docker_image: e.docker_images?.[0] || null,
214
+ model_id: e.models?.[0] || null,
215
+ launch_command: e.commands?.[0] || null,
216
+ error_log: null,
217
+ expected_result: e.expected_result || null,
218
+ environment_variable_names: e.environment_variable_names || [],
219
+ gpu_requirements: Array.isArray(e.gpu_requirements)
220
+ ? e.gpu_requirements.join(', ')
221
+ : e.gpu_requirements || null,
222
+ source_summary: e.summary || null,
223
+ };
224
+
225
+ let caseData;
226
+ try {
227
+ console.log(chalk.dim(' Creating case...'));
228
+ caseData = await callApi('/run-issue/confirm', {
229
+ method: 'POST',
230
+ apiKey: config.apiKey || '',
231
+ baseUrl: config.baseUrl,
232
+ body: confirmBody,
233
+ timeoutMs: 20_000,
234
+ });
235
+ } catch (err) {
236
+ console.error(chalk.red(`\n ✗ Could not create case: ${err.message}\n`));
237
+ process.exitCode = 1;
238
+ return;
239
+ }
240
+
241
+ const { case_id: caseId, status, test_plan: plan, missing_information: missing } = caseData;
242
+
243
+ if (status === 'incompatible') {
244
+ console.log(chalk.yellow('\n Static incompatibility — no GPU test needed.'));
245
+ console.log();
246
+ return;
247
+ }
248
+ if (status === 'missing_information') {
249
+ console.log(chalk.yellow('\n Cannot approve — missing information:'));
250
+ for (const m of (missing || [])) console.log(` ? ${m}`);
251
+ console.log();
252
+ return;
253
+ }
254
+ if (status === 'template_matched') {
255
+ console.log(chalk.green('\n Matched a verified template — no new GPU test needed.'));
256
+ if (plan?.final_command) {
257
+ console.log();
258
+ console.log(` ${chalk.bold('Verified command:')}`);
259
+ console.log(` ${chalk.cyan(plan.final_command)}`);
260
+ }
261
+ console.log();
262
+ return;
263
+ }
264
+
265
+ if (plan) {
266
+ console.log();
267
+ if (plan.gpu_type) console.log(` ${chalk.bold('GPU:')} ${plan.gpu_type}`);
268
+ if (plan.estimated_cost) console.log(` ${chalk.bold('Maximum cost:')} $${Number(plan.estimated_cost).toFixed(2)}`);
269
+ if (plan.max_runtime_min) console.log(` ${chalk.bold('Max runtime:')} ${plan.max_runtime_min} min`);
270
+ if (plan.requires_reduced_test) console.log(` ${chalk.yellow('!')} Multi-GPU detected — test uses a single GPU`);
271
+ if (plan.requires_funding_approval) console.log(` ${chalk.yellow('!')} Large download — funding required`);
272
+ }
273
+
274
+ await _approveAndRun(config, caseId, plan, chalk);
275
+ }
276
+
277
+ // Claim → approve → run for a case that already exists (just created via
278
+ // /confirm above, or resumed via an existing_case input). Auth is required
279
+ // from here on — diagnosis and case creation stay anonymous and free, but a
280
+ // real GPU test needs a signed-in account. The CLI never grants a free-run
281
+ // entitlement (web admin only, see grant_free_run in run_issue_routes.py);
282
+ // it only redeems one a case already carries, same as normal billing.
283
+ async function _approveAndRun(config, caseId, plan, chalk) {
284
+ const printResumeHint = () =>
285
+ console.error(chalk.dim(` Resume this case: https://aibadgr.com/run-issue?case_id=${caseId}\n`));
286
+
287
+ let authConfig = config;
288
+ if (!authConfig.apiKey) {
289
+ if (!process.stdin.isTTY || !process.stdout.isTTY) {
290
+ console.error(chalk.red('\n ✗ Sign in required to approve a GPU test. Run: badgr login\n'));
291
+ printResumeHint();
292
+ process.exitCode = 1;
293
+ return;
294
+ }
295
+ console.log(chalk.yellow('\n Sign in to continue. Opening browser...'));
296
+ try {
297
+ authConfig = await ensureLoggedIn(authConfig, chalk);
298
+ } catch (err) {
299
+ console.error(chalk.red(`\n ✗ Sign-in failed: ${err.message}\n`));
300
+ printResumeHint();
301
+ process.exitCode = 1;
302
+ return;
303
+ }
304
+ }
305
+
306
+ try {
307
+ await callApi(`/run-issue/cases/${caseId}/claim`, {
308
+ method: 'POST', apiKey: authConfig.apiKey, baseUrl: authConfig.baseUrl, timeoutMs: 10_000,
309
+ });
310
+ } catch { /* already claimed — continue */ }
311
+
312
+ let approved;
313
+ try {
314
+ approved = await callApi(`/run-issue/cases/${caseId}/approve`, {
315
+ method: 'POST',
316
+ apiKey: authConfig.apiKey,
317
+ baseUrl: authConfig.baseUrl,
318
+ body: {
319
+ confirmed: true,
320
+ funding_approved: Boolean(plan?.requires_funding_approval),
321
+ reduced_test_confirmed: Boolean(plan?.requires_reduced_test),
322
+ },
323
+ timeoutMs: 15_000,
324
+ });
325
+ } catch (err) {
326
+ if (err.httpStatus === 402 || err.message?.includes('402')) {
327
+ console.error(chalk.yellow('\n ✗ Insufficient credits. Run: badgr billing\n'));
328
+ } else {
329
+ console.error(chalk.red(`\n ✗ Approve failed: ${err.message}\n`));
330
+ }
331
+ process.exitCode = 1;
332
+ return;
333
+ }
334
+
335
+ console.log(chalk.dim('\n Launching capped smoke test...'));
336
+ let ran;
337
+ try {
338
+ ran = await callApi(`/run-issue/cases/${caseId}/run`, {
339
+ method: 'POST', apiKey: authConfig.apiKey, baseUrl: authConfig.baseUrl, timeoutMs: 30_000,
340
+ });
341
+ } catch (err) {
342
+ console.error(chalk.red(`\n ✗ Launch failed: ${err.message}\n`));
343
+ process.exitCode = 1;
344
+ return;
345
+ }
346
+
347
+ console.log();
348
+ console.log(chalk.green(' Smoke test launched.'));
349
+ console.log(` ${chalk.bold('Case:')} ${caseId}`);
350
+ if (ran?.job_id) console.log(` ${chalk.bold('Job:')} ${ran.job_id}`);
351
+ if (ran?.free_run_used) console.log(` ${chalk.bold('Billing:')} Free run — no charge`);
352
+ console.log(` ${chalk.bold('Evidence:')} https://aibadgr.com/repro/${caseId}`);
353
+ console.log();
354
+ }
355
+
356
+ // Resume an existing case (bare case_id or a shared /repro or /run-issue
357
+ // link) instead of diagnosing new input. No /confirm — the case already
358
+ // exists; this only reads its current state and, with --approve, redeems
359
+ // whatever entitlement/billing state it already carries.
360
+ async function _diagnoseExistingCase(config, caseId, flags, chalk) {
361
+ let caseData;
362
+ try {
363
+ caseData = await callApi(`/run-issue/cases/${caseId}`, {
364
+ apiKey: config.apiKey || '', baseUrl: config.baseUrl, timeoutMs: 15_000,
365
+ });
366
+ } catch (err) {
367
+ console.error(chalk.red(`\n ✗ Could not load case ${caseId}: ${err.message}\n`));
368
+ process.exitCode = 1;
369
+ return;
370
+ }
371
+
372
+ if (flags.json) {
373
+ console.log(JSON.stringify(caseData, null, 2));
374
+ if (flags.approve) await _approveAndRun(config, caseId, caseData.test_plan, chalk);
375
+ return;
376
+ }
377
+
378
+ console.log();
379
+ console.log(` ${chalk.bold('Case:')} ${caseId}`);
380
+ console.log(` ${chalk.bold('Status:')} ${caseData.status}`);
381
+ if (caseData.workload_type) console.log(` ${chalk.bold('Workload:')} ${caseData.workload_type}`);
382
+ if (caseData.known_command) console.log(` ${chalk.bold('Command:')} ${caseData.known_command}`);
383
+ if ((caseData.missing_information || []).length) {
384
+ console.log();
385
+ console.log(` ${chalk.bold('Missing information:')}`);
386
+ for (const m of caseData.missing_information) console.log(` ? ${m}`);
387
+ }
388
+
389
+ console.log();
390
+ if (caseData.free_verification_consumed) {
391
+ console.log(` ${chalk.dim('This case\'s free run has already been used — further verification is normal billing.')}`);
392
+ } else if (caseData.free_verification_available) {
393
+ console.log(` ${chalk.green(`1 free verification job available — up to $${(caseData.free_verification_max_cost_usd ?? 5).toFixed(0)}`)}`);
394
+ } else if (caseData.test_plan) {
395
+ console.log(` ${chalk.dim(`Verification available — max $${Number(caseData.test_plan.max_cost_usd ?? 0).toFixed(2)}`)}`);
396
+ }
397
+ console.log();
398
+
399
+ if (flags.approve) {
400
+ await _approveAndRun(config, caseId, caseData.test_plan, chalk);
401
+ } else {
402
+ console.log(` ${chalk.bold('Resume:')} badgr diagnose ${caseId} --approve`);
403
+ console.log();
404
+ }
405
+ }
406
+
407
+ export async function diagnoseCommand(config, args, chalk) {
408
+ if (args.includes('--help') || args.includes('-h')) {
409
+ console.log(DIAGNOSE_HELP);
410
+ return;
411
+ }
412
+
413
+ const flags = {};
414
+ let positional = null;
415
+ let i = 0;
416
+ while (i < args.length) {
417
+ const a = args[i];
418
+ if (a === '--docker') { flags.docker = args[++i]; i++; continue; }
419
+ if (a === '--repo') { flags.repo = args[++i]; i++; continue; }
420
+ if (a === '--comfyui') { flags.comfyui = args[++i]; i++; continue; }
421
+ if (a === '--github') { flags.github = args[++i]; i++; continue; }
422
+ if (a === '--approve') { flags.approve = true; i++; continue; }
423
+ if (a === '--json') { flags.json = true; i++; continue; }
424
+ if (!a.startsWith('-') && positional === null) positional = a;
425
+ i++;
426
+ }
427
+
428
+ if (!positional && !flags.docker && !flags.repo && !flags.comfyui) {
429
+ console.log(DIAGNOSE_HELP);
430
+ return;
431
+ }
432
+
433
+ let detected;
434
+ try {
435
+ detected = detectInput(positional, flags);
436
+ } catch (err) {
437
+ console.error(chalk.red(`\n ✗ ${err.message}\n`));
438
+ process.exitCode = 1;
439
+ return;
440
+ }
441
+
442
+ if (!detected) {
443
+ console.log(DIAGNOSE_HELP);
444
+ return;
445
+ }
446
+
447
+ if (detected.kind === 'existing_case') {
448
+ await _diagnoseExistingCase(config, detected.caseId, flags, chalk);
449
+ return;
450
+ }
451
+
452
+ // --github opts into fetching a GitHub issue URL found inside pasted text
453
+ // (see the "Additional context found" notice) — never fetched by default.
454
+ if (flags.github) detected.body.github_url = flags.github;
455
+
456
+ if (!flags.json) {
457
+ console.log();
458
+ console.log(` ${chalk.bold('Detected:')} ${detected.label}`);
459
+ console.log(chalk.dim(' Analysing...'));
460
+ }
461
+
462
+ let result;
463
+ try {
464
+ result = await callApi('/run-issue/extract', {
465
+ method: 'POST',
466
+ apiKey: config.apiKey || '',
467
+ baseUrl: config.baseUrl,
468
+ body: detected.body,
469
+ timeoutMs: 120_000,
470
+ });
471
+ } catch (err) {
472
+ console.error(chalk.red(`\n ✗ ${err.message}\n`));
473
+ process.exitCode = 1;
474
+ return;
475
+ }
476
+
477
+ if (flags.json) {
478
+ console.log(JSON.stringify({ detected: detected.kind, ...result }, null, 2));
479
+ return;
480
+ }
481
+
482
+ console.log();
483
+ _printResult(result, chalk);
484
+
485
+ console.log();
486
+ console.log(chalk.dim(' Free diagnosis. No GPU was provisioned.'));
487
+ console.log(chalk.dim(' Full interactive flow: https://aibadgr.com/run-issue'));
488
+ console.log();
489
+
490
+ if (flags.approve) {
491
+ await _doApprove(config, result, chalk);
492
+ }
493
+ }
@@ -14,6 +14,8 @@ import { requireApiKey } from '../config.js';
14
14
  import { addReceipt, generateReceiptId } from '../store.js';
15
15
  import { normalizeTier, callWithFallback } from '../fallback.js';
16
16
  import { monitorBatchJob, fmtRuntime } from '../batch.js';
17
+ import { parseEnvFlag } from '../envFlag.js';
18
+ import { printCapacityPreview, formatTierLabel } from '../progress.js';
17
19
 
18
20
  const EMBED_IMAGE = 'vllm/vllm-openai:latest';
19
21
  const DEFAULT_EMBED_MODEL = 'BAAI/bge-large-en-v1.5';
@@ -35,6 +37,7 @@ export function parseEmbedArgs(args) {
35
37
  if (a === '--region') { flags.region = args[++i]; i++; continue; }
36
38
  if (a === '--batch-size') { flags.batchSize = parseInt(args[++i], 10); i++; continue; }
37
39
  if (a === '--detach') { flags.detach = true; i++; continue; }
40
+ if (a === '--dry-run') { flags.dryRun = true; i++; continue; }
38
41
  if (a === '--env') {
39
42
  const kv = args[++i]; i++;
40
43
  if (!flags.env) flags.env = [];
@@ -49,15 +52,6 @@ export function parseEmbedArgs(args) {
49
52
  return { model, input, flags };
50
53
  }
51
54
 
52
- function parseEnvFlag(envList) {
53
- const obj = {};
54
- for (const kv of (envList || [])) {
55
- const idx = kv.indexOf('=');
56
- if (idx > 0) obj[kv.slice(0, idx)] = kv.slice(idx + 1);
57
- }
58
- return obj;
59
- }
60
-
61
55
  function isUrl(s) {
62
56
  return /^(https?|s3|gs|hf):\/\//i.test(s);
63
57
  }
@@ -120,14 +114,23 @@ export async function embedCommand(config, args, chalk) {
120
114
  ...(flags.batchSize ? { EMBED_BATCH_SIZE: String(flags.batchSize) } : {}),
121
115
  };
122
116
 
123
- console.log(chalk.bold('\n📐 Embeddings\n'));
117
+ console.log(chalk.bold(flags.dryRun ? '\n📐 Dry run — no GPU will be provisioned — Embeddings\n' : '\n📐 Embeddings\n'));
124
118
  console.log(` ${chalk.bold('Input:')} ${resolved.inputLabel}`);
125
119
  console.log(` ${chalk.bold('Model:')} ${model}`);
126
120
  console.log(` ${chalk.bold('Image:')} ${EMBED_IMAGE}`);
127
121
  console.log(` ${chalk.bold('GPU:')} ${gpu === 'AUTO' ? chalk.dim('auto (8+ GB VRAM)') : gpu}`);
122
+ if (flags.dryRun) console.log(` ${chalk.bold('Tier:')} ${formatTierLabel(effectiveTier)}`);
128
123
  console.log(` ${chalk.bold('Max runtime:')} ${maxRuntimeMin}min`);
129
124
  if (maxCost) console.log(` ${chalk.bold('Max cost:')} $${maxCost.toFixed(2)}`);
125
+ if (flags.env?.length) console.log(` ${chalk.bold('Env:')} ${flags.env.join(', ')}`);
130
126
  console.log();
127
+
128
+ if (flags.dryRun) {
129
+ await printCapacityPreview(chalk, config, { gpu, region: flags.region?.toUpperCase(), maxPrice: flags.maxPrice });
130
+ console.log(chalk.dim(' Remove --dry-run to submit.\n'));
131
+ return;
132
+ }
133
+
131
134
  process.stdout.write(chalk.dim(' Finding suitable capacity...\n'));
132
135
 
133
136
  function buildBody(tierOverride) {