badgr-cli 1.0.48 → 1.1.1

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 (68) hide show
  1. package/README.md +164 -1
  2. package/package.json +1 -1
  3. package/src/api.js +16 -2
  4. package/src/artifactDownload.js +55 -0
  5. package/src/badgr.js +104 -0
  6. package/src/batch.js +22 -4
  7. package/src/browser.js +23 -0
  8. package/src/commands/artifacts.js +75 -0
  9. package/src/commands/batch.js +221 -28
  10. package/src/commands/billing.js +1 -12
  11. package/src/commands/capacity.js +9 -4
  12. package/src/commands/comfyui.js +3 -3
  13. package/src/commands/connect.js +83 -0
  14. package/src/commands/doctor.js +127 -0
  15. package/src/commands/down.js +29 -6
  16. package/src/commands/launch.js +431 -0
  17. package/src/commands/pull.js +137 -0
  18. package/src/commands/run.js +253 -37
  19. package/src/commands/sbatch.js +232 -0
  20. package/src/commands/serve.js +3 -3
  21. package/src/commands/status.js +12 -4
  22. package/src/commands/task.js +25 -0
  23. package/src/commands/test-run.js +4 -2
  24. package/src/credentials.js +65 -0
  25. package/src/fallback.js +7 -2
  26. package/src/fanout.js +70 -0
  27. package/src/gpuDoctor/diskInfo.js +42 -0
  28. package/src/gpuDoctor/doctor.js +451 -0
  29. package/src/gpuDoctor/gpuInfo.js +70 -0
  30. package/src/gpuDoctor/healthCheck.js +63 -0
  31. package/src/gpuDoctor/logClassifier.js +138 -0
  32. package/src/gpuDoctor/modelFit.js +107 -0
  33. package/src/gpuDoctor/probeCache.js +38 -0
  34. package/src/gpuDoctor/redact.js +29 -0
  35. package/src/gpuDoctor/torchInfo.js +61 -0
  36. package/src/gpuDoctor/workflowDoctor.js +96 -0
  37. package/src/onboarding.js +124 -0
  38. package/src/slurm.js +193 -0
  39. package/src/spec.js +59 -2
  40. package/src/store.js +16 -0
  41. package/tests/agent-images.test.js +17 -0
  42. package/tests/artifactDownload.test.js +113 -0
  43. package/tests/artifacts.test.js +168 -0
  44. package/tests/batch.test.js +312 -0
  45. package/tests/browser.test.js +51 -0
  46. package/tests/capacity.test.js +68 -0
  47. package/tests/commands.test.js +44 -0
  48. package/tests/connect.test.js +83 -0
  49. package/tests/down.test.js +23 -1
  50. package/tests/fallback-timeout.test.js +41 -0
  51. package/tests/fanout.test.js +124 -0
  52. package/tests/gpu-doctor-classifiers.test.js +402 -0
  53. package/tests/gpu-doctor-doctor.test.js +304 -0
  54. package/tests/gpu-doctor-probe-cache.test.js +110 -0
  55. package/tests/gpu-doctor-probes.test.js +257 -0
  56. package/tests/launch-command-argv.test.js +93 -0
  57. package/tests/launch-readiness.test.js +1 -0
  58. package/tests/launch.test.js +440 -0
  59. package/tests/onboarding.test.js +134 -0
  60. package/tests/pull.test.js +266 -0
  61. package/tests/run-lifecycle.test.js +405 -6
  62. package/tests/sbatch.test.js +190 -0
  63. package/tests/secrets.test.js +16 -0
  64. package/tests/slurm.test.js +77 -0
  65. package/tests/spec.test.js +59 -1
  66. package/tests/status.test.js +73 -0
  67. package/tests/task.test.js +109 -0
  68. package/tests/template.test.js +7 -0
package/README.md CHANGED
@@ -36,6 +36,76 @@ badgr receipts
36
36
 
37
37
  ---
38
38
 
