badgr-cli 1.1.2 → 1.1.4
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/README.md +182 -17
- package/package.json +2 -1
- package/src/badgr.js +24 -8
- package/src/commands/billing.js +3 -3
- package/src/commands/connect.js +4 -1
- package/src/commands/diagnose.js +798 -0
- package/src/commands/job.js +246 -0
- package/src/commands/launch.js +144 -15
- package/src/commands/run.js +29 -3
- package/src/commands/serve.js +13 -3
- package/src/credentials.js +33 -0
- package/src/errors.js +5 -0
- package/src/fallback.js +17 -3
- package/src/commands/task.js +0 -25
|
@@ -0,0 +1,798 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import { classifyPastedInput } from 'badgr-shared';
|
|
3
|
+
import { callApi } from '../api.js';
|
|
4
|
+
import { ensureLoggedIn } from '../onboarding.js';
|
|
5
|
+
|
|
6
|
+
const DIAGNOSE_HELP = `
|
|
7
|
+
Paste anything. Badgr detects the input, diagnoses it for free, then
|
|
8
|
+
shows either missing information, a verified template, or a capped
|
|
9
|
+
smoke-test plan. No GPU launches without explicit --approve.
|
|
10
|
+
|
|
11
|
+
Usage:
|
|
12
|
+
badgr diagnose "<input>"
|
|
13
|
+
badgr diagnose "<input>" --smoke
|
|
14
|
+
badgr diagnose "<input>" --approve
|
|
15
|
+
badgr diagnose "<input>" --json
|
|
16
|
+
badgr diagnose <case_id_or_url> --approve Resume an existing case (e.g.
|
|
17
|
+
one shared via a case link)
|
|
18
|
+
|
|
19
|
+
Input (auto-detected):
|
|
20
|
+
GitHub issue URL https://github.com/org/repo/issues/N
|
|
21
|
+
GitHub repo URL https://github.com/org/repo
|
|
22
|
+
Docker image owner/image:tag or image:tag
|
|
23
|
+
Local log / file ./error.log or vllm.log
|
|
24
|
+
ComfyUI workflow workflow.json (JSON file)
|
|
25
|
+
Raw conversation paste Discord / Slack / support thread text
|
|
26
|
+
Existing case repro_xxxxxxxx or https://aibadgr.com/repro/repro_xxxxxxxx
|
|
27
|
+
|
|
28
|
+
Flags:
|
|
29
|
+
--smoke Free. Runs real, cheap mechanical validation of the
|
|
30
|
+
produced command on top of the normal diagnosis --
|
|
31
|
+
command syntax, CLI-entrypoint corroboration,
|
|
32
|
+
referenced-file presence, Docker ENTRYPOINT/CMD
|
|
33
|
+
consistency, required env vars -- plus a client-side
|
|
34
|
+
check of any local file path you pasted (e.g. a
|
|
35
|
+
.gguf path), which only this machine can see. Never
|
|
36
|
+
starts a GPU, never requires login, never prints
|
|
37
|
+
"Verified" -- only "Smoke Checked" (AGENTS.md proof
|
|
38
|
+
levels). Can be combined with --approve: the smoke
|
|
39
|
+
checks print first, then the normal approve flow
|
|
40
|
+
runs (they are independent, non-blocking steps).
|
|
41
|
+
Unrelated to "badgr run --smoke", which launches a
|
|
42
|
+
real, billable cheapest-GPU job.
|
|
43
|
+
--approve Approve the capped smoke test after diagnosis (opens
|
|
44
|
+
browser sign-in automatically if not logged in).
|
|
45
|
+
This is a real, billable GPU-provisioned run --
|
|
46
|
+
distinct from the free --smoke flag above.
|
|
47
|
+
--docker <image> Force Docker-image intake (override auto-detect)
|
|
48
|
+
--repo <url> Force repository intake (override auto-detect)
|
|
49
|
+
--comfyui <path> Force ComfyUI workflow intake (override auto-detect)
|
|
50
|
+
--github <url> Include a GitHub issue URL found inside pasted text
|
|
51
|
+
as additional context. Opt-in only, never fetched
|
|
52
|
+
automatically (see "Additional context found" below)
|
|
53
|
+
--json Machine-readable JSON output
|
|
54
|
+
--help, -h Show this help
|
|
55
|
+
|
|
56
|
+
Status: every run prints one of NEEDS INFO / READY / SMOKE CHECKED / INVALID / VERIFIED.
|
|
57
|
+
NEEDS INFO -- cannot yet produce a complete command
|
|
58
|
+
READY -- complete evidence-backed command, no smoke run
|
|
59
|
+
SMOKE CHECKED -- --smoke ran; all applicable checks PASS or SKIPPED
|
|
60
|
+
INVALID -- --smoke ran and an applicable check actually FAILED
|
|
61
|
+
VERIFIED -- only after a real successful run (never from --smoke)
|
|
62
|
+
|
|
63
|
+
Run page: READY and SMOKE CHECKED results also create a free, anonymous
|
|
64
|
+
"Run" link (the same prepared case /run-issue's own confirm form
|
|
65
|
+
creates) -- a saved, runnable configuration you or anyone with the
|
|
66
|
+
link can open and click Run on. NEEDS INFO and INVALID never get
|
|
67
|
+
one (nothing runnable to save, or a check just proved it broken)
|
|
68
|
+
-- both print the correction instead. The page shows the exact
|
|
69
|
+
canonical "badgr ..." command --approve would run, not merely the
|
|
70
|
+
source command extraction found. Still no GPU starts until that
|
|
71
|
+
click. --approve reuses this same case rather than creating a
|
|
72
|
+
second one.
|
|
73
|
+
|
|
74
|
+
Safety: nothing runs from AI-extracted data without --approve.
|
|
75
|
+
No GPU launches without explicit approval and a credit check.
|
|
76
|
+
--smoke never provisions a GPU and never requires login.
|
|
77
|
+
|
|
78
|
+
Full interactive flow: https://aibadgr.com/run-issue`;
|
|
79
|
+
|
|
80
|
+
function detectInput(raw, flags) {
|
|
81
|
+
if (flags.docker) {
|
|
82
|
+
return {
|
|
83
|
+
kind: 'docker_image',
|
|
84
|
+
label: `Docker image: ${flags.docker}`,
|
|
85
|
+
body: { docker_image: flags.docker },
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
if (flags.repo) {
|
|
89
|
+
return {
|
|
90
|
+
kind: 'repo_url',
|
|
91
|
+
label: `GitHub repository: ${flags.repo}`,
|
|
92
|
+
body: { repo_url: flags.repo },
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
if (flags.comfyui) {
|
|
96
|
+
if (!fs.existsSync(flags.comfyui)) throw new Error(`File not found: ${flags.comfyui}`);
|
|
97
|
+
return {
|
|
98
|
+
kind: 'comfyui_workflow',
|
|
99
|
+
label: `ComfyUI workflow: ${flags.comfyui}`,
|
|
100
|
+
body: { workflow_json: fs.readFileSync(flags.comfyui, 'utf8') },
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (!raw) return null;
|
|
105
|
+
|
|
106
|
+
// Case-id / GitHub-URL-shape classification is shared with the web page
|
|
107
|
+
// (`classifyPastedInput`, packages/shared/src/index.ts) so the CLI and
|
|
108
|
+
// /run-issue never disagree about what a given paste is. A case_id (bare,
|
|
109
|
+
// or embedded in a /repro/<id> or /run-issue?case_id=<id> link — the two
|
|
110
|
+
// shapes an admin-prepared or self-created case gets shared as) resumes
|
|
111
|
+
// that existing case instead of diagnosing new input.
|
|
112
|
+
const classified = classifyPastedInput(raw);
|
|
113
|
+
if (classified.kind === 'existing_case') {
|
|
114
|
+
return { kind: 'existing_case', label: `Existing case: ${classified.body.case_id}`, caseId: classified.body.case_id };
|
|
115
|
+
}
|
|
116
|
+
if (classified.kind === 'github_issue') {
|
|
117
|
+
return { kind: 'github_issue', label: `GitHub issue: ${raw}`, body: classified.body };
|
|
118
|
+
}
|
|
119
|
+
if (classified.kind === 'repo_url') {
|
|
120
|
+
return { kind: 'repo_url', label: `GitHub repository: ${raw}`, body: classified.body };
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Local filesystem paths -- only the CLI has a filesystem to check
|
|
124
|
+
// against, so this stays CLI-only, sitting between the shared URL checks
|
|
125
|
+
// above and the shared Docker-image/text fallback below (matches
|
|
126
|
+
// classifyPastedInput's own precedence for everything except this).
|
|
127
|
+
if (fs.existsSync(raw)) {
|
|
128
|
+
const content = fs.readFileSync(raw, 'utf8');
|
|
129
|
+
if (raw.endsWith('.json') || raw.endsWith('.JSON')) {
|
|
130
|
+
let parsed;
|
|
131
|
+
try { parsed = JSON.parse(content); } catch { /* not valid JSON */ }
|
|
132
|
+
if (parsed && typeof parsed === 'object' && (parsed.nodes || parsed['1'] || parsed['0'])) {
|
|
133
|
+
return {
|
|
134
|
+
kind: 'comfyui_workflow',
|
|
135
|
+
label: `ComfyUI workflow: ${raw}`,
|
|
136
|
+
body: { workflow_json: content },
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
return {
|
|
141
|
+
kind: 'log_file',
|
|
142
|
+
label: `Log / error file: ${raw}`,
|
|
143
|
+
body: { text: content.slice(0, 50_000) },
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (classified.kind === 'docker_image') {
|
|
148
|
+
return { kind: 'docker_image', label: `Docker image: ${raw}`, body: classified.body };
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return { kind: 'text', label: 'Text / conversation', body: classified.body };
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function _printResult(result, chalk) {
|
|
155
|
+
const { extraction: e, static_incompatibilities, multi_gpu_detected, large_download, github_issue, detected_github_url } = result;
|
|
156
|
+
|
|
157
|
+
if (github_issue) {
|
|
158
|
+
console.log(` ${chalk.bold('Issue:')} ${github_issue.title} ${chalk.dim(`[${github_issue.state}]`)}`);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// The conversation stays primary — a GitHub URL found inside pasted text
|
|
162
|
+
// is only ever offered here, never fetched automatically.
|
|
163
|
+
if (detected_github_url) {
|
|
164
|
+
console.log();
|
|
165
|
+
console.log(` ${chalk.bold('Additional context found:')} ${detected_github_url}`);
|
|
166
|
+
console.log(` ${chalk.dim(`Include it: badgr diagnose "<input>" --github ${detected_github_url}`)}`);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const wt = e.workload_type && e.workload_type !== 'unknown' ? e.workload_type : null;
|
|
170
|
+
const conf = e.confidence ? ` ${chalk.dim(`(${Math.round(e.confidence * 100)}% confidence)`)}` : '';
|
|
171
|
+
if (wt) console.log(` ${chalk.bold('Workload:')} ${wt}${conf}`);
|
|
172
|
+
if (e.summary) console.log(` ${chalk.bold('Summary:')} ${e.summary}`);
|
|
173
|
+
console.log();
|
|
174
|
+
|
|
175
|
+
if (e.docker_images?.length) {
|
|
176
|
+
console.log(` ${chalk.bold('Image:')} ${e.docker_images[0]}${e.docker_images.length > 1 ? chalk.dim(` +${e.docker_images.length - 1} more`) : ''}`);
|
|
177
|
+
}
|
|
178
|
+
if (e.models?.length) {
|
|
179
|
+
console.log(` ${chalk.bold('Model:')} ${e.models[0]}${e.models.length > 1 ? chalk.dim(` +${e.models.length - 1} more`) : ''}`);
|
|
180
|
+
}
|
|
181
|
+
if (e.commands?.length) {
|
|
182
|
+
console.log(` ${chalk.bold('Command:')} ${e.commands[0]}`);
|
|
183
|
+
}
|
|
184
|
+
if (e.errors?.length) {
|
|
185
|
+
console.log(` ${chalk.bold('Errors:')}`);
|
|
186
|
+
for (const err of e.errors.slice(0, 3)) console.log(` ${chalk.red('✗')} ${err}`);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
if (static_incompatibilities?.length) {
|
|
190
|
+
console.log();
|
|
191
|
+
console.log(` ${chalk.bold('Diagnosis:')} ${chalk.yellow('Static incompatibility — no GPU needed')}`);
|
|
192
|
+
for (const inc of static_incompatibilities) console.log(` ${chalk.yellow('!')} ${inc}`);
|
|
193
|
+
} else if (e.missing_information?.length) {
|
|
194
|
+
console.log();
|
|
195
|
+
console.log(` ${chalk.bold('Diagnosis:')} Missing information`);
|
|
196
|
+
for (const m of e.missing_information) console.log(` ${chalk.dim('?')} ${m}`);
|
|
197
|
+
} else if (wt) {
|
|
198
|
+
console.log();
|
|
199
|
+
console.log(` ${chalk.bold('Diagnosis:')} Workload detected — ready for capped test`);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
if (multi_gpu_detected) {
|
|
203
|
+
console.log(` ${chalk.yellow('!')} Multi-GPU detected: ${multi_gpu_detected}`);
|
|
204
|
+
console.log(` ${chalk.dim('Capped test will use a single GPU only.')}`);
|
|
205
|
+
}
|
|
206
|
+
if (large_download) {
|
|
207
|
+
console.log(` ${chalk.yellow('!')} Large model download: ~${large_download.estimated_gb} GB`);
|
|
208
|
+
console.log(` ${chalk.dim('Funding approval required before test launches.')}`);
|
|
209
|
+
}
|
|
210
|
+
if (result.secret_types_detected?.length) {
|
|
211
|
+
console.log(` ${chalk.dim(`Secrets redacted before analysis: ${result.secret_types_detected.join(', ')}`)}`);
|
|
212
|
+
}
|
|
213
|
+
if (!result.ai_available) {
|
|
214
|
+
console.log(` ${chalk.dim('(AI extraction unavailable — pattern matching only)')}`);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
console.log();
|
|
218
|
+
if (static_incompatibilities?.length) {
|
|
219
|
+
console.log(` ${chalk.bold('Recommended action:')} Fix the incompatibility listed above — no GPU test needed`);
|
|
220
|
+
} else if (e.missing_information?.length) {
|
|
221
|
+
console.log(` ${chalk.bold('Recommended action:')} Provide the missing details above, then re-run`);
|
|
222
|
+
} else {
|
|
223
|
+
console.log(` ${chalk.bold('Recommended action:')} Run ${chalk.cyan('badgr diagnose "<input>" --approve')} to launch a capped test`);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// Local-machine file paths (e.g. a pasted .gguf path) can only be checked
|
|
228
|
+
// client-side -- the backend never sees this machine's filesystem, so it
|
|
229
|
+
// must not (and does not) claim to validate them. Only paths that look like
|
|
230
|
+
// a real filesystem reference (absolute, `./`, `../`, or `~/`) are checked;
|
|
231
|
+
// bare words, URLs, and Docker image references are left alone.
|
|
232
|
+
//
|
|
233
|
+
// Path checks must respect context (locked Free Command Check spec, problem
|
|
234
|
+
// #3): a repo path is checked against the repo tree, a container path
|
|
235
|
+
// against Dockerfile/image evidence -- both server-side, inside
|
|
236
|
+
// `mechanical_checks` -- and only a genuinely local path belongs to this
|
|
237
|
+
// machine. Once a repository or Docker image is in evidence for this
|
|
238
|
+
// workload, an absolute path *inside the resolved command* (e.g. "python3
|
|
239
|
+
// /src/main.py") is a repo/container path, not a claim about this laptop,
|
|
240
|
+
// so it must never be re-checked against the local filesystem here -- doing
|
|
241
|
+
// so is exactly the "/src/main.py NOT FOUND on this machine" false failure
|
|
242
|
+
// the spec calls out. `model_artifact` is different: it is always this
|
|
243
|
+
// machine's own claim about a locally-downloaded checkpoint, never repo or
|
|
244
|
+
// container evidence, so it stays checked regardless.
|
|
245
|
+
function _localPathCandidates(e) {
|
|
246
|
+
const candidates = new Set();
|
|
247
|
+
const hasRepoOrContainerContext = Boolean(
|
|
248
|
+
e.repositories?.length || e.docker_images?.length || e.dockerfile
|
|
249
|
+
);
|
|
250
|
+
const maybeAdd = (value) => {
|
|
251
|
+
if (typeof value !== 'string') return;
|
|
252
|
+
const s = value.trim().replace(/^['"]|['"]$/g, '');
|
|
253
|
+
if (!s || /^https?:\/\//i.test(s)) return;
|
|
254
|
+
if (s.startsWith('/') || s.startsWith('./') || s.startsWith('../') || s.startsWith('~/')) {
|
|
255
|
+
candidates.add(s);
|
|
256
|
+
}
|
|
257
|
+
};
|
|
258
|
+
maybeAdd(e.model_artifact);
|
|
259
|
+
if (!hasRepoOrContainerContext) {
|
|
260
|
+
for (const tok of (e.commands?.[0] || '').split(/\s+/)) maybeAdd(tok);
|
|
261
|
+
}
|
|
262
|
+
return [...candidates];
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// Locked status ladder (product-level labels, not the backend's internal
|
|
266
|
+
// proof_level enum): NEEDS INFO / READY / SMOKE CHECKED / INVALID / VERIFIED.
|
|
267
|
+
// Pure presentation over data /run-issue/extract already returns -- no new
|
|
268
|
+
// endpoint, no new GPU behaviour. --smoke can reach SMOKE CHECKED or INVALID
|
|
269
|
+
// (never VERIFIED, see AGENTS.md §12 and _printCopySummary below).
|
|
270
|
+
//
|
|
271
|
+
// The evidence-only judgment (needs_info/invalid/smoke_checked) is computed
|
|
272
|
+
// once, server-side, in `_compute_smoke_status` (backend/run_issue_routes.py)
|
|
273
|
+
// and read here via `e.smoke_status` -- this and /run-issue's `smokeStatus()`
|
|
274
|
+
// (frontend/app/run-issue/page.tsx) both just render that one backend
|
|
275
|
+
// verdict rather than each re-deriving pass/fail from mechanical_checks, so
|
|
276
|
+
// the CLI and the web page can never disagree on what a given result means.
|
|
277
|
+
// A `smoke_status` fallback derivation is kept only for a backend response
|
|
278
|
+
// that predates this field (defensive, not the normal path).
|
|
279
|
+
//
|
|
280
|
+
// READY means "no smoke run" -- a command that's merely complete is never
|
|
281
|
+
// itself proof of validity, so mechanical/local-file results only ever
|
|
282
|
+
// change the printed word when --smoke actually ran them (locked spec
|
|
283
|
+
// problem #6: a real FAIL must demote SMOKE CHECKED to INVALID; SKIPPED
|
|
284
|
+
// checks never do). This part stays CLI-side: it depends on the --smoke
|
|
285
|
+
// flag and this machine's own filesystem, neither of which the backend can
|
|
286
|
+
// see.
|
|
287
|
+
function _statusWord(result, smokeRequested) {
|
|
288
|
+
const e = result.extraction || {};
|
|
289
|
+
if (result.static_incompatibilities?.length || e.missing_information?.length) return 'NEEDS INFO';
|
|
290
|
+
if (!smokeRequested) return 'READY';
|
|
291
|
+
const backendStatus = e.smoke_status
|
|
292
|
+
|| ((e.mechanical_checks || []).some((check) => check.status === 'fail') ? 'invalid' : 'smoke_checked');
|
|
293
|
+
const localFail = _localPathCandidates(e).some((p) => !fs.existsSync(p));
|
|
294
|
+
if (backendStatus === 'invalid' || localFail) return 'INVALID';
|
|
295
|
+
return backendStatus === 'ready' ? 'READY' : 'SMOKE CHECKED';
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
const _CHECK_LABELS = {
|
|
299
|
+
command_syntax: 'command syntax',
|
|
300
|
+
cli_entrypoint: 'CLI entrypoint',
|
|
301
|
+
referenced_files: 'referenced repo files',
|
|
302
|
+
docker_entrypoint: 'Docker metadata',
|
|
303
|
+
required_env_vars: 'required env vars',
|
|
304
|
+
};
|
|
305
|
+
|
|
306
|
+
// Compact, copy-pasteable result -- the literal STATUS word plus just enough
|
|
307
|
+
// to forward to someone else. Additive to the detailed report above it;
|
|
308
|
+
// does not replace it. Never prints "Verified" -- SMOKE CHECKED is the
|
|
309
|
+
// ceiling for this free path.
|
|
310
|
+
function _printCopySummary(result, flags, chalk, caseData) {
|
|
311
|
+
const e = result.extraction || {};
|
|
312
|
+
const status = _statusWord(result, flags.smoke);
|
|
313
|
+
|
|
314
|
+
console.log(` ${chalk.bold(`STATUS: ${status}`)}`);
|
|
315
|
+
console.log();
|
|
316
|
+
|
|
317
|
+
if (status === 'NEEDS INFO') {
|
|
318
|
+
// Static incompatibilities and missing-information both mean "no
|
|
319
|
+
// command yet" -- whichever fired is the reason to surface here.
|
|
320
|
+
const reasons = result.static_incompatibilities?.length
|
|
321
|
+
? result.static_incompatibilities
|
|
322
|
+
: (e.missing_information || []);
|
|
323
|
+
console.log(` ${chalk.bold('Missing:')}`);
|
|
324
|
+
for (const r of reasons) console.log(` - ${r}`);
|
|
325
|
+
console.log();
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// The prepared case's canonical `badgr ...` command (the exact line
|
|
330
|
+
// --approve would run) is preferred over the raw source command --
|
|
331
|
+
// /confirm can only compute it once a case exists (READY/SMOKE CHECKED),
|
|
332
|
+
// and only when there's enough evidence to convert (see
|
|
333
|
+
// _canonical_badgr_command's docstring, backend/run_issue_routes.py, for
|
|
334
|
+
// what it can't yet convert, e.g. training/ComfyUI). Falls back to the
|
|
335
|
+
// raw extracted command otherwise -- still true and still runnable input,
|
|
336
|
+
// just not guaranteed to already be `badgr`-shaped.
|
|
337
|
+
const badgrJob = caseData?.findings?.canonical_command || e.commands?.[0] || '(no command produced)';
|
|
338
|
+
console.log(` ${chalk.bold('Badgr job:')}`);
|
|
339
|
+
console.log(` ${badgrJob}`);
|
|
340
|
+
console.log();
|
|
341
|
+
|
|
342
|
+
if (caseData?.case_id) {
|
|
343
|
+
console.log(` ${chalk.bold('Run:')}`);
|
|
344
|
+
console.log(` ${chalk.cyan(_runPageUrl(caseData.case_id))}`);
|
|
345
|
+
console.log();
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
if (status === 'SMOKE CHECKED' || status === 'INVALID') {
|
|
349
|
+
console.log(` ${chalk.bold('Checks:')}`);
|
|
350
|
+
const iconFor = (s) => (s === 'pass' ? '✓' : s === 'fail' ? '✗' : '-');
|
|
351
|
+
for (const check of (e.mechanical_checks || [])) {
|
|
352
|
+
console.log(` ${iconFor(check.status)} ${_CHECK_LABELS[check.name] || check.name}`);
|
|
353
|
+
}
|
|
354
|
+
const localPaths = _localPathCandidates(e);
|
|
355
|
+
if (!localPaths.length) {
|
|
356
|
+
console.log(' - local file check skipped');
|
|
357
|
+
} else {
|
|
358
|
+
const allExist = localPaths.every((p) => fs.existsSync(p));
|
|
359
|
+
console.log(` ${allExist ? '✓' : '✗'} local file check${allExist ? '' : ' (not found)'}`);
|
|
360
|
+
}
|
|
361
|
+
console.log();
|
|
362
|
+
if (status === 'INVALID') {
|
|
363
|
+
console.log(' A check above actually failed -- this command is not runnable as-is.');
|
|
364
|
+
} else {
|
|
365
|
+
console.log(' Not GPU-verified.');
|
|
366
|
+
}
|
|
367
|
+
console.log();
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
// --smoke's mechanical checks: prints the backend's evidence-backed
|
|
372
|
+
// pass/fail/skip results (`extraction.mechanical_checks`, computed for
|
|
373
|
+
// free from data already gathered by /run-issue/extract -- no extra
|
|
374
|
+
// network call), plus this machine's own local-file existence check.
|
|
375
|
+
// Deliberately never prints "Verified" -- see AGENTS.md §12; a genuinely
|
|
376
|
+
// passing smoke check still only earns "Smoke Checked".
|
|
377
|
+
function _printSmokeChecks(result, chalk) {
|
|
378
|
+
const e = result.extraction || {};
|
|
379
|
+
const mech = e.mechanical_checks || [];
|
|
380
|
+
const localPaths = _localPathCandidates(e);
|
|
381
|
+
|
|
382
|
+
console.log();
|
|
383
|
+
console.log(` ${chalk.bold('Smoke Checked (free, no GPU, no login):')}`);
|
|
384
|
+
|
|
385
|
+
const icon = (status) => (
|
|
386
|
+
status === 'pass' ? chalk.green('✓') : status === 'fail' ? chalk.red('✗') : chalk.dim('-')
|
|
387
|
+
);
|
|
388
|
+
for (const check of mech) {
|
|
389
|
+
console.log(` ${icon(check.status)} ${chalk.dim(`[${check.name}]`)} ${check.detail}`);
|
|
390
|
+
}
|
|
391
|
+
for (const p of localPaths) {
|
|
392
|
+
const exists = fs.existsSync(p);
|
|
393
|
+
console.log(` ${exists ? chalk.green('✓') : chalk.red('✗')} ${chalk.dim('[local_file]')} ${p} ${exists ? 'exists on this machine' : 'NOT FOUND on this machine'}`);
|
|
394
|
+
}
|
|
395
|
+
if (!mech.length && !localPaths.length) {
|
|
396
|
+
console.log(` ${chalk.dim('No mechanical checks applicable to this input.')}`);
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
const proofLevel = e.smoke_check?.proof_level;
|
|
400
|
+
if (proofLevel === 'smoke_checked') {
|
|
401
|
+
console.log(` ${chalk.green('✓')} ${chalk.dim('[command_scope]')} Command is the repository/image/service's own documented default.`);
|
|
402
|
+
}
|
|
403
|
+
console.log();
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
function _confirmBodyFromExtraction(e) {
|
|
407
|
+
return {
|
|
408
|
+
workload_type: e.workload_type || 'generic',
|
|
409
|
+
docker_image: e.docker_images?.[0] || null,
|
|
410
|
+
model_id: e.models?.[0] || null,
|
|
411
|
+
launch_command: e.commands?.[0] || null,
|
|
412
|
+
error_log: null,
|
|
413
|
+
expected_result: e.expected_result || null,
|
|
414
|
+
environment_variable_names: e.environment_variable_names || [],
|
|
415
|
+
gpu_requirements: Array.isArray(e.gpu_requirements)
|
|
416
|
+
? e.gpu_requirements.join(', ')
|
|
417
|
+
: e.gpu_requirements || null,
|
|
418
|
+
source_summary: e.summary || null,
|
|
419
|
+
};
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
function _runPageUrl(caseId) {
|
|
423
|
+
return `https://aibadgr.com/run-issue?case_id=${caseId}`;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
// Creates the free, anonymous "prepared run" case that backs a shareable
|
|
427
|
+
// Run Link -- the exact same /run-issue/confirm call and case object
|
|
428
|
+
// /run-issue's own confirm step creates, so the CLI and the web page never
|
|
429
|
+
// diverge on what a given diagnosis resolves to. No GPU, no login, no
|
|
430
|
+
// payment -- confirm only ever inspects and plans (see its own docstring).
|
|
431
|
+
// Called once per diagnosis (main flow), then reused by --approve below
|
|
432
|
+
// instead of confirming a second time.
|
|
433
|
+
//
|
|
434
|
+
// Returns `{ caseData }` on success, `{ error }` on a network/API failure --
|
|
435
|
+
// never throws, since a plain diagnosis must still succeed even if the
|
|
436
|
+
// free run-page creation itself fails.
|
|
437
|
+
async function _createRunPage(config, e, chalk) {
|
|
438
|
+
try {
|
|
439
|
+
const caseData = await callApi('/run-issue/confirm', {
|
|
440
|
+
method: 'POST',
|
|
441
|
+
apiKey: config.apiKey || '',
|
|
442
|
+
baseUrl: config.baseUrl,
|
|
443
|
+
body: _confirmBodyFromExtraction(e),
|
|
444
|
+
timeoutMs: 20_000,
|
|
445
|
+
});
|
|
446
|
+
return caseData?.case_id ? { caseData } : { caseData: null };
|
|
447
|
+
} catch (err) {
|
|
448
|
+
return { error: err };
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
async function _doApprove(config, caseData, chalk) {
|
|
453
|
+
const { case_id: caseId, status, test_plan: plan, missing_information: missing } = caseData;
|
|
454
|
+
|
|
455
|
+
if (status === 'incompatible') {
|
|
456
|
+
console.log(chalk.yellow('\n Static incompatibility — no GPU test needed.'));
|
|
457
|
+
console.log();
|
|
458
|
+
return;
|
|
459
|
+
}
|
|
460
|
+
if (status === 'missing_information') {
|
|
461
|
+
console.log(chalk.yellow('\n Cannot approve — missing information:'));
|
|
462
|
+
for (const m of (missing || [])) console.log(` ? ${m}`);
|
|
463
|
+
console.log();
|
|
464
|
+
return;
|
|
465
|
+
}
|
|
466
|
+
if (status === 'template_matched') {
|
|
467
|
+
console.log(chalk.green('\n Matched a verified template — no new GPU test needed.'));
|
|
468
|
+
if (plan?.final_command) {
|
|
469
|
+
console.log();
|
|
470
|
+
console.log(` ${chalk.bold('Verified command:')}`);
|
|
471
|
+
console.log(` ${chalk.cyan(plan.final_command)}`);
|
|
472
|
+
}
|
|
473
|
+
console.log();
|
|
474
|
+
return;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
if (plan) {
|
|
478
|
+
console.log();
|
|
479
|
+
if (plan.gpu_type) console.log(` ${chalk.bold('GPU:')} ${plan.gpu_type}`);
|
|
480
|
+
if (plan.estimated_cost) console.log(` ${chalk.bold('Maximum cost:')} $${Number(plan.estimated_cost).toFixed(2)}`);
|
|
481
|
+
if (plan.max_runtime_min) console.log(` ${chalk.bold('Max runtime:')} ${plan.max_runtime_min} min`);
|
|
482
|
+
if (plan.requires_reduced_test) console.log(` ${chalk.yellow('!')} Multi-GPU detected — test uses a single GPU`);
|
|
483
|
+
if (plan.requires_funding_approval) console.log(` ${chalk.yellow('!')} Large download — funding required`);
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
await _approveAndRun(config, caseId, plan, chalk);
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
// Claim → approve → run for a case that already exists (just created via
|
|
490
|
+
// /confirm above, or resumed via an existing_case input). Auth is required
|
|
491
|
+
// from here on — diagnosis and case creation stay anonymous and free, but a
|
|
492
|
+
// real GPU test needs a signed-in account. The CLI never grants a free-run
|
|
493
|
+
// entitlement (web admin only, see grant_free_run in run_issue_routes.py);
|
|
494
|
+
// it only redeems one a case already carries, same as normal billing.
|
|
495
|
+
async function _approveAndRun(config, caseId, plan, chalk) {
|
|
496
|
+
const printResumeHint = () =>
|
|
497
|
+
console.error(chalk.dim(` Resume this case: https://aibadgr.com/run-issue?case_id=${caseId}\n`));
|
|
498
|
+
|
|
499
|
+
let authConfig = config;
|
|
500
|
+
if (!authConfig.apiKey) {
|
|
501
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
502
|
+
console.error(chalk.red('\n ✗ Sign in required to approve a GPU test. Run: badgr login\n'));
|
|
503
|
+
printResumeHint();
|
|
504
|
+
process.exitCode = 1;
|
|
505
|
+
return;
|
|
506
|
+
}
|
|
507
|
+
console.log(chalk.yellow('\n Sign in to continue. Opening browser...'));
|
|
508
|
+
try {
|
|
509
|
+
authConfig = await ensureLoggedIn(authConfig, chalk);
|
|
510
|
+
} catch (err) {
|
|
511
|
+
console.error(chalk.red(`\n ✗ Sign-in failed: ${err.message}\n`));
|
|
512
|
+
printResumeHint();
|
|
513
|
+
process.exitCode = 1;
|
|
514
|
+
return;
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
try {
|
|
519
|
+
await callApi(`/run-issue/cases/${caseId}/claim`, {
|
|
520
|
+
method: 'POST', apiKey: authConfig.apiKey, baseUrl: authConfig.baseUrl, timeoutMs: 10_000,
|
|
521
|
+
});
|
|
522
|
+
} catch { /* already claimed — continue */ }
|
|
523
|
+
|
|
524
|
+
let approved;
|
|
525
|
+
try {
|
|
526
|
+
approved = await callApi(`/run-issue/cases/${caseId}/approve`, {
|
|
527
|
+
method: 'POST',
|
|
528
|
+
apiKey: authConfig.apiKey,
|
|
529
|
+
baseUrl: authConfig.baseUrl,
|
|
530
|
+
body: {
|
|
531
|
+
confirmed: true,
|
|
532
|
+
funding_approved: Boolean(plan?.requires_funding_approval),
|
|
533
|
+
reduced_test_confirmed: Boolean(plan?.requires_reduced_test),
|
|
534
|
+
},
|
|
535
|
+
timeoutMs: 15_000,
|
|
536
|
+
});
|
|
537
|
+
} catch (err) {
|
|
538
|
+
if (err.httpStatus === 402 || err.message?.includes('402')) {
|
|
539
|
+
console.error(chalk.yellow('\n ✗ Insufficient credits. Run: badgr billing\n'));
|
|
540
|
+
} else {
|
|
541
|
+
console.error(chalk.red(`\n ✗ Approve failed: ${err.message}\n`));
|
|
542
|
+
}
|
|
543
|
+
process.exitCode = 1;
|
|
544
|
+
return;
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
console.log(chalk.dim('\n Launching capped smoke test...'));
|
|
548
|
+
let ran;
|
|
549
|
+
try {
|
|
550
|
+
ran = await callApi(`/run-issue/cases/${caseId}/run`, {
|
|
551
|
+
method: 'POST', apiKey: authConfig.apiKey, baseUrl: authConfig.baseUrl, timeoutMs: 30_000,
|
|
552
|
+
});
|
|
553
|
+
} catch (err) {
|
|
554
|
+
console.error(chalk.red(`\n ✗ Launch failed: ${err.message}\n`));
|
|
555
|
+
process.exitCode = 1;
|
|
556
|
+
return;
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
console.log();
|
|
560
|
+
console.log(chalk.green(' Smoke test launched.'));
|
|
561
|
+
console.log(` ${chalk.bold('Case:')} ${caseId}`);
|
|
562
|
+
if (ran?.job_id) console.log(` ${chalk.bold('Job:')} ${ran.job_id}`);
|
|
563
|
+
if (ran?.free_run_used) console.log(` ${chalk.bold('Billing:')} Free run — no charge`);
|
|
564
|
+
console.log(` ${chalk.bold('Evidence:')} https://aibadgr.com/repro/${caseId}`);
|
|
565
|
+
console.log();
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
// Resume an existing case (bare case_id or a shared /repro or /run-issue
|
|
569
|
+
// link) instead of diagnosing new input. No /confirm — the case already
|
|
570
|
+
// exists; this only reads its current state and, with --approve, redeems
|
|
571
|
+
// whatever entitlement/billing state it already carries.
|
|
572
|
+
async function _diagnoseExistingCase(config, caseId, flags, chalk) {
|
|
573
|
+
let caseData;
|
|
574
|
+
try {
|
|
575
|
+
caseData = await callApi(`/run-issue/cases/${caseId}`, {
|
|
576
|
+
apiKey: config.apiKey || '', baseUrl: config.baseUrl, timeoutMs: 15_000,
|
|
577
|
+
});
|
|
578
|
+
} catch (err) {
|
|
579
|
+
console.error(chalk.red(`\n ✗ Could not load case ${caseId}: ${err.message}\n`));
|
|
580
|
+
process.exitCode = 1;
|
|
581
|
+
return;
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
if (flags.json) {
|
|
585
|
+
console.log(JSON.stringify(caseData, null, 2));
|
|
586
|
+
if (flags.approve) await _approveAndRun(config, caseId, caseData.test_plan, chalk);
|
|
587
|
+
return;
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
console.log();
|
|
591
|
+
console.log(` ${chalk.bold('Case:')} ${caseId}`);
|
|
592
|
+
console.log(` ${chalk.bold('Status:')} ${caseData.status}`);
|
|
593
|
+
if (caseData.workload_type) console.log(` ${chalk.bold('Workload:')} ${caseData.workload_type}`);
|
|
594
|
+
if (caseData.known_command) console.log(` ${chalk.bold('Command:')} ${caseData.known_command}`);
|
|
595
|
+
if ((caseData.missing_information || []).length) {
|
|
596
|
+
console.log();
|
|
597
|
+
console.log(` ${chalk.bold('Missing information:')}`);
|
|
598
|
+
for (const m of caseData.missing_information) console.log(` ? ${m}`);
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
// "Verified" is only ever earned here -- a case whose real GPU run
|
|
602
|
+
// actually completed (`lead_summary.status`, computed server-side by
|
|
603
|
+
// `_build_lead_summary` from `ReproCase.status`/`final_command`). Never
|
|
604
|
+
// reachable from --smoke, which never provisions anything.
|
|
605
|
+
console.log();
|
|
606
|
+
const existingStatus = caseData.lead_summary?.status === 'verified'
|
|
607
|
+
|| caseData.status === 'verified' || caseData.status === 'completed'
|
|
608
|
+
? 'VERIFIED'
|
|
609
|
+
: (caseData.missing_information || []).length ? 'NEEDS INFO' : 'READY';
|
|
610
|
+
console.log(` ${chalk.bold(`STATUS: ${existingStatus}`)}`);
|
|
611
|
+
if (existingStatus === 'VERIFIED') {
|
|
612
|
+
console.log();
|
|
613
|
+
console.log(` ${chalk.bold('Command:')}`);
|
|
614
|
+
console.log(` ${caseData.final_command || caseData.known_command}`);
|
|
615
|
+
console.log();
|
|
616
|
+
console.log(` ${chalk.bold('Result:')}`);
|
|
617
|
+
const resultLines = caseData.lead_summary?.test_checklist?.length
|
|
618
|
+
? caseData.lead_summary.test_checklist
|
|
619
|
+
: ['Verified'];
|
|
620
|
+
for (const line of resultLines) console.log(` - ${line}`);
|
|
621
|
+
if (caseData.actual_cost_aud != null) {
|
|
622
|
+
console.log(` Verified on Badgr for AUD $${Number(caseData.actual_cost_aud).toFixed(2)}. GPU torn down after the test.`);
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
console.log();
|
|
627
|
+
if (caseData.free_verification_consumed) {
|
|
628
|
+
console.log(` ${chalk.dim('This case\'s free run has already been used — further verification is normal billing.')}`);
|
|
629
|
+
} else if (caseData.free_verification_available) {
|
|
630
|
+
console.log(` ${chalk.green(`1 free verification job available — up to $${(caseData.free_verification_max_cost_usd ?? 5).toFixed(0)}`)}`);
|
|
631
|
+
} else if (caseData.test_plan) {
|
|
632
|
+
console.log(` ${chalk.dim(`Verification available — max $${Number(caseData.test_plan.max_cost_usd ?? 0).toFixed(2)}`)}`);
|
|
633
|
+
}
|
|
634
|
+
console.log();
|
|
635
|
+
|
|
636
|
+
if (flags.approve) {
|
|
637
|
+
await _approveAndRun(config, caseId, caseData.test_plan, chalk);
|
|
638
|
+
} else {
|
|
639
|
+
console.log(` ${chalk.bold('Resume:')} badgr diagnose ${caseId} --approve`);
|
|
640
|
+
console.log();
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
export async function diagnoseCommand(config, args, chalk) {
|
|
645
|
+
if (args.includes('--help') || args.includes('-h')) {
|
|
646
|
+
console.log(DIAGNOSE_HELP);
|
|
647
|
+
return;
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
const flags = {};
|
|
651
|
+
let positional = null;
|
|
652
|
+
let i = 0;
|
|
653
|
+
while (i < args.length) {
|
|
654
|
+
const a = args[i];
|
|
655
|
+
if (a === '--docker') { flags.docker = args[++i]; i++; continue; }
|
|
656
|
+
if (a === '--repo') { flags.repo = args[++i]; i++; continue; }
|
|
657
|
+
if (a === '--comfyui') { flags.comfyui = args[++i]; i++; continue; }
|
|
658
|
+
if (a === '--github') { flags.github = args[++i]; i++; continue; }
|
|
659
|
+
if (a === '--approve') { flags.approve = true; i++; continue; }
|
|
660
|
+
if (a === '--smoke') { flags.smoke = true; i++; continue; }
|
|
661
|
+
if (a === '--json') { flags.json = true; i++; continue; }
|
|
662
|
+
if (!a.startsWith('-') && positional === null) positional = a;
|
|
663
|
+
i++;
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
if (!positional && !flags.docker && !flags.repo && !flags.comfyui) {
|
|
667
|
+
console.log(DIAGNOSE_HELP);
|
|
668
|
+
return;
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
let detected;
|
|
672
|
+
try {
|
|
673
|
+
detected = detectInput(positional, flags);
|
|
674
|
+
} catch (err) {
|
|
675
|
+
console.error(chalk.red(`\n ✗ ${err.message}\n`));
|
|
676
|
+
process.exitCode = 1;
|
|
677
|
+
return;
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
if (!detected) {
|
|
681
|
+
console.log(DIAGNOSE_HELP);
|
|
682
|
+
return;
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
if (detected.kind === 'existing_case') {
|
|
686
|
+
await _diagnoseExistingCase(config, detected.caseId, flags, chalk);
|
|
687
|
+
return;
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
// --github opts into fetching a GitHub issue URL found inside pasted text
|
|
691
|
+
// (see the "Additional context found" notice) — never fetched by default.
|
|
692
|
+
if (flags.github) detected.body.github_url = flags.github;
|
|
693
|
+
|
|
694
|
+
if (!flags.json) {
|
|
695
|
+
console.log();
|
|
696
|
+
console.log(` ${chalk.bold('Detected:')} ${detected.label}`);
|
|
697
|
+
console.log(chalk.dim(' Analysing...'));
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
let result;
|
|
701
|
+
try {
|
|
702
|
+
result = await callApi('/run-issue/extract', {
|
|
703
|
+
method: 'POST',
|
|
704
|
+
apiKey: config.apiKey || '',
|
|
705
|
+
baseUrl: config.baseUrl,
|
|
706
|
+
body: detected.body,
|
|
707
|
+
timeoutMs: 120_000,
|
|
708
|
+
});
|
|
709
|
+
} catch (err) {
|
|
710
|
+
console.error(chalk.red(`\n ✗ ${err.message}\n`));
|
|
711
|
+
process.exitCode = 1;
|
|
712
|
+
return;
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
const status = _statusWord(result, flags.smoke);
|
|
716
|
+
|
|
717
|
+
// A command that's actually runnable also gets a free prepared Run
|
|
718
|
+
// page -- the same case /run-issue's own confirm step would create --
|
|
719
|
+
// so diagnosis ends at a page someone can click Run on, not just a
|
|
720
|
+
// printed command. Only READY and SMOKE CHECKED qualify: NEEDS INFO has
|
|
721
|
+
// nothing runnable to save yet, and INVALID is a command a real check
|
|
722
|
+
// just proved broken -- turning that into a clickable "Run" page would
|
|
723
|
+
// contradict the label. Both print their own guidance instead (Missing:
|
|
724
|
+
// / the failed check) and stop there.
|
|
725
|
+
let caseData = null;
|
|
726
|
+
let caseError = null;
|
|
727
|
+
if (status === 'READY' || status === 'SMOKE CHECKED') {
|
|
728
|
+
const created = await _createRunPage(config, result.extraction || {}, chalk);
|
|
729
|
+
if (created.error) caseError = created.error;
|
|
730
|
+
else caseData = created.caseData;
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
if (flags.json) {
|
|
734
|
+
const output = { detected: detected.kind, ...result, status };
|
|
735
|
+
if (flags.smoke) {
|
|
736
|
+
const e = result.extraction || {};
|
|
737
|
+
output.smoke = {
|
|
738
|
+
mechanical_checks: e.mechanical_checks || [],
|
|
739
|
+
local_file_checks: _localPathCandidates(e).map(p => ({ path: p, exists: fs.existsSync(p) })),
|
|
740
|
+
};
|
|
741
|
+
}
|
|
742
|
+
if (caseData?.case_id) {
|
|
743
|
+
output.run = {
|
|
744
|
+
case_id: caseData.case_id,
|
|
745
|
+
url: _runPageUrl(caseData.case_id),
|
|
746
|
+
canonical_command: caseData.findings?.canonical_command || null,
|
|
747
|
+
};
|
|
748
|
+
}
|
|
749
|
+
console.log(JSON.stringify(output, null, 2));
|
|
750
|
+
|
|
751
|
+
// --json must not silently skip --approve -- the JSON branch used to
|
|
752
|
+
// return here unconditionally, so a scripted `--approve --json` call
|
|
753
|
+
// never claimed/approved/launched anything (bug: approval was silently
|
|
754
|
+
// dropped in the one output mode automation actually uses). Approval
|
|
755
|
+
// output itself stays human-readable (chalk console lines), matching
|
|
756
|
+
// _diagnoseExistingCase's existing --json + --approve behaviour below.
|
|
757
|
+
if (flags.approve) {
|
|
758
|
+
if (caseError) {
|
|
759
|
+
console.error(chalk.red(`\n ✗ Could not create case: ${caseError.message}\n`));
|
|
760
|
+
process.exitCode = 1;
|
|
761
|
+
return;
|
|
762
|
+
}
|
|
763
|
+
if (!caseData) {
|
|
764
|
+
console.error(chalk.yellow('\n Cannot approve — missing information above.\n'));
|
|
765
|
+
return;
|
|
766
|
+
}
|
|
767
|
+
await _doApprove(config, caseData, chalk);
|
|
768
|
+
}
|
|
769
|
+
return;
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
console.log();
|
|
773
|
+
_printResult(result, chalk);
|
|
774
|
+
|
|
775
|
+
if (flags.smoke) {
|
|
776
|
+
_printSmokeChecks(result, chalk);
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
console.log();
|
|
780
|
+
_printCopySummary(result, flags, chalk, caseData);
|
|
781
|
+
|
|
782
|
+
console.log(chalk.dim(' Free diagnosis. No GPU was provisioned.'));
|
|
783
|
+
console.log(chalk.dim(' Full interactive flow: https://aibadgr.com/run-issue'));
|
|
784
|
+
console.log();
|
|
785
|
+
|
|
786
|
+
if (flags.approve) {
|
|
787
|
+
if (caseError) {
|
|
788
|
+
console.error(chalk.red(`\n ✗ Could not create case: ${caseError.message}\n`));
|
|
789
|
+
process.exitCode = 1;
|
|
790
|
+
return;
|
|
791
|
+
}
|
|
792
|
+
if (!caseData) {
|
|
793
|
+
console.log(chalk.yellow('\n Cannot approve — missing information above.\n'));
|
|
794
|
+
return;
|
|
795
|
+
}
|
|
796
|
+
await _doApprove(config, caseData, chalk);
|
|
797
|
+
}
|
|
798
|
+
}
|