@ornncompute/cli 0.1.9 → 0.2.0

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.
@@ -1,4 +1,4 @@
1
- import { clearAuthSession, configuredAuthBaseUrl, loadAuthSession } from "./auth-store.mjs";
1
+ import { clearAuthSession, configuredApiBaseUrl, loadAuthSession } from "./auth-store.mjs";
2
2
  import { resolveAuthBaseUrl } from "./device-auth.mjs";
3
3
 
4
4
  export class CliApiError extends Error {
@@ -11,22 +11,56 @@ export class CliApiError extends Error {
11
11
  }
12
12
 
13
13
  export function resolveApiBaseUrl({ env = process.env, explicit, session } = {}) {
14
- // Prefer the first configured source; do not fall through when it strips empty.
15
- const candidates = [
14
+ // Prefer the first configured API source; do not fall through when it strips empty.
15
+ const apiCandidates = [
16
16
  [explicit, "API base URL"],
17
17
  [env.ORNN_API_BASE_URL, "ORNN_API_BASE_URL"],
18
+ [session?.apiBaseUrl, "session apiBaseUrl"],
19
+ [configuredApiBaseUrl(env), "configured apiBaseUrl"],
20
+ ];
21
+ for (const [value, sourceName] of apiCandidates) {
22
+ const trimmed = value?.trim();
23
+ if (!trimmed) {
24
+ continue;
25
+ }
26
+ return requireValidBaseUrl(trimmed, sourceName);
27
+ }
28
+
29
+ // Older CLI installs persisted only the web/auth origin. Migrate the known
30
+ // deployed Ornn hosts in memory so an update immediately reaches the gateway
31
+ // without requiring a reinstall; custom and localhost origins remain intact.
32
+ const authCandidates = [
18
33
  [env.ORNN_AUTH_BASE_URL, "ORNN_AUTH_BASE_URL"],
19
34
  [session?.authBaseUrl, "session authBaseUrl"],
20
35
  ];
21
- for (const [value, sourceName] of candidates) {
36
+ for (const [value, sourceName] of authCandidates) {
22
37
  const trimmed = value?.trim();
23
38
  if (!trimmed) {
24
39
  continue;
25
40
  }
26
- return requireValidBaseUrl(trimmed, sourceName);
41
+ return gatewayOriginForAuthOrigin(trimmed, sourceName);
27
42
  }
28
43
 
29
- return requireValidBaseUrl(resolveAuthBaseUrl({ env }), "API base URL");
44
+ return gatewayOriginForAuthOrigin(resolveAuthBaseUrl({ env }), "API base URL");
45
+ }
46
+
47
+ function gatewayOriginForAuthOrigin(value, sourceName) {
48
+ const authOrigin = requireValidBaseUrl(value, sourceName);
49
+ const url = new URL(authOrigin);
50
+ if (url.hostname === "compute.ornn.com") {
51
+ return "https://api.ornn.com";
52
+ }
53
+ if (
54
+ url.hostname === "staging.compute.ornn.com" ||
55
+ url.hostname === "staging.fabric.ornn.com"
56
+ ) {
57
+ return "https://staging.api.ornn.com";
58
+ }
59
+ const dynamicMatch = url.hostname.match(/^([a-z0-9-]+)\.fabric\.ornn\.com$/i);
60
+ if (dynamicMatch) {
61
+ return `https://${dynamicMatch[1]}.api.ornn.com`;
62
+ }
63
+ return authOrigin;
30
64
  }
31
65
 
32
66
  export async function loadRequiredSession({ env = process.env } = {}) {
@@ -46,6 +80,7 @@ export async function cliRequest({
46
80
  method = "GET",
47
81
  raw = false,
48
82
  session,
83
+ timeoutMs,
49
84
  } = {}) {
50
85
  const authSession = session ?? (authRequired ? await loadRequiredSession({ env }) : null);
51
86
  const baseUrl = resolveApiBaseUrl({ env, session: authSession });
@@ -56,6 +91,10 @@ export async function cliRequest({
56
91
 
57
92
  if (authSession?.accessToken) {
58
93
  headers.Authorization = `Bearer ${authSession.accessToken}`;
94
+ // Explicitly select the gateway's user-credential resolver. This marker
95
+ // grants nothing by itself; the gateway still has to resolve the bearer
96
+ // through authorization and derive the caller's organization server-side.
97
+ headers["X-Ornn-Credential-Type"] = "user";
59
98
  }
60
99
 
61
100
  let requestBody;
@@ -70,8 +109,18 @@ export async function cliRequest({
70
109
  body: requestBody,
71
110
  headers,
72
111
  method,
112
+ ...(timeoutMs ? { signal: AbortSignal.timeout(timeoutMs) } : {}),
73
113
  });
74
114
  } catch (error) {
115
+ // Detect the timeout/abort case before the generic network-failure wrapping
116
+ // below, so a wedged compute call (e.g. status blocked behind an
117
+ // unresponsive FUSE mount) reports a customer-facing timeout instead of an
118
+ // opaque "Could not reach Ornn" message.
119
+ if (timeoutMs && (error?.name === "TimeoutError" || error?.name === "AbortError")) {
120
+ throw new CliApiError(
121
+ `Ornn did not respond within ${Math.round(timeoutMs / 1000)} seconds. The service or a node mount may be unresponsive; try again shortly.`,
122
+ );
123
+ }
75
124
  const message = error instanceof Error ? error.message : String(error);
76
125
  throw new CliApiError(`Could not reach Ornn at ${baseUrl}: ${message}`);
77
126
  }
@@ -87,196 +136,71 @@ export async function cliRequest({
87
136
 
88
137
  const text = await response.text();
89
138
  if (!response.ok) {
90
- // Clear a dead token before parsing, so a 401 with a non-JSON body
91
- // (e.g. a gateway/WAF error page) still triggers the re-login flow.
92
- await handleAuthExpiry({ env, response, hadToken: Boolean(authSession?.accessToken) });
93
- }
94
- const data = text ? parseJson(text, url.toString(), response) : null;
95
- if (!response.ok) {
96
- const detail = data?.detail ?? data?.error ?? response.statusText;
97
- const message = detailMessage(detail) || response.statusText;
98
- throw new CliApiError(`Ornn request failed: ${message}`, {
99
- detail: data,
100
- status: response.status,
139
+ // Always throws: handles the 401/expired-session case (clearing the
140
+ // stored token) and the generic detail-surfacing case.
141
+ await raiseForFailedResponse({
142
+ env,
143
+ hadToken: Boolean(authSession?.accessToken),
144
+ response,
145
+ text,
146
+ url: url.toString(),
147
+ formatMessage: (message) => `Ornn request failed: ${message}`,
101
148
  });
102
149
  }
150
+ const data = text ? parseJson(text, url.toString(), response) : null;
103
151
  return data;
104
152
  }
105
153
 
106
- export function resolveComputeBaseUrl({ env = process.env } = {}) {
107
- const base = env.ORNN_COMPUTE_BASE_URL?.trim();
108
- if (!base) {
109
- throw new CliApiError(
110
- "Set ORNN_COMPUTE_BASE_URL to the internal compute service origin to run operator commands."
111
- );
112
- }
113
- return requireValidBaseUrl(base, "ORNN_COMPUTE_BASE_URL");
114
- }
115
-
116
154
  /**
117
- * Origin used in `curl …/enrollment/install` one-liners.
155
+ * Shared failure path for both the JSON `cliRequest` helper and callers that
156
+ * stream a raw `fetchImpl` response directly (e.g. file downloads, which
157
+ * can't go through `cliRequest`'s JSON body handling). Always throws:
118
158
  *
119
- * Prefer an explicit compute/installer URL. When staff use the web operator
120
- * proxy (no local review secret), derive compute from the auth/web origin:
121
- * - `https://<slug>.fabric.ornn.com` `https://<slug>-api.fabric.ornn.com`
122
- * - `https://compute.ornn.com` `https://api-compute.ornn.com`
123
- * - `http://localhost:3000` `http://localhost:8000`
159
+ * - On 401 with a previously-valid token, clears the stored session and
160
+ * throws the standard "session expired" `CliApiError` so every call site
161
+ * gets the same re-login guidance instead of reimplementing it.
162
+ * - Otherwise, parses the body as JSON (if present) and surfaces its
163
+ * `detail`/`error` field through the same `detailMessage` conventions
164
+ * `cliRequest` uses, falling back to `fallbackMessage` (or the response's
165
+ * status text) when the body has no usable detail.
124
166
  */
125
- export function resolveInstallerBaseUrl({ env = process.env, session } = {}) {
126
- const explicit =
127
- env.ORNN_INSTALLER_URL?.trim() ||
128
- env.ORNN_COMPUTE_BASE_URL?.trim() ||
129
- env.NEXT_PUBLIC_INSTALLER_URL?.trim();
130
- if (explicit) {
131
- return requireValidBaseUrl(explicit, "installer base URL");
132
- }
133
-
134
- // The session records where the caller actually logged in, so it outranks the
135
- // installed default.
136
- const authCandidates = [
137
- env.ORNN_AUTH_BASE_URL,
138
- env.ORNN_API_BASE_URL,
139
- session?.authBaseUrl,
140
- configuredAuthBaseUrl(env),
141
- ];
142
- for (const candidate of authCandidates) {
143
- const trimmed = candidate?.trim();
144
- if (!trimmed) {
145
- continue;
146
- }
147
- const authOrigin = requireValidBaseUrl(trimmed, "ORNN_AUTH_BASE_URL");
148
- return deriveInstallerOriginFromAuth(authOrigin);
149
- }
150
- return null;
151
- }
152
-
153
- function deriveInstallerOriginFromAuth(authOrigin) {
154
- const url = new URL(authOrigin);
155
- if (url.hostname === "localhost" || url.hostname === "127.0.0.1") {
156
- url.port = "8000";
157
- url.protocol = "http:";
158
- return url.origin;
159
- }
160
- const fabricMatch = url.hostname.match(/^([a-z0-9-]+)\.fabric\.ornn\.com$/i);
161
- if (fabricMatch && !fabricMatch[1].endsWith("-api")) {
162
- url.hostname = `${fabricMatch[1]}-api.fabric.ornn.com`;
163
- return url.origin;
164
- }
165
- if (url.hostname === "compute.ornn.com") {
166
- url.hostname = "api-compute.ornn.com";
167
- return url.origin;
168
- }
169
- return url.origin;
170
- }
171
-
172
- export function resolveInternalReviewSecret({ env = process.env } = {}) {
173
- const secret = env.ORNN_INTERNAL_REVIEW_SECRET?.trim();
174
- if (!secret) {
175
- throw new CliApiError(
176
- "Set ORNN_INTERNAL_REVIEW_SECRET to authenticate operator commands, or run `ornn login` as Ornn staff."
177
- );
178
- }
179
- return secret;
180
- }
181
-
182
- function operatorProxyEndpoint(endpoint) {
183
- return `/api/cli/operator${normalizeComputePath(endpoint)}`;
184
- }
185
-
186
- async function operatorRequestViaStaffSession({
187
- body,
188
- endpoint,
167
+ export async function raiseForFailedResponse({
189
168
  env = process.env,
190
- fetchImpl = fetch,
191
- method = "GET",
169
+ fallbackMessage,
170
+ formatMessage = (message) => message,
171
+ hadToken,
172
+ response,
173
+ text,
174
+ url,
192
175
  } = {}) {
193
- // Staff path: CLI bearer web /api/cli/operator compute with server-side review secret.
194
- return cliRequest({
195
- body,
196
- endpoint: operatorProxyEndpoint(endpoint),
197
- env,
198
- fetchImpl,
199
- method,
200
- authRequired: true,
176
+ // Clear a dead token before parsing, so a 401 with a non-JSON body
177
+ // (e.g. a gateway/WAF error page) still triggers the re-login flow.
178
+ await handleAuthExpiry({ env, response, hadToken });
179
+ const data = text ? parseJson(text, url, response) : null;
180
+ const detail = data?.detail ?? data?.error ?? response.statusText;
181
+ const message = detailMessage(detail) || fallbackMessage || response.statusText;
182
+ throw new CliApiError(formatMessage(message), {
183
+ detail: data,
184
+ status: response.status,
201
185
  });
202
186
  }
203
187
 
204
- async function operatorRequestViaReviewSecret({
205
- body,
206
- endpoint,
207
- env = process.env,
208
- fetchImpl = fetch,
209
- method = "GET",
210
- } = {}) {
211
- // Break-glass / CI: hit compute directly with the review secret.
212
- const baseUrl = resolveComputeBaseUrl({ env });
213
- const secret = resolveInternalReviewSecret({ env });
214
- const url = new URL(endpoint, `${baseUrl}/`);
215
- const headers = {
216
- Accept: "application/json",
217
- "X-Internal-Actor": env.ORNN_INTERNAL_ACTOR?.trim() || "break-glass-cli",
218
- "X-Internal-Review-Secret": secret,
219
- };
220
-
221
- let requestBody;
222
- if (body !== undefined) {
223
- headers["Content-Type"] = "application/json";
224
- requestBody = typeof body === "string" ? body : JSON.stringify(body);
225
- }
226
-
227
- let response;
228
- try {
229
- response = await fetchImpl(url, { body: requestBody, headers, method });
230
- } catch (error) {
231
- const message = error instanceof Error ? error.message : String(error);
232
- throw new CliApiError(`Could not reach Ornn compute at ${baseUrl}: ${message}`);
233
- }
234
-
235
- const text = await response.text();
236
- const data = text ? parseJson(text, url.toString(), response) : null;
237
- if (!response.ok) {
238
- const detail = data?.detail ?? data?.error ?? response.statusText;
239
- const message = detailMessage(detail) || response.statusText;
240
- throw new CliApiError(`Ornn request failed: ${message}`, {
241
- detail: data,
242
- status: response.status,
243
- });
188
+ export function resolveInstallerBaseUrl({ env = process.env } = {}) {
189
+ const explicit = env.ORNN_INSTALLER_URL?.trim() || env.NEXT_PUBLIC_INSTALLER_URL?.trim();
190
+ if (!explicit) {
191
+ return null;
244
192
  }
245
- return data;
193
+ return requireValidBaseUrl(explicit, "ORNN_INSTALLER_URL", { allowPath: true });
246
194
  }
247
195
 
248
- export async function operatorRequest({
249
- body,
250
- endpoint,
251
- env = process.env,
252
- fetchImpl = fetch,
253
- method = "GET",
254
- } = {}) {
255
- // Prefer an explicit review secret (CI / break-glass). Otherwise use a staff
256
- // `ornn login` session through the web operator proxy — the secret never
257
- // leaves the server.
258
- if (env.ORNN_INTERNAL_REVIEW_SECRET?.trim()) {
259
- return operatorRequestViaReviewSecret({ body, endpoint, env, fetchImpl, method });
260
- }
261
-
262
- const session = await loadAuthSession({ env });
263
- if (session?.accessToken) {
264
- return operatorRequestViaStaffSession({ body, endpoint, env, fetchImpl, method });
265
- }
266
-
267
- throw new CliApiError(
268
- "Operator commands need Ornn staff auth: run `ornn login` as an internal user " +
269
- "(ORNN_AUTH_BASE_URL), or set ORNN_INTERNAL_REVIEW_SECRET (+ ORNN_COMPUTE_BASE_URL) for direct compute access.",
270
- { status: 401 }
271
- );
196
+ function operatorProxyEndpoint(endpoint) {
197
+ return `/v1/cli/operator${normalizeComputePath(endpoint)}`;
272
198
  }
273
199
 
274
- /**
275
- * Commerce-backed operator actions must cross the Web trust boundary. Web
276
- * validates Commerce state before it forwards to Compute, so direct
277
- * review-secret access is intentionally unavailable for these requests.
278
- */
279
- export async function webOperatorRequest({
200
+ // Staff path: CLI bearer → gateway /v1/cli/operator → the owning service.
201
+ // The gateway resolves the bearer and applies staff policy; the CLI never ships
202
+ // a reviewer or service secret.
203
+ export async function operatorRequest({
280
204
  body,
281
205
  endpoint,
282
206
  env = process.env,
@@ -286,16 +210,23 @@ export async function webOperatorRequest({
286
210
  const session = await loadAuthSession({ env });
287
211
  if (!session?.accessToken) {
288
212
  throw new CliApiError(
289
- "Commerce-backed deployment requires Ornn staff auth through Web. Run `ornn login` first.",
213
+ "Operator commands need Ornn staff auth: run `ornn login` as an internal user (ORNN_AUTH_BASE_URL).",
290
214
  { status: 401 }
291
215
  );
292
216
  }
293
- return operatorRequestViaStaffSession({ body, endpoint, env, fetchImpl, method });
217
+ return cliRequest({
218
+ body,
219
+ endpoint: operatorProxyEndpoint(endpoint),
220
+ env,
221
+ fetchImpl,
222
+ method,
223
+ authRequired: true,
224
+ });
294
225
  }
295
226
 
296
227
  export function computeEndpoint(path) {
297
228
  const normalized = normalizeComputePath(path);
298
- return `/api/cli/compute${normalized}`;
229
+ return `/v1/cli/compute${normalized}`;
299
230
  }
300
231
 
301
232
  export function normalizeComputePath(path) {
@@ -309,7 +240,7 @@ export function normalizeComputePath(path) {
309
240
  return trimmed;
310
241
  }
311
242
 
312
- function requireValidBaseUrl(raw, sourceName) {
243
+ function requireValidBaseUrl(raw, sourceName, { allowPath = false } = {}) {
313
244
  const stripped = raw.replace(/\/+$/, "");
314
245
  if (!stripped) {
315
246
  throw new CliApiError(
@@ -322,10 +253,13 @@ function requireValidBaseUrl(raw, sourceName) {
322
253
  } catch {
323
254
  throw new CliApiError(`${sourceName} must be a valid origin (got ${JSON.stringify(raw)}).`);
324
255
  }
325
- // Path/search/hash are dropped when composing absolute endpoints against this base.
326
- if (parsed.pathname !== "/" || parsed.search || parsed.hash) {
256
+ // API endpoints are composed from absolute paths and therefore require a
257
+ // bare origin. The installer is the one exception: its gateway-owned base
258
+ // intentionally includes `/v1/orchestrator` and is joined explicitly.
259
+ if ((!allowPath && parsed.pathname !== "/") || parsed.search || parsed.hash) {
260
+ const requirement = allowPath ? "base URL with no query or fragment" : "origin with no path";
327
261
  throw new CliApiError(
328
- `${sourceName} must be a valid origin with no path (got ${JSON.stringify(raw)}).`
262
+ `${sourceName} must be a valid ${requirement} (got ${JSON.stringify(raw)}).`
329
263
  );
330
264
  }
331
265
  return stripped;
@@ -6,7 +6,6 @@ import { homedir } from "node:os";
6
6
  const APP_DIR = "ornn";
7
7
  const AUTH_FILE = "auth.json";
8
8
  const CONFIG_FILE = "config.json";
9
- const FLEETS_DIR = "fleets";
10
9
 
11
10
  export function getAuthConfigDir(env = process.env) {
12
11
  if (env.ORNN_CONFIG_HOME?.trim()) {
@@ -28,10 +27,6 @@ export function getCliConfigPath(env = process.env) {
28
27
  return join(getAuthConfigDir(env), CONFIG_FILE);
29
28
  }
30
29
 
31
- export function getFleetConfigDir(env = process.env) {
32
- return join(getAuthConfigDir(env), FLEETS_DIR);
33
- }
34
-
35
30
  // Read synchronously so callers resolving a base URL stay synchronous.
36
31
  export function configuredAuthBaseUrl(env = process.env) {
37
32
  try {
@@ -42,6 +37,15 @@ export function configuredAuthBaseUrl(env = process.env) {
42
37
  }
43
38
  }
44
39
 
40
+ export function configuredApiBaseUrl(env = process.env) {
41
+ try {
42
+ const value = JSON.parse(readFileSync(getCliConfigPath(env), "utf8"))?.apiBaseUrl;
43
+ return typeof value === "string" ? value.trim() : "";
44
+ } catch {
45
+ return "";
46
+ }
47
+ }
48
+
45
49
  export async function loadAuthSession({ env = process.env } = {}) {
46
50
  const path = getAuthConfigPath(env);
47
51
 
@@ -0,0 +1,186 @@
1
+ import { createInterface } from "node:readline/promises";
2
+ import { stdin as defaultStdin, stdout as defaultStdout } from "node:process";
3
+
4
+ import { cliRequest, operatorRequest } from "./api-client.mjs";
5
+
6
+ async function loadCapabilities() {
7
+ try {
8
+ return await import("@ornn/capabilities");
9
+ } catch {
10
+ return await import("../vendor/capabilities/index.js");
11
+ }
12
+ }
13
+
14
+ function parseYes(value) {
15
+ return /^(y|yes)$/i.test(String(value ?? "").trim());
16
+ }
17
+
18
+ export async function confirmCatalogMutation({ stdin, stdout, yes, label }) {
19
+ if (yes) return true;
20
+ const input = stdin ?? defaultStdin;
21
+ const output = stdout ?? defaultStdout;
22
+ if (!input.isTTY || !output.isTTY) {
23
+ throw new Error("Refusing to create without --yes on a non-interactive terminal.");
24
+ }
25
+ const rl = createInterface({ input, output });
26
+ try {
27
+ const answer = await rl.question(`${label} [y/N] `);
28
+ return parseYes(answer);
29
+ } finally {
30
+ rl.close();
31
+ }
32
+ }
33
+
34
+ function catalogFetch({ env, fetchImpl }) {
35
+ return async (path, init) => {
36
+ const method = init?.method ?? "GET";
37
+ const staffPath =
38
+ path.startsWith("/internal") ||
39
+ path.startsWith("/provisioning") ||
40
+ path === "/inventory" ||
41
+ path.startsWith("/inventory/");
42
+ const request = staffPath ? operatorRequest : cliRequest;
43
+ return request({
44
+ endpoint: path,
45
+ env,
46
+ fetchImpl,
47
+ method,
48
+ ...(init?.body !== undefined ? { body: init.body } : {}),
49
+ });
50
+ };
51
+ }
52
+
53
+ export async function runCatalogCapability(capability, input, context) {
54
+ const { parseRole } = await loadCapabilities();
55
+ const role = parseRole(context.role) ?? (context.staff ? "reviewer" : "user");
56
+ return capability.execute(
57
+ {
58
+ actor: context.actor ?? "",
59
+ role,
60
+ confirmed: context.confirmed === true,
61
+ fetch: catalogFetch(context),
62
+ },
63
+ input,
64
+ );
65
+ }
66
+
67
+ export async function listingsCreateCommand(args, context) {
68
+ const { createListingCapability } = await loadCapabilities();
69
+ const options = context.parseCommandOptions(
70
+ args,
71
+ {
72
+ boolean: ["json", "yes"],
73
+ value: [
74
+ "gpu-type",
75
+ "cpu",
76
+ "node-model",
77
+ "fabric-type",
78
+ "node-count",
79
+ "gpus-per-node",
80
+ "site-nickname",
81
+ "site-operator",
82
+ "ram",
83
+ "storage",
84
+ "available-start-at",
85
+ "available-end-at",
86
+ "buy-now-price-per-gpu-hour",
87
+ "cost-per-gpu-hour",
88
+ "deposit-percent",
89
+ ],
90
+ },
91
+ "Usage: ornn listings create [--gpu-type H100] [--node-count 8] [--yes] [--json]",
92
+ );
93
+ const input = {
94
+ gpu_type: options.gpuType,
95
+ cpu: options.cpu,
96
+ node_model: options.nodeModel,
97
+ fabric_type: options.fabricType,
98
+ node_count: options.nodeCount != null ? Number(options.nodeCount) : undefined,
99
+ gpus_per_node: options.gpusPerNode != null ? Number(options.gpusPerNode) : undefined,
100
+ site_nickname: options.siteNickname,
101
+ site_operator: options.siteOperator,
102
+ ram: options.ram,
103
+ storage: options.storage,
104
+ available_start_at: options.availableStartAt,
105
+ available_end_at: options.availableEndAt,
106
+ buy_now_price_per_gpu_hour:
107
+ options.buyNowPricePerGpuHour != null ? Number(options.buyNowPricePerGpuHour) : undefined,
108
+ cost_per_gpu_hour: options.costPerGpuHour != null ? Number(options.costPerGpuHour) : undefined,
109
+ deposit_percent: options.depositPercent != null ? Number(options.depositPercent) : undefined,
110
+ };
111
+ const preview = await runCatalogCapability(createListingCapability, input, {
112
+ ...context,
113
+ confirmed: false,
114
+ staff: true,
115
+ role: "reviewer",
116
+ });
117
+ if (preview?.ok === false) {
118
+ throw new Error(preview.error ?? "listing create failed");
119
+ }
120
+ const confirmed = await confirmCatalogMutation({
121
+ stdin: context.stdin,
122
+ stdout: context.stdout,
123
+ yes: options.yes,
124
+ label: "Create this listing?",
125
+ });
126
+ if (!confirmed) {
127
+ context.stdout.write("Aborted.\n");
128
+ return 1;
129
+ }
130
+ const result = await runCatalogCapability(createListingCapability, input, {
131
+ ...context,
132
+ confirmed: true,
133
+ staff: true,
134
+ role: "reviewer",
135
+ });
136
+ if (options.json) {
137
+ context.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
138
+ } else {
139
+ context.stdout.write(`${JSON.stringify(result)}\n`);
140
+ }
141
+ return 0;
142
+ }
143
+
144
+ export async function operatorListCommand(args, context) {
145
+ const { listOperatorsCapability } = await loadCapabilities();
146
+ context.parseCommandOptions(args, { boolean: ["json"] }, "Usage: ornn operator list [--json]");
147
+ const result = await runCatalogCapability(listOperatorsCapability, {}, {
148
+ ...context,
149
+ staff: true,
150
+ role: "reviewer",
151
+ });
152
+ context.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
153
+ return 0;
154
+ }
155
+
156
+ export async function usersQueryCommand(args, context) {
157
+ const { usersQueryCapability } = await loadCapabilities();
158
+ const options = context.parseCommandOptions(
159
+ args,
160
+ { boolean: ["json"], value: ["name", "email", "organization", "max"] },
161
+ "Usage: ornn users query [--name Ada] [--email a@b.com] [--organization Acme] [--max 20]",
162
+ );
163
+ const result = await runCatalogCapability(
164
+ usersQueryCapability,
165
+ {
166
+ name: options.name,
167
+ email: options.email,
168
+ organization: options.organization,
169
+ max: options.max != null ? Number(options.max) : undefined,
170
+ },
171
+ { ...context, staff: true, role: "admin" },
172
+ );
173
+ context.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
174
+ return 0;
175
+ }
176
+
177
+ export async function nodesConsoleCommand(nodeId, context) {
178
+ const { nodesConsoleCapability } = await loadCapabilities();
179
+ const result = await runCatalogCapability(
180
+ nodesConsoleCapability,
181
+ { node_id: nodeId },
182
+ { ...context, role: context.staff ? "reviewer" : "user" },
183
+ );
184
+ context.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
185
+ return 0;
186
+ }