39
+ ## Coding & testing agents: `badgr launch`
40
+
41
+ Run a coding agent or a test suite on a CPU VM with one command — no image, source, or `--max-cost` required for the four built-in workloads. Three separate things are going on here, worth keeping straight: **which agent CLI** runs (`cline`, `claude`, `codex`, `playwright`), **who pays for model usage** (Badgr, or your own Anthropic/OpenAI account), and **who provisions the VM and runs the command** (always Badgr).
42
+
43
+ ```bash
44
+ badgr launch cline "Fix the checkout bug" # Badgr provides model access — no account to connect
45
+ badgr launch claude "Fix the checkout bug" # runs Claude Code — connect your Anthropic account
46
+ badgr launch codex "Write tests" # runs the Codex CLI — connect your OpenAI account
47
+ badgr launch playwright "Test the checkout flow" # no model account involved at all
48
+ ```
49
+
50
+ **Authentication model:**
51
+
52
+ | Workload | CLI that runs | Who pays for model usage |
53
+ |----------|---------------|---------------------------|
54
+ | `cline` | Cline | Badgr provides and pays for model access |
55
+ | `claude` | Claude Code | Your Anthropic account — Badgr only provides the VM |
56
+ | `codex` | Codex CLI | Your OpenAI/ChatGPT account — Badgr only provides the VM |
57
+ | `playwright` | Playwright | No model account required |
58
+
59
+ Installing/running the `claude` or `codex` CLI doesn't by itself give a disposable Badgr VM model access — the CLI still needs to authenticate, and a VM's sign-in doesn't inherit from your laptop. `badgr launch claude`/`badgr launch codex` prompt inline the first time to connect your account and store it for next time; `cline` and `playwright` need nothing. Each workload picks its own VM size automatically (`small`, or `browser` for playwright's preinstalled Chromium) — override with `--size small|medium|browser`. A $2 default `--max-cost` applies unless you pass your own.
60
+
61
+ Today, "connect your account" means securely storing an API key (`ANTHROPIC_API_KEY` / `OPENAI_API_KEY`) — both Claude Code and the Codex CLI also support signing in via a Claude.ai/ChatGPT account, and a future version of `badgr connect` may add that OAuth flow instead of a pasted key; the one-time-connection UX is the same either way.
62
+
63
+ ```bash
64
+ badgr connect anthropic --key sk-ant-... # connect your Anthropic account ahead of time (optional — badgr launch prompts inline if missing)
65
+ badgr connect # list what's already connected
66
+ badgr connect anthropic --remove # disconnect
67
+ ```
68
+
69
+ Retrieve results after a run finishes:
70
+
71
+ ```bash
72
+ badgr pull <deployment-id> # pull a code-editing agent's patch as a local git diff/branch
73
+ badgr artifacts <deployment-id> # download everything else — test reports, screenshots, traces
74
+ ```
75
+
76
+ ### Advanced escape hatch — any other command
77
+
78
+ For anything beyond the four built-in workloads, run an arbitrary command on a CPU VM the same way `badgr run` does for GPU jobs:
79
+
80
+ ```bash
81
+ badgr launch . --max-cost 1 -- npm test
82
+ badgr launch https://github.com/user/repo --max-cost 1 -- python narrgo.py
83
+ ```
84
+
85
+ Everything before `--` is a `badgr launch` flag; everything after `--` is passed to your command verbatim, including anything that looks like a flag (`badgr launch . -- claude --max-cost 1` sends `--max-cost 1` to `claude`, not to Badgr). The default runner image has no Node.js/npm — use `--image <image>` for anything that needs it.
86
+
87
+ | Flag | Default | Description |
88
+ |------|---------|-------------|
89
+ | `--cmd "<command>"` | — | Quoted command form, equivalent to `-- <command>` |
90
+ | `--image <image>` | CPU runtime default | Custom image instead of the default |
91
+ | `--detach` / `--no-detach` | detach | `launch` detaches by default; `--no-detach` streams logs and waits |
92
+ | `--env KEY=VALUE` | — | Environment variable (repeatable) — Badgr warns if a value looks like a secret |
93
+ | `--artifacts <path>` | — | Extra path to capture and upload (repeatable), e.g. `--artifacts playwright-report`. Retrieve with `badgr artifacts <id>` |
94
+ | `--max-cost <$>` | `$2` | Auto-stop when spend reaches this amount |
95
+ | `--max-runtime <min>` | 60 | Auto-stop after N minutes |
96
+ | `--region US\|EU\|AU` | — | Region preference |
97
+ | `--size small\|medium\|browser` | per-workload default | VM class override |
98
+
99
+ ### `badgr task` — a labeled launch
100
+
101
+ ```bash
102
+ badgr task "Run the Chromium tests and tell me what failed" --max-cost 1 -- npm run test:chromium
103
+ ```
104
+
105
+ A thin wrapper over `badgr launch . -- <command>` — the description is printed for the human and otherwise discarded; the receipt and `badgr status` still key off the deployment ID like any other launch.
106
+
107
+ ---
108
+
39
109
  ## Also try: image generation
40
110
 
41
111
  ```bash
@@ -46,14 +116,56 @@ Runs a blessed ComfyUI workflow, no setup, and prints image URLs when done. No m
46
116
 
47
117
  ---
48
118
 
119
+ ## Something not working? `badgr doctor`
120
+
121
+ Read-only diagnosis for GPU workload failures — checks your GPU, drivers,
122
+ CUDA, PyTorch, and disk, then tells you what's likely wrong and what to try
123
+ next. No login, no setup, never mutates your machine.
124
+
125
+ ```bash
126
+ badgr doctor
127
+ ```
128
+
129
+ ```bash
130
+ # A model won't fit / you're not sure it'll fit before you try
131
+ badgr doctor --model meta-llama/Llama-3.1-8B-Instruct
132
+
133
+ # You have an error from a crashed job — save it to a file first
134
+ badgr doctor --logs error.log
135
+
136
+ # A ComfyUI workflow is failing or referencing something missing
137
+ badgr doctor --workflow my-workflow.json
138
+
139
+ # You started a server and it's not responding (ComfyUI, llama.cpp, and
140
+ # generic health formats are recognized too, not just OpenAI-style)
141
+ badgr doctor --url http://localhost:8000/v1/models
142
+
143
+ # Splitting a big model across multiple GPUs (tensor parallel)
144
+ badgr doctor --model meta-llama/Llama-3.1-70B-Instruct-AWQ --serve --gpu-count 2
145
+
146
+ # Machine-readable output for scripts/CI
147
+ badgr doctor --json
148
+ ```
149
+
150
+ Run `badgr doctor --help` for the full flag list. Details in
151
+ [`docs/gpu-doctor.md`](../../docs/gpu-doctor.md).
152
+
153
+ ---
154
+
49
155
  ## Commands
50
156
 
51
157
  ```text
52
158
  login
159
+ connect
160
+ doctor
53
161
  run
162
+ launch
163
+ task
54
164
  serve
55
165
  status
