badgr-cli 1.0.48 → 1.1.0
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 +38 -0
- package/package.json +1 -1
- package/src/api.js +16 -2
- package/src/artifactDownload.js +55 -0
- package/src/badgr.js +104 -0
- package/src/batch.js +22 -4
- package/src/browser.js +23 -0
- package/src/commands/artifacts.js +75 -0
- package/src/commands/batch.js +221 -28
- package/src/commands/billing.js +1 -12
- package/src/commands/capacity.js +9 -4
- package/src/commands/comfyui.js +3 -3
- package/src/commands/connect.js +83 -0
- package/src/commands/doctor.js +127 -0
- package/src/commands/down.js +29 -6
- package/src/commands/launch.js +431 -0
- package/src/commands/pull.js +137 -0
- package/src/commands/run.js +253 -37
- package/src/commands/sbatch.js +232 -0
- package/src/commands/serve.js +3 -3
- package/src/commands/status.js +12 -4
- package/src/commands/task.js +25 -0
- package/src/commands/test-run.js +4 -2
- package/src/credentials.js +65 -0
- package/src/fallback.js +7 -2
- package/src/fanout.js +70 -0
- package/src/gpuDoctor/diskInfo.js +42 -0
- package/src/gpuDoctor/doctor.js +451 -0
- package/src/gpuDoctor/gpuInfo.js +70 -0
- package/src/gpuDoctor/healthCheck.js +63 -0
- package/src/gpuDoctor/logClassifier.js +138 -0
- package/src/gpuDoctor/modelFit.js +107 -0
- package/src/gpuDoctor/probeCache.js +38 -0
- package/src/gpuDoctor/redact.js +29 -0
- package/src/gpuDoctor/torchInfo.js +61 -0
- package/src/gpuDoctor/workflowDoctor.js +96 -0
- package/src/onboarding.js +124 -0
- package/src/slurm.js +193 -0
- package/src/spec.js +59 -2
- package/src/store.js +16 -0
- package/tests/agent-images.test.js +17 -0
- package/tests/artifactDownload.test.js +113 -0
- package/tests/artifacts.test.js +168 -0
- package/tests/batch.test.js +312 -0
- package/tests/browser.test.js +51 -0
- package/tests/capacity.test.js +68 -0
- package/tests/commands.test.js +44 -0
- package/tests/connect.test.js +83 -0
- package/tests/down.test.js +23 -1
- package/tests/fallback-timeout.test.js +41 -0
- package/tests/fanout.test.js +124 -0
- package/tests/gpu-doctor-classifiers.test.js +402 -0
- package/tests/gpu-doctor-doctor.test.js +304 -0
- package/tests/gpu-doctor-probe-cache.test.js +110 -0
- package/tests/gpu-doctor-probes.test.js +257 -0
- package/tests/launch-command-argv.test.js +93 -0
- package/tests/launch-readiness.test.js +1 -0
- package/tests/launch.test.js +440 -0
- package/tests/onboarding.test.js +134 -0
- package/tests/pull.test.js +266 -0
- package/tests/run-lifecycle.test.js +405 -6
- package/tests/sbatch.test.js +190 -0
- package/tests/secrets.test.js +16 -0
- package/tests/slurm.test.js +77 -0
- package/tests/spec.test.js +59 -1
- package/tests/status.test.js +73 -0
- package/tests/task.test.js +109 -0
- package/tests/template.test.js +7 -0
package/README.md
CHANGED
|
@@ -46,10 +46,47 @@ Runs a blessed ComfyUI workflow, no setup, and prints image URLs when done. No m
|
|
|
46
46
|
|
|
47
47
|
---
|
|
48
48
|
|
|
49
|
+
## Something not working? `badgr doctor`
|
|
50
|
+
|
|
51
|
+
Read-only diagnosis for GPU workload failures — checks your GPU, drivers,
|
|
52
|
+
CUDA, PyTorch, and disk, then tells you what's likely wrong and what to try
|
|
53
|
+
next. No login, no setup, never mutates your machine.
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
badgr doctor
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
# A model won't fit / you're not sure it'll fit before you try
|
|
61
|
+
badgr doctor --model meta-llama/Llama-3.1-8B-Instruct
|
|
62
|
+
|
|
63
|
+
# You have an error from a crashed job — save it to a file first
|
|
64
|
+
badgr doctor --logs error.log
|
|
65
|
+
|
|
66
|
+
# A ComfyUI workflow is failing or referencing something missing
|
|
67
|
+
badgr doctor --workflow my-workflow.json
|
|
68
|
+
|
|
69
|
+
# You started a server and it's not responding (ComfyUI, llama.cpp, and
|
|
70
|
+
# generic health formats are recognized too, not just OpenAI-style)
|
|
71
|
+
badgr doctor --url http://localhost:8000/v1/models
|
|
72
|
+
|
|
73
|
+
# Splitting a big model across multiple GPUs (tensor parallel)
|
|
74
|
+
badgr doctor --model meta-llama/Llama-3.1-70B-Instruct-AWQ --serve --gpu-count 2
|
|
75
|
+
|
|
76
|
+
# Machine-readable output for scripts/CI
|
|
77
|
+
badgr doctor --json
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Run `badgr doctor --help` for the full flag list. Details in
|
|
81
|
+
[`docs/gpu-doctor.md`](../../docs/gpu-doctor.md).
|
|
82
|
+
|
|
83
|
+
---
|
|
84
|
+
|
|
49
85
|
## Commands
|
|
50
86
|
|
|
51
87
|
```text
|
|
52
88
|
login
|
|
89
|
+
doctor
|
|
53
90
|
run
|
|
54
91
|
serve
|
|
55
92
|
status
|
|
@@ -62,6 +99,7 @@ test
|
|
|
62
99
|
| Command | What it does |
|
|
63
100
|
|---------|-------------|
|
|
64
101
|
| `badgr login` | Save API key to `~/.badgr/config.json` |
|
|
102
|
+
| `badgr doctor` | Diagnose a GPU workload failure — read-only, no login needed |
|
|
65
103
|
| `badgr run <command>` | Run a one-off GPU job (any container command) |
|
|
66
104
|
| `badgr serve <model>` | Start a persistent OpenAI-compatible endpoint |
|
|
67
105
|
| `badgr status` | Show what's running and what's billing |
|
package/package.json
CHANGED
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:
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
+
}
|