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 +1 -1
- package/src/commands/run.js +95 -5
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
|
}
|
|
@@ -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 ──
|
|
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 = {
|