56
166
  logs
167
+ pull
168
+ artifacts
57
169
  down
58
170
  receipts
59
171
  test
@@ -62,15 +174,21 @@ test
62
174
  | Command | What it does |
63
175
  |---------|-------------|
64
176
  | `badgr login` | Save API key to `~/.badgr/config.json` |
177
+ | `badgr connect <provider>` | Store a provider credential (`anthropic`, `openai`) for `badgr launch` |
178
+ | `badgr doctor` | Diagnose a GPU workload failure — read-only, no login needed |
65
179
  | `badgr run <command>` | Run a one-off GPU job (any container command) |
180
+ | `badgr launch cline\|claude\|codex\|playwright "<task>"` | Run a coding/testing agent on a CPU VM — image + command auto-selected |
181
+ | `badgr task "<desc>" -- <command>` | Label + launch — thin wrapper over `badgr launch . -- <command>` |
66
182
  | `badgr serve <model>` | Start a persistent OpenAI-compatible endpoint |
67
183
  | `badgr status` | Show what's running and what's billing |
68
184
  | `badgr logs <id>` | Fetch log output from a deployment |
185
+ | `badgr pull <id>` | Pull a code-editing agent's patch as a local git diff/branch |
186
+ | `badgr artifacts <id>` | Download non-patch outputs (test reports, screenshots, traces) |
69
187
  | `badgr down <id>` | Terminate a deployment — stops billing immediately |
70
188
  | `badgr receipts [n]` | Cost, route, and retry receipts (default 10) |
71
189
  | `badgr test` | Run an end-to-end test (provision → run → teardown) |
72
190
 
73
- More commands below, under [Advanced](#advanced): `comfyui`, `train`, `transcribe`, `embed`, `workload`, `workspace`, `capacity`, `billing`.
191
+ More commands below, under [Advanced](#advanced): `comfyui`, `train`, `transcribe`, `embed`, `workload`, `workspace`, `batch`, `sbatch`, `capacity`, `billing`.
74
192
 
75
193
  `badgr serve` — for anything that needs a persistent endpoint: LLM serving, embeddings, image generation APIs, transcription APIs.
76
194
 
@@ -369,6 +487,51 @@ badgr workspace list
369
487
  badgr workspace delete my-project
370
488
  ```
371
489
 
490
+ ### `badgr batch` — generic containerized batch jobs
491
+
492
+ ```bash
493
+ badgr batch run workload.yml
494
+ badgr batch status dep-abc123
495
+ badgr batch artifacts dep-abc123
496
+ badgr batch receipt dep-abc123
497
+ badgr batch compare dep-abc123 dep-def456
498
+ ```
499
+
500
+ For CV/video/scientific batch, simulation, and physical-AI eval workloads — runs a container from a `workload.yml` spec and captures output artifacts automatically.
501
+
502
+ **Fan-out** — run the same program once per file in a directory, one deployment per input, in parallel:
503
+
504
+ ```bash
505
+ badgr batch run workload.yml --fan-out ./scenarios
506
+ badgr batch run workload.yml --fan-out ./scenarios --max-concurrency 10
507
+ badgr batch run workload.yml --fan-out ./scenarios --only failed1.json,failed2.json
508
+ ```
509
+
510
+ `workload.yml` must declare exactly one `inputs:` entry (the path that varies per task).
511
+
512
+ | Flag | Default | Description |
513
+ |------|---------|-------------|
514
+ | `--fan-out <dir>` | — | Run once per file in this directory instead of a single job |
515
+ | `--max-concurrency <n>` | 5 | Cap in-flight fan-out deployments |
516
+ | `--only <f1,f2>` | — | Rerun just the named input files |
517
+ | `--dry-run` | — | Preview the batch/fan-out plan without provisioning |
518
+
519
+ ### `badgr sbatch` — existing Slurm scripts
520
+
521
+ ```bash
522
+ badgr sbatch job.slurm
523
+ badgr sbatch job.slurm --dry-run
524
+ badgr sbatch array_job.slurm # #SBATCH --array=1-100 fans out into one deployment per task
525
+ badgr sbatch array_job.slurm --max-concurrency 10
526
+ ```
527
+
528
+ Translates `--cpus-per-task`/`--mem`/`--gres`/`--time`/`--export` from a real `.slurm` file. `#SBATCH --array=...` directives fan out into bounded-concurrency deployments the same way `batch run --fan-out` does; unsupported directives (`--partition`, `--qos`, `--account`) print a visible warning but don't block translation of the rest.
529
+
530
+ | Flag | Default | Description |
531
+ |------|---------|-------------|
532
+ | `--max-concurrency <n>` | 5 | Cap in-flight array tasks |
533
+ | `--dry-run` | — | Preview the translated job without provisioning |
534
+
372
535
  ### Other commands
373
536
 
374
537
  - `badgr capacity [--gpu <type>]` — check available GPU capacity right now
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "badgr-cli",
3
- "version": "1.0.48",
3
+ "version": "1.1.1",
4
4
  "description": "Badgr — run or serve GPU workloads from one command",
5
5
  "type": "module",
6
6
  "bin": {
package/src/api.js CHANGED
@@ -135,7 +135,21 @@ export function runJob(config, body) {
135
135
  apiKey: config.apiKey,
136
136
  baseUrl: config.baseUrl,
137
137
  body,
138
- timeoutMs: 130_000,
138
+ timeoutMs: 220_000, // must stay above backend's ~200s provision deadline — see fallback.js's callWithFallback
139
+ });
140
+ }
141
+
142
+ // Preview the VM class + Badgr rate for a CPU launch before provisioning
143
+ // anything (see backend run_serve_routes.py's POST /v1/run/quote). Callers
144
+ // should treat a failure here as non-fatal — this is a best-effort preview,
145
+ // never a precondition for launching.
146
+ export function quoteRun(config, body) {
147
+ return callApi('/run/quote', {
148
+ method: 'POST',
149
+ apiKey: config.apiKey,
150
+ baseUrl: config.baseUrl,
151
+ body,
152
+ timeoutMs: 5_000,
139
153
  });
140
154
  }
141
155
 
@@ -145,7 +159,7 @@ export function serveModel(config, body) {
145
159
  apiKey: config.apiKey,
146
160
  baseUrl: config.baseUrl,
147
161
  body,
148
- timeoutMs: 130_000,
162
+ timeoutMs: 220_000, // must stay above backend's ~200s provision deadline — see fallback.js's callWithFallback
149
163
  });
