@ornncompute/cli 1.0.122 → 1.0.124

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
@@ -69,6 +69,10 @@ The CLI uses Authorization's newest membership on every request, regardless of
69
69
  approval. `ornn context` displays that current organization; login and launch
70
70
  use the same context. Reserve browsing and purchasing still require approval.
71
71
 
72
+ Node and VM requests use `@ornn/api-client/orchestrator`; CLI selection, polling,
73
+ and SSH remain here. VM readiness comes from the guest resource, separately from
74
+ physical-host readiness. See [machine behavior](../../docs/cli.md#customer-workflows).
75
+
72
76
  Advanced customer resources are registered under one noun and one verb:
73
77
 
74
78
  ```text
@@ -271,6 +275,10 @@ Global interaction modifiers are `--format`, `--guided`, `--wait`,
271
275
  as `--erase-local-data` or `--delete-node-record`. Machine retries require a
272
276
  caller-stable idempotency key and reconcile against durable owner state.
273
277
 
278
+ Access-mode changes preserve the reported network mode and require an
279
+ explicit `--network` when it is unreported. A same-mode request stays read-only,
280
+ including its bounded wait; an existing request key reconciles its owner receipt.
281
+
274
282
  Permanent deletion and teardown requests execute directly when explicitly
275
283
  requested. Private networks, detached volumes, and
276
284
  Kubernetes or Slurm clusters use their command arguments to express the exact
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ornncompute/cli",
3
- "version": "1.0.122",
3
+ "version": "1.0.124",
4
4
  "description": "Command-line interface for Ornn compute access workflows.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,3 +1,4 @@
1
+ import * as orchestrator from "../vendor/api-client/orchestrator.js";
1
2
  import { createHash } from "node:crypto";
2
3
 
3
4
  import { ADMIN_MUTATION_SAFETY, AdminMutationError, runAdminMutation } from "./admin-mutations.mjs";
@@ -140,10 +141,7 @@ function reviewFields(target) {
140
141
  }
141
142
 
142
143
  async function prepareTarget(input, runtime, fetch) {
143
- const payload = await fetch(`/nodes/${encodeURIComponent(input.node_id)}`, {
144
- method: "GET",
145
- ...(runtime.signal ? { signal: runtime.signal } : {}),
146
- });
144
+ const payload = await orchestrator.getNode(fetch, input.node_id, runtime.signal);
147
145
  return nodeContext(payload, input.node_id);
148
146
  }
149
147
 
@@ -1,3 +1,4 @@
1
+ import * as orchestrator from "../vendor/api-client/orchestrator.js";
1
2
  import { getReservation } from "../vendor/api-client/commerce.js";
2
3
  import { ADMIN_MUTATION_SAFETY, AdminMutationError, runAdminMutation } from "./admin-mutations.mjs";
3
4
  import { isRecoverableWaitFailure } from "./interaction-runtime.mjs";
@@ -210,10 +211,7 @@ function isNotFound(error) {
210
211
 
211
212
  async function readNodeById(fetch, id, { allowMissing = false, signal } = {}) {
212
213
  try {
213
- return await fetch(`/nodes/${encodeURIComponent(id)}`, {
214
- method: "GET",
215
- ...(signal ? { signal } : {}),
216
- });
214
+ return await orchestrator.getNode(fetch, id, signal);
217
215
  } catch (error) {
218
216
  if (allowMissing && isNotFound(error)) return null;
219
217
  throw error;
@@ -238,7 +236,7 @@ async function resolveNode(fetch, reference, { allowMissing = false, machine = f
238
236
  });
239
237
  }
240
238
  const folded = reference.toLocaleLowerCase("en-US");
241
- const matches = nodeRows(await fetch("/nodes", { method: "GET" })).filter((node) => {
239
+ const matches = nodeRows(await orchestrator.listStaffNodes(fetch)).filter((node) => {
242
240
  const names = [node?.hostname, node?.k8s_node_name]
243
241
  .map(text)
244
242
  .filter(Boolean)
@@ -282,9 +280,7 @@ async function resolveLiveMachine(fetch, node) {
282
280
  );
283
281
  }
284
282
  const rows = machineRows(
285
- await fetch(`/nodes?reservation_id=${encodeURIComponent(reservation)}`, {
286
- method: "GET",
287
- })
283
+ await orchestrator.listStaffNodes(fetch, { reservation_id: reservation })
288
284
  );
289
285
  const refs = new Set([id, text(node?.id), text(node?.k8s_node_name)].filter(Boolean));
290
286
  const matches = rows.filter((machine) => refs.has(text(machine?.node_id)));
package/src/admin-ssh.mjs CHANGED
@@ -1,3 +1,4 @@
1
+ import * as orchestrator from "../vendor/api-client/orchestrator.js";
1
2
  import { spawn } from "node:child_process";
2
3
  import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
3
4
  import { constants, tmpdir } from "node:os";
@@ -21,7 +22,7 @@ function run(program, args, signal) {
21
22
 
22
23
  export async function openAdminSsh({ input, fetch, execute, runtime }, { runProcess = run } = {}) {
23
24
  // Read the existing node route; issuance returns only its own certificate.
24
- const node = await fetch(`/nodes/${encodeURIComponent(input.node_id)}`, { method: "GET" });
25
+ const node = await orchestrator.getNode(fetch, input.node_id);
25
26
  const host = node.ip;
26
27
  const port = node.port ?? 22;
27
28
  if (
@@ -251,9 +251,12 @@ export function resolveInstallerBaseUrl({ env = process.env } = {}) {
251
251
  }
252
252
 
253
253
  function operatorProxyEndpoint(endpoint) {
254
- // Shared clients use canonical staff or Benchmark paths. Keep the existing
254
+ // Shared clients use canonical staff or owner paths. Keep the existing
255
255
  // staff envelope, including its admin-role and impersonation checks.
256
- const path = normalizeComputePath(endpoint).replace(/^\/v1\/(?:staff|benchmark)(?=\/|$)/, "");
256
+ const path = normalizeComputePath(endpoint).replace(
257
+ /^\/v1\/(?:staff|benchmark|orchestrator)(?=\/|$)/,
258
+ ""
259
+ );
257
260
  return `/v1/staff${path}`;
258
261
  }
259
262
 
@@ -1,7 +1,11 @@
1
1
  import * as gatewayApi from "../vendor/api-client/gateway.js";
2
2
  import { gpuLabeler } from "./gpu-catalog.mjs";
3
3
  import { clusterFactLabel, reservationFactsForCluster } from "./kubernetes-commands.mjs";
4
- import { listOwnedActiveMachines, resolveMachineReference } from "./machine-reference.mjs";
4
+ import {
5
+ canonicalMachineId as machineId,
6
+ listOwnedActiveMachines,
7
+ resolveMachineReference,
8
+ } from "./machine-reference.mjs";
5
9
  import { machineReachability } from "./vm-commands.mjs";
6
10
 
7
11
  const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
@@ -106,11 +110,6 @@ function containsUnsafeFields(value) {
106
110
  });
107
111
  }
108
112
 
109
- function machineId(value) {
110
- const source = record(value);
111
- return text(source.id) ?? text(source.machine_id) ?? text(source.instance_id);
112
- }
113
-
114
113
  function reservationId(value) {
115
114
  const source = record(value);
116
115
  return text(source.reservation_id) ?? text(source.reservationId);
@@ -250,6 +249,11 @@ function lifecycle(value, kind) {
250
249
  if (["failed", "error", "unavailable"].includes(state)) {
251
250
  return { class: "failed", phase: "failed" };
252
251
  }
252
+ if (state === "bare_metal") {
253
+ return source.health_readiness?.ready === true
254
+ ? { class: "ready", phase: "active" }
255
+ : { class: "working", phase: "provisioning" };
256
+ }
253
257
  if (["active", "online", "ready", "running", "provisioned"].includes(state)) {
254
258
  return { class: "ready", phase: kind === "cluster" ? "access_ready" : "active" };
255
259
  }
@@ -2,6 +2,7 @@ import { randomUUID } from "node:crypto";
2
2
  import { createInterface } from "node:readline/promises";
3
3
 
4
4
  import { resolveAuthBaseUrl } from "./device-auth.mjs";
5
+ import { CliApiError } from "./api-client.mjs";
5
6
 
6
7
  const BOOLEAN_OPTIONS = new Set([
7
8
  "acknowledge-retained-os",
@@ -53,6 +54,7 @@ export class InteractionError extends Error {
53
54
  }
54
55
 
55
56
  export function isRecoverableWaitFailure(error) {
57
+ if (error instanceof CliApiError && error.status === undefined) return true;
56
58
  const status = Number(error?.status);
57
59
  if (status === 401 || status === 403 || (Number.isFinite(status) && status >= 500)) {
58
60
  return true;
@@ -1,3 +1,4 @@
1
+ import * as orchestrator from "../vendor/api-client/orchestrator.js";
1
2
  import { getCliSession } from "../vendor/api-client/authorization.js";
2
3
  import { listAllReservations } from "../vendor/api-client/commerce.js";
3
4
  import { gpuLabeler } from "./gpu-catalog.mjs";
@@ -139,8 +140,9 @@ export async function listActiveDirectReservations(context, interaction) {
139
140
  );
140
141
  }
141
142
 
142
- export async function listOwnedActiveMachines(context, interaction) {
143
- const reservations = await listActiveDirectReservations(context, interaction);
143
+ export async function listOwnedActiveMachines(context, interaction, activeReservations) {
144
+ const reservations =
145
+ activeReservations ?? (await listActiveDirectReservations(context, interaction));
144
146
  const pages = await Promise.all(
145
147
  reservations.map(async (reservation) => {
146
148
  const ref = reservationId(reservation);
@@ -150,21 +152,13 @@ export async function listOwnedActiveMachines(context, interaction) {
150
152
  "The gateway returned a capacity reservation without a canonical identifier."
151
153
  );
152
154
  }
153
- const payload = await context.fetch(
154
- `/v1/workflows/compute/nodes/reservations/${encodeURIComponent(ref)}/machines`,
155
- { method: "GET" }
156
- );
155
+ const payload = await orchestrator.listNodes(context.fetch, { reservation_id: ref });
157
156
  const machineRows = rows(payload, "machines");
158
157
  if (!machineRows)
159
158
  invalidGateway(interaction, "The gateway returned an invalid machine collection.");
160
159
  return machineRows
161
160
  .map(record)
162
- .filter((machine) => !TERMINAL_MACHINE_STATES.has(machineState(machine)))
163
- .map((machine) => ({
164
- ...machine,
165
- reservation_id:
166
- text(machine.reservation_id) ?? text(machine.commerce_reservation_id) ?? ref,
167
- }));
161
+ .filter((machine) => !TERMINAL_MACHINE_STATES.has(machineState(machine)));
168
162
  })
169
163
  );
170
164
 
@@ -185,11 +179,7 @@ export async function listOwnedActiveMachines(context, interaction) {
185
179
 
186
180
  async function fetchCanonicalMachine(reference, context, interaction, { allowMiss = false } = {}) {
187
181
  try {
188
- const machine = record(
189
- await context.fetch(`/v1/workflows/compute/nodes/${encodeURIComponent(reference)}`, {
190
- method: "GET",
191
- })
192
- );
182
+ const machine = record(await orchestrator.getNode(context.fetch, reference));
193
183
  if (canonicalMachineId(machine)?.toLowerCase() !== reference.toLowerCase()) {
194
184
  invalidGateway(
195
185
  interaction,