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 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 `~/.gpu/config.json`. Every subsequent command reads that file — no env vars required.
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 `~/.gpu/deployments.json`, with provider, retries, latency, and cost.
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 `~/.gpu/`:
205
+ All CLI state lives in `~/.badgr/`:
206
206
 
207
207
  ```
208
- ~/.gpu/
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "badgr-cli",
3
- "version": "1.0.16",
3
+ "version": "1.0.18",
4
4
  "description": "Badgr, run or serve GPU workloads from one command",
5
5
  "type": "module",
6
6
  "bin": {
@@ -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 seenLines = 0;
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
- for (let i = seenLines; i < lines.length; i++) {
121
- console.log(` ${chalk.dim(lines[i])}`);
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
- console.error(chalk.red(`\n ✗ Badgr found ${chosen.gpu} capacity but could not start the machine. Please try again.\n`));
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
- console.error(chalk.red(`\n ✗ Badgr found ${gpu} capacity but could not start the machine. Please try again.\n`));
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 ── Streaming logs (Ctrl+C to stop job) ─────────────────────────\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 = {
@@ -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
- console.error(chalk.red(`\n ✗ Badgr found ${chosen.gpu} capacity but could not start the endpoint. Please try again.\n`));
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
- console.error(chalk.red(`\n ✗ Badgr found capacity but could not start the endpoint. Please try again.\n`));
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 ~/.gpu/deployments.json.
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.