150
164
  }
151
165
 
@@ -0,0 +1,55 @@
1
+ import fs from 'fs';
2
+ import os from 'os';
3
+ import path from 'path';
4
+
5
+ /**
6
+ * Resolves the /v1/deployments/{id}/artifacts/download URL from a
7
+ * configured baseUrl, whether or not it already ends in /v1.
8
+ */
9
+ export function artifactDownloadUrl(baseUrl, deploymentId) {
10
+ const base = String(baseUrl || '').replace(/\/v1\/?$/, '').replace(/\/+$/, '');
11
+ return `${base}/v1/deployments/${deploymentId}/artifacts/download`;
12
+ }
13
+
14
+ /**
15
+ * Downloads a deployment's artifact tar.gz and extracts it into destDir
16
+ * (created if needed). Shared by `badgr pull`, `badgr artifacts`, and
17
+ * `badgr batch artifacts` — the only thing that differs between them is
18
+ * how each wraps a non-OK response into a user-facing message.
19
+ *
20
+ * On a non-OK response, throws an Error with `.httpStatus`/`.statusText`/
21
+ * `.bodyText` set so callers can build their own message; the thrown
22
+ * Error's own `.message` is a reasonable generic fallback. A network-level
23
+ * failure (DNS, connection refused, etc.) propagates as whatever `fetch`
24
+ * itself throws, with no `.httpStatus` — callers can use that distinction
25
+ * to tell "couldn't reach Badgr" apart from "Badgr said no".
26
+ */
27
+ export async function downloadAndExtractArtifact(config, deploymentId, destDir) {
28
+ fs.mkdirSync(destDir, { recursive: true });
29
+
30
+ const res = await fetch(artifactDownloadUrl(config.baseUrl, deploymentId), {
31
+ headers: { Authorization: `Bearer ${config.apiKey}` },
32
+ });
33
+ if (!res.ok) {
34
+ const bodyText = await res.text().catch(() => '');
35
+ const err = new Error(`artifact download failed: ${res.status} ${res.statusText}${bodyText ? ` — ${bodyText}` : ''}`);
36
+ err.httpStatus = res.status;
37
+ err.statusText = res.statusText;
38
+ err.bodyText = bodyText;
39
+ throw err;
40
+ }
41
+
42
+ const bytes = Buffer.from(await res.arrayBuffer());
43
+ const tmpFile = path.join(os.tmpdir(), `badgr-artifact-${deploymentId}-${Date.now()}.tar.gz`);
44
+ fs.writeFileSync(tmpFile, bytes);
45
+ try {
46
+ // node-tar (v7) has no default export — only named exports (x, c, ...).
47
+ // Destructuring `default` here always resolved to undefined, crashing
48
+ // every real extraction with "Cannot read properties of undefined
49
+ // (reading 'x')" — found via a live badgr artifacts/pull download.
50
+ const { x } = await import('tar');
51
+ await x({ file: tmpFile, cwd: destDir });
52
+ } finally {
53
+ fs.rmSync(tmpFile, { force: true });
54
+ }
55
+ }
package/src/badgr.js CHANGED
@@ -8,6 +8,9 @@ import { statusCommand } from './commands/status.js';
8
8
  import { logsCommand } from './commands/logs.js';
9
9
  import { receiptsCommand } from './commands/receipts.js';
10
10
  import { runCommand } from './commands/run.js';
11
+ import { launchCommand } from './commands/launch.js';
12
+ import { taskCommand } from './commands/task.js';
13
+ import { artifactsCommand } from './commands/artifacts.js';
11
14
  import { serveCommand } from './commands/serve.js';
12
15
  import { modelsCommand } from './commands/models.js';
13
16
  import { capacityCommand } from './commands/capacity.js';
@@ -20,23 +23,34 @@ import { embedCommand } from './commands/embed.js';
20
23
  import { templateCommand } from './commands/template.js';
21
24
  import { workloadCommand } from './commands/workload.js';
22
25
  import { batchCommand } from './commands/batch.js';
26
+ import { sbatchCommand } from './commands/sbatch.js';
23
27
  import { workspaceCommand } from './commands/workspace.js';
24
28
  import { detectCommand } from './commands/detect.js';
25
29
  import { restartCommand } from './commands/restart.js';
26
30
  import { rerunCommand } from './commands/rerun.js';
27
31
  import { heartbeatCommand } from './commands/heartbeat.js';
