badgr-cli 1.0.35 → 1.0.37
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 +102 -0
- package/package.json +1 -1
- package/src/badgr.js +43 -31
- package/src/batch.js +157 -0
- package/src/commands/comfyui.js +318 -0
- package/src/commands/down.js +97 -2
- package/src/commands/embed.js +229 -0
- package/src/commands/run.js +17 -10
- package/src/commands/serve.js +55 -2
- package/src/commands/train.js +255 -0
- package/src/commands/transcribe.js +221 -0
- package/tests/down.test.js +128 -0
- package/tests/launch-readiness.test.js +51 -0
- package/tests/serve-lifecycle.test.js +18 -17
- package/tests/workload-templates.test.js +808 -0
package/README.md
CHANGED
|
@@ -51,6 +51,13 @@ badgr down <deployment-id>
|
|
|
51
51
|
|
|
52
52
|
`badgr run` — for anything that starts, runs, and exits: batch inference, fine-tuning, evals, image/video batch jobs, audio processing.
|
|
53
53
|
|
|
54
|
+
**Shortcuts** — wrappers around `run` / `serve` for common workloads:
|
|
55
|
+
|
|
56
|
+
| `badgr comfyui run <workflow.json>` | Launch ComfyUI, queue workflow, return endpoint URL |
|
|
57
|
+
| `badgr train <config.yaml>` | LoRA / fine-tuning job, stream logs |
|
|
58
|
+
| `badgr transcribe <audio>` | Whisper transcription, print transcript |
|
|
59
|
+
| `badgr embed <model> <input>` | Text embeddings, output JSONL |
|
|
60
|
+
|
|
54
61
|
---
|
|
55
62
|
|
|
56
63
|
## `badgr serve` options
|
|
@@ -97,6 +104,101 @@ badgr run python train.py --gpu A100 --env HF_TOKEN=$HF_TOKEN --max-runtime 60
|
|
|
97
104
|
|
|
98
105
|
---
|
|
99
106
|
|
|
107
|
+
## `badgr comfyui run` options
|
|
108
|
+
|
|
109
|
+
```bash
|
|
110
|
+
badgr comfyui run workflow.json --max-cost 10
|
|
111
|
+
badgr comfyui run workflow.json --gpu RTX_4090 --check-nodes KSampler,CLIPTextEncode
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
Requires either `--max-cost` or `--persistent` to prevent runaway billing.
|
|
115
|
+
|
|
116
|
+
| Flag | Default | Description |
|
|
117
|
+
|------|---------|-------------|
|
|
118
|
+
| `--gpu <type>` | auto (16+ GB VRAM) | GPU type override |
|
|
119
|
+
| `--max-cost <$>` | — | Auto-stop when total spend reaches this amount |
|
|
120
|
+
| `--max-price <$/hr>` | — | Hard spend cap per GPU-hour |
|
|
121
|
+
| `--tier 1\|2` | 1 | Provider tier |
|
|
122
|
+
| `--region US\|EU\|AU` | — | Region preference |
|
|
123
|
+
| `--check-nodes <n1,n2>` | — | Verify custom nodes are installed after startup |
|
|
124
|
+
| `--no-wait` | — | Skip health check, return immediately |
|
|
125
|
+
| `--persistent` | — | Run until manually stopped (no spending cap) |
|
|
126
|
+
| `--env KEY=VALUE` | — | Environment variable (repeatable) |
|
|
127
|
+
| `--yes` / `-y` | — | Skip duplicate-deployment warning |
|
|
128
|
+
|
|
129
|
+
---
|
|
130
|
+
|
|
131
|
+
## `badgr train` options
|
|
132
|
+
|
|
133
|
+
```bash
|
|
134
|
+
badgr train config.yaml --gpu A100 --max-runtime 240 --env HF_TOKEN=$HF_TOKEN
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
Auto-detects framework (axolotl, unsloth, trl) from config content. Default max-runtime is 120 min.
|
|
138
|
+
|
|
139
|
+
| Flag | Default | Description |
|
|
140
|
+
|------|---------|-------------|
|
|
141
|
+
| `--gpu <type>` | auto (40+ GB VRAM preferred) | GPU type override |
|
|
142
|
+
| `--max-runtime <min>` | 120 | Auto-stop after N minutes |
|
|
143
|
+
| `--max-cost <$>` | — | Auto-stop when total spend reaches this amount |
|
|
144
|
+
| `--max-price <$/hr>` | — | Hard spend cap per GPU-hour |
|
|
145
|
+
| `--tier 1\|2` | 1 | Provider tier |
|
|
146
|
+
| `--region US\|EU\|AU` | — | Region preference |
|
|
147
|
+
| `--framework <name>` | auto-detect | Force framework: `axolotl`, `unsloth`, `trl` |
|
|
148
|
+
| `--env KEY=VALUE` | — | Environment variable (repeatable) |
|
|
149
|
+
| `--detach` | — | Launch and return immediately, don't stream logs |
|
|
150
|
+
|
|
151
|
+
---
|
|
152
|
+
|
|
153
|
+
## `badgr transcribe` options
|
|
154
|
+
|
|
155
|
+
```bash
|
|
156
|
+
badgr transcribe recording.mp3 --max-cost 2
|
|
157
|
+
badgr transcribe s3://bucket/meeting.mp3 --model large-v3 --language en
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
Accepts a public URL, S3/GCS URI, or a local file under 50 MB. Default max-runtime is 30 min.
|
|
161
|
+
|
|
162
|
+
| Flag | Default | Description |
|
|
163
|
+
|------|---------|-------------|
|
|
164
|
+
| `--model <name>` | `large-v3` | Whisper model |
|
|
165
|
+
| `--gpu <type>` | auto (8+ GB VRAM) | GPU type override |
|
|
166
|
+
| `--language <code>` | — | Language hint (e.g. `en`, `fr`) |
|
|
167
|
+
| `--output <format>` | — | Output format: `txt`, `srt`, `vtt` |
|
|
168
|
+
| `--max-runtime <min>` | 30 | Auto-stop after N minutes |
|
|
169
|
+
| `--max-cost <$>` | — | Auto-stop when total spend reaches this amount |
|
|
170
|
+
| `--max-price <$/hr>` | — | Hard spend cap per GPU-hour |
|
|
171
|
+
| `--tier 1\|2` | 1 | Provider tier |
|
|
172
|
+
| `--region US\|EU\|AU` | — | Region preference |
|
|
173
|
+
| `--env KEY=VALUE` | — | Environment variable (repeatable) |
|
|
174
|
+
| `--detach` | — | Launch and return immediately, don't stream logs |
|
|
175
|
+
|
|
176
|
+
---
|
|
177
|
+
|
|
178
|
+
## `badgr embed` options
|
|
179
|
+
|
|
180
|
+
```bash
|
|
181
|
+
badgr embed BAAI/bge-large-en-v1.5 documents.txt --max-cost 2
|
|
182
|
+
badgr embed BAAI/bge-large-en-v1.5 s3://bucket/corpus.jsonl
|
|
183
|
+
badgr embed documents.txt # uses default model
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
Accepts a public URL, S3/GCS URI, or a local text file under 10 MB. Outputs JSONL (`{"text": ..., "embedding": [...]}`). Default max-runtime is 30 min.
|
|
187
|
+
|
|
188
|
+
| Flag | Default | Description |
|
|
189
|
+
|------|---------|-------------|
|
|
190
|
+
| `--gpu <type>` | auto (8+ GB VRAM) | GPU type override |
|
|
191
|
+
| `--batch-size <n>` | — | Embedding batch size |
|
|
192
|
+
| `--max-runtime <min>` | 30 | Auto-stop after N minutes |
|
|
193
|
+
| `--max-cost <$>` | — | Auto-stop when total spend reaches this amount |
|
|
194
|
+
| `--max-price <$/hr>` | — | Hard spend cap per GPU-hour |
|
|
195
|
+
| `--tier 1\|2` | 1 | Provider tier |
|
|
196
|
+
| `--region US\|EU\|AU` | — | Region preference |
|
|
197
|
+
| `--env KEY=VALUE` | — | Environment variable (repeatable) |
|
|
198
|
+
| `--detach` | — | Launch and return immediately, don't stream logs |
|
|
199
|
+
|
|
200
|
+
---
|
|
201
|
+
|
|
100
202
|
## Routing
|
|
101
203
|
|
|
102
204
|
Badgr Auto selects the best eligible route based on GPU type, VRAM, availability, region, workload requirements, and reliability. Advanced users can optionally choose an execution tier or hardware constraint.
|
package/package.json
CHANGED
package/src/badgr.js
CHANGED
|
@@ -1,18 +1,22 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import chalk from 'chalk';
|
|
3
3
|
import { loadConfig, saveConfig } from './config.js';
|
|
4
|
-
import { loginCommand }
|
|
5
|
-
import { upCommand }
|
|
6
|
-
import { downCommand }
|
|
7
|
-
import { statusCommand }
|
|
8
|
-
import { logsCommand }
|
|
9
|
-
import { receiptsCommand }
|
|
10
|
-
import { runCommand }
|
|
11
|
-
import { serveCommand }
|
|
12
|
-
import { modelsCommand }
|
|
13
|
-
import { capacityCommand }
|
|
14
|
-
import { testCommand }
|
|
15
|
-
import { billingCommand }
|
|
4
|
+
import { loginCommand } from './commands/login.js';
|
|
5
|
+
import { upCommand } from './commands/up.js';
|
|
6
|
+
import { downCommand } from './commands/down.js';
|
|
7
|
+
import { statusCommand } from './commands/status.js';
|
|
8
|
+
import { logsCommand } from './commands/logs.js';
|
|
9
|
+
import { receiptsCommand } from './commands/receipts.js';
|
|
10
|
+
import { runCommand } from './commands/run.js';
|
|
11
|
+
import { serveCommand } from './commands/serve.js';
|
|
12
|
+
import { modelsCommand } from './commands/models.js';
|
|
13
|
+
import { capacityCommand } from './commands/capacity.js';
|
|
14
|
+
import { testCommand } from './commands/test-run.js';
|
|
15
|
+
import { billingCommand } from './commands/billing.js';
|
|
16
|
+
import { comfyuiCommand } from './commands/comfyui.js';
|
|
17
|
+
import { trainCommand } from './commands/train.js';
|
|
18
|
+
import { transcribeCommand } from './commands/transcribe.js';
|
|
19
|
+
import { embedCommand } from './commands/embed.js';
|
|
16
20
|
|
|
17
21
|
const HELP = `
|
|
18
22
|
${chalk.bold('badgr')} — run or serve GPU workloads from one command
|
|
@@ -29,33 +33,37 @@ ${chalk.bold('COMMANDS')}
|
|
|
29
33
|
${chalk.cyan('badgr capacity')} Check what GPU capacity is available right now
|
|
30
34
|
${chalk.cyan('badgr billing')} Show balance and add funds
|
|
31
35
|
|
|
36
|
+
${chalk.bold('SHORTCUTS')} ${chalk.dim('(wrappers around run / serve for common workloads)')}
|
|
37
|
+
${chalk.cyan('badgr comfyui run <workflow.json>')} Launch ComfyUI, return endpoint URL
|
|
38
|
+
${chalk.cyan('badgr train <config.yaml>')} LoRA / fine-tuning job, stream logs
|
|
39
|
+
${chalk.cyan('badgr transcribe <audio>')} Whisper transcription, print transcript
|
|
40
|
+
${chalk.cyan('badgr embed <model> <input>')} Text embeddings, output JSONL
|
|
41
|
+
|
|
32
42
|
${chalk.bold('EXAMPLES')}
|
|
33
43
|
${chalk.dim('# Verify the stack works end-to-end:')}
|
|
34
44
|
badgr test
|
|
35
45
|
|
|
36
|
-
${chalk.dim('#
|
|
37
|
-
badgr
|
|
38
|
-
badgr serve meta-llama/Llama-3.1-8B-Instruct
|
|
46
|
+
${chalk.dim('# Serve a model (OpenAI-compatible):')}
|
|
47
|
+
badgr serve meta-llama/Llama-3.1-8B-Instruct --max-cost 10
|
|
39
48
|
|
|
40
|
-
${chalk.dim('#
|
|
41
|
-
badgr run
|
|
42
|
-
badgr serve meta-llama/Llama-3.1-8B-Instruct --tier 2
|
|
49
|
+
${chalk.dim('# Launch ComfyUI with a workflow:')}
|
|
50
|
+
badgr comfyui run workflow.json --max-cost 5
|
|
43
51
|
|
|
44
|
-
${chalk.dim('#
|
|
45
|
-
badgr
|
|
46
|
-
badgr serve meta-llama/Llama-3.1-8B-Instruct --gpu L40S
|
|
52
|
+
${chalk.dim('# LoRA fine-tuning:')}
|
|
53
|
+
badgr train axolotl_config.yaml --max-runtime 120
|
|
47
54
|
|
|
48
|
-
${chalk.dim('#
|
|
49
|
-
badgr
|
|
55
|
+
${chalk.dim('# Transcribe audio (URL or local file ≤50 MB):')}
|
|
56
|
+
badgr transcribe s3://my-bucket/meeting.mp3 --max-cost 2
|
|
57
|
+
badgr transcribe recording.mp3
|
|
50
58
|
|
|
51
|
-
${chalk.dim('#
|
|
52
|
-
badgr
|
|
59
|
+
${chalk.dim('# Generate embeddings:')}
|
|
60
|
+
badgr embed BAAI/bge-large-en-v1.5 documents.txt --max-cost 2
|
|
53
61
|
|
|
54
|
-
${chalk.dim('#
|
|
55
|
-
badgr serve
|
|
62
|
+
${chalk.dim('# Tier 2 — marketplace routing, lower-cost options:')}
|
|
63
|
+
badgr serve meta-llama/Llama-3.1-8B-Instruct --tier 2 --max-cost 10
|
|
56
64
|
|
|
57
|
-
${chalk.dim('#
|
|
58
|
-
badgr
|
|
65
|
+
${chalk.dim('# Pin a specific GPU:')}
|
|
66
|
+
badgr serve meta-llama/Llama-3.1-8B-Instruct --gpu L40S --max-cost 10
|
|
59
67
|
|
|
60
68
|
${chalk.dim('# Manage a running deployment:')}
|
|
61
69
|
badgr status
|
|
@@ -120,9 +128,13 @@ async function main() {
|
|
|
120
128
|
case 'models': return modelsCommand(config, chalk);
|
|
121
129
|
case 'capacity': return capacityCommand(config, rest, chalk);
|
|
122
130
|
case 'test': return testCommand(config, rest, chalk);
|
|
123
|
-
case 'billing':
|
|
131
|
+
case 'billing': return billingCommand(config, rest, chalk);
|
|
132
|
+
case 'comfyui': return comfyuiCommand(config, rest, chalk);
|
|
133
|
+
case 'train': return trainCommand(config, rest, chalk);
|
|
134
|
+
case 'transcribe': return transcribeCommand(config, rest, chalk);
|
|
135
|
+
case 'embed': return embedCommand(config, rest, chalk);
|
|
124
136
|
// legacy aliases kept for compatibility
|
|
125
|
-
case 'up':
|
|
137
|
+
case 'up': return upCommand(config, rest, chalk);
|
|
126
138
|
case 'config': {
|
|
127
139
|
const { apiKey, ...safe } = config;
|
|
128
140
|
const display = { ...safe, apiKey: apiKey ? `${apiKey.slice(0, 8)}...` : '(not set)' };
|
package/src/batch.js
ADDED
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared batch job monitor.
|
|
3
|
+
*
|
|
4
|
+
* After a /run job is provisioned, this module handles:
|
|
5
|
+
* wait for container start → stream logs → enforce spend/runtime caps → teardown
|
|
6
|
+
*
|
|
7
|
+
* Used by: badgr train, badgr transcribe, badgr embed
|
|
8
|
+
*/
|
|
9
|
+
import { callApi, terminateDeployment } from './api.js';
|
|
10
|
+
import { updateReceipt } from './store.js';
|
|
11
|
+
import { formatCliError } from './errors.js';
|
|
12
|
+
|
|
13
|
+
// Log lines that carry telemetry metadata, not user output.
|
|
14
|
+
const LOG_META_RE = /^\[dep-[^\]]+\] (status|gpu|region|cost|receipt|provider_status|uptime)=/;
|
|
15
|
+
|
|
16
|
+
export function fmtRuntime(ms) {
|
|
17
|
+
const s = Math.round(ms / 1000);
|
|
18
|
+
return s < 60 ? `${s}s` : `${Math.floor(s / 60)}m ${s % 60}s`;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const STARTUP = new Set(['queued', 'provisioning', 'starting']);
|
|
22
|
+
const TERMINAL = new Set(['stopped', 'failed', 'completed', 'succeeded']);
|
|
23
|
+
|
|
24
|
+
async function waitForRunning(config, depId, chalk) {
|
|
25
|
+
const POLL_MS = 3000;
|
|
26
|
+
const TIMEOUT_MS = 5 * 60 * 1000;
|
|
27
|
+
const startMs = Date.now();
|
|
28
|
+
|
|
29
|
+
process.stdout.write(chalk.dim(' Starting container'));
|
|
30
|
+
while (Date.now() - startMs < TIMEOUT_MS) {
|
|
31
|
+
await new Promise(r => setTimeout(r, POLL_MS));
|
|
32
|
+
let dep;
|
|
33
|
+
try {
|
|
34
|
+
dep = await callApi(`/deployments/${depId}`, {
|
|
35
|
+
apiKey: config.apiKey, baseUrl: config.baseUrl, timeoutMs: 10_000,
|
|
36
|
+
});
|
|
37
|
+
} catch { process.stdout.write('.'); continue; }
|
|
38
|
+
if (!STARTUP.has(dep.status)) { process.stdout.write('\n'); return dep; }
|
|
39
|
+
process.stdout.write('.');
|
|
40
|
+
}
|
|
41
|
+
process.stdout.write('\n');
|
|
42
|
+
return callApi(`/deployments/${depId}`, {
|
|
43
|
+
apiKey: config.apiKey, baseUrl: config.baseUrl, timeoutMs: 10_000,
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Monitor a running batch job until completion, cap, or interrupt.
|
|
49
|
+
*
|
|
50
|
+
* Installs a SIGINT handler — callers must not install their own.
|
|
51
|
+
*
|
|
52
|
+
* @param {object} config
|
|
53
|
+
* @param {string} depId
|
|
54
|
+
* @param {string} rcptId
|
|
55
|
+
* @param {{chalk, maxRuntimeMs, maxCost, ratePerHour}} opts
|
|
56
|
+
* @returns {Promise<{ status, exitCode, runtimeMs, reason }>}
|
|
57
|
+
*/
|
|
58
|
+
export async function monitorBatchJob(config, depId, rcptId, opts) {
|
|
59
|
+
const { chalk, maxRuntimeMs, maxCost, ratePerHour } = opts;
|
|
60
|
+
const POLL_MS = 5000;
|
|
61
|
+
|
|
62
|
+
const startDep = await waitForRunning(config, depId, chalk);
|
|
63
|
+
|
|
64
|
+
if (startDep.status === 'failed') {
|
|
65
|
+
updateReceipt(rcptId, { status: 'failed', runtimeSeconds: 0, finalCost: 0 });
|
|
66
|
+
console.error(formatCliError('JOB_INFRASTRUCTURE_FAILURE', { receiptId: rcptId }, chalk));
|
|
67
|
+
return { status: 'failed', exitCode: null, runtimeMs: 0, reason: 'infrastructure' };
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
console.log(chalk.dim('\n ── Running (Ctrl+C to stop) ─────────────────────────────────────\n'));
|
|
71
|
+
|
|
72
|
+
const seenLines = new Set();
|
|
73
|
+
const attachStart = Date.now();
|
|
74
|
+
let tearing = false;
|
|
75
|
+
let shuttingDown = false;
|
|
76
|
+
|
|
77
|
+
function handleSIGINT() { shuttingDown = true; }
|
|
78
|
+
process.once('SIGINT', handleSIGINT);
|
|
79
|
+
|
|
80
|
+
async function teardown(reason) {
|
|
81
|
+
if (tearing) return;
|
|
82
|
+
tearing = true;
|
|
83
|
+
const msgs = {
|
|
84
|
+
'max-runtime': chalk.yellow('\n ⏱ Max runtime reached — stopping...'),
|
|
85
|
+
'max-cost': chalk.yellow('\n 💰 Spend cap reached — stopping...'),
|
|
86
|
+
'interrupted': chalk.yellow('\n Stopping...'),
|
|
87
|
+
};
|
|
88
|
+
console.log(msgs[reason] ?? chalk.yellow('\n Stopping...'));
|
|
89
|
+
try { await terminateDeployment(config, depId); } catch {}
|
|
90
|
+
const runtimeMs = Date.now() - attachStart;
|
|
91
|
+
const finalCost = ratePerHour * (runtimeMs / 3_600_000);
|
|
92
|
+
updateReceipt(rcptId, {
|
|
93
|
+
status: reason,
|
|
94
|
+
runtimeSeconds: Math.round(runtimeMs / 1000),
|
|
95
|
+
finalCost,
|
|
96
|
+
});
|
|
97
|
+
console.log(chalk.dim(` Runtime: ${fmtRuntime(runtimeMs)} • Est. cost: $${finalCost.toFixed(4)}`));
|
|
98
|
+
console.log(chalk.dim(' Stopped. Billing ended.\n'));
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
while (true) {
|
|
102
|
+
if (shuttingDown || tearing) break;
|
|
103
|
+
await new Promise(r => setTimeout(r, POLL_MS));
|
|
104
|
+
if (shuttingDown || tearing) break;
|
|
105
|
+
|
|
106
|
+
const elapsed = Date.now() - attachStart;
|
|
107
|
+
const spent = ratePerHour * (elapsed / 3_600_000);
|
|
108
|
+
|
|
109
|
+
if (maxCost !== null && spent >= maxCost) { await teardown('max-cost'); break; }
|
|
110
|
+
if (elapsed >= maxRuntimeMs) { await teardown('max-runtime'); break; }
|
|
111
|
+
|
|
112
|
+
let dep;
|
|
113
|
+
try {
|
|
114
|
+
dep = await callApi(`/deployments/${depId}`, {
|
|
115
|
+
apiKey: config.apiKey, baseUrl: config.baseUrl, timeoutMs: 10_000,
|
|
116
|
+
});
|
|
117
|
+
} catch { continue; }
|
|
118
|
+
|
|
119
|
+
try {
|
|
120
|
+
const logData = await callApi(`/deployments/${depId}/logs`, {
|
|
121
|
+
apiKey: config.apiKey, baseUrl: config.baseUrl, timeoutMs: 10_000,
|
|
122
|
+
});
|
|
123
|
+
for (const line of (logData?.logs ?? [])) {
|
|
124
|
+
if (seenLines.has(line)) continue;
|
|
125
|
+
seenLines.add(line);
|
|
126
|
+
if (LOG_META_RE.test(line)) continue;
|
|
127
|
+
if (/\b(gpu_util|cpu_util|provider_status|uptime)=/.test(line)) continue;
|
|
128
|
+
const isErr = /^error\b/i.test(line) || /Error response from daemon/i.test(line);
|
|
129
|
+
console.log(` ${isErr ? chalk.red(line) : chalk.dim(line)}`);
|
|
130
|
+
}
|
|
131
|
+
} catch {}
|
|
132
|
+
|
|
133
|
+
if (TERMINAL.has(dep.status)) {
|
|
134
|
+
process.removeListener('SIGINT', handleSIGINT);
|
|
135
|
+
const exitCode = dep.exit_code ?? null;
|
|
136
|
+
const runtimeMs = Date.now() - attachStart;
|
|
137
|
+
const finalCost = ratePerHour * (runtimeMs / 3_600_000);
|
|
138
|
+
try { await terminateDeployment(config, depId); } catch {}
|
|
139
|
+
updateReceipt(rcptId, {
|
|
140
|
+
status: dep.status,
|
|
141
|
+
exitCode,
|
|
142
|
+
runtimeSeconds: Math.round(runtimeMs / 1000),
|
|
143
|
+
finalCost,
|
|
144
|
+
});
|
|
145
|
+
return { status: dep.status, exitCode, runtimeMs, reason: 'complete' };
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
process.removeListener('SIGINT', handleSIGINT);
|
|
150
|
+
|
|
151
|
+
if (shuttingDown) {
|
|
152
|
+
await teardown('interrupted');
|
|
153
|
+
return { status: 'interrupted', exitCode: null, runtimeMs: Date.now() - attachStart, reason: 'interrupted' };
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return { status: 'capped', exitCode: null, runtimeMs: Date.now() - attachStart, reason: 'capped' };
|
|
157
|
+
}
|