badgr-cli 1.1.1 → 1.1.2

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.
Files changed (66) hide show
  1. package/LICENSE +207 -0
  2. package/README.md +9 -2
  3. package/package.json +44 -2
  4. package/src/api.js +16 -0
  5. package/src/badgr.js +2 -2
  6. package/src/commands/batch.js +11 -0
  7. package/src/commands/comfyui.js +31 -15
  8. package/src/commands/embed.js +13 -10
  9. package/src/commands/launch.js +8 -1
  10. package/src/commands/login.js +75 -20
  11. package/src/commands/run.js +45 -13
  12. package/src/commands/sbatch.js +6 -1
  13. package/src/commands/serve.js +44 -30
  14. package/src/commands/train.js +8 -12
  15. package/src/commands/transcribe.js +13 -10
  16. package/src/envFlag.js +10 -0
  17. package/src/onboarding.js +8 -1
  18. package/src/progress.js +48 -0
  19. package/tests/agent-images.test.js +0 -17
  20. package/tests/api.test.js +0 -168
  21. package/tests/artifactDownload.test.js +0 -113
  22. package/tests/artifacts.test.js +0 -168
  23. package/tests/batch.test.js +0 -641
  24. package/tests/browser.test.js +0 -51
  25. package/tests/capacity.test.js +0 -68
  26. package/tests/commands.test.js +0 -417
  27. package/tests/config.test.js +0 -96
  28. package/tests/connect.test.js +0 -83
  29. package/tests/detect.test.js +0 -191
  30. package/tests/down.test.js +0 -150
  31. package/tests/errors.test.js +0 -130
  32. package/tests/fallback-timeout.test.js +0 -41
  33. package/tests/fanout.test.js +0 -124
  34. package/tests/gpu-doctor-classifiers.test.js +0 -402
  35. package/tests/gpu-doctor-doctor.test.js +0 -304
  36. package/tests/gpu-doctor-probe-cache.test.js +0 -110
  37. package/tests/gpu-doctor-probes.test.js +0 -257
  38. package/tests/heartbeat.test.js +0 -70
  39. package/tests/job-progress-poll.test.js +0 -136
  40. package/tests/launch-command-argv.test.js +0 -93
  41. package/tests/launch-readiness.test.js +0 -403
  42. package/tests/launch.test.js +0 -440
  43. package/tests/onboarding.test.js +0 -134
  44. package/tests/productized-dry-run.test.js +0 -141
  45. package/tests/productized-runners.test.js +0 -237
  46. package/tests/pull.test.js +0 -266
  47. package/tests/rerun.test.js +0 -94
  48. package/tests/restart.test.js +0 -88
  49. package/tests/router.test.js +0 -98
  50. package/tests/run-lifecycle.test.js +0 -1054
  51. package/tests/sbatch.test.js +0 -190
  52. package/tests/secrets.test.js +0 -16
  53. package/tests/serve-apps.test.js +0 -189
  54. package/tests/serve-lifecycle.test.js +0 -931
  55. package/tests/slurm.test.js +0 -77
  56. package/tests/spec.test.js +0 -201
  57. package/tests/status.test.js +0 -73
  58. package/tests/store.test.js +0 -187
  59. package/tests/task.test.js +0 -109
  60. package/tests/template.test.js +0 -556
  61. package/tests/train-lora-dataset.test.js +0 -176
  62. package/tests/upload.test.js +0 -79
  63. package/tests/workload-rerun.test.js +0 -56
  64. package/tests/workload-spec.test.js +0 -180
  65. package/tests/workload-templates.test.js +0 -865
  66. package/tests/workload-workspace-paths.test.js +0 -46