32
+ import { pullCommand } from './commands/pull.js';
33
+ import { doctorCommand } from './commands/doctor.js';
34
+ import { connectCommand } from './commands/connect.js';
28
35
 
29
36
  const HELP = `
30
37
  ${chalk.bold('badgr')} — run or serve GPU workloads from one command
31
38
 
32
39
  ${chalk.bold('COMMANDS')}
33
40
  ${chalk.cyan('badgr login')} Authenticate with your API key
41
+ ${chalk.cyan('badgr connect <provider>')} Credential setup (badgr connect anthropic/openai) — also prompted inline by badgr launch if missing
34
42
  ${chalk.cyan('badgr detect <path>')} Inspect a project and report the GPU job Badgr would run
35
43
  ${chalk.cyan('badgr run <command>')} Run a one-off GPU job
44
+ ${chalk.cyan('badgr launch cline|claude|codex|playwright "<task>"')} Run a coding/testing workload on a CPU VM — image + command auto-selected
45
+ ${chalk.cyan('badgr launch <source> -- <command>')} Advanced escape hatch: run any other command on a CPU VM
46
+ ${chalk.cyan('badgr task "<desc>" -- <command>')} Label + launch — thin wrapper over badgr launch . -- <command>
36
47
  ${chalk.cyan('badgr serve <model>')} Serve a model with an OpenAI-compatible endpoint
37
48
  ${chalk.cyan('badgr serve openwebui')} Serve Open WebUI — chat UI, connects to a model endpoint
49
+ ${chalk.cyan('badgr doctor')} Diagnose why a GPU workload is likely failing (read-only, no login needed)
38
50
  ${chalk.cyan('badgr status')} Show what's running and what's billing
39
51
  ${chalk.cyan('badgr logs <id>')} Stream logs for a running job or endpoint
52
+ ${chalk.cyan('badgr pull <id>')} Safely pull cloud-agent patch artifacts
53
+ ${chalk.cyan('badgr artifacts <id>')} Download non-patch outputs (test reports, screenshots, traces)
40
54
  ${chalk.cyan('badgr down <id>')} Stop a deployment and end billing
41
55
  ${chalk.cyan('badgr restart <id>')} Relaunch an endpoint with the same config and API key
42
56
  ${chalk.cyan('badgr rerun <id>')} Replay a past job or endpoint with its exact original spec
@@ -50,10 +64,12 @@ ${chalk.bold('COMMANDS')}
50
64
  ${chalk.cyan('badgr workspace list')} List workspace trackers (job history + cost per named context)
51
65
  ${chalk.cyan('badgr workspace create')} Create a workspace tracker (link jobs to a named storage path)
52
66
  ${chalk.cyan('badgr batch run <workload.yml>')} Run a generic containerized batch job with artifact capture
67
+ ${chalk.cyan('badgr batch run <yml> --fan-out <dir>')} Run the same program once per file in <dir> — one deployment per input, in parallel
53
68
  ${chalk.cyan('badgr batch status <run_id>')} Show status, failure reason, cost, teardown
54
69
  ${chalk.cyan('badgr batch artifacts <run_id>')} Download and extract output artifacts
55
70
  ${chalk.cyan('badgr batch receipt <run_id>')} Show the full batch receipt
56
71
  ${chalk.cyan('badgr batch compare <a> <b>')} Compare success_metric between two runs
72
+ ${chalk.cyan('badgr sbatch <job.slurm>')} Run an existing Slurm batch script (cpus/mem/gres/array/time translated)
57
73
 
58
74
  ${chalk.bold('SHORTCUTS')} ${chalk.dim('(wrappers around run / serve for common workloads)')}
59
75
  ${chalk.cyan('badgr comfyui run <workflow.json>')} Launch ComfyUI, return endpoint URL
@@ -68,6 +84,40 @@ ${chalk.bold('SHORTCUTS')} ${chalk.dim('(wrappers around run / serve for common
68
84
  ${chalk.cyan('badgr serve --list-aliases')} List blessed vLLM model shortcuts (qwen-7b, llama-8b, …)
69
85
 
70
86
  ${chalk.bold('EXAMPLES')}
87
+ ${chalk.dim('# Frictionless launch — no source, no --, no image, no --max-cost required')}
88
+ ${chalk.dim('# (a $2 default cost cap applies automatically; override with --max-cost).')}
89
+ ${chalk.dim('# cline is Badgr-native (no credential needed); claude/codex prompt inline for a')}
90
+ ${chalk.dim('# provider key the first time, then remember it; playwright needs no credential.')}
91
+ ${chalk.dim('# Each workload auto-selects its VM class (small, or "browser" for playwright);')}
92
+ ${chalk.dim('# override with --size small|medium|browser.')}
93
+ badgr launch cline "Fix the checkout bug"
94
+ badgr launch claude "Fix the checkout bug"
95
+ badgr launch codex "Write tests"
96
+ badgr launch playwright "Test the checkout flow"
97
+ badgr launch claude --size medium "Run the complete test suite"
98
+ badgr pull <id>
99
+ badgr artifacts <id>
100
+ badgr logs <id>
101
+ badgr receipts <id>
102
+
103
+ ${chalk.dim('# Explicit form — advanced escape hatch for anything not in the four workloads above.')}
104
+ ${chalk.dim('# Badgr flags go BEFORE --; everything after -- is passed to your command unchanged.')}
105
+ ${chalk.dim('# The default runner image is Python-only (no Node.js/npm) — a Node-based command')}
106
+ ${chalk.dim('# must install what it needs, or use --image with a custom image (see images/badgr-agent-*):')}
107
+ badgr launch . --max-cost 1 -- npm test
108
+ badgr launch https://github.com/user/repo --max-cost 1 -- python narrgo.py
109
+ badgr artifacts <id>
110
+
111
+ ${chalk.dim('# badgr task is a thin label wrapper over badgr launch . -- <command>:')}
112
+ badgr task "Run the Chromium tests and tell me what failed" --max-cost 1 -- npm run test:chromium
113
+
114
+ ${chalk.dim('# Diagnose why a GPU workload is likely failing (local, read-only, no login):')}
115
+ badgr doctor
116
+ badgr doctor --model Qwen/Qwen2.5-7B-Instruct --serve
117
+ badgr doctor --logs ./vllm.log
118
+ badgr doctor --workflow ./workflow.json
119
+ badgr doctor --url http://localhost:8000/health
120
+
71
121
  ${chalk.dim('# Point Badgr at any project and see what it detects:')}
72
122
  badgr detect .
73
123
  badgr run . --max-cost 5 --save my-job
@@ -106,12 +156,31 @@ ${chalk.bold('EXAMPLES')}
106
156
  badgr batch artifacts dep-abc123
107
157
  badgr batch compare dep-abc123 dep-def456
108
158
 
159
+ ${chalk.dim('# Run one program across many inputs — one deployment per file, in parallel:')}
160
+ ${chalk.dim('# workload.yml must declare exactly one inputs: entry (the path that varies per task).')}
161
+ ${chalk.dim('# --max-concurrency caps in-flight deployments (default: 5). --only reruns just the named files.')}
162
+ badgr batch run workload.yml --fan-out ./scenarios
163
+ badgr batch run workload.yml --fan-out ./scenarios --max-concurrency 10
164
+ badgr batch run workload.yml --fan-out ./scenarios --only failed1.json,failed2.json
165
+
166
+ ${chalk.dim('# Run an existing Slurm batch script — cpus-per-task/mem/gres/array/time translated:')}
167
+ ${chalk.dim('# --max-concurrency caps in-flight array tasks (default: 5).')}
168
+ badgr sbatch job.slurm
169
+ badgr sbatch job.slurm --dry-run
170
+ badgr sbatch array_job.slurm ${chalk.dim('# #SBATCH --array=1-100 fans out into one deployment per task')}
171
+ badgr sbatch array_job.slurm --max-concurrency 10
172
+
109
173
  ${chalk.dim('# Tier 2 — marketplace routing, lower-cost options:')}
110
174
  badgr serve meta-llama/Llama-3.1-8B-Instruct --tier 2 --max-cost 10
111
175
 
112
176
  ${chalk.dim('# Pin a specific GPU:')}
113
177
  badgr serve meta-llama/Llama-3.1-8B-Instruct --gpu L40S --max-cost 10
114
178
 
179
+ ${chalk.dim('# Describe basic compute needs instead of a GPU model — Badgr finds a compatible machine:')}
180
+ badgr run . --cpu 16 --memory 64GB --gpu-memory 24GB --max-cost 5
181
+ badgr run . --gpu-memory 24GB ${chalk.dim('# any GPU with at least 24GB VRAM')}
182
+ badgr run . --gpu A100 --count 4 ${chalk.dim('# exact hardware, for workloads that need it')}
183
+
115
184
  ${chalk.dim('# Manage a running deployment:')}
116
185
  badgr status
117
186
  badgr logs dep-abc123
@@ -120,6 +189,10 @@ ${chalk.bold('EXAMPLES')}
120
189
 
121
190
  ${chalk.bold('badgr run OPTIONS')}
122
191
  --gpu <type> GPU type (default: auto — Badgr picks best available)
192
+ --gpu-memory <size> Minimum GPU VRAM, e.g. 24GB — Badgr picks any GPU that satisfies it (alias: --min-vram)
193
+ --cpu <cores> Minimum CPU cores
194
+ --memory <size> Minimum RAM, e.g. 64GB
195
+ --no-gpu Run on a CPU-only VM — no GPU is provisioned (conflicts with --gpu/--gpu-memory)
123
196
  --tier 1 Managed provider routing (default)
124
197
  --tier 2 Marketplace provider routing, lower-cost options
125
198
  --image <image> Docker image (default: python:3.11-slim)
@@ -131,6 +204,30 @@ ${chalk.bold('badgr run OPTIONS')}
131
204
  --max-cost <$> Auto-stop when spend reaches this amount
132
205
  --detach Return immediately, don't stream logs
133
206
 
207
+ ${chalk.bold('badgr launch OPTIONS')}
208
+ ${chalk.yellow('All badgr launch flags below must come BEFORE --. Everything after -- is')}
209
+ ${chalk.yellow('passed to your command verbatim, including anything that looks like a flag —')}
210
+ ${chalk.yellow('badgr launch . -- claude --max-cost 1 sends --max-cost 1 to claude, not badgr.')}
211
+ --cmd "<command>" Quoted command form (equivalent to \`-- <command>\`)
212
+ --image <image> Custom image instead of the default CPU runtime image
213
+ --detach Keep running after disconnect (default for launch)
214
+ --no-detach Stream logs and wait instead of detaching
215
+ --env KEY=VALUE Set an environment variable (repeatable). Provider keys (ANTHROPIC_API_KEY,
216
+ etc.) passed this way may land in shell history and \`ps\` output — badgr
217
+ launch warns when a key looks secret. Dashboard-managed --profile secrets
218
+ are planned but not built yet.
219
+ --artifacts <path> Extra path to capture and upload as a downloadable artifact (repeatable) —
220
+ e.g. --artifacts playwright-report --artifacts test-results. Retrieve with
221
+ \`badgr artifacts <id>\`. Relative paths are resolved from the workspace root.
222
+ --max-cost <$> Auto-stop when spend reaches this amount
223
+ --max-runtime <min> Auto-stop after N minutes (default: 60)
224
+ --region US|EU|AU Region preference
225
+ --size small|medium|browser VM class override. Defaults per workload: small for cline/claude/
226
+ codex/explicit form, browser for playwright (Chromium preinstalled).
227
+
228
+ ${chalk.bold('badgr artifacts OPTIONS')}
229
+ --output <dir> Directory to extract into (default: ~/.badgr/artifacts/<id>)
230
+
134
231
  ${chalk.bold('badgr serve OPTIONS')}
135
232
  --gpu <type> GPU type (default: auto — inferred from model size)
136
233
  --image <image> Serve a custom container instead of a HuggingFace model
@@ -169,8 +266,13 @@ async function main() {
169
266
 
170
267
  switch (cmd) {
171
268
  case 'login': return loginCommand(chalk, saveConfig);
269
+ case 'connect': return connectCommand(rest, chalk);
172
270
  case 'detect': return detectCommand(config, rest, chalk);
173
271
  case 'run': return runCommand(config, rest, chalk);
272
+ case 'launch': return launchCommand(config, rest, chalk);
273
+ case 'task': return taskCommand(config, rest, chalk);
274
+ case 'artifacts': return artifactsCommand(config, rest, chalk);
275
+ case 'pull': return pullCommand(config, rest, chalk);
174
276
  case 'serve': return serveCommand(config, rest, chalk);
175
277
  case 'status': return statusCommand(config, rest, chalk);
176
278
  case 'logs': return logsCommand(config, rest, chalk);
@@ -178,6 +280,7 @@ async function main() {
178
280
  case 'restart': return restartCommand(config, rest, chalk);
179
281
  case 'rerun': return rerunCommand(config, rest, chalk);
180
282
  case 'heartbeat': return heartbeatCommand(config, rest, chalk);
283
+ case 'doctor': return doctorCommand(config, rest, chalk);
181
284
  case 'receipts': return receiptsCommand(config, rest, chalk);
182
285
  case 'models': return modelsCommand(config, chalk);
183
286
  case 'capacity': return capacityCommand(config, rest, chalk);
@@ -190,6 +293,7 @@ async function main() {
190
293
  case 'template': return templateCommand(config, rest, chalk);
191
294
  case 'workload': return workloadCommand(config, rest, chalk);
192
295
  case 'batch': return batchCommand(config, rest, chalk);
296
+ case 'sbatch': return sbatchCommand(config, rest, chalk);
193
297
  case 'workspace': return workspaceCommand(config, rest, chalk);
194
298
  // legacy aliases kept for compatibility
195
299
  case 'up': return upCommand(config, rest, chalk);
package/src/batch.js CHANGED
@@ -86,16 +86,27 @@ export async function monitorBatchJob(config, depId, rcptId, opts) {
86
86
  'interrupted': chalk.yellow('\n Stopping...'),
87
87
  };
88
88
  console.log(msgs[reason] ?? chalk.yellow('\n Stopping...'));
89
- try { await terminateDeployment(config, depId); } catch {}
89
+ // A 200 response only means deletion was requested, not confirmed
90
+ // teardown_ok reflects whether the provider resource is actually gone.
91
+ let teardownConfirmed = false;
92
+ try {
93
+ const result = await terminateDeployment(config, depId);
94
+ teardownConfirmed = result?.teardown_ok === 'ok';
95
+ } catch {}
90
96
  const runtimeMs = Date.now() - attachStart;
91
97
  const finalCost = ratePerHour * (runtimeMs / 3_600_000);
92
98
  updateReceipt(rcptId, {
93
99
  status: reason,
100
+ teardownStatus: teardownConfirmed ? 'terminated' : 'failed',
94
101
  runtimeSeconds: Math.round(runtimeMs / 1000),
95
102
  finalCost,
96
103
  });
97
104
  console.log(chalk.dim(` Runtime: ${fmtRuntime(runtimeMs)} • Est. cost: $${finalCost.toFixed(4)}`));
98
- console.log(chalk.dim(' Stopped. Billing ended.\n'));
105
+ if (teardownConfirmed) {
106
+ console.log(chalk.dim(' Stopped. Billing ended.\n'));
107
+ } else {
108
+ console.log(chalk.yellow(` Stop requested but not confirmed — resource may still be billing, check \`badgr status\` and \`badgr down ${depId}\`\n`));
109
+ }
99
110
  }
