@ornncompute/cli 0.1.1 → 0.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.
package/README.md CHANGED
@@ -1,11 +1,11 @@
1
- # Ornn Fabric CLI
1
+ # Ornn CLI
2
2
 
3
- Command-line access for Ornn Fabric bare-metal compute workflows: find
3
+ Command-line access for Ornn bare-metal compute workflows: find
4
4
  available capacity, buy or bid, manage reservations, activate SSH access,
5
5
  resell capacity, and open billing.
6
6
 
7
7
  For the end-user install, browser-login, command, and troubleshooting guide, see
8
- the root [`CLI.md`](../../CLI.md).
8
+ the [`docs/cli.md`](../../docs/cli.md) guide.
9
9
 
10
10
  ## Install
11
11
 
@@ -14,9 +14,9 @@ curl -fsSL https://compute.ornn.com/cli/install | sh
14
14
  ```
15
15
 
16
16
  The installer requires Node.js 20 or newer and npm. It installs `@ornncompute/cli`
17
- globally, exposes both `fabric` and `ornn` binaries, adds the npm global bin
18
- directory to the user's shell profile when needed, and writes
19
- `ORNN_AUTH_BASE_URL` for the Fabric host that served the installer. The
17
+ globally, exposes the `ornn` binary, adds the npm global bin directory to the
18
+ user's shell profile when needed, and writes
19
+ `ORNN_AUTH_BASE_URL` for the Ornn host that served the installer. The
20
20
  installer prints a short Ornn Compute welcome banner and login prompt; set
21
21
  `ORNN_INSTALL_BANNER=0` to skip it or `ORNN_INSTALL_ANIMATION=0` to keep it
22
22
  static.
@@ -24,46 +24,51 @@ static.
24
24
  ## Commands
25
25
 
26
26
  ```bash
27
- fabric help [command]
28
- fabric login [--auth-base <url>] [--no-browser] [--timeout <seconds>]
29
- fabric whoami [--json]
30
- fabric status [--json]
31
- fabric availability list [--gpu-type <type>] [--facility <name>] [--operator <name>] [--json]
32
- fabric availability show <listing-id> [--open] [--json]
33
- fabric buy <listing-id> [--no-open] [--json]
34
- fabric bid create <listing-id> --gpu-count <n> --min-gpu-count <n> --start-date <yyyy-mm-dd> --end-date <yyyy-mm-dd> --price <usd> [--no-open] [--json]
35
- fabric bid list [--json]
36
- fabric bid show <bid-id> [--open] [--json]
37
- fabric bid update <bid-id> --gpu-count <n> --min-gpu-count <n> --start-date <yyyy-mm-dd> --end-date <yyyy-mm-dd> --price <usd>
38
- fabric bid withdraw <bid-id>
39
- fabric reservations list [--status <status>] [--json]
40
- fabric reservations show <reservation-id> [--open] [--json]
41
- fabric reservations checkout <reservation-id> [--no-open] [--json]
42
- fabric access show <reservation-id> [--json]
43
- fabric access activate <reservation-id> --ssh-key-id <id> [--username <name>] [--no-open] [--json]
44
- fabric access push-keys <reservation-id> --ssh-key-id <id> [--json]
45
- fabric resale browse [--json]
46
- fabric resale list <reservation-id> --ask-price <usd> [--no-open] [--json]
47
- fabric resale update <reservation-id> --ask-price <usd> [--no-open] [--json]
48
- fabric resale delist <reservation-id> [--no-open] [--json]
49
- fabric ssh-keys list [--json]
50
- fabric ssh-keys add --public-key <key> [--label <label>] [--json]
51
- fabric ssh-keys add --public-key-file <path> [--label <label>] [--json]
52
- fabric ssh-keys delete <key-id> [--json]
53
- fabric billing open [--no-open] [--json]
54
- fabric api <get|post|patch|put|delete> <compute-path> [--data <json>] [--raw]
55
- fabric logout
27
+ ornn help [command]
28
+ ornn login [--auth-base <url>] [--no-browser] [--timeout <seconds>]
29
+ ornn whoami [--json]
30
+ ornn status [--json]
31
+ ornn availability list [--gpu-type <type>] [--facility <name>] [--operator <name>] [--json]
32
+ ornn availability show <listing-id> [--open] [--json]
33
+ ornn buy <listing-id> [--no-open] [--json]
34
+ ornn bid create <listing-id> --gpu-count <n> --min-gpu-count <n> --start-date <yyyy-mm-dd> --end-date <yyyy-mm-dd> --price <usd> [--no-open] [--json]
35
+ ornn bid list [--json]
36
+ ornn bid show <bid-id> [--open] [--json]
37
+ ornn bid update <bid-id> --gpu-count <n> --min-gpu-count <n> --start-date <yyyy-mm-dd> --end-date <yyyy-mm-dd> --price <usd>
38
+ ornn bid withdraw <bid-id>
39
+ ornn reservations list [--status <status>] [--json]
40
+ ornn reservations show <reservation-id> [--open] [--json]
41
+ ornn reservations checkout <reservation-id> [--no-open] [--json]
42
+ ornn access show <reservation-id> [--json]
43
+ ornn access activate <reservation-id> --ssh-key-id <id> [--username <name>] [--no-open] [--json]
44
+ ornn access push-keys <reservation-id> --ssh-key-id <id> [--json]
45
+ ornn access keys list <reservation-id> [--json]
46
+ ornn access keys add <reservation-id> --public-key <key> [--label <label>] [--json]
47
+ ornn access keys add <reservation-id> --public-key-file <path> [--label <label>] [--json]
48
+ ornn access keys push <reservation-id> --ssh-key-id <id> [--json]
49
+ ornn access keys status <reservation-id> [--json]
50
+ ornn resale browse [--json]
51
+ ornn resale list <reservation-id> --ask-price <usd> [--no-open] [--json]
52
+ ornn resale update <reservation-id> --ask-price <usd> [--no-open] [--json]
53
+ ornn resale delist <reservation-id> [--no-open] [--json]
54
+ ornn ssh-keys list [--json]
55
+ ornn ssh-keys add --public-key <key> [--label <label>] [--json]
56
+ ornn ssh-keys add --public-key-file <path> [--label <label>] [--json]
57
+ ornn ssh-keys delete <key-id> [--json]
58
+ ornn billing open [--no-open] [--json]
59
+ ornn api <get|post|patch|put|delete> <compute-path> [--data <json>] [--raw]
60
+ ornn logout
56
61
  ```
57
62
 
58
63
  Commands print concise human-readable output by default. Use `--json` on
59
64
  read/show/list commands and transaction handoffs when you need structured
60
65
  stdout for scripts. Errors and login/browser progress go to stderr.
61
66
 
62
- `fabric login` opens a browser to the Fabric auth origin, lets the user sign in
67
+ `ornn login` opens a browser to the Ornn auth origin, lets the user sign in
63
68
  with the normal web account flow, and then completes automatically in the
64
69
  terminal once the browser session approves the CLI device request. If the CLI
65
70
  cannot open a browser, copy the printed URL into any browser and sign in there;
66
71
  the terminal keeps polling until the login is approved or expires.
67
72
 
68
- Run `fabric --help` for the full command list and `fabric api` for the
73
+ Run `ornn --help` for the full command list and `ornn api` for the
69
74
  allowlisted compute API escape hatch.
package/package.json CHANGED
@@ -1,10 +1,9 @@
1
1
  {
2
2
  "name": "@ornncompute/cli",
3
- "version": "0.1.1",
4
- "description": "Command-line interface for Ornn Fabric bare-metal compute workflows.",
3
+ "version": "0.1.2",
4
+ "description": "Command-line interface for Ornn bare-metal compute workflows.",
5
5
  "type": "module",
6
6
  "bin": {
7
- "fabric": "bin/ornn.js",
8
7
  "ornn": "bin/ornn.js"
9
8
  },
10
9
  "files": [
@@ -23,7 +23,7 @@ export function resolveApiBaseUrl({ env = process.env, explicit, session } = {})
23
23
  export async function loadRequiredSession({ env = process.env } = {}) {
24
24
  const session = await loadAuthSession({ env });
25
25
  if (!session?.accessToken) {
26
- throw new CliApiError("Not logged in. Run `fabric login` first.", { status: 401 });
26
+ throw new CliApiError("Not logged in. Run `ornn login` first.", { status: 401 });
27
27
  }
28
28
  return session;
29
29
  }
@@ -64,7 +64,7 @@ export async function cliRequest({
64
64
  });
65
65
  } catch (error) {
66
66
  const message = error instanceof Error ? error.message : String(error);
67
- throw new CliApiError(`Could not reach Fabric at ${baseUrl}: ${message}`);
67
+ throw new CliApiError(`Could not reach Ornn at ${baseUrl}: ${message}`);
68
68
  }
69
69
 
70
70
  if (raw) {
@@ -80,7 +80,7 @@ export async function cliRequest({
80
80
  if (!response.ok) {
81
81
  const detail = data?.detail ?? data?.error ?? response.statusText;
82
82
  const message = detailMessage(detail) || response.statusText;
83
- throw new CliApiError(`Fabric request failed: ${message}`, {
83
+ throw new CliApiError(`Ornn request failed: ${message}`, {
84
84
  detail: data,
85
85
  status: response.status,
86
86
  });
@@ -111,9 +111,9 @@ function parseJson(text, url, response) {
111
111
  const contentType = response?.headers?.get?.("content-type") || "";
112
112
  const isHtml = /html/i.test(contentType) || /^\s*</.test(text);
113
113
  const hint = isHtml
114
- ? " The Fabric host may not have this CLI endpoint deployed yet."
114
+ ? " The Ornn host may not have this CLI endpoint deployed yet."
115
115
  : "";
116
- throw new CliApiError(`Fabric returned a non-JSON response from ${url}.${hint}`, {
116
+ throw new CliApiError(`Ornn returned a non-JSON response from ${url}.${hint}`, {
117
117
  status: response?.status,
118
118
  });
119
119
  }
package/src/cli.mjs CHANGED
@@ -14,56 +14,82 @@ import {
14
14
  waitForDeviceApproval,
15
15
  } from "./device-auth.mjs";
16
16
 
17
- const VERSION = "0.1.1";
17
+ const VERSION = "0.1.2";
18
18
  const DEFAULT_BUY_NOW_USD_PER_GPU_HOUR = 10;
19
19
 
20
- const CONCISE_HELP = `Ornn Fabric CLI
20
+ function resolveBuyNowUsdPerGpuHour(record) {
21
+ const stored = record?.buy_now_price_per_gpu_hour;
22
+ if (stored != null && Number.isFinite(stored) && stored > 0) {
23
+ return stored;
24
+ }
25
+ const normalized = String(record?.gpu_type ?? "").toLowerCase();
26
+ if (normalized.includes("h200")) {
27
+ return 2.49;
28
+ }
29
+ if (normalized.includes("h100")) {
30
+ return 2.29;
31
+ }
32
+ if (normalized.includes("a100")) {
33
+ return 1.89;
34
+ }
35
+ if (normalized) {
36
+ return 1.75;
37
+ }
38
+ return DEFAULT_BUY_NOW_USD_PER_GPU_HOUR;
39
+ }
40
+
41
+ const CONCISE_HELP = `Ornn CLI
21
42
 
22
43
  Find, buy, access, and resell bare-metal compute.
23
44
 
24
45
  Usage:
25
- fabric <command> [options]
46
+ ornn <command> [options]
26
47
 
27
48
  Examples:
28
- fabric availability list
29
- fabric buy <listing-id>
30
- fabric access activate <reservation-id> --ssh-key-id <key-id>
49
+ ornn availability list
50
+ ornn buy <listing-id>
51
+ ornn access activate <reservation-id> --ssh-key-id <key-id>
31
52
 
32
- Run \`fabric --help\` for all commands, or \`fabric help <command>\`.
53
+ Run \`ornn --help\` for all commands, or \`ornn help <command>\`.
33
54
  `;
