badgr-cli 1.0.16 → 1.0.18
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/HOW_IT_WORKS.md +4 -4
- package/package.json +1 -1
- package/src/commands/run.js +109 -7
- package/src/commands/serve.js +14 -2
- package/src/store.js +1 -1
package/HOW_IT_WORKS.md
CHANGED
|
@@ -11,7 +11,7 @@ npm install -g badgr-cli
|
|
|
11
11
|
badgr login
|
|
12
12
|
```
|
|
13
13
|
|
|
14
|
-
`badgr login` prompts for your API key and base URL, then writes them to `~/.
|
|
14
|
+
`badgr login` prompts for your API key and base URL, then writes them to `~/.badgr/config.json`. Every subsequent command reads that file — no env vars required.
|
|
15
15
|
|
|
16
16
|
---
|
|
17
17
|
|
|
@@ -142,7 +142,7 @@ badgr receipts [n] # default: last 10
|
|
|
142
142
|
|
|
143
143
|
Shows two sets of receipts:
|
|
144
144
|
|
|
145
|
-
1. **CLI action receipts** — every `badgr serve` / `badgr down` recorded locally in `~/.
|
|
145
|
+
1. **CLI action receipts** — every `badgr serve` / `badgr down` recorded locally in `~/.badgr/deployments.json`, with provider, retries, latency, and cost.
|
|
146
146
|
2. **Inference receipts** — per-request records fetched from `GET /v1/receipts` on the API (requires `badgr login`).
|
|
147
147
|
|
|
148
148
|
Every action — including failures — generates a receipt. Receipt IDs are printed on every command output so you can look them up later.
|
|
@@ -202,10 +202,10 @@ GPU type aliases are normalized automatically: `rtx-4090`, `rtx4090`, `4090`, `R
|
|
|
202
202
|
|
|
203
203
|
## Local State
|
|
204
204
|
|
|
205
|
-
All CLI state lives in `~/.
|
|
205
|
+
All CLI state lives in `~/.badgr/`:
|
|
206
206
|
|
|
207
207
|
```
|
|
208
|
-
~/.
|
|
208
|
+
~/.badgr/
|
|
209
209
|
config.json API key + base URL
|
|
210
210
|
deployments.json Active deployments + receipt log (last 200)
|
|
211
211
|
```
|
package/package.json
CHANGED
package/src/commands/run.js
CHANGED
|
@@ -46,18 +46,75 @@ export function classifyFailure(finalStatus, exitCode) {
|
|
|
46
46
|
return null;
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
+
// Lines the log stream never needs to print — we surface them in the status bar instead.
|
|
50
|
+
const LOG_META_RE = /^\[dep-[^\]]+\] (status|gpu|region|endpoint|cost|receipt|provider_status|uptime)=/;
|
|
51
|
+
|
|
52
|
+
// Extract structured values from provider status lines so we can show them nicely.
|
|
53
|
+
function parseProviderLine(line) {
|
|
54
|
+
const gpuUtil = line.match(/\bgpu_util=([\d.]+)%/);
|
|
55
|
+
const cpuUtil = line.match(/\bcpu_util=([\d.]+)%/);
|
|
56
|
+
const ssh = line.match(/\bssh=(\S+)/);
|
|
57
|
+
const provSt = line.match(/\bprovider_status=(\S+)/);
|
|
58
|
+
return {
|
|
59
|
+
gpuUtil: gpuUtil ? parseFloat(gpuUtil[1]) : null,
|
|
60
|
+
cpuUtil: cpuUtil ? parseFloat(cpuUtil[1]) : null,
|
|
61
|
+
ssh: ssh ? ssh[1] : null,
|
|
62
|
+
providerStatus: provSt ? provSt[1] : null,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function renderStatusBar(chalk, { elapsedMs, ratePerHour, gpuUtil, cpuUtil, maxRuntimeMs, maxCost }) {
|
|
67
|
+
const spent = ratePerHour * (elapsedMs / 3_600_000);
|
|
68
|
+
const parts = [`⏱ ${fmtRuntime(elapsedMs)}`];
|
|
69
|
+
if (ratePerHour > 0) parts.push(`$${spent.toFixed(4)} spent`);
|
|
70
|
+
if (gpuUtil !== null) parts.push(`GPU ${gpuUtil.toFixed(0)}%`);
|
|
71
|
+
if (cpuUtil !== null) parts.push(`CPU ${cpuUtil.toFixed(0)}%`);
|
|
72
|
+
if (maxRuntimeMs) {
|
|
73
|
+
const left = Math.max(0, maxRuntimeMs - elapsedMs);
|
|
74
|
+
parts.push(`${fmtRuntime(left)} left`);
|
|
75
|
+
}
|
|
76
|
+
if (maxCost && ratePerHour > 0) {
|
|
77
|
+
const budgetLeft = Math.max(0, maxCost - spent);
|
|
78
|
+
parts.push(`$${budgetLeft.toFixed(4)} budget left`);
|
|
79
|
+
}
|
|
80
|
+
parts.push('Ctrl+C to stop');
|
|
81
|
+
return chalk.dim(' ' + parts.join(' • '));
|
|
82
|
+
}
|
|
83
|
+
|
|
49
84
|
async function attachToJob(config, depId, { chalk, maxRuntimeMs = null, maxCost = null, ratePerHour = 0, onTeardown }) {
|
|
50
85
|
const TERMINAL = new Set(['stopped', 'failed', 'completed']);
|
|
51
86
|
const POLL_MS = 4000;
|
|
52
|
-
let
|
|
87
|
+
let seenContent = new Set(); // track by content, not index, to avoid reprinting stale lines
|
|
53
88
|
let lastStatus = '';
|
|
54
89
|
let consecutiveErrs = 0;
|
|
90
|
+
let gpuUtil = null;
|
|
91
|
+
let cpuUtil = null;
|
|
92
|
+
let sshShown = false;
|
|
93
|
+
let statusBarActive = false;
|
|
55
94
|
const startMs = Date.now();
|
|
56
95
|
|
|
96
|
+
// Ticker: update status bar in place every second between polls.
|
|
97
|
+
let tickerInterval = null;
|
|
98
|
+
const startTicker = () => {
|
|
99
|
+
if (tickerInterval) return;
|
|
100
|
+
statusBarActive = true;
|
|
101
|
+
tickerInterval = setInterval(() => {
|
|
102
|
+
const bar = renderStatusBar(chalk, {
|
|
103
|
+
elapsedMs: Date.now() - startMs, ratePerHour, gpuUtil, cpuUtil, maxRuntimeMs, maxCost,
|
|
104
|
+
});
|
|
105
|
+
process.stdout.write(`\r${bar} `);
|
|
106
|
+
}, 1000);
|
|
107
|
+
};
|
|
108
|
+
const stopTicker = () => {
|
|
109
|
+
if (tickerInterval) { clearInterval(tickerInterval); tickerInterval = null; }
|
|
110
|
+
if (statusBarActive) { process.stdout.write('\r\x1b[2K'); statusBarActive = false; } // clear line
|
|
111
|
+
};
|
|
112
|
+
|
|
57
113
|
let tearing = false;
|
|
58
114
|
const sigintHandler = () => {
|
|
59
115
|
if (tearing) return;
|
|
60
116
|
tearing = true;
|
|
117
|
+
stopTicker();
|
|
61
118
|
onTeardown('interrupted');
|
|
62
119
|
};
|
|
63
120
|
process.once('SIGINT', sigintHandler);
|
|
@@ -71,12 +128,14 @@ async function attachToJob(config, depId, { chalk, maxRuntimeMs = null, maxCost
|
|
|
71
128
|
|
|
72
129
|
if (maxCost !== null && spentSoFar >= maxCost) {
|
|
73
130
|
tearing = true;
|
|
131
|
+
stopTicker();
|
|
74
132
|
onTeardown('max-cost');
|
|
75
133
|
return { status: 'timeout', exitCode: null, runtimeMs: elapsedMs, failureType: null };
|
|
76
134
|
}
|
|
77
135
|
|
|
78
136
|
if (maxRuntimeMs !== null && elapsedMs >= maxRuntimeMs) {
|
|
79
137
|
tearing = true;
|
|
138
|
+
stopTicker();
|
|
80
139
|
onTeardown('max-runtime');
|
|
81
140
|
return { status: 'timeout', exitCode: null, runtimeMs: elapsedMs, failureType: null };
|
|
82
141
|
}
|
|
@@ -93,9 +152,12 @@ async function attachToJob(config, depId, { chalk, maxRuntimeMs = null, maxCost
|
|
|
93
152
|
if (lastStatus === 'running') {
|
|
94
153
|
const lostSec = Math.round((consecutiveErrs * POLL_MS) / 1000);
|
|
95
154
|
if (consecutiveErrs === HEARTBEAT_WARN_POLLS) {
|
|
155
|
+
stopTicker();
|
|
96
156
|
console.log(chalk.yellow(`\n ⚠ No heartbeat for ${lostSec}s — cloud machine may be unresponsive`));
|
|
157
|
+
startTicker();
|
|
97
158
|
} else if (consecutiveErrs >= HEARTBEAT_KILL_POLLS) {
|
|
98
159
|
tearing = true;
|
|
160
|
+
stopTicker();
|
|
99
161
|
onTeardown('heartbeat-lost');
|
|
100
162
|
return { status: 'failed', exitCode: null, runtimeMs: elapsedMs, failureType: 'infrastructure' };
|
|
101
163
|
}
|
|
@@ -106,6 +168,7 @@ async function attachToJob(config, depId, { chalk, maxRuntimeMs = null, maxCost
|
|
|
106
168
|
const status = dep.status;
|
|
107
169
|
if (status !== lastStatus) {
|
|
108
170
|
if (status === 'running' && lastStatus === 'provisioning') {
|
|
171
|
+
stopTicker();
|
|
109
172
|
console.log(chalk.dim(' [running]'));
|
|
110
173
|
}
|
|
111
174
|
lastStatus = status;
|
|
@@ -117,15 +180,41 @@ async function attachToJob(config, depId, { chalk, maxRuntimeMs = null, maxCost
|
|
|
117
180
|
baseUrl: config.baseUrl,
|
|
118
181
|
});
|
|
119
182
|
const lines = logData?.logs ?? [];
|
|
120
|
-
|
|
121
|
-
|
|
183
|
+
|
|
184
|
+
for (const line of lines) {
|
|
185
|
+
// Extract structured provider values (gpu_util, ssh, etc.) from any line.
|
|
186
|
+
const parsed = parseProviderLine(line);
|
|
187
|
+
if (parsed.gpuUtil !== null) gpuUtil = parsed.gpuUtil;
|
|
188
|
+
if (parsed.cpuUtil !== null) cpuUtil = parsed.cpuUtil;
|
|
189
|
+
|
|
190
|
+
// Show SSH address once, prominently.
|
|
191
|
+
if (parsed.ssh && !sshShown) {
|
|
192
|
+
stopTicker();
|
|
193
|
+
console.log(` ${chalk.bold('SSH:')} ${chalk.cyan(parsed.ssh)}`);
|
|
194
|
+
sshShown = true;
|
|
195
|
+
startTicker();
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// Skip lines we've already printed and pure metadata lines.
|
|
199
|
+
if (seenContent.has(line)) continue;
|
|
200
|
+
seenContent.add(line);
|
|
201
|
+
if (LOG_META_RE.test(line)) continue;
|
|
202
|
+
// Skip provider util lines — they're shown in the status bar instead.
|
|
203
|
+
if (/\b(gpu_util|cpu_util|provider_status|uptime)=/.test(line)) continue;
|
|
204
|
+
|
|
205
|
+
stopTicker();
|
|
206
|
+
console.log(` ${chalk.dim(line)}`);
|
|
207
|
+
startTicker();
|
|
122
208
|
}
|
|
123
|
-
seenLines = lines.length;
|
|
124
209
|
} catch {
|
|
125
210
|
// logs not ready yet
|
|
126
211
|
}
|
|
127
212
|
|
|
213
|
+
// Start the ticker once the job is confirmed running.
|
|
214
|
+
if (status === 'running') startTicker();
|
|
215
|
+
|
|
128
216
|
if (TERMINAL.has(status)) {
|
|
217
|
+
stopTicker();
|
|
129
218
|
process.removeListener('SIGINT', sigintHandler);
|
|
130
219
|
const exitCode = dep.exit_code ?? null;
|
|
131
220
|
return {
|
|
@@ -137,6 +226,7 @@ async function attachToJob(config, depId, { chalk, maxRuntimeMs = null, maxCost
|
|
|
137
226
|
}
|
|
138
227
|
}
|
|
139
228
|
} finally {
|
|
229
|
+
stopTicker();
|
|
140
230
|
process.removeListener('SIGINT', sigintHandler);
|
|
141
231
|
}
|
|
142
232
|
}
|
|
@@ -308,7 +398,11 @@ export async function runCommand(config, args, chalk) {
|
|
|
308
398
|
} catch (err2) {
|
|
309
399
|
const d2 = err2.errorData;
|
|
310
400
|
if (d2?.code === 'PROVISIONING_FAILED' || d2?.code === 'PROVIDER_ADAPTER_ERROR') {
|
|
311
|
-
|
|
401
|
+
if (d2?.low_cost_provider_failed) {
|
|
402
|
+
console.error(chalk.red(`\n ✗ Low-cost provider failed. Primary provider also unavailable.\n`));
|
|
403
|
+
} else {
|
|
404
|
+
console.error(chalk.red(`\n ✗ Badgr found ${chosen.gpu} capacity but could not start the machine. Please try again.\n`));
|
|
405
|
+
}
|
|
312
406
|
if (process.env.BADGR_DEBUG === '1' || process.env.BADGR_DEBUG === 'true') {
|
|
313
407
|
if (d2?.debug_error) console.error(chalk.dim(` Provider detail: ${d2.debug_error}`));
|
|
314
408
|
}
|
|
@@ -321,7 +415,11 @@ export async function runCommand(config, args, chalk) {
|
|
|
321
415
|
process.exit(1);
|
|
322
416
|
}
|
|
323
417
|
} else if (d?.code === 'PROVISIONING_FAILED' || d?.code === 'PROVIDER_ADAPTER_ERROR') {
|
|
324
|
-
|
|
418
|
+
if (d?.low_cost_provider_failed) {
|
|
419
|
+
console.error(chalk.red(`\n ✗ Low-cost provider failed. Primary provider also unavailable.\n`));
|
|
420
|
+
} else {
|
|
421
|
+
console.error(chalk.red(`\n ✗ Badgr found ${gpu} capacity but could not start the machine. Please try again.\n`));
|
|
422
|
+
}
|
|
325
423
|
if (process.env.BADGR_DEBUG === '1' || process.env.BADGR_DEBUG === 'true') {
|
|
326
424
|
if (d?.debug_error) console.error(chalk.dim(` Provider detail: ${d.debug_error}`));
|
|
327
425
|
} else {
|
|
@@ -336,6 +434,10 @@ export async function runCommand(config, args, chalk) {
|
|
|
336
434
|
}
|
|
337
435
|
}
|
|
338
436
|
|
|
437
|
+
if (dep.provider_fallback_note === 'low_cost_provider_failed') {
|
|
438
|
+
console.log(chalk.yellow(' ℹ Low-cost provider failed. Using primary provider instead.\n'));
|
|
439
|
+
}
|
|
440
|
+
|
|
339
441
|
const rcptId = dep.receipt_id || generateReceiptId();
|
|
340
442
|
addReceipt({
|
|
341
443
|
receiptId: rcptId,
|
|
@@ -359,7 +461,7 @@ export async function runCommand(config, args, chalk) {
|
|
|
359
461
|
}
|
|
360
462
|
|
|
361
463
|
const ratePerHour = dep.cost_per_hour || 0;
|
|
362
|
-
console.log(chalk.dim('\n ──
|
|
464
|
+
console.log(chalk.dim('\n ── Live status (Ctrl+C to stop) ─────────────────────────────────\n'));
|
|
363
465
|
|
|
364
466
|
async function teardown(reason) {
|
|
365
467
|
const labels = {
|
package/src/commands/serve.js
CHANGED
|
@@ -124,7 +124,11 @@ export async function serveCommand(config, args, chalk) {
|
|
|
124
124
|
} catch (err2) {
|
|
125
125
|
const d2 = err2.errorData;
|
|
126
126
|
if (d2?.code === 'PROVISIONING_FAILED' || d2?.code === 'PROVIDER_ADAPTER_ERROR') {
|
|
127
|
-
|
|
127
|
+
if (d2?.low_cost_provider_failed) {
|
|
128
|
+
console.error(chalk.red(`\n ✗ Low-cost provider failed. Primary provider also unavailable.\n`));
|
|
129
|
+
} else {
|
|
130
|
+
console.error(chalk.red(`\n ✗ Badgr found ${chosen.gpu} capacity but could not start the endpoint. Please try again.\n`));
|
|
131
|
+
}
|
|
128
132
|
} else {
|
|
129
133
|
console.error(chalk.red(`\n ✗ Failed to start endpoint on ${chosen.gpu}: ${err2.message}\n`));
|
|
130
134
|
}
|
|
@@ -132,7 +136,11 @@ export async function serveCommand(config, args, chalk) {
|
|
|
132
136
|
process.exit(1);
|
|
133
137
|
}
|
|
134
138
|
} else if (d?.code === 'PROVISIONING_FAILED' || d?.code === 'PROVIDER_ADAPTER_ERROR') {
|
|
135
|
-
|
|
139
|
+
if (d?.low_cost_provider_failed) {
|
|
140
|
+
console.error(chalk.red(`\n ✗ Low-cost provider failed. Primary provider also unavailable.\n`));
|
|
141
|
+
} else {
|
|
142
|
+
console.error(chalk.red(`\n ✗ Badgr found capacity but could not start the endpoint. Please try again.\n`));
|
|
143
|
+
}
|
|
136
144
|
console.error(chalk.dim(` Run BADGR_DEBUG=1 badgr serve … for a full trace.\n`));
|
|
137
145
|
process.exit(1);
|
|
138
146
|
} else {
|
|
@@ -153,6 +161,10 @@ export async function serveCommand(config, args, chalk) {
|
|
|
153
161
|
}
|
|
154
162
|
}
|
|
155
163
|
|
|
164
|
+
if (dep.provider_fallback_note === 'low_cost_provider_failed') {
|
|
165
|
+
console.log(chalk.yellow(' ℹ Low-cost provider failed. Using primary provider instead.\n'));
|
|
166
|
+
}
|
|
167
|
+
|
|
156
168
|
addDeployment({
|
|
157
169
|
id: dep.deployment_id,
|
|
158
170
|
name: dep.name,
|
package/src/store.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Local deployment state — persisted to ~/.
|
|
2
|
+
* Local deployment state — persisted to ~/.badgr/deployments.json.
|
|
3
3
|
*
|
|
4
4
|
* Tracks what `gpu up` has provisioned so `gpu down/status/logs/receipts`
|
|
5
5
|
* have something to work with before a backend deployments API exists.
|