100
111
 
101
112
  while (true) {
@@ -135,14 +146,21 @@ export async function monitorBatchJob(config, depId, rcptId, opts) {
135
146
  const exitCode = dep.exit_code ?? null;
136
147
  const runtimeMs = Date.now() - attachStart;
137
148
  const finalCost = ratePerHour * (runtimeMs / 3_600_000);
138
- try { await terminateDeployment(config, depId); } catch {}
149
+ // A 200 response only means deletion was requested, not confirmed
150
+ // teardown_ok reflects whether the provider resource is actually gone.
151
+ let teardownConfirmed = false;
152
+ try {
153
+ const result = await terminateDeployment(config, depId);
154
+ teardownConfirmed = result?.teardown_ok === 'ok';
155
+ } catch {}
139
156
  updateReceipt(rcptId, {
140
157
  status: dep.status,
141
158
  exitCode,
159
+ teardownStatus: teardownConfirmed ? 'terminated' : 'failed',
142
160
  runtimeSeconds: Math.round(runtimeMs / 1000),
143
161
  finalCost,
144
162
  });
145
- return { status: dep.status, exitCode, runtimeMs, reason: 'complete' };
163
+ return { status: dep.status, exitCode, runtimeMs, teardownConfirmed, reason: 'complete' };
146
164
  }
147
165
  }