package/LICENSE ADDED
@@ -0,0 +1,207 @@
1
+ Apache License
2
+ Version 2.0, January 2004
3
+ http://www.apache.org/licenses/
4
+
5
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6
+
7
+ 1. Definitions.
8
+
9
+ "License" shall mean the terms and conditions for use, reproduction,
10
+ and distribution as defined by Sections 1 through 9 of this document.
11
+
12
+ "Licensor" shall mean the copyright owner or entity authorized by
13
+ the copyright owner that is granting the License.
14
+
15
+ "Legal Entity" shall mean the union of the acting entity and all
16
+ other entities that control, are controlled by, or are under common
17
+ control with that entity. For the purposes of this definition,
18
+ "control" means (i) the power, direct or indirect, to cause the
19
+ direction or management of such entity, whether by contract or
20
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
21
+ outstanding shares, or (iii) beneficial ownership of such entity.
22
+
23
+ "You" (or "Your") shall mean an individual or Legal Entity
24
+ exercising permissions granted by this License.
25
+
26
+ "Source" form shall mean the preferred form for making modifications,
27
+ including but not limited to software source code, documentation
28
+ source, and configuration files.
29
+
30
+ "Object" form shall mean any form resulting from mechanical
31
+ transformation or translation of a Source form, including but
32
+ not limited to compiled object code, generated documentation,
33
+ and conversions to other media types.
34
+
35
+ "Work" shall mean the work of authorship made available under
36
+ the License, as indicated by a copyright notice that is included in
37
+ or attached to the work (an example is provided in the Appendix below).
38
+
39
+ "Derivative Works" shall mean any work, whether in Source or Object
40
+ form, that is based on (or derived from) the Work and for which the
41
+ editorial revisions, annotations, elaborations, or other modifications
42
+ represent, as a whole, an original work of authorship. For the purposes
43
+ of this License, Derivative Works shall not include works that remain
44
+ separable from, or merely link (or bind by name) to the interfaces of,
45
+ the Work and Derivative Works thereof.
46
+
47
+ "Contribution" shall mean, as submitted to the Licensor for inclusion
48
+ in the Work by the copyright owner or by an individual or Legal Entity
49
+ authorized to submit on behalf of the copyright owner. For the purposes
50
+ of this definition, "submitted" means any form of electronic, verbal,
51
+ or written communication sent to the Licensor or its representatives,
52
+ including but not limited to communication on electronic mailing lists,
53
+ source code control systems, and issue tracking systems that are managed
54
+ by, or on behalf of, the Licensor for the purpose of discussing and
55
+ improving the Work, but excluding communication that is conspicuously
56
+ marked or designated in writing by the copyright owner as "Not a
57
+ Contribution."
58
+
59
+ "Contributor" shall mean Licensor and any Legal Entity on behalf of
60
+ whom a Contribution has been received by the Licensor and included
61
+ within the Work.
62
+
63
+ 2. Grant of Copyright License. Subject to the terms and conditions of
64
+ this License, each Contributor hereby grants to You a perpetual,
65
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
66
+ copyright license to reproduce, prepare Derivative Works of,
67
+ publicly display, publicly perform, sublicense, and distribute the
68
+ Work and such Derivative Works in Source or Object form.
69
+
70
+ 3. Grant of Patent License. Subject to the terms and conditions of
71
+ this License, each Contributor hereby grants to You a perpetual,
72
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
73
+ (except as stated in this section) patent license to make, have made,
74
+ use, offer to sell, sell, import, and otherwise transfer the Work,
75
+ where such license applies only to those patent claims licensable
76
+ by such Contributor that are necessarily infringed by their
77
+ Contribution(s) alone or by the combination of their Contribution(s)
78
+ with the Work to which such Contribution(s) was submitted. If You
79
+ institute patent litigation against any entity (including a cross-claim
80
+ or counterclaim in a lawsuit) alleging that the Work or any
81
+ Contribution embodied within the Work constitutes direct or
82
+ contributory patent infringement, then any patent licenses granted to
83
+ You under this License for that Work shall terminate as of the date
84
+ such litigation is filed.
85
+
86
+ 4. Redistribution. You may reproduce and distribute copies of the
87
+ Work or Derivative Works thereof in any medium, with or without
88
+ modifications, and in Source or Object form, provided that You
89
+ meet the following conditions:
90
+
91
+ (a) You must give any other recipients of the Work or Derivative
92
+ Works a copy of this License; and
93
+
94
+ (b) You must cause any modified files to carry prominent notices
95
+ stating that You changed the files; and
96
+
97
+ (c) You must retain, in the Source form of any Derivative Works
98
+ that You distribute, all copyright, patent, trademark, and
99
+ attribution notices from the Source form of the Work,
100
+ excluding those notices that do not pertain to any part of
101
+ the Derivative Works; and
102
+
103
+ (d) If the Work includes a "NOTICE" text file as part of its
104
+ distribution, You must include a readable copy of the
105
+ attribution notices contained within such NOTICE file, in
106
+ at least one of the following places: within a NOTICE text
107
+ file distributed as part of the Derivative Works; within
108
+ the Source form or documentation, if provided along with the
109
+ Derivative Works; or, within a display generated by the
110
+ Derivative Works, if and wherever such third-party notices
111
+ normally appear. The contents of the NOTICE file are for
112
+ informational purposes only and do not modify the License.
113
+ You may add Your own attribution notices within Derivative
114
+ Works that You distribute, alongside or as an addendum to
115
+ the NOTICE text from the Work, provided that such additional
116
+ attribution notices cannot be construed as modifying the License.
117
+
118
+ You may add Your own license statement for Your modifications and
119
+ may provide additional grant of rights to use, copy, modify, merge,
120
+ publish, distribute, sublicense, and/or sell copies of the Work,
121
+ and to permit persons to whom the Work is furnished to do so,
122
+ subject to the following conditions:
123
+
124
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
125
+ any Contribution intentionally submitted for inclusion in the Work
126
+ by You to the Licensor shall be under the terms and conditions of
127
+ this License, without any additional terms or conditions.
128
+ Notwithstanding the above, nothing herein shall supersede or modify
129
+ the terms of any separate license agreement you may have executed
130
+ with Licensor regarding such Contributions.
131
+
132
+ 6. Trademarks. This License does not grant permission to use the trade
133
+ names, trademarks, service marks, or product names of the Licensor,
134
+ except as required for reasonable and customary use in describing the
135
+ origin of the Work and reproducing the content of the NOTICE file.
136
+
137
+ 7. Disclaimer of Warranty. Unless required by applicable law or
138
+ agreed to in writing, Licensor provides the Work (and each
139
+ Contributor provides its Contributions) on an "AS IS" BASIS,
140
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
141
+ implied, including, without limitation, any warranties or conditions
142
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
143
+ PARTICULAR PURPOSE. You are solely responsible for determining the
144
+ appropriateness of using or reproducing the Work and assume any
145
+ risks associated with Your exercise of permissions under this License.
146
+
147
+ 8. Limitation of Liability. In no event and under no legal theory,
148
+ whether in tort (including negligence), contract, or otherwise,
149
+ unless required by applicable law (such as deliberate and grossly
150
+ negligent acts) or agreed to in writing, shall any Contributor be
151
+ liable to You for damages, including any direct, indirect, special,
152
+ incidental, or exemplary damages of any character arising as a
153
+ result of this License or out of the use or inability to use the
154
+ Work (including but not limited to damages for loss of goodwill,
155
+ work stoppage, computer failure or malfunction, or all other
156
+ commercial damages or losses), even if such Contributor has been
157
+ advised of the possibility of such damages.
158
+
159
+ 9. Accepting Warranty or Additional Liability. While redistributing
160
+ the Work or Derivative Works thereof, You may choose to offer,
161
+ and charge a fee for, acceptance of support, warranty, indemnity,
162
+ or other liability obligations and/or rights consistent with this
163
+ License. However, in accepting such obligations, You may offer such
164
+ conditions only on Your own behalf and on Your sole responsibility,
165
+ not on behalf of any other Contributor, and only if You agree to
166
+ indemnify, defend, and hold each Contributor harmless for any
167
+ liability incurred by, or claims asserted against, such Contributor
168
+ by reason of your accepting any such warranty or additional liability.
169
+
170
+ END OF TERMS AND CONDITIONS
171
+
172
+ APPENDIX: How to apply the Apache License to your work.
173
+
174
+ To apply the Apache License to your work, attach the following
175
+ boilerplate notice, with the fields enclosed by brackets "[]"
176
+ replaced with your own identifying information. (Don't include
177
+ the brackets!) The text should be enclosed in the appropriate
178
+ comment syntax for the format in use. Please also include a
179
+ "NOTICE" file as part of the distribution with the contents:
180
+
181
+ Copyright [yyyy] [name of copyright owner]
182
+
183
+ Licensed under the Apache License, Version 2.0 (the "License");
184
+ you may not use this file except in compliance with the License.
185
+ You may obtain a copy of the License at
186
+
187
+ http://www.apache.org/licenses/LICENSE-2.0
188
+
189
+ Unless required by applicable law or agreed to in writing, software
190
+ distributed under the License is distributed on an "AS IS" BASIS,
191
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
192
+ See the License for the specific language governing permissions and
193
+ limitations under the License.
194
+
195
+ Copyright 2024 AI Badgr
196
+
197
+ Licensed under the Apache License, Version 2.0 (the "License");
198
+ you may not use this file except in compliance with the License.
199
+ You may obtain a copy of the License at
200
+
201
+ http://www.apache.org/licenses/LICENSE-2.0
202
+
203
+ Unless required by applicable law or agreed to in writing, software
204
+ distributed under the License is distributed on an "AS IS" BASIS,
205
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
206
+ See the License for the specific language governing permissions and
207
+ limitations under the License.
package/README.md CHANGED
@@ -1,5 +1,10 @@
1
1
  # badgr-cli
2
2
 
3
+ [![npm version](https://img.shields.io/npm/v/badgr-cli.svg)](https://www.npmjs.com/package/badgr-cli)
4
+ [![npm downloads](https://img.shields.io/npm/dm/badgr-cli.svg)](https://www.npmjs.com/package/badgr-cli)
5
+ [![license](https://img.shields.io/npm/l/badgr-cli.svg)](https://github.com/michaelmanly/badgr-cli/blob/main/LICENSE)
6
+ [![node](https://img.shields.io/node/v/badgr-cli.svg)](https://www.npmjs.com/package/badgr-cli)
7
+
3
8
  Badgr supports many GPU workloads through two commands: `serve` for persistent endpoints, `run` for jobs.
4
9
 
5
10
  > **Safety promise:** every run has `--max-cost`, live logs, automatic teardown, and a receipt. Run `badgr down <id>` any time to stop billing immediately.
@@ -8,6 +13,8 @@ Badgr supports many GPU workloads through two commands: `serve` for persistent e
8
13
  npm install -g badgr-cli
9
14
  ```
10
15
 
16
+ **Jump to:** [Quick start](#quick-start) · [Coding agents (`badgr launch`)](#coding--testing-agents-badgr-launch) · [Image generation](#also-try-image-generation) · [`badgr doctor`](#something-not-working-badgr-doctor) · [Commands](#commands) · [`serve` options](#badgr-serve-options) · [`run` options](#badgr-run-options) · [Receipts](#receipts) · [OpenAI compatibility](#openai-compatibility) · [GPU options](#gpu-options) · [Advanced](#advanced) · [Requirements](#requirements)
17
+
11
18
  ---
12
19
 
13
20
  ## Quick start
@@ -148,7 +155,7 @@ badgr doctor --json
148
155
  ```
149
156
 
150
157
  Run `badgr doctor --help` for the full flag list. Details in
151
- [`docs/gpu-doctor.md`](../../docs/gpu-doctor.md).
158
+ [`docs/gpu-doctor.md`](https://github.com/michaelmanly/gpu-ai/blob/main/docs/gpu-doctor.md).
152
159
 
153
160
  ---
154
161
 
@@ -384,7 +391,7 @@ Badgr Auto selects the best eligible GPU for your workload. Add `--gpu <type>` o
384
391
  | A100 | NVIDIA A100 | 40–80 GB | Training, inference |
385
392
  | H100 | NVIDIA H100 | 80 GB | Large model training |
386
393
 
387
- Additional GPU types may be routable depending on current capacity — check with `badgr capacity`. Pricing is confirmed before provisioning; use `--dry-run` to see it first. Full GPU support details: see [NOTES.md](../../NOTES.md#gpu-support) in the repo root.
394
+ Additional GPU types may be routable depending on current capacity — check with `badgr capacity`. Pricing is confirmed before provisioning; use `--dry-run` to see it first. Full GPU support details: see [NOTES.md](https://github.com/michaelmanly/gpu-ai/blob/main/NOTES.md#gpu-support).
388
395
 
389
396
  ---
390
397
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "badgr-cli",
3
- "version": "1.1.1",
3
+ "version": "1.1.2",
4
4
  "description": "Badgr — run or serve GPU workloads from one command",
5
5
  "type": "module",
6
6
  "bin": {
@@ -24,6 +24,14 @@
24
24
  "engines": {
25
25
  "node": ">=20.10.0"
26
26
  },
27
+ "homepage": "https://aibadgr.com",
28
+ "repository": {
29
+ "type": "git",
30
+ "url": "https://github.com/michaelmanly/badgr-cli.git"
31
+ },
32
+ "bugs": {
33
+ "url": "https://github.com/michaelmanly/badgr-cli/issues"
34
+ },
27
35
  "keywords": [
28
36
  "gpu",
29
37
  "cli",
@@ -31,11 +39,45 @@
31
39
  "compute",
32
40
  "gateway",
33
41
  "openai",
42
+ "openai-compatible",
34
43
  "workload",
35
44
  "workspace",
36
45
  "llm",
37
46
  "inference",
38
- "fine-tuning"
47
+ "fine-tuning",
48
+ "lora",
49
+ "vllm",
50
+ "gpu-cloud",
51
+ "cloud-gpu",
52
+ "gpu-rental",
53
+ "on-demand-gpu",
54
+ "serverless-gpu",
55
+ "gpu-provisioning",
56
+ "machine-learning",
57
+ "deep-learning",
58
+ "model-serving",
59
+ "model-training",
60
+ "training",
61
+ "huggingface",
62
+ "cuda",
63
+ "h100",
64
+ "a100",
65
+ "rtx-4090",
66
+ "coding-agent",
67
+ "ai-agent",
68
+ "claude-code",
69
+ "codex",
70
+ "playwright",
71
+ "comfyui",
72
+ "stable-diffusion",
73
+ "image-generation",
74
+ "transcription",
75
+ "whisper",
76
+ "embeddings",
77
+ "slurm",
78
+ "batch-jobs",
79
+ "devops",
80
+ "mlops"
39
81
  ],
40
82
  "license": "Apache-2.0"
41
83
  }
package/src/api.js CHANGED
@@ -153,6 +153,22 @@ export function quoteRun(config, body) {
153
153
  });
154
154
  }
155
155
 
156
+ // Live, no-provisioning price/capacity check for a GPU dry-run preview (see
157
+ // backend run_serve_routes.py's GET /v1/capacity/suggestions). Used by every
158
+ // GPU-provisioning command's --dry-run to show a real observed rate instead
159
+ // of just the requested config — callers must treat a failure here as
160
+ // non-fatal (best-effort) and never let it block or delay the dry-run.
161
+ export function getCapacityPreview(config, { gpu, region, maxPrice } = {}) {
162
+ const params = new URLSearchParams({ gpu: gpu || 'RTX_4090' });
163
+ if (region) params.set('region', region);
164
+ if (maxPrice != null) params.set('max_price', String(maxPrice));
165
+ return callApi(`/capacity/suggestions?${params}`, {
166
+ apiKey: config.apiKey,
167
+ baseUrl: config.baseUrl,
168
+ timeoutMs: 8_000,
169
+ });
170
+ }
171
+
156
172
  export function serveModel(config, body) {
157
173
  return callApi('/serve', {
158
174
  method: 'POST',
package/src/badgr.js CHANGED
@@ -37,7 +37,7 @@ const HELP = `
37
37
  ${chalk.bold('badgr')} — run or serve GPU workloads from one command
38
38
 
39
39
  ${chalk.bold('COMMANDS')}
40
- ${chalk.cyan('badgr login')} Authenticate with your API key
40
+ ${chalk.cyan('badgr login')} Open a browser to log in (or: badgr login --key <value>)
41
41
  ${chalk.cyan('badgr connect <provider>')} Credential setup (badgr connect anthropic/openai) — also prompted inline by badgr launch if missing
42
42
  ${chalk.cyan('badgr detect <path>')} Inspect a project and report the GPU job Badgr would run
43
43
  ${chalk.cyan('badgr run <command>')} Run a one-off GPU job
@@ -265,7 +265,7 @@ async function main() {
265
265
  }
266
266
 
267
267
  switch (cmd) {
268
- case 'login': return loginCommand(chalk, saveConfig);
268
+ case 'login': return loginCommand(chalk, saveConfig, rest);
269
269
  case 'connect': return connectCommand(rest, chalk);
270
270
  case 'detect': return detectCommand(config, rest, chalk);
271
271
  case 'run': return runCommand(config, rest, chalk);
@@ -25,6 +25,7 @@ import { monitorBatchJob, fmtRuntime } from '../batch.js';
25
25
  import { runFanOut, DEFAULT_CONCURRENCY } from '../fanout.js';
26
26
  import { uploadBlob } from '../api.js';
27
27
  import { downloadAndExtractArtifact } from '../artifactDownload.js';
28
+ import { printCapacityPreview, formatTierLabel } from '../progress.js';
28
29
  import { logsCommand } from './logs.js';
29
30
 
30
31
  // Server-side watchdog / batch.js reason strings, mapped to the vocabulary
@@ -240,17 +241,21 @@ async function runFanOutSubcommand(config, spec, flags, chalk, yamlPath) {
240
241
  const [{ containerPath }] = spec.inputs;
241
242
  const effectiveTier = normalizeTier(flags.tier);
242
243
  const concurrency = flags.maxConcurrency ?? DEFAULT_CONCURRENCY;
244
+ const resolvedGpu = flags.gpu ? flags.gpu.toUpperCase().replace(/-/g, '_') : 'AUTO';
243
245
 
244
246
  console.log(chalk.bold(`\n📦 Batch fan-out: ${spec.name}\n`));
245
247
  console.log(` ${chalk.bold('Image:')} ${spec.image}`);
246
248
  console.log(` ${chalk.bold('Command:')} ${spec.command.join(' ')}`);
247
249
  console.log(` ${chalk.bold('Inputs dir:')} ${fanOutDir} (${files.length} task(s), max ${Math.min(concurrency, files.length)} concurrent)`);
250
+ console.log(` ${chalk.bold('GPU:')} ${resolvedGpu}`);
251
+ console.log(` ${chalk.bold('Tier:')} ${formatTierLabel(effectiveTier)}`);
248
252
  console.log(` ${chalk.bold('Max cost:')} $${spec.maxCost.toFixed(2)} per task`);
249
253
  console.log(` ${chalk.bold('Max runtime:')} ${spec.maxRuntimeMinutes}min per task`);
250
254
  console.log();
251
255
 
252
256
  if (flags.dryRun) {
253
257
  console.log(chalk.bold(`⚡ Dry run — no GPU will be provisioned (${files.length} task(s) would be submitted)\n`));
258
+ await printCapacityPreview(chalk, config, { gpu: resolvedGpu, region: flags.region?.toUpperCase() });
254
259
  return;
255
260
  }
256
261
 
@@ -402,9 +407,14 @@ async function runSubcommand(config, args, chalk) {
402
407
  return runFanOutSubcommand(config, spec, flags, chalk, yamlPath);
403
408
  }
404
409
 
410
+ const resolvedGpu = flags.gpu ? flags.gpu.toUpperCase().replace(/-/g, '_') : 'AUTO';
411
+ const previewTier = normalizeTier(flags.tier);
412
+
405
413
  console.log(chalk.bold(`\n📦 Batch: ${spec.name}\n`));
406
414
  console.log(` ${chalk.bold('Image:')} ${spec.image}`);
407
415
  console.log(` ${chalk.bold('Command:')} ${spec.command.join(' ')}`);
416
+ console.log(` ${chalk.bold('GPU:')} ${resolvedGpu}`);
417
+ console.log(` ${chalk.bold('Tier:')} ${formatTierLabel(previewTier)}`);
408
418
  console.log(` ${chalk.bold('Max cost:')} $${spec.maxCost.toFixed(2)}`);
409
419
  console.log(` ${chalk.bold('Max runtime:')} ${spec.maxRuntimeMinutes}min`);
410
420
  if (spec.inputs.length) console.log(` ${chalk.bold('Inputs:')} ${spec.inputs.length} path(s)`);
@@ -413,6 +423,7 @@ async function runSubcommand(config, args, chalk) {
413
423
 
414
424
  if (flags.dryRun) {
415
425
  console.log(chalk.bold('⚡ Dry run — no GPU will be provisioned\n'));
426
+ await printCapacityPreview(chalk, config, { gpu: resolvedGpu, region: flags.region?.toUpperCase() });
416
427
  return;
417
428
  }
418
429
 
@@ -7,12 +7,14 @@
7
7
  */
8
8
  import { readFileSync, existsSync } from 'fs';
9
9
  import { ensureBadgrReady } from '../onboarding.js';
10
+ import { requireApiKey } from '../config.js';
10
11
  import { callApi, listDeployments } from '../api.js';
11
12
  import { addDeployment, addReceipt, updateReceipt, generateReceiptId } from '../store.js';
12
13
  import { normalizeTier, callWithFallback } from '../fallback.js';
13
14
  import { formatCliError } from '../errors.js';
14
15
  import { BLESSED_COMFY_WORKFLOWS } from '../catalog.js';
15
- import { pollJobUntilTerminal, renderJobClosingBlock, stage as _stage, stageDone as _stageDone, writeBlock as _writeBlock, clearBlock as _clearBlock, renderLiveBlock as _renderLiveBlock, printFailureClass as _printFailureClass } from '../progress.js';
16
+ import { parseEnvFlag } from '../envFlag.js';
17
+ import { pollJobUntilTerminal, renderJobClosingBlock, stage as _stage, stageDone as _stageDone, writeBlock as _writeBlock, clearBlock as _clearBlock, renderLiveBlock as _renderLiveBlock, printFailureClass as _printFailureClass, printCapacityPreview, formatTierLabel } from '../progress.js';
16
18
 
17
19
  const COMFYUI_IMAGE = process.env.COMFYUI_IMAGE || 'yanwk/comfyui-boot:cu126-megapak';
18
20
  const HEALTH_PATH = '/system_stats';
@@ -35,6 +37,7 @@ export function parseComfyuiArgs(args) {
35
37
  if (a === '--check-nodes') { flags.checkNodes = args[++i]; i++; continue; }
36
38
  if (a === '--persistent') { flags.persistent = true; i++; continue; }
37
39
  if (a === '--yes' || a === '-y') { flags.yes = true; i++; continue; }
40
+ if (a === '--dry-run') { flags.dryRun = true; i++; continue; }
38
41
  if (a === '--env') {
39
42
  const kv = args[++i]; i++;
40
43
  if (!flags.env) flags.env = [];
@@ -46,15 +49,6 @@ export function parseComfyuiArgs(args) {
46
49
  return { workflow: positional[0] || null, flags };
47
50
  }
48
51
 
49
- function parseEnvFlag(envList) {
50
- const obj = {};
51
- for (const kv of (envList || [])) {
52
- const idx = kv.indexOf('=');
53
- if (idx > 0) obj[kv.slice(0, idx)] = kv.slice(idx + 1);
54
- }
55
- return obj;
56
- }
57
-
58
52
  // Live block while ComfyUI comes up — same generic "Checking health" stage
59
53
  // line and status block as every other launch path (see serve.js's
60
54
  // waitForEndpoint), so ComfyUI doesn't read as a different UX than vLLM.
@@ -179,16 +173,20 @@ export async function comfyBatchCommand(config, args, chalk) {
179
173
 
180
174
  if (flags.dryRun) {
181
175
  const workflowSpec = BLESSED_COMFY_WORKFLOWS[flags.workflow];
176
+ const resolvedGpu = flags.gpuType || workflowSpec?.gpu_type;
182
177
  console.log(chalk.bold('\n⚡ Dry run — no GPU will be provisioned\n'));
183
178
  console.log(` ${chalk.bold('Workflow:')} ${flags.workflow}${workflowSpec ? '' : chalk.yellow(' (unknown — server will reject this)')}`);
184
179
  if (workflowSpec) {
185
- console.log(` ${chalk.bold('GPU:')} ${flags.gpuType || workflowSpec.gpu_type}`);
180
+ console.log(` ${chalk.bold('GPU:')} ${resolvedGpu}`);
186
181
  console.log(` ${chalk.dim(workflowSpec.description)}`);
187
182
  }
188
183
  console.log(` ${chalk.bold('Prompts:')} ${prompts.length}${prompts.length > 20 ? chalk.yellow(' (exceeds the 20-prompt limit — server will reject this)') : ''}`);
184
+ console.log(` ${chalk.bold('Tier:')} ${formatTierLabel(normalizeTier(flags.tier))}`);
189
185
  if (flags.maxCost) console.log(` ${chalk.bold('Max cost:')} $${flags.maxCost}`);
190
186
  console.log(` ${chalk.bold('Max runtime:')} ${flags.maxRuntime ?? 60}min`);
191
- console.log(chalk.dim('\n Remove --dry-run to submit.\n'));
187
+ console.log();
188
+ if (resolvedGpu) await printCapacityPreview(chalk, config, { gpu: resolvedGpu });
189
+ console.log(chalk.dim(' Remove --dry-run to submit.\n'));
192
190
  return;
193
191
  }
194
192
 
@@ -282,9 +280,15 @@ export async function comfyuiCommand(config, args, chalk) {
282
280
  return;
283
281
  }
284
282
 
285
- config = await ensureBadgrReady(config, chalk);
283
+ // A dry run previews the plan only and never provisions or spends anything,
284
+ // so it doesn't need the interactive browser login — just a stored key.
285
+ if (flags.dryRun) {
286
+ requireApiKey(config);
287
+ } else {
288
+ config = await ensureBadgrReady(config, chalk);
289
+ }
286
290
 
287
- if (!flags.maxCost && !flags.persistent) {
291
+ if (!flags.maxCost && !flags.persistent && !flags.dryRun) {
288
292
  console.error(chalk.red('\n ✗ ComfyUI endpoints bill continuously. Specify a spending limit:\n'));
289
293
  console.error(chalk.dim(' --max-cost 5 auto-stop when $5 is reached'));
290
294
  console.error(chalk.dim(' --persistent run until you stop it manually\n'));
@@ -328,17 +332,29 @@ export async function comfyuiCommand(config, args, chalk) {
328
332
  const STAGE_TOTAL = 4;
329
333
  let stageN = 1;
330
334
 
331
- console.log(chalk.bold('\n⚡ Running ComfyUI\n'));
335
+ console.log(chalk.bold(flags.dryRun
336
+ ? '\n⚡ Dry run — no GPU will be provisioned — ComfyUI\n'
337
+ : '\n⚡ Running ComfyUI\n'));
332
338
  console.log(` ${chalk.bold('Workflow:')} ${workflow} (${nodeCount} nodes)`);
333
339
  console.log(` ${chalk.bold('Image:')} ${COMFYUI_IMAGE}`);
334
340
  console.log(` ${chalk.bold('GPU:')} ${gpu === 'AUTO' ? chalk.dim('auto (16+ GB VRAM)') : gpu}`);
341
+ if (flags.env?.length) console.log(` ${chalk.bold('Env:')} ${flags.env.join(', ')}`);
342
+ if (flags.dryRun) console.log(` ${chalk.bold('Tier:')} ${formatTierLabel(effectiveTier)}`);
335
343
  if (flags.maxCost) {
336
344
  console.log(` ${chalk.bold('Max cost:')} $${flags.maxCost.toFixed(2)} (auto-stop)`);
345
+ } else if (flags.dryRun) {
346
+ console.log(` ${chalk.bold('Max cost:')} ${chalk.dim('none')}`);
337
347
  } else {
338
348
  console.log(chalk.yellow(' ⚠ Persistent — billing until: badgr down <id>'));
339
349
  }
340
350
  console.log();
341
351
 
352
+ if (flags.dryRun) {
353
+ await printCapacityPreview(chalk, config, { gpu, region: flags.region?.toUpperCase(), maxPrice: flags.maxPrice });
354
+ console.log(chalk.dim(' Remove --dry-run to provision.\n'));
355
+ return;
356
+ }
357
+
342
358
  // Duplicate check
343
359
  if (config.apiKey) {
344
360
  try {
@@ -14,6 +14,8 @@ import { requireApiKey } from '../config.js';
14
14
  import { addReceipt, generateReceiptId } from '../store.js';
15
15
  import { normalizeTier, callWithFallback } from '../fallback.js';
16
16
  import { monitorBatchJob, fmtRuntime } from '../batch.js';
17
+ import { parseEnvFlag } from '../envFlag.js';
18
+ import { printCapacityPreview, formatTierLabel } from '../progress.js';
17
19
 
18
20
  const EMBED_IMAGE = 'vllm/vllm-openai:latest';
19
21
  const DEFAULT_EMBED_MODEL = 'BAAI/bge-large-en-v1.5';
@@ -35,6 +37,7 @@ export function parseEmbedArgs(args) {
35
37
  if (a === '--region') { flags.region = args[++i]; i++; continue; }
36
38
  if (a === '--batch-size') { flags.batchSize = parseInt(args[++i], 10); i++; continue; }
37
39
  if (a === '--detach') { flags.detach = true; i++; continue; }
40
+ if (a === '--dry-run') { flags.dryRun = true; i++; continue; }
38
41
  if (a === '--env') {
39
42
  const kv = args[++i]; i++;
40
43
  if (!flags.env) flags.env = [];
@@ -49,15 +52,6 @@ export function parseEmbedArgs(args) {
49
52
  return { model, input, flags };
50
53
  }
51
54
 
52
- function parseEnvFlag(envList) {
53
- const obj = {};
54
- for (const kv of (envList || [])) {
55
- const idx = kv.indexOf('=');
56
- if (idx > 0) obj[kv.slice(0, idx)] = kv.slice(idx + 1);
57
- }
58
- return obj;
59
- }
60
-
61
55
  function isUrl(s) {
62
56
  return /^(https?|s3|gs|hf):\/\//i.test(s);
63
57
  }
@@ -120,14 +114,23 @@ export async function embedCommand(config, args, chalk) {
120
114
  ...(flags.batchSize ? { EMBED_BATCH_SIZE: String(flags.batchSize) } : {}),
121
115
  };
122
116
 
123
- console.log(chalk.bold('\n📐 Embeddings\n'));
117
+ console.log(chalk.bold(flags.dryRun ? '\n📐 Dry run — no GPU will be provisioned — Embeddings\n' : '\n📐 Embeddings\n'));
124
118
  console.log(` ${chalk.bold('Input:')} ${resolved.inputLabel}`);
125
119
  console.log(` ${chalk.bold('Model:')} ${model}`);
126
120
  console.log(` ${chalk.bold('Image:')} ${EMBED_IMAGE}`);
127
121
  console.log(` ${chalk.bold('GPU:')} ${gpu === 'AUTO' ? chalk.dim('auto (8+ GB VRAM)') : gpu}`);
122
+ if (flags.dryRun) console.log(` ${chalk.bold('Tier:')} ${formatTierLabel(effectiveTier)}`);
128
123
  console.log(` ${chalk.bold('Max runtime:')} ${maxRuntimeMin}min`);
129
124
  if (maxCost) console.log(` ${chalk.bold('Max cost:')} $${maxCost.toFixed(2)}`);
125
+ if (flags.env?.length) console.log(` ${chalk.bold('Env:')} ${flags.env.join(', ')}`);
130
126
  console.log();
127
+
128
+ if (flags.dryRun) {
129
+ await printCapacityPreview(chalk, config, { gpu, region: flags.region?.toUpperCase(), maxPrice: flags.maxPrice });
130
+ console.log(chalk.dim(' Remove --dry-run to submit.\n'));
131
+ return;
132
+ }
133
+
131
134
  process.stdout.write(chalk.dim(' Finding suitable capacity...\n'));
132
135
 
133
136
  function buildBody(tierOverride) {
@@ -286,15 +286,20 @@ async function launchAgentWorkload(config, agentName, rawArgs, chalk) {
286
286
  const envKey = PROVIDER_ENV_KEYS[spec.provider];
287
287
  const userSuppliedKey = flags.env?.some(kv => kv.startsWith(`${envKey}=`));
288
288
  let credential = userSuppliedKey ? null : getCredential(spec.provider);
289
+ flags.authRequired = { provider: spec.provider, status: credential || userSuppliedKey ? 'connected' : 'missing' };
289
290
  if (!credential && !userSuppliedKey) {
290
291
  // Missing credential no longer forces a separate `badgr connect` +
291
292
  // rerun in an interactive terminal — prompt inline and continue the
292
- // same launch. Non-interactive contexts still hard-fail as before.
293
+ // same launch (including under --dry-run, so the dry-run preview
294
+ // reflects the connected state the real launch will actually use, and
295
+ // a follow-up real launch never needs a second prompt). Non-interactive
296
+ // contexts still hard-fail as before.
293
297
  credential = await resolveMissingCredential(spec.provider, chalk);
294
298
  if (!credential) {
295
299
  process.exitCode = 1;
296
300
  return;
297
301
  }
302
+ flags.authRequired.status = 'connected';
298
303
  }
299
304
  // An explicit --env always wins over the stored credential — never
300
305
  // append the injected default alongside a user-supplied value for the
@@ -304,6 +309,8 @@ async function launchAgentWorkload(config, agentName, rawArgs, chalk) {
304
309
  if (!flags.env) flags.env = [];
305
310
  flags.env.push(`${envKey}=${credential}`);
306
311
  }
312
+ } else {
313
+ flags.authRequired = { provider: null, status: 'none' };
307
314
  }
308
315
  // cline has no `provider` — Badgr mints and injects its own short-lived
309
316
  // job-scoped model token server-side (see backend jobs_routes.py); the