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.
- package/LICENSE +207 -0
- package/README.md +13 -6
- package/package.json +44 -2
- package/src/api.js +16 -0
- package/src/badgr.js +26 -10
- package/src/commands/batch.js +11 -0
- package/src/commands/billing.js +3 -3
- package/src/commands/comfyui.js +31 -15
- package/src/commands/connect.js +4 -1
- package/src/commands/diagnose.js +493 -0
- package/src/commands/embed.js +13 -10
- package/src/commands/job.js +246 -0
- package/src/commands/launch.js +152 -16
- package/src/commands/login.js +75 -20
- package/src/commands/run.js +74 -16
- package/src/commands/sbatch.js +6 -1
- package/src/commands/serve.js +44 -30
- package/src/commands/train.js +8 -12
- package/src/commands/transcribe.js +13 -10
- package/src/credentials.js +33 -0
- package/src/envFlag.js +10 -0
- package/src/fallback.js +13 -2
- package/src/onboarding.js +8 -1
- package/src/progress.js +48 -0
- package/src/commands/task.js +0 -25
- package/tests/agent-images.test.js +0 -17
- package/tests/api.test.js +0 -168
- package/tests/artifactDownload.test.js +0 -113
- package/tests/artifacts.test.js +0 -168
- package/tests/batch.test.js +0 -641
- package/tests/browser.test.js +0 -51
- package/tests/capacity.test.js +0 -68
- package/tests/commands.test.js +0 -417
- package/tests/config.test.js +0 -96
- package/tests/connect.test.js +0 -83
- package/tests/detect.test.js +0 -191
- package/tests/down.test.js +0 -150
- package/tests/errors.test.js +0 -130
- package/tests/fallback-timeout.test.js +0 -41
- package/tests/fanout.test.js +0 -124
- package/tests/gpu-doctor-classifiers.test.js +0 -402
- package/tests/gpu-doctor-doctor.test.js +0 -304
- package/tests/gpu-doctor-probe-cache.test.js +0 -110
- package/tests/gpu-doctor-probes.test.js +0 -257
- package/tests/heartbeat.test.js +0 -70
- package/tests/job-progress-poll.test.js +0 -136
- package/tests/launch-command-argv.test.js +0 -93
- package/tests/launch-readiness.test.js +0 -403
- package/tests/launch.test.js +0 -440
- package/tests/onboarding.test.js +0 -134
- package/tests/productized-dry-run.test.js +0 -141
- package/tests/productized-runners.test.js +0 -237
- package/tests/pull.test.js +0 -266
- package/tests/rerun.test.js +0 -94
- package/tests/restart.test.js +0 -88
- package/tests/router.test.js +0 -98
- package/tests/run-lifecycle.test.js +0 -1054
- package/tests/sbatch.test.js +0 -190
- package/tests/secrets.test.js +0 -16
- package/tests/serve-apps.test.js +0 -189
- package/tests/serve-lifecycle.test.js +0 -931
- package/tests/slurm.test.js +0 -77
- package/tests/spec.test.js +0 -201
- package/tests/status.test.js +0 -73
- package/tests/store.test.js +0 -187
- package/tests/task.test.js +0 -109
- package/tests/template.test.js +0 -556
- package/tests/train-lora-dataset.test.js +0 -176
- package/tests/upload.test.js +0 -79
- package/tests/workload-rerun.test.js +0 -56
- package/tests/workload-spec.test.js +0 -180
- package/tests/workload-templates.test.js +0 -865
- package/tests/workload-workspace-paths.test.js +0 -46
package/src/commands/connect.js
CHANGED
|
@@ -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
|
-
|
|
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
|
+
}
|
package/src/commands/embed.js
CHANGED
|
@@ -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) {
|