148
166
 
package/src/browser.js ADDED
@@ -0,0 +1,23 @@
1
+ import { execFileSync } from 'child_process';
2
+
3
+ /** Best-effort open a URL in the user's default browser. Never throws —
4
+ * callers should always also print the URL, since headless/CI environments
5
+ * (or an unrecognized platform) may have nothing to open it with.
6
+ *
7
+ * Uses execFileSync (argv array, no shell) rather than execSync (a shell
8
+ * string) — url is not always a local constant (onboarding.js passes
9
+ * session.login_url, a backend API response), so building a shell command
10
+ * via string interpolation would be a real command-injection vector if a
11
+ * response ever contained shell metacharacters. execFileSync passes url as
12
+ * one literal argv element to the opener command directly; no shell parses
13
+ * it at all. */
14
+ export function openBrowser(url) {
15
+ const platform = process.platform;
16
+ try {
17
+ if (platform === 'darwin') execFileSync('open', [url]);
18
+ else if (platform === 'win32') execFileSync('cmd', ['/c', 'start', '', url]);
19
+ else execFileSync('xdg-open', [url]);
20
+ } catch {
21
+ // Silently ignore — the caller prints the URL as a fallback.
22
+ }
23
+ }
@@ -0,0 +1,75 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import { requireApiKey, CONFIG_DIR } from '../config.js';
4
+ import { downloadAndExtractArtifact } from '../artifactDownload.js';
5
+
6
+ /**
7
+ * badgr artifacts <id> [--output <dir>]
8
+ *
9
+ * Generic artifact retrieval for `badgr launch`/`badgr run` output paths
10
+ * (e.g. --artifacts playwright-report --artifacts test-results) — anything
11
+ * captured that isn't a git patch. `badgr pull` is for code changes;
12
+ * `badgr artifacts` is for everything else a command produced.
13
+ */
14
+
15
+ function defaultDestDir(deploymentId) {
16
+ return path.join(CONFIG_DIR, 'artifacts', deploymentId);
17
+ }
18
+
19
+ export function parseArtifactsArgs(args) {
20
+ const flags = {};
21
+ const positional = [];
22
+ for (let i = 0; i < args.length; i++) {
23
+ if (args[i] === '--output') { flags.output = args[++i]; continue; }
24
+ positional.push(args[i]);
25
+ }
26
+ return { deploymentId: positional[0], flags };
27
+ }
28
+
29
+ export function listFilesRecursive(dir, prefix = '') {
30
+ const out = [];
31
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
32
+ const rel = prefix ? `${prefix}/${entry.name}` : entry.name;
33
+ if (entry.isDirectory()) out.push(...listFilesRecursive(path.join(dir, entry.name), rel));
34
+ else out.push(rel);
35
+ }
36
+ return out.sort();
37
+ }
38
+
39
+ export async function artifactsCommand(config, args, chalk) {
40
+ requireApiKey(config);
41
+ const { deploymentId, flags } = parseArtifactsArgs(args);
42
+ if (!deploymentId) {
43
+ console.error(chalk.red('\n Usage: badgr artifacts <deployment-id> [--output <dir>]\n'));
44
+ console.error(chalk.dim(' Example: badgr artifacts dep-abc123\n'));
45
+ process.exitCode = 1;
46
+ return;
47
+ }
48
+
49
+ const destDir = flags.output ? path.resolve(flags.output) : defaultDestDir(deploymentId);
50
+
51
+ try {
52
+ await downloadAndExtractArtifact(config, deploymentId, destDir);
53
+ } catch (err) {
54
+ if (err.httpStatus === 404) {
55
+ console.error(chalk.red(`\n ✗ No artifact found for ${deploymentId}.\n`));
56
+ console.error(chalk.dim(' This means either the run had no --artifacts paths declared, it failed before'));
57
+ console.error(chalk.dim(' producing any of them, or it is still running. Check `badgr logs ' + deploymentId + '`.\n'));
58
+ } else if (err.httpStatus !== undefined) {
59
+ console.error(chalk.red(`\n ✗ Artifact download failed: ${err.httpStatus} ${err.statusText}${err.bodyText ? ` — ${err.bodyText}` : ''}\n`));
60
+ } else {
61
+ console.error(chalk.red(`\n ✗ Could not reach Badgr: ${err.message}\n`));
62
+ }
63
+ process.exitCode = 1;
64
+ return;
65
+ }
66
+
67
+ const files = listFilesRecursive(destDir);
68
+ console.log(chalk.green(`\n Downloaded artifacts for ${deploymentId} → ${destDir}\n`));
69
+ if (files.length === 0) {
70
+ console.log(chalk.dim(' (artifact was empty)'));
71
+ } else {
72
+ for (const f of files) console.log(` - ${f}`);
73
+ }
74
+ console.log();
75
+ }