34
55
 
35
- const HELP = `Ornn Fabric CLI
56
+ const HELP = `Ornn CLI
36
57
 
37
58
  Usage:
38
- fabric help [command]
39
- fabric login [--auth-base <url>] [--no-browser] [--timeout <seconds>]
40
- fabric whoami [--json]
41
- fabric status [--json]
42
- fabric availability list [--gpu-type <type>] [--facility <name>] [--operator <name>] [--json]
43
- fabric availability show <listing-id> [--open] [--json]
44
- fabric buy <listing-id> [--no-open] [--json]
45
- fabric bid create <listing-id> --gpu-count <n> --min-gpu-count <n> --start-date <yyyy-mm-dd> --end-date <yyyy-mm-dd> --price <usd> [--no-open] [--json]
46
- fabric bid list [--json]
47
- fabric bid show <bid-id> [--open] [--json]
48
- fabric bid update <bid-id> --gpu-count <n> --min-gpu-count <n> --start-date <yyyy-mm-dd> --end-date <yyyy-mm-dd> --price <usd>
49
- fabric bid withdraw <bid-id>
50
- fabric reservations list [--status <status>] [--json]
51
- fabric reservations show <reservation-id> [--open] [--json]
52
- fabric reservations checkout <reservation-id> [--no-open] [--json]
53
- fabric access show <reservation-id> [--json]
54
- fabric access activate <reservation-id> --ssh-key-id <id> [--username <name>] [--no-open] [--json]
55
- fabric access push-keys <reservation-id> --ssh-key-id <id> [--json]
56
- fabric resale browse [--json]
57
- fabric resale list <reservation-id> --ask-price <usd> [--no-open] [--json]
58
- fabric resale update <reservation-id> --ask-price <usd> [--no-open] [--json]
59
- fabric resale delist <reservation-id> [--no-open] [--json]
60
- fabric ssh-keys list [--json]
61
- fabric ssh-keys add --public-key <key> [--label <label>] [--json]
62
- fabric ssh-keys add --public-key-file <path> [--label <label>] [--json]
63
- fabric ssh-keys delete <key-id> [--json]
64
- fabric billing open [--no-open] [--json]
65
- fabric api <get|post|patch|put|delete> <compute-path> [--data <json>] [--raw]
66
- fabric logout
59
+ ornn help [command]
60
+ ornn login [--auth-base <url>] [--no-browser] [--timeout <seconds>]
61
+ ornn whoami [--json]
62
+ ornn status [--json]
63
+ ornn availability list [--gpu-type <type>] [--facility <name>] [--operator <name>] [--json]
64
+ ornn availability show <listing-id> [--open] [--json]
65
+ ornn buy <listing-id> [--no-open] [--json]
66
+ ornn bid create <listing-id> --gpu-count <n> --min-gpu-count <n> --start-date <yyyy-mm-dd> --end-date <yyyy-mm-dd> --price <usd> [--no-open] [--json]
67
+ ornn bid list [--json]
68
+ ornn bid show <bid-id> [--open] [--json]
69
+ ornn bid update <bid-id> --gpu-count <n> --min-gpu-count <n> --start-date <yyyy-mm-dd> --end-date <yyyy-mm-dd> --price <usd>
70
+ ornn bid withdraw <bid-id>
71
+ ornn reservations list [--status <status>] [--json]
72
+ ornn reservations show <reservation-id> [--open] [--json]
73
+ ornn reservations checkout <reservation-id> [--no-open] [--json]
74
+ ornn access show <reservation-id> [--json]
75
+ ornn access activate <reservation-id> --ssh-key-id <id> [--username <name>] [--no-open] [--json]
76
+ ornn access push-keys <reservation-id> --ssh-key-id <id> [--json]
77
+ ornn access keys list <reservation-id> [--json]
78
+ ornn access keys add <reservation-id> --public-key <key> [--label <label>] [--json]
79
+ ornn access keys add <reservation-id> --public-key-file <path> [--label <label>] [--json]
80
+ ornn access keys push <reservation-id> --ssh-key-id <id> [--json]
81
+ ornn access keys status <reservation-id> [--json]
82
+ ornn resale browse [--json]
83
+ ornn resale list <reservation-id> --ask-price <usd> [--no-open] [--json]
84
+ ornn resale update <reservation-id> --ask-price <usd> [--no-open] [--json]
85
+ ornn resale delist <reservation-id> [--no-open] [--json]
86
+ ornn ssh-keys list [--json]
87
+ ornn ssh-keys add --public-key <key> [--label <label>] [--json]
88
+ ornn ssh-keys add --public-key-file <path> [--label <label>] [--json]
89
+ ornn ssh-keys delete <key-id> [--json]
90
+ ornn billing open [--no-open] [--json]
91
+ ornn api <get|post|patch|put|delete> <compute-path> [--data <json>] [--raw]
92
+ ornn logout
67
93
 
68
94
  Environment:
69
95
  ORNN_AUTH_BASE_URL Web/auth server origin. Defaults to http://localhost:3000.
@@ -185,7 +211,7 @@ export async function run(argv = [], io = {}) {
185
211
  }
186
212
 
187
213
  if (command === "logout") {
188
- parseCommandOptions(args, {}, "Usage: fabric logout");
214
+ parseCommandOptions(args, {}, "Usage: ornn logout");
189
215
  await clearAuthSession({ env });
190
216
  stdout.write("Logged out of Ornn.\n");
191
217
  return 0;
@@ -204,26 +230,26 @@ function validateHelpInvocation(command, args) {
204
230
  if (command === "help") {
205
231
  const [topic, ...extra] = args;
206
232
  if (extra.length) {
207
- return "Usage: fabric help [command]";
233
+ return "Usage: ornn help [command]";
208
234
  }
209
235
  return topic && !HELP_TOPICS.has(topic) ? `Unknown command: ${topic}` : null;
210
236
  }
211
237
 
212
238
  if (command === "login") {
213
239
  const parsed = parseHelpArgs(args, { booleanOptions: ["--no-browser"], valueOptions: ["--auth-base", "--poll-interval", "--timeout"] });
214
- return parsed.error || (parsed.positionals.length ? "Usage: fabric login [--auth-base <url>] [--no-browser] [--timeout <seconds>]" : null);
240
+ return parsed.error || (parsed.positionals.length ? "Usage: ornn login [--auth-base <url>] [--no-browser] [--timeout <seconds>]" : null);
215
241
  }
216
242
 
217
243
  if (command === "logout") {
218
- return validateNoPositionals(args, "Usage: fabric logout");
244
+ return validateNoPositionals(args, "Usage: ornn logout");
219
245
  }
220
246
 
221
247
  if (command === "whoami" || command === "account") {
222
- return validateNoPositionals(args, "Usage: fabric whoami [--json]", { booleanOptions: ["--json"] });
248
+ return validateNoPositionals(args, "Usage: ornn whoami [--json]", { booleanOptions: ["--json"] });
223
249
  }
224
250
 
225
251
  if (command === "status") {
226
- return validateNoPositionals(args, "Usage: fabric status [--json]", { booleanOptions: ["--json"] });
252
+ return validateNoPositionals(args, "Usage: ornn status [--json]", { booleanOptions: ["--json"] });
227
253
  }
228
254
 
229
255
  if (command === "api") {
@@ -236,7 +262,7 @@ function validateHelpInvocation(command, args) {
236
262
  }
237
263
  const [method, path, ...extra] = parsed.positionals;
238
264
  if (!path || extra.length) {
239
- return "Usage: fabric api <get|post|patch|put|delete> <compute-path> [--data <json>] [--raw]";
265
+ return "Usage: ornn api <get|post|patch|put|delete> <compute-path> [--data <json>] [--raw]";
240
266
  }
241
267
  return ["delete", "get", "patch", "post", "put"].includes(method.toLowerCase()) ? null : `Unsupported API method: ${method}`;
242
268
  }
@@ -248,12 +274,12 @@ function validateHelpInvocation(command, args) {
248
274
  }
249
275
  const [subcommand, id, ...extra] = parsed.positionals;
250
276
  if (!subcommand || subcommand === "list") {
251
- return id || extra.length ? "Usage: fabric availability list [--gpu-type <type>] [--facility <name>] [--operator <name>] [--json]" : null;
277
+ return id || extra.length ? "Usage: ornn availability list [--gpu-type <type>] [--facility <name>] [--operator <name>] [--json]" : null;
252
278
  }
253
279
  if (subcommand === "show") {
254
- return !id || extra.length ? "Usage: fabric availability show <listing-id> [--open] [--json]" : null;
280
+ return !id || extra.length ? "Usage: ornn availability show <listing-id> [--open] [--json]" : null;
255
281
  }
256
- return "Usage: fabric availability list|show";
282
+ return "Usage: ornn availability list|show";
257
283
  }
258
284
 
259
285
  if (command === "buy") {
@@ -261,7 +287,7 @@ function validateHelpInvocation(command, args) {
261
287
  if (parsed.error) {
262
288
  return parsed.error;
263
289
  }
264
- return parsed.positionals.length === 1 ? null : "Usage: fabric buy <listing-id> [--no-open] [--json]";
290
+ return parsed.positionals.length === 1 ? null : "Usage: ornn buy <listing-id> [--no-open] [--json]";
265
291
  }
266
292
 
267
293
  if (command === "bid" || command === "bids") {
@@ -271,12 +297,12 @@ function validateHelpInvocation(command, args) {
271
297
  }
272
298
  const [subcommand, id, ...extra] = parsed.positionals;
273
299
  if (!subcommand || subcommand === "list") {
274
- return id || extra.length ? "Usage: fabric bid list|show|create|update|withdraw" : null;
300
+ return id || extra.length ? "Usage: ornn bid list|show|create|update|withdraw" : null;
275
301
  }
276
302
  if (["create", "show", "update", "withdraw", "delete"].includes(subcommand)) {
277
- return !id || extra.length ? "Usage: fabric bid list|show|create|update|withdraw" : null;
303
+ return !id || extra.length ? "Usage: ornn bid list|show|create|update|withdraw" : null;
278
304
  }
279
- return "Usage: fabric bid list|show|create|update|withdraw";
305
+ return "Usage: ornn bid list|show|create|update|withdraw";
280
306
  }
281
307
 
282
308
  if (command === "reservations") {
@@ -286,24 +312,30 @@ function validateHelpInvocation(command, args) {
286
312
  }
287
313
  const [subcommand, id, ...extra] = parsed.positionals;
288
314
  if (!subcommand || subcommand === "list") {
289
- return id || extra.length ? "Usage: fabric reservations list|show|checkout" : null;
315
+ return id || extra.length ? "Usage: ornn reservations list|show|checkout" : null;
290
316
  }
291
317
  if (["show", "checkout"].includes(subcommand)) {
292
- return !id || extra.length ? "Usage: fabric reservations list|show|checkout" : null;
318
+ return !id || extra.length ? "Usage: ornn reservations list|show|checkout" : null;
293
319
  }
294
- return "Usage: fabric reservations list|show|checkout";
320
+ return "Usage: ornn reservations list|show|checkout";
295
321
  }
296
322
 
297
323
  if (command === "access") {
298
- const parsed = parseHelpArgs(args, { booleanOptions: ["--json", "--no-open"], valueOptions: ["--machine-count", "--request-id", "--ssh-key-id", "--tenant-username", "--username"] });
324
+ const parsed = parseHelpArgs(args, { booleanOptions: ["--json", "--no-open"], valueOptions: ["--label", "--machine-count", "--public-key", "--public-key-file", "--request-id", "--ssh-key-id", "--tenant-username", "--username"] });
299
325
  if (parsed.error) {
300
326
  return parsed.error;
301
327
  }
302
- const [subcommand, id, ...extra] = parsed.positionals;
328
+ const [subcommand, id, nestedId, ...extra] = parsed.positionals;
329
+ if (subcommand === "keys") {
330
+ if (["list", "add", "push", "status"].includes(id)) {
331
+ return !nestedId || extra.length ? "Usage: ornn access keys list|add|push|status <reservation-id>" : null;
332
+ }
333
+ return !id ? null : "Usage: ornn access keys list|add|push|status <reservation-id>";
334
+ }
303
335
  if (["show", "activate", "push-keys"].includes(subcommand)) {
304
- return !id || extra.length ? "Usage: fabric access show|activate|push-keys <reservation-id>" : null;
336
+ return !id || nestedId || extra.length ? "Usage: ornn access show|activate|push-keys <reservation-id>" : null;
305
337
  }
306
- return !subcommand ? null : "Usage: fabric access show|activate|push-keys <reservation-id>";
338
+ return !subcommand ? null : "Usage: ornn access show|activate|push-keys|keys <reservation-id>";
307
339
  }
308
340
 
309
341
  if (command === "resale") {
@@ -313,12 +345,12 @@ function validateHelpInvocation(command, args) {
313
345
  }
314
346
  const [subcommand, id, ...extra] = parsed.positionals;
315
347
  if (!subcommand || subcommand === "browse") {
316
- return id || extra.length ? "Usage: fabric resale browse|list|update|delist" : null;
348
+ return id || extra.length ? "Usage: ornn resale browse|list|update|delist" : null;
317
349
  }
318
350
  if (["list", "update", "delist"].includes(subcommand)) {
319
- return !id || extra.length ? "Usage: fabric resale browse|list|update|delist" : null;
351
+ return !id || extra.length ? "Usage: ornn resale browse|list|update|delist" : null;
320
352
  }
321
- return "Usage: fabric resale browse|list|update|delist";
353
+ return "Usage: ornn resale browse|list|update|delist";
322
354
  }
323
355
 
324
356
  if (command === "billing") {
@@ -328,9 +360,9 @@ function validateHelpInvocation(command, args) {
328
360
  }
329
361
  const [subcommand, ...extra] = parsed.positionals;
330
362
  if (!subcommand || subcommand === "open") {
331
- return extra.length ? "Usage: fabric billing open [--no-open] [--json]" : null;
363
+ return extra.length ? "Usage: ornn billing open [--no-open] [--json]" : null;
332
364
  }
333
- return "Usage: fabric billing open [--no-open] [--json]";
365
+ return "Usage: ornn billing open [--no-open] [--json]";
334
366
  }
335
367
 
336
368
  if (command === "ssh-keys") {
@@ -340,15 +372,15 @@ function validateHelpInvocation(command, args) {
340
372
  }
341
373
  const [subcommand, id, ...extra] = parsed.positionals;
342
374
  if (!subcommand || subcommand === "list") {
343
- return id || extra.length ? "Usage: fabric ssh-keys list|add|delete" : null;
375
+ return id || extra.length ? "Usage: ornn ssh-keys list|add|delete" : null;
344
376
  }
345
377
  if (subcommand === "add") {
346
- return id || extra.length ? "Usage: fabric ssh-keys list|add|delete" : null;
378
+ return id || extra.length ? "Usage: ornn ssh-keys list|add|delete" : null;
347
379
  }
348
380
  if (["delete", "remove"].includes(subcommand)) {
349
- return extra.length ? "Usage: fabric ssh-keys list|add|delete" : null;
381
+ return extra.length ? "Usage: ornn ssh-keys list|add|delete" : null;
350
382
  }
351
- return "Usage: fabric ssh-keys list|add|delete";
383
+ return "Usage: ornn ssh-keys list|add|delete";
352
384
  }
353
385
 
354
386
  return `Unknown command: ${command}`;
@@ -357,7 +389,7 @@ function validateHelpInvocation(command, args) {
357
389
  async function help(args, { stdout }) {
358
390
  const [topic, ...extra] = args;
359
391
  if (extra.length) {
360
- throw new Error("Usage: fabric help [command]");
392
+ throw new Error("Usage: ornn help [command]");
361
393
  }
362
394
  if (topic && !HELP_TOPICS.has(topic)) {
363
395
  throw new Error(`Unknown command: ${topic}`);
@@ -444,11 +476,11 @@ async function login(args, { env, fetchImpl, openBrowserImpl, stderr, stdout })
444
476
  }
445
477
 
446
478
  async function whoami(args, { env, fetchImpl, stderr, stdout }) {
447
- const options = parseCommandOptions(args, { boolean: ["json"] }, "Usage: fabric whoami [--json]");
479
+ const options = parseCommandOptions(args, { boolean: ["json"] }, "Usage: ornn whoami [--json]");
448
480
  const session = await loadAuthSession({ env });
449
481
 
450
482
  if (!session) {
451
- stderr.write("Not logged in. Run `fabric login` first.\n");
483
+ stderr.write("Not logged in. Run `ornn login` first.\n");
452
484
  return 1;
453
485
  }
454
486
 
@@ -484,7 +516,7 @@ async function whoami(args, { env, fetchImpl, stderr, stdout }) {
484
516
  }
485
517
 
486
518
  async function status(args, { env, fetchImpl, stdout }) {
487
- const options = parseCommandOptions(args, { boolean: ["json"] }, "Usage: fabric status [--json]");
519
+ const options = parseCommandOptions(args, { boolean: ["json"] }, "Usage: ornn status [--json]");
488
520
  const snapshot = await cliRequest({
489
521
  authRequired: false,
490
522
  endpoint: "/api/cli/status",
@@ -507,7 +539,7 @@ async function status(args, { env, fetchImpl, stdout }) {
507
539
  async function api(args, { env, fetchImpl, stdout }) {
508
540
  const [method, path, ...rest] = args;
509
541
  if (!method || !path) {
510
- throw new Error("Usage: fabric api <get|post|patch|put|delete> <compute-path> [--data <json>] [--raw]");
542
+ throw new Error("Usage: ornn api <get|post|patch|put|delete> <compute-path> [--data <json>] [--raw]");
511
543
  }
512
544
  const normalizedMethod = method.toUpperCase();
513
545
  if (!["DELETE", "GET", "PATCH", "POST", "PUT"].includes(normalizedMethod)) {
@@ -516,7 +548,7 @@ async function api(args, { env, fetchImpl, stdout }) {
516
548
  const options = parseCommandOptions(
517
549
  rest,
518
550
  { boolean: ["raw", "json"], value: ["data"] },
519
- "Usage: fabric api <get|post|patch|put|delete> <compute-path> [--data <json>] [--raw]",
551
+ "Usage: ornn api <get|post|patch|put|delete> <compute-path> [--data <json>] [--raw]",
520
552
  );
521
553
  const body = await readJsonOption(options.data);
522
554
  const response = await cliRequest({
@@ -537,7 +569,7 @@ async function availability(args, context) {
537
569
  const options = parseCommandOptions(
538
570
  [id, ...rest].filter(Boolean),
539
571
  { boolean: ["json"], value: ["facility", "gpu-type", "operator"] },
540
- "Usage: fabric availability list [--gpu-type <type>] [--facility <name>] [--operator <name>] [--json]",
572
+ "Usage: ornn availability list [--gpu-type <type>] [--facility <name>] [--operator <name>] [--json]",
541
573
  );
542
574
  const listings = await loadAvailabilityListings(context);
543
575
  const filtered = filterAvailabilityListings(listings, options);
@@ -553,7 +585,7 @@ async function availability(args, context) {
553
585
  const options = parseCommandOptions(
554
586
  rest,
555
587
  { boolean: ["json", "open"] },
556
- "Usage: fabric availability show <listing-id> [--open] [--json]",
588
+ "Usage: ornn availability show <listing-id> [--open] [--json]",
557
589
  );
558
590
  const listing = await resolveListing(id, context);
559
591
  if (options.json) {
@@ -571,18 +603,18 @@ async function availability(args, context) {
571
603
  return 0;
572
604
  }
573
605
 
574
- throw new Error("Usage: fabric availability list|show");
606
+ throw new Error("Usage: ornn availability list|show");
575
607
  }
576
608
 
577
609
  async function buy(args, context) {
578
610
  const [listingId, ...rest] = args;
579
611
  if (!listingId) {
580
- throw new Error("Usage: fabric buy <listing-id> [--no-open] [--json]");
612
+ throw new Error("Usage: ornn buy <listing-id> [--no-open] [--json]");
581
613
  }
582
614
  const options = parseCommandOptions(
583
615
  rest,
584
616
  { boolean: ["json", "no-open"] },
585
- "Usage: fabric buy <listing-id> [--no-open] [--json]",
617
+ "Usage: ornn buy <listing-id> [--no-open] [--json]",
586
618
  );
587
619
  const listing = await resolveListing(listingId, context);
588
620
 
@@ -605,7 +637,7 @@ async function bid(args, context) {
605
637
  const options = parseCommandOptions(
606
638
  [id, ...rest].filter(Boolean),
607
639
  { boolean: ["json"] },
608
- "Usage: fabric bid list [--json]",
640
+ "Usage: ornn bid list [--json]",
609
641
  );
610
642
  const bids = await cliRequest({
611
643
  endpoint: computeEndpoint("/tenants/me/bids"),
@@ -624,7 +656,7 @@ async function bid(args, context) {
624
656
  const options = parseCommandOptions(
625
657
  rest,
626
658
  { boolean: ["json", "open"] },
627
- "Usage: fabric bid show <bid-id> [--open] [--json]",
659
+ "Usage: ornn bid show <bid-id> [--open] [--json]",
628
660
  );
629
661
  const found = await findBid(id, context);
630
662
  if (options.json) {
@@ -648,7 +680,7 @@ async function bid(args, context) {
648
680
  boolean: ["json", "no-open"],
649
681
  value: ["bid-price-per-gpu-hour", "end-date", "gpu-count", "min-gpu-count", "price", "start-date"],
650
682
  },
651
- "Usage: fabric bid create <listing-id> --gpu-count <n> --min-gpu-count <n> --start-date <yyyy-mm-dd> --end-date <yyyy-mm-dd> --price <usd> [--no-open] [--json]",
683
+ "Usage: ornn bid create <listing-id> --gpu-count <n> --min-gpu-count <n> --start-date <yyyy-mm-dd> --end-date <yyyy-mm-dd> --price <usd> [--no-open] [--json]",
652
684
  );
653
685
  if (id.startsWith("resale-")) {
654
686
  throw new Error("Bids can only be placed on primary availability listings, not resale listings.");
@@ -694,7 +726,7 @@ async function bid(args, context) {
694
726
  boolean: ["json"],
695
727
  value: ["bid-price-per-gpu-hour", "end-date", "gpu-count", "min-gpu-count", "price", "start-date"],
696
728
  },
697
- "Usage: fabric bid update <bid-id> --gpu-count <n> --min-gpu-count <n> --start-date <yyyy-mm-dd> --end-date <yyyy-mm-dd> --price <usd>",
729
+ "Usage: ornn bid update <bid-id> --gpu-count <n> --min-gpu-count <n> --start-date <yyyy-mm-dd> --end-date <yyyy-mm-dd> --price <usd>",
698
730
  );
699
731
  const { endDate, startDate } = dateRangeOptions(options);
700
732
  const gpuCount = positiveIntegerOption(options.gpuCount, "--gpu-count");
@@ -727,7 +759,7 @@ async function bid(args, context) {
727
759
  const options = parseCommandOptions(
728
760
  rest,
729
761
  { boolean: ["json"] },
730
- "Usage: fabric bid withdraw <bid-id> [--json]",
762
+ "Usage: ornn bid withdraw <bid-id> [--json]",
731
763
  );
732
764
  const response = await cliRequest({
733
765
  endpoint: computeEndpoint(`/tenants/me/bids/${id}`),
@@ -743,7 +775,7 @@ async function bid(args, context) {
743
775
  return 0;
744
776
  }
745
777
 
746
- throw new Error("Usage: fabric bid list|show|create|update|withdraw");
778
+ throw new Error("Usage: ornn bid list|show|create|update|withdraw");
747
779
  }
748
780
 
749
781
  async function reservations(args, context) {
@@ -753,7 +785,7 @@ async function reservations(args, context) {
753
785
  const options = parseCommandOptions(
754
786
  [id, ...rest].filter(Boolean),
755
787
  { boolean: ["json"], value: ["status"] },
756
- "Usage: fabric reservations list [--status <status>] [--json]",
788
+ "Usage: ornn reservations list [--status <status>] [--json]",
757
789
  );
758
790
  const query = buildQuery({ status: options.status });
759
791
  const rows = await cliRequest({
@@ -773,7 +805,7 @@ async function reservations(args, context) {
773
805
  const options = parseCommandOptions(
774
806
  rest,
775
807
  { boolean: ["json", "open"] },
776
- "Usage: fabric reservations show <reservation-id> [--open] [--json]",
808
+ "Usage: ornn reservations show <reservation-id> [--open] [--json]",
777
809
  );
778
810
  const found = await findReservation(id, context);
779
811
  if (options.json) {
@@ -794,7 +826,7 @@ async function reservations(args, context) {
794
826
  const options = parseCommandOptions(
795
827
  rest,
796
828
  { boolean: ["json", "no-open"] },
797
- "Usage: fabric reservations checkout <reservation-id> [--no-open] [--json]",
829
+ "Usage: ornn reservations checkout <reservation-id> [--no-open] [--json]",
798
830
  );
799
831
  const found = await findReservation(id, context);
800
832
  if (found.status !== "pending_payment") {
@@ -812,20 +844,25 @@ async function reservations(args, context) {
812
844
  return 0;
813
845
  }
814
846
 
815
- throw new Error("Usage: fabric reservations list|show|checkout");
847
+ throw new Error("Usage: ornn reservations list|show|checkout");
816
848
  }
817
849
 
818
850
  async function access(args, context) {
819
- const [subcommand, reservationId, ...rest] = args;
851
+ const [subcommand, ...subcommandArgs] = args;
852
+ if (subcommand === "keys") {
853
+ return await accessKeys(subcommandArgs, context);
854
+ }
855
+
856
+ const [reservationId, ...rest] = subcommandArgs;
820
857
  if (!subcommand || !reservationId) {
821
- throw new Error("Usage: fabric access show|activate|push-keys <reservation-id>");
858
+ throw new Error("Usage: ornn access show|activate|push-keys|keys <reservation-id>");
822
859
  }
823
860
 
824
861
  if (subcommand === "show") {
825
862
  const options = parseCommandOptions(
826
863
  rest,
827
864
  { boolean: ["json"] },
828
- "Usage: fabric access show <reservation-id> [--json]",
865
+ "Usage: ornn access show <reservation-id> [--json]",
829
866
  );
830
867
  const payload = await getReservationMachines(reservationId, context);
831
868
  if (options.json) {
@@ -843,7 +880,7 @@ async function access(args, context) {
843
880
  boolean: ["json", "no-open"],
844
881
  value: ["machine-count", "request-id", "ssh-key-id", "tenant-username", "username"],
845
882
  },
846
- "Usage: fabric access activate <reservation-id> --ssh-key-id <id> [--username <name>] [--no-open] [--json]",
883
+ "Usage: ornn access activate <reservation-id> --ssh-key-id <id> [--username <name>] [--no-open] [--json]",
847
884
  );
848
885
  const sshKeyIds = requiredArrayOption(options.sshKeyId, "--ssh-key-id");
849
886
  const launchPayload = {
@@ -887,28 +924,81 @@ async function access(args, context) {
887
924
  const options = parseCommandOptions(
888
925
  rest,
889
926
  { boolean: ["json"], value: ["request-id", "ssh-key-id"] },
890
- "Usage: fabric access push-keys <reservation-id> --ssh-key-id <id> [--json]",
927
+ "Usage: ornn access push-keys <reservation-id> --ssh-key-id <id> [--json]",
891
928
  );
892
929
  const sshKeyIds = requiredArrayOption(options.sshKeyId, "--ssh-key-id");
893
- const machinesPayload = await getReservationMachines(reservationId, context);
894
- const machines = activeMachines(machinesPayload.machines || []);
895
- if (!machines.length) {
896
- throw new Error("No active bare-metal machines were found for this reservation.");
930
+ const pushed = await pushReservationKeys(reservationId, sshKeyIds, options.requestId, context);
931
+ if (options.json) {
932
+ writeJson(context.stdout, { machines: pushed });
933
+ } else {
934
+ context.stdout.write(`SSH key update queued for ${pushed.length} machine${pushed.length === 1 ? "" : "s"}.\n`);
897
935
  }
898
- const pushed = [];
899
- for (const machine of machines) {
900
- const response = await cliRequest({
901
- body: {
902
- request_id: options.requestId || `cli-push-keys:${machine.id}:${Date.now()}`,
903
- ssh_key_ids: sshKeyIds,
904
- },
905
- endpoint: computeEndpoint(`/standalone-vms/${machine.id}/push-keys`),
906
- env: context.env,
907
- fetchImpl: context.fetchImpl,
908
- method: "POST",
909
- });
910
- pushed.push(response);
936
+ return 0;
937
+ }
938
+
939
+ throw new Error(`Unsupported access action: ${subcommand}`);
940
+ }
941
+
942
+ async function accessKeys(args, context) {
943
+ const [action, reservationId, ...rest] = args;
944
+ if (!action || !reservationId) {
945
+ throw new Error("Usage: ornn access keys list|add|push|status <reservation-id>");
946
+ }
947
+
948
+ if (action === "list") {
949
+ const options = parseCommandOptions(
950
+ rest,
951
+ { boolean: ["json"] },
952
+ "Usage: ornn access keys list <reservation-id> [--json]",
953
+ );
954
+ const payload = await fetchReservationSshKeys(reservationId, context);
955
+ if (options.json) {
956
+ writeJson(context.stdout, payload);
957
+ } else {
958
+ writeReservationSshKeyList(context.stdout, sshKeysFromPayload(payload), reservationId);
911
959
  }
960
+ return 0;
961
+ }
962
+
963
+ if (action === "add") {
964
+ const options = parseCommandOptions(
965
+ rest,
966
+ { boolean: ["json"], value: ["label", "public-key", "public-key-file"] },
967
+ "Usage: ornn access keys add <reservation-id> --public-key <key> [--label <label>] [--json]",
968
+ );
969
+ const publicKey = options.publicKeyFile
970
+ ? (await readFile(options.publicKeyFile, "utf8")).trim()
971
+ : requiredOption(options.publicKey, "--public-key");
972
+ const key = await cliRequest({
973
+ body: {
974
+ label: options.label || null,
975
+ public_key: publicKey,
976
+ },
977
+ endpoint: reservationSshKeysEndpoint(reservationId),
978
+ env: context.env,
979
+ fetchImpl: context.fetchImpl,
980
+ method: "POST",
981
+ });
982
+ if (options.json) {
983
+ writeJson(context.stdout, key);
984
+ } else {
985
+ context.stdout.write("Reservation SSH key added.\n");
986
+ context.stdout.write(`Reservation: ${reservationId}\n`);
987
+ if (key?.id) {
988
+ context.stdout.write(`Key: ${key.id}\n`);
989
+ }
990
+ }
991
+ return 0;
992
+ }
993
+
994
+ if (action === "push") {
995
+ const options = parseCommandOptions(
996
+ rest,
997
+ { boolean: ["json"], value: ["request-id", "ssh-key-id"] },
998
+ "Usage: ornn access keys push <reservation-id> --ssh-key-id <id> [--json]",
999
+ );
1000
+ const sshKeyIds = requiredArrayOption(options.sshKeyId, "--ssh-key-id");
1001
+ const pushed = await pushReservationKeys(reservationId, sshKeyIds, options.requestId, context);
912
1002
  if (options.json) {
913
1003
  writeJson(context.stdout, { machines: pushed });
914
1004
  } else {
@@ -917,7 +1007,22 @@ async function access(args, context) {
917
1007
  return 0;
918
1008
  }
919
1009
 
920
- throw new Error(`Unsupported access action: ${subcommand}`);
1010
+ if (action === "status") {
1011
+ const options = parseCommandOptions(
1012
+ rest,
1013
+ { boolean: ["json"] },
1014
+ "Usage: ornn access keys status <reservation-id> [--json]",
1015
+ );
1016
+ const status = await buildReservationKeyStatus(reservationId, context);
1017
+ if (options.json) {
1018
+ writeJson(context.stdout, status);
1019
+ } else {
1020
+ writeReservationKeyStatus(context.stdout, status);
1021
+ }
1022
+ return 0;
1023
+ }
1024
+
1025
+ throw new Error("Usage: ornn access keys list|add|push|status <reservation-id>");
921
1026
  }
922
1027
 
923
1028
  async function resale(args, context) {
@@ -927,7 +1032,7 @@ async function resale(args, context) {
927
1032
  const options = parseCommandOptions(
928
1033
  [reservationId, ...rest].filter(Boolean),
929
1034
  { boolean: ["json"] },
930
- "Usage: fabric resale browse [--json]",
1035
+ "Usage: ornn resale browse [--json]",
931
1036
  );
932
1037
  const listings = (await loadAvailabilityListings(context)).filter((listing) => listing.kind === "resale");
933
1038
  if (options.json) {
@@ -944,7 +1049,7 @@ async function resale(args, context) {
944
1049
  const options = parseCommandOptions(
945
1050
  rest,
946
1051
  { boolean: ["json", "no-open"], value: ["ask-price"] },
947
- "Usage: fabric resale list <reservation-id> --ask-price <usd> [--no-open] [--json]",
1052
+ "Usage: ornn resale list <reservation-id> --ask-price <usd> [--no-open] [--json]",
948
1053
  );
949
1054
  const reservation = await cliRequest({
950
1055
  body: { ask_price_per_gpu_hour: positiveNumberOption(options.askPrice, "--ask-price") },
@@ -965,7 +1070,7 @@ async function resale(args, context) {
965
1070
  const options = parseCommandOptions(
966
1071
  rest,
967
1072
  { boolean: ["json", "no-open"], value: ["ask-price"] },
968
- "Usage: fabric resale update <reservation-id> --ask-price <usd> [--no-open] [--json]",
1073
+ "Usage: ornn resale update <reservation-id> --ask-price <usd> [--no-open] [--json]",
969
1074
  );
970
1075
  const reservation = await cliRequest({
971
1076
  body: { ask_price_per_gpu_hour: positiveNumberOption(options.askPrice, "--ask-price") },
@@ -986,7 +1091,7 @@ async function resale(args, context) {
986
1091
  const options = parseCommandOptions(
987
1092
  rest,
988
1093
  { boolean: ["json", "no-open"] },
989
- "Usage: fabric resale delist <reservation-id> [--no-open] [--json]",
1094
+ "Usage: ornn resale delist <reservation-id> [--no-open] [--json]",
990
1095
  );
991
1096
  const reservation = await cliRequest({
992
1097
  endpoint: computeEndpoint(`/tenants/me/reservations/${reservationId}/delist`),
@@ -1002,7 +1107,7 @@ async function resale(args, context) {
1002
1107
  return 0;
1003
1108
  }
1004
1109
 
1005
- throw new Error("Usage: fabric resale browse|list|update|delist");
1110
+ throw new Error("Usage: ornn resale browse|list|update|delist");
1006
1111
  }
1007
1112
 
1008
1113
  async function billing(args, context) {
@@ -1011,7 +1116,7 @@ async function billing(args, context) {
1011
1116
  const options = parseCommandOptions(
1012
1117
  rest,
1013
1118
  { boolean: ["json", "no-open"] },
1014
- "Usage: fabric billing open [--no-open] [--json]",
1119
+ "Usage: ornn billing open [--no-open] [--json]",
1015
1120
  );
1016
1121
  const opened = await openFabricPage(context, "/account?tab=billing", {
1017
1122
  json: options.json,
@@ -1023,7 +1128,7 @@ async function billing(args, context) {
1023
1128
  }
1024
1129
  return 0;
1025
1130
  }
1026
- throw new Error("Usage: fabric billing open [--no-open] [--json]");
1131
+ throw new Error("Usage: ornn billing open [--no-open] [--json]");
1027
1132
  }
1028
1133
 
1029
1134
  async function sshKeys(args, context) {
@@ -1032,7 +1137,7 @@ async function sshKeys(args, context) {
1032
1137
  const options = parseCommandOptions(
1033
1138
  [id, ...rest].filter(Boolean),
1034
1139
  { boolean: ["json"] },
1035
- "Usage: fabric ssh-keys list [--json]",
1140
+ "Usage: ornn ssh-keys list [--json]",
1036
1141
  );
1037
1142
  const keys = await cliRequest({
1038
1143
  endpoint: computeEndpoint("/standalone-vms/tenants/me/ssh-keys"),
@@ -1051,7 +1156,7 @@ async function sshKeys(args, context) {
1051
1156
  const options = parseCommandOptions(
1052
1157
  [id, ...rest].filter(Boolean),
1053
1158
  { boolean: ["json"], value: ["label", "public-key", "public-key-file"] },
1054
- "Usage: fabric ssh-keys add --public-key <key> [--label <label>] [--json]",
1159
+ "Usage: ornn ssh-keys add --public-key <key> [--label <label>] [--json]",
1055
1160
  );
1056
1161
  const publicKey = options.publicKeyFile
1057
1162
  ? (await readFile(options.publicKeyFile, "utf8")).trim()
@@ -1081,7 +1186,7 @@ async function sshKeys(args, context) {
1081
1186
  const options = parseCommandOptions(
1082
1187
  rest,
1083
1188
  { boolean: ["json"] },
1084
- "Usage: fabric ssh-keys delete <key-id> [--json]",
1189
+ "Usage: ornn ssh-keys delete <key-id> [--json]",
1085
1190
  );
1086
1191
  const response = await cliRequest({
1087
1192
  endpoint: computeEndpoint(`/standalone-vms/tenants/me/ssh-keys/${id}`),
@@ -1097,7 +1202,7 @@ async function sshKeys(args, context) {
1097
1202
  return 0;
1098
1203
  }
1099
1204
 
1100
- throw new Error("Usage: fabric ssh-keys list|add|delete");
1205
+ throw new Error("Usage: ornn ssh-keys list|add|delete");
1101
1206
  }
1102
1207
 
1103
1208
  async function loadAvailabilityListings(context) {
@@ -1108,6 +1213,7 @@ async function loadAvailabilityListings(context) {
1108
1213
  const inventoryById = new Map(inventoryRecords.map((record) => [record.id, record]));
1109
1214
  const primaryListings = inventoryRecords
1110
1215
  .filter((record) => isTermEndDateActive(record.available_to))
1216
+ .filter((record) => inventoryAvailableGpuCount(record) > 0)
1111
1217
  .map(normalizeInventoryListing);
1112
1218
  const resaleListings = resaleReservations
1113
1219
  .filter((reservation) => reservation.is_marketplace_listed !== false)
@@ -1148,7 +1254,7 @@ async function fetchResaleListings(context) {
1148
1254
 
1149
1255
  function requireArrayPayload(payload, label) {
1150
1256
  if (!Array.isArray(payload)) {
1151
- throw new Error(`Fabric returned invalid ${label} data.`);
1257
+ throw new Error(`Ornn returned invalid ${label} data.`);
1152
1258
  }
1153
1259
  return payload;
1154
1260
  }
@@ -1163,23 +1269,32 @@ function sshKeysFromPayload(payload) {
1163
1269
  if (Array.isArray(payload?.keys)) {
1164
1270
  return payload.keys;
1165
1271
  }
1166
- throw new Error("Fabric returned invalid SSH keys data.");
1272
+ throw new Error("Ornn returned invalid SSH keys data.");
1273
+ }
1274
+
1275
+ function inventoryAvailableGpuCount(record) {
1276
+ if (record.available_gpu_count != null) {
1277
+ return Math.max(0, record.available_gpu_count);
1278
+ }
1279
+ const committed = Math.max(0, record.committed_gpu_count ?? 0);
1280
+ return Math.max(0, (record.gpu_count ?? 0) - committed);
1167
1281
  }
1168
1282
 
1169
1283
  function normalizeInventoryListing(record) {
1284
+ const gpuCount = inventoryAvailableGpuCount(record);
1170
1285
  return {
1171
1286
  id: record.id,
1172
1287
  kind: "inventory",
1173
1288
  source: "primary",
1174
1289
  gpu_type: record.gpu_type ?? "GPU",
1175
- gpu_count: record.gpu_count ?? null,
1290
+ gpu_count: gpuCount || null,
1176
1291
  operator: record.site_operator ?? record.operator ?? "Unknown operator",
1177
1292
  facility: record.site_nickname ?? record.facility ?? record.site ?? "Unknown facility",
1178
1293
  location: record.location ?? record.region ?? null,
1179
1294
  network: record.fabric_type ?? record.network_hardware ?? record.internet ?? null,
1180
1295
  start_date: record.available_from ?? null,
1181
1296
  end_date: record.available_to ?? null,
1182
- price_per_gpu_hour: DEFAULT_BUY_NOW_USD_PER_GPU_HOUR,
1297
+ price_per_gpu_hour: resolveBuyNowUsdPerGpuHour(record),
1183
1298
  checkout_url: `/checkout?inventory=${encodeURIComponent(record.id)}`,
1184
1299
  marketplace_url: `/marketplace/${encodeURIComponent(record.id)}`,
1185
1300
  inventory: record,
@@ -1473,6 +1588,99 @@ async function getReservationMachines(reservationId, context) {
1473
1588
  return Array.isArray(payload) ? { machines: payload } : payload;
1474
1589
  }
1475
1590
 
1591
+ function reservationSshKeysEndpoint(reservationId) {
1592
+ return computeEndpoint(`/standalone-vms/reservations/${reservationId}/ssh-keys`);
1593
+ }
1594
+
1595
+ async function fetchReservationSshKeys(reservationId, context) {
1596
+ return await cliRequest({
1597
+ endpoint: reservationSshKeysEndpoint(reservationId),
1598
+ env: context.env,
1599
+ fetchImpl: context.fetchImpl,
1600
+ });
1601
+ }
1602
+
1603
+ async function pushReservationKeys(reservationId, sshKeyIds, requestId, context) {
1604
+ const machinesPayload = await getReservationMachines(reservationId, context);
1605
+ const machines = activeMachines(machinesPayload.machines || []);
1606
+ if (!machines.length) {
1607
+ throw new Error("No active bare-metal machines were found for this reservation.");
1608
+ }
1609
+
1610
+ const pushed = [];
1611
+ for (const machine of machines) {
1612
+ const id = machineId(machine);
1613
+ const response = await cliRequest({
1614
+ body: {
1615
+ request_id: requestId || `cli-push-keys:${id}:${Date.now()}`,
1616
+ ssh_key_ids: sshKeyIds,
1617
+ },
1618
+ endpoint: computeEndpoint(`/standalone-vms/${id}/push-keys`),
1619
+ env: context.env,
1620
+ fetchImpl: context.fetchImpl,
1621
+ method: "POST",
1622
+ });
1623
+ pushed.push(response);
1624
+ }
1625
+ return pushed;
1626
+ }
1627
+
1628
+ async function buildReservationKeyStatus(reservationId, context) {
1629
+ const [keysPayload, machinesPayload] = await Promise.all([
1630
+ fetchReservationSshKeys(reservationId, context),
1631
+ getReservationMachines(reservationId, context),
1632
+ ]);
1633
+ const machines = activeMachines(machinesPayload.machines || []);
1634
+ const keys = activeSshKeys(sshKeysFromPayload(keysPayload)).map((key) => ({
1635
+ id: key.id || null,
1636
+ label: key.label || null,
1637
+ fingerprint: key.fingerprint || null,
1638
+ linux_username: key.linux_username || null,
1639
+ status: key.status || "active",
1640
+ machines: machines.map((machine) => reservationKeyMachineStatus(key, machine)),
1641
+ }));
1642
+ return {
1643
+ reservation_id: reservationId,
1644
+ machine_count: machines.length,
1645
+ keys,
1646
+ };
1647
+ }
1648
+
1649
+ function reservationKeyMachineStatus(key, machine) {
1650
+ const metadata = reservationKeyMetadataForMachine(key, machine);
1651
+ const fallbackInstalled =
1652
+ !metadata &&
1653
+ key.fingerprint &&
1654
+ Array.isArray(machine.authorized_key_fingerprints) &&
1655
+ machine.authorized_key_fingerprints.includes(key.fingerprint) &&
1656
+ machine.keys_pushed_at;
1657
+
1658
+ return {
1659
+ machine_id: machineId(machine),
1660
+ machine_state: machineState(machine),
1661
+ status: metadata?.status || (fallbackInstalled ? "installed" : "associated"),
1662
+ linux_username: metadata?.linux_username || key.linux_username || machine.linux_username || null,
1663
+ queued_at: metadata?.queued_at || null,
1664
+ pushed_at: metadata?.pushed_at || (fallbackInstalled ? machine.keys_pushed_at : null),
1665
+ failed_at: metadata?.failed_at || null,
1666
+ failure_reason: metadata?.failure_reason || null,
1667
+ request_id: metadata?.request_id || null,
1668
+ };
1669
+ }
1670
+
1671
+ function reservationKeyMetadataForMachine(key, machine) {
1672
+ if (!Array.isArray(machine.authorized_key_metadata)) {
1673
+ return null;
1674
+ }
1675
+ return (
1676
+ machine.authorized_key_metadata.find((item) => {
1677
+ const itemKeyId = item.ssh_key_id === undefined || item.ssh_key_id === null ? "" : String(item.ssh_key_id);
1678
+ const keyId = key.id === undefined || key.id === null ? "" : String(key.id);
1679
+ return (keyId && itemKeyId === keyId) || (key.fingerprint && item.fingerprint === key.fingerprint);
1680
+ }) || null
1681
+ );
1682
+ }
1683
+
1476
1684
  function writeAccessDetail(stdout, payload = {}) {
1477
1685
  const machines = Array.isArray(payload.machines) ? payload.machines : [];
1478
1686
  if (!machines.length) {
@@ -1628,6 +1836,62 @@ function writeSshKeyList(stdout, keys) {
1628
1836
  }
1629
1837
  }
1630
1838
 
1839
+ function writeReservationSshKeyList(stdout, keys, reservationId) {
1840
+ const activeKeys = activeSshKeys(keys);
1841
+ if (!activeKeys.length) {
1842
+ stdout.write("No reservation SSH keys found.\n");
1843
+ return;
1844
+ }
1845
+ stdout.write(`Reservation SSH keys for ${reservationId}:\n`);
1846
+ for (const key of activeKeys) {
1847
+ const label = key.label ? ` ${key.label}` : "";
1848
+ const fingerprint = key.fingerprint ? ` ${key.fingerprint}` : "";
1849
+ const linuxUsername = key.linux_username ? ` ${key.linux_username}` : "";
1850
+ stdout.write(`- ${key.id || "key"}${label}${fingerprint}${linuxUsername}\n`);
1851
+ }
1852
+ }
1853
+
1854
+ function writeReservationKeyStatus(stdout, status) {
1855
+ const keys = Array.isArray(status.keys) ? status.keys : [];
1856
+ if (!keys.length) {
1857
+ stdout.write("No reservation SSH keys found.\n");
1858
+ stdout.write(
1859
+ `Add one with: ornn access keys add ${status.reservation_id} --public-key-file ~/.ssh/id_ed25519.pub\n`,
1860
+ );
1861
+ return;
1862
+ }
1863
+
1864
+ stdout.write(`Reservation SSH key status for ${status.reservation_id}:\n`);
1865
+ for (const key of keys) {
1866
+ const label = key.label ? ` ${key.label}` : "";
1867
+ const fingerprint = key.fingerprint ? ` ${key.fingerprint}` : "";
1868
+ stdout.write(`- ${key.id || "key"}${label}${fingerprint}\n`);
1869
+ if (key.linux_username) {
1870
+ stdout.write(` Linux user: ${key.linux_username}\n`);
1871
+ }
1872
+ const machines = Array.isArray(key.machines) ? key.machines : [];
1873
+ if (!machines.length) {
1874
+ stdout.write(" No active bare-metal machines yet.\n");
1875
+ continue;
1876
+ }
1877
+ for (const machine of machines) {
1878
+ stdout.write(` ${machine.machine_id} ${machine.machine_state}: ${machine.status}\n`);
1879
+ writeOptionalStatusLine(stdout, "Linux user", machine.linux_username);
1880
+ writeOptionalStatusLine(stdout, "Queued", machine.queued_at);
1881
+ writeOptionalStatusLine(stdout, "Pushed", machine.pushed_at);
1882
+ writeOptionalStatusLine(stdout, "Failed", machine.failed_at);
1883
+ writeOptionalStatusLine(stdout, "Reason", machine.failure_reason);
1884
+ writeOptionalStatusLine(stdout, "Request", machine.request_id);
1885
+ }
1886
+ }
1887
+ }
1888
+
1889
+ function writeOptionalStatusLine(stdout, label, value) {
1890
+ if (value !== undefined && value !== null && value !== "") {
1891
+ stdout.write(` ${label}: ${value}\n`);
1892
+ }
1893
+ }
1894
+
1631
1895
  function activeSshKeys(keys) {
1632
1896
  return keys.filter((key) => String(key.status || "active").toLowerCase() === "active" && !key.revoked_at);
1633
1897
  }
@@ -1948,7 +2212,7 @@ function isStructuredErrorCode(value) {
1948
2212
 
1949
2213
  function formatStructuredProvisioningError(detail) {
1950
2214
  const lines = [];
1951
- const summary = textValue(detail.summary) || messageForErrorCode(detail.code) || "Fabric request failed.";
2215
+ const summary = textValue(detail.summary) || messageForErrorCode(detail.code) || "Ornn request failed.";
1952
2216
  lines.push(summary);
1953
2217
 
1954
2218
  const code = textValue(detail.code);
@@ -2020,7 +2284,7 @@ function defaultNextStepForErrorCode(code) {
2020
2284
  normalized.includes("forbidden") ||
2021
2285
  normalized.includes("unauthorized")
2022
2286
  ) {
2023
- return "Run `fabric login` with an approved tenant account and try again.";
2287
+ return "Run `ornn login` with an approved tenant account and try again.";
2024
2288
  }
2025
2289
  return "Contact Ornn support with this error code.";
2026
2290
  }
@@ -121,7 +121,7 @@ export async function waitForDeviceApproval({
121
121
  }
122
122
 
123
123
  if (status === "expired") {
124
- throw new DeviceAuthError("Device login expired. Run `fabric login` again.");
124
+ throw new DeviceAuthError("Device login expired. Run `ornn login` again.");
125
125
  }
126
126
 
127
127
  throw new DeviceAuthError(`Auth server returned unexpected status: ${status}`);