@ornncompute/cli 0.1.2 → 0.1.4

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,8 +1,10 @@
1
1
  # Ornn CLI
2
2
 
3
- Command-line access for Ornn bare-metal compute workflows: find
4
- available capacity, buy or bid, manage reservations, activate SSH access,
5
- resell capacity, and open billing.
3
+ Command-line access for Ornn compute workflows: browse Listings, buy capacity,
4
+ bid in Exchange, manage GPU reservations, launch VM or Bare Metal machines,
5
+ create Kubernetes or Slurm clusters, manage private networks and storage
6
+ volumes, attach SSH keys, SSH into launched machines, watch machine health
7
+ metrics, and view billing readouts.
6
8
 
7
9
  For the end-user install, browser-login, command, and troubleshooting guide, see
8
10
  the [`docs/cli.md`](../../docs/cli.md) guide.
@@ -26,40 +28,126 @@ static.
26
28
  ```bash
27
29
  ornn help [command]
28
30
  ornn login [--auth-base <url>] [--no-browser] [--timeout <seconds>]
31
+ ornn update
29
32
  ornn whoami [--json]
30
33
  ornn status [--json]
31
- ornn availability list [--gpu-type <type>] [--facility <name>] [--operator <name>] [--json]
32
- ornn availability show <listing-id> [--open] [--json]
34
+ ornn listings list [--gpu-type <type>] [--facility <name>] [--operator <name>] [--json]
35
+ ornn listings show <listing-id> [--open] [--json]
33
36
  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]
37
+ ornn exchange 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]
38
+ ornn exchange list [--json]
39
+ ornn exchange show <bid-id> [--open] [--json]
40
+ ornn exchange update <bid-id> --gpu-count <n> --min-gpu-count <n> --start-date <yyyy-mm-dd> --end-date <yyyy-mm-dd> --price <usd>
41
+ ornn exchange withdraw <bid-id>
42
+ ornn gpus list [--status <status>] [--json]
43
+ ornn gpus show <reservation-id> [--open] [--json]
44
+ ornn gpus checkout <reservation-id> [--no-open] [--json]
45
+ ornn nodes list [--json]
46
+ ornn nodes show <node-id> [--json]
47
+ ornn nodes launch <reservation-id> --key <path|id|label> [--mode bare-metal|vm] [--username <name>] [--network public|private] [--storage-load-drive-id <id>] [--storage-save-drive-id <id>] [--wait] [--json]
48
+ ornn nodes wait <node-or-reservation-id> [--timeout <seconds>] [--json]
49
+ ornn nodes start <node-id> [--json]
50
+ ornn nodes stop <node-id> [--json]
51
+ ornn nodes teardown <node-id> [--json]
52
+ ornn nodes revoke <node-id> [--json]
53
+ ornn nodes ssh-command <node-or-reservation-id> [--json]
54
+ ornn nodes keys attach <node-id> --key <path|id|label> [--json]
55
+ ornn nodes keys list <node-id> [--json]
56
+ ornn node health <node-id> [--json]
57
+ ornn node diagnose <node-id> [--json]
58
+ ornn node drain <node-id> [--reason <text>] [--json]
59
+ ornn node restore <node-id> [--reason <text>] [--json]
60
+ ornn ssh <node-or-reservation-id> [--print] [--identity-file <path>] [--user <name>] [--json]
61
+ ornn metrics nodes [--json]
62
+ ornn metrics node <node-id> [--json]
63
+ ornn metrics history <node-id> [--start <iso>] [--end <iso>] [--max-points <n>] [--json]
64
+ ornn metrics watch <node-id> [--interval <seconds>] [--count <n>] [--timeout <seconds>] [--json]
65
+ ornn clusters list [--json]
66
+ ornn clusters reservations [--json]
67
+ ornn clusters eligible-nodes <reservation-id> [--type kubernetes|slurm] [--network public|private] [--json]
68
+ ornn clusters create <reservation-id> --type kubernetes|slurm [--network public|private] [--node <node-id>] [--node-count <n>] [--wait] [--json]
69
+ ornn clusters show <reservation-id> [--type kubernetes|slurm] [--json]
70
+ ornn clusters wait <reservation-id> [--type kubernetes|slurm] [--timeout <seconds>] [--json]
71
+ ornn clusters credentials <reservation-id> [--type kubernetes|slurm] [--json]
72
+ ornn clusters kubeconfig <reservation-id> [--output <path>] [--json]
73
+ ornn clusters ssh-command <reservation-id> [--identity-file <path>] [--user <name>] [--json]
74
+ ornn clusters ssh <reservation-id> [--print] [--identity-file <path>] [--user <name>] [--json]
75
+ ornn clusters add-node <reservation-id> --node <node-id> [--json]
76
+ ornn clusters remove-node <reservation-id> --node <node-id> [--json]
77
+ ornn clusters teardown <reservation-id> [--type kubernetes|slurm] [--json]
78
+ ornn networks list [--json]
79
+ ornn networks show <network-id> [--json]
80
+ ornn networks create --name <name> [--cidr <cidr>] [--description <text>] [--json]
81
+ ornn networks update <network-id> [--name <name>] [--description <text>] [--clear-description] [--json]
82
+ ornn networks delete <network-id> [--json]
83
+ ornn networks reservation <reservation-id> [--json]
84
+ ornn networks attach <reservation-id> --network <network-id> [--json]
85
+ ornn networks detach <reservation-id> [--json]
86
+ ornn storage volumes list [--json]
87
+ ornn storage volumes show <drive-id> [--json]
88
+ ornn storage volumes create --name <name> [--source <drive-id>] [--json]
89
+ ornn storage volumes refresh <drive-id> [--json]
90
+ ornn storage volumes clear <drive-id> [--json]
91
+ ornn storage volumes delete <drive-id> [--json]
92
+ ornn storage deploy <drive-id> --reservation <reservation-id> [--mount-path <path>] [--read-only|--read-write] [--json]
93
+ ornn storage deploy status --reservation <reservation-id> [--json]
94
+ ornn storage buckets list [--json]
95
+ ornn storage buckets show <drive-id> [--json]
96
+ ornn storage buckets connect gcs|s3|r2 --bucket <bucket>|--url <url> [--name <name>] [--prefix <prefix>] [--region <region>] [--account-id <id>|--endpoint-url <url>] [--access-key-id <id> --secret-access-key-file <path>] [--read-only|--read-write] [--verify] [--json]
97
+ ornn storage buckets update-credentials <drive-id> --access-key-id <id> --secret-access-key-file <path> [--json]
98
+ ornn storage buckets verify <drive-id> [--json]
99
+ ornn storage buckets disconnect <drive-id> [--json]
100
+ ornn keys list [--json]
101
+ ornn keys add [<public-key-file>] [--public-key <key>] [--label <label>] [--json]
102
+ ornn keys delete <key-id> [--json]
42
103
  ornn access show <reservation-id> [--json]
43
- ornn access activate <reservation-id> --ssh-key-id <id> [--username <name>] [--no-open] [--json]
104
+ ornn access activate <reservation-id> --key <path|id|label> [--mode bare-metal|vm] [--username <name>] [--network public|private] [--storage-load-drive-id <id>] [--storage-save-drive-id <id>] [--wait] [--no-open] [--json]
44
105
  ornn access push-keys <reservation-id> --ssh-key-id <id> [--json]
45
106
  ornn access keys list <reservation-id> [--json]
46
107
  ornn access keys add <reservation-id> --public-key <key> [--label <label>] [--json]
47
108
  ornn access keys add <reservation-id> --public-key-file <path> [--label <label>] [--json]
48
109
  ornn access keys push <reservation-id> --ssh-key-id <id> [--json]
49
110
  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
111
  ornn ssh-keys list [--json]
55
112
  ornn ssh-keys add --public-key <key> [--label <label>] [--json]
56
113
  ornn ssh-keys add --public-key-file <path> [--label <label>] [--json]
57
114
  ornn ssh-keys delete <key-id> [--json]
115
+ ornn billing summary [--json]
116
+ ornn billing invoices [--json]
117
+ ornn billing showback --start <yyyy-mm-dd> --end <yyyy-mm-dd> [--json]
58
118
  ornn billing open [--no-open] [--json]
59
119
  ornn api <get|post|patch|put|delete> <compute-path> [--data <json>] [--raw]
60
120
  ornn logout
61
121
  ```
62
122
 
123
+ UI aliases are first-class in the CLI: `listings` maps to the older
124
+ `availability` command, `exchange` maps to `bid`, and `gpus` maps to
125
+ `reservations`. The older command names remain available for scripts.
126
+
127
+ ## Operator commands
128
+
129
+ The `ornn node` commands are for Ornn operators and reviewers, not tenants. They
130
+ strictly wrap the internal-reviewer-gated compute endpoints, so they do not use
131
+ the tenant CLI login. Instead they talk to the compute service directly and
132
+ authenticate with the shared reviewer secret:
133
+
134
+ - `ornn node health <node-id>` shows launch readiness (ready/blocked plus
135
+ reasons), passive fault reasons, and the last validation status.
136
+ - `ornn node diagnose <node-id>` queues an active health run for the node.
137
+ - `ornn node drain <node-id>` / `ornn node restore <node-id>` take the node out
138
+ of and back into service. Both accept an optional `--reason`.
139
+
140
+ Set these environment variables before running them:
141
+
142
+ - `ORNN_COMPUTE_BASE_URL` — origin of the internal compute service (for example
143
+ `http://localhost:8000`). The review secret travels as a request header, so
144
+ in any non-local deployment this should be an `https://` origin.
145
+ - `ORNN_INTERNAL_REVIEW_SECRET` — the reviewer secret, sent as the
146
+ `X-Internal-Review-Secret` header.
147
+
148
+ If either is missing the command fails with a clear error before making any
149
+ network request.
150
+
63
151
  Commands print concise human-readable output by default. Use `--json` on
64
152
  read/show/list commands and transaction handoffs when you need structured
65
153
  stdout for scripts. Errors and login/browser progress go to stderr.
@@ -72,3 +160,7 @@ the terminal keeps polling until the login is approved or expires.
72
160
 
73
161
  Run `ornn --help` for the full command list and `ornn api` for the
74
162
  allowlisted compute API escape hatch.
163
+
164
+ ## Updating
165
+
166
+ The CLI checks the npm registry for a newer version at most once per day and prints a one-line notice on stderr when one exists. Run `ornn update` to install it (the command detects npm, pnpm, yarn, or bun installs). The check is skipped in CI and non-interactive shells.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@ornncompute/cli",
3
- "version": "0.1.2",
4
- "description": "Command-line interface for Ornn bare-metal compute workflows.",
3
+ "version": "0.1.4",
4
+ "description": "Command-line interface for Ornn compute access workflows.",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "ornn": "bin/ornn.js"
@@ -1,4 +1,4 @@
1
- import { loadAuthSession } from "./auth-store.mjs";
1
+ import { clearAuthSession, loadAuthSession } from "./auth-store.mjs";
2
2
  import { resolveAuthBaseUrl } from "./device-auth.mjs";
3
3
 
4
4
  export class CliApiError extends Error {
@@ -70,11 +70,80 @@ export async function cliRequest({
70
70
  if (raw) {
71
71
  const text = await response.text();
72
72
  if (!response.ok) {
73
+ await handleAuthExpiry({ env, response, hadToken: Boolean(authSession?.accessToken) });
73
74
  throw new CliApiError(text || response.statusText, { status: response.status });
74
75
  }
75
76
  return text;
76
77
  }
77
78
 
79
+ const text = await response.text();
80
+ if (!response.ok) {
81
+ // Clear a dead token before parsing, so a 401 with a non-JSON body
82
+ // (e.g. a gateway/WAF error page) still triggers the re-login flow.
83
+ await handleAuthExpiry({ env, response, hadToken: Boolean(authSession?.accessToken) });
84
+ }
85
+ const data = text ? parseJson(text, url.toString(), response) : null;
86
+ if (!response.ok) {
87
+ const detail = data?.detail ?? data?.error ?? response.statusText;
88
+ const message = detailMessage(detail) || response.statusText;
89
+ throw new CliApiError(`Ornn request failed: ${message}`, {
90
+ detail: data,
91
+ status: response.status,
92
+ });
93
+ }
94
+ return data;
95
+ }
96
+
97
+ export function resolveComputeBaseUrl({ env = process.env } = {}) {
98
+ const base = env.ORNN_COMPUTE_BASE_URL?.trim();
99
+ if (!base) {
100
+ throw new CliApiError(
101
+ "Set ORNN_COMPUTE_BASE_URL to the internal compute service origin to run operator commands.",
102
+ );
103
+ }
104
+ return base.replace(/\/+$/, "");
105
+ }
106
+
107
+ export function resolveInternalReviewSecret({ env = process.env } = {}) {
108
+ const secret = env.ORNN_INTERNAL_REVIEW_SECRET?.trim();
109
+ if (!secret) {
110
+ throw new CliApiError(
111
+ "Set ORNN_INTERNAL_REVIEW_SECRET to authenticate operator commands.",
112
+ );
113
+ }
114
+ return secret;
115
+ }
116
+
117
+ export async function operatorRequest({
118
+ body,
119
+ endpoint,
120
+ env = process.env,
121
+ fetchImpl = fetch,
122
+ method = "GET",
123
+ } = {}) {
124
+ // Operator path: hit compute directly with the review secret (not the tenant /api/cli proxy).
125
+ const baseUrl = resolveComputeBaseUrl({ env });
126
+ const secret = resolveInternalReviewSecret({ env });
127
+ const url = new URL(endpoint, `${baseUrl}/`);
128
+ const headers = {
129
+ Accept: "application/json",
130
+ "X-Internal-Review-Secret": secret,
131
+ };
132
+
133
+ let requestBody;
134
+ if (body !== undefined) {
135
+ headers["Content-Type"] = "application/json";
136
+ requestBody = typeof body === "string" ? body : JSON.stringify(body);
137
+ }
138
+
139
+ let response;
140
+ try {
141
+ response = await fetchImpl(url, { body: requestBody, headers, method });
142
+ } catch (error) {
143
+ const message = error instanceof Error ? error.message : String(error);
144
+ throw new CliApiError(`Could not reach Ornn compute at ${baseUrl}: ${message}`);
145
+ }
146
+
78
147
  const text = await response.text();
79
148
  const data = text ? parseJson(text, url.toString(), response) : null;
80
149
  if (!response.ok) {
@@ -119,6 +188,16 @@ function parseJson(text, url, response) {
119
188
  }
120
189
  }
121
190
 
191
+ async function handleAuthExpiry({ env, response, hadToken }) {
192
+ if (response.status !== 401 || !hadToken) {
193
+ return;
194
+ }
195
+ await clearAuthSession({ env });
196
+ throw new CliApiError("Your Ornn session has expired or was revoked. Run `ornn login` to sign in again.", {
197
+ status: 401,
198
+ });
199
+ }
200
+
122
201
  function detailMessage(detail) {
123
202
  if (typeof detail === "string") {
124
203
  return detail;