badgr-cli 1.0.17 → 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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "badgr-cli",
3
- "version": "1.0.17",
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
  }
@@ -371,7 +461,7 @@ export async function runCommand(config, args, chalk) {
371
461
  }
372
462
 
373
463
  const ratePerHour = dep.cost_per_hour || 0;
374
- 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'));
375
465
 
376
466
  async function teardown(reason) {
377
467
  const labels = {