@ornncompute/cli 0.2.24 → 1.0.34

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (90) hide show
  1. package/README.md +351 -426
  2. package/package.json +12 -6
  3. package/src/admin-capacity-reservations.mjs +305 -0
  4. package/src/admin-command-handlers.mjs +57 -0
  5. package/src/admin-enrollment.mjs +681 -0
  6. package/src/admin-mutations.mjs +187 -0
  7. package/src/admin-node-diagnostics.mjs +412 -0
  8. package/src/admin-node-lifecycle.mjs +905 -0
  9. package/src/admin-shell.mjs +456 -0
  10. package/src/api-client.mjs +81 -28
  11. package/src/atomic-output.mjs +112 -0
  12. package/src/auth-store.mjs +182 -11
  13. package/src/capability-catalog.mjs +7 -0
  14. package/src/catalog-shell.mjs +1363 -0
  15. package/src/cli.mjs +123 -10266
  16. package/src/connect-mode-handlers.mjs +170 -0
  17. package/src/device-auth.mjs +179 -62
  18. package/src/evidence-commands.mjs +1354 -0
  19. package/src/external-bucket-commands.mjs +1374 -0
  20. package/src/gpu-catalog.mjs +21 -0
  21. package/src/identity-file.mjs +28 -0
  22. package/src/interaction-runtime.mjs +310 -0
  23. package/src/kubernetes-commands.mjs +2235 -0
  24. package/src/launch-mode-handlers.mjs +119 -0
  25. package/src/machine-reference.mjs +285 -0
  26. package/src/mount-commands.mjs +1148 -0
  27. package/src/session-commands.mjs +565 -0
  28. package/src/slurm-commands.mjs +274 -0
  29. package/src/startup.mjs +34 -11
  30. package/src/storage-commands.mjs +1164 -0
  31. package/src/update.mjs +172 -24
  32. package/src/vm-commands.mjs +4308 -0
  33. package/src/vpn-secret-output.mjs +150 -0
  34. package/src/wait-runtime.mjs +231 -0
  35. package/vendor/capabilities/capabilities/admin-capacity-reservations.js +96 -0
  36. package/vendor/capabilities/capabilities/admin-enrollment.js +321 -0
  37. package/vendor/capabilities/capabilities/admin-node-diagnostics.js +88 -0
  38. package/vendor/capabilities/capabilities/admin-nodes-lifecycle.js +235 -0
  39. package/vendor/capabilities/capabilities/admin.js +89 -0
  40. package/vendor/capabilities/capabilities/cli.js +156 -0
  41. package/vendor/capabilities/capabilities/clusters.js +150 -1
  42. package/vendor/capabilities/capabilities/commerce-market.js +330 -0
  43. package/vendor/capabilities/capabilities/compute.js +522 -0
  44. package/vendor/capabilities/capabilities/evidence.js +145 -0
  45. package/vendor/capabilities/capabilities/external-buckets.js +191 -0
  46. package/vendor/capabilities/capabilities/fleet.js +77 -25
  47. package/vendor/capabilities/capabilities/identity.js +114 -3
  48. package/vendor/capabilities/capabilities/listings.js +141 -144
  49. package/vendor/capabilities/capabilities/market-shared.js +44 -0
  50. package/vendor/capabilities/capabilities/markets.js +449 -0
  51. package/vendor/capabilities/capabilities/mounts.js +176 -0
  52. package/vendor/capabilities/capabilities/networks.js +1264 -0
  53. package/vendor/capabilities/capabilities/observability.js +38 -8
  54. package/vendor/capabilities/capabilities/tokens.js +32 -17
  55. package/vendor/capabilities/capabilities/volumes.js +178 -0
  56. package/vendor/capabilities/capability.js +21 -14
  57. package/vendor/capabilities/cli-catalog.js +67 -0
  58. package/vendor/capabilities/cli-migrations.js +261 -0
  59. package/vendor/capabilities/cli.js +7 -0
  60. package/vendor/capabilities/software-catalog.js +36 -0
  61. package/vendor/capabilities/spec-catalog.js +28 -5
  62. package/vendor/capabilities/supplier-directory.js +20 -0
  63. package/src/catalog-dispatch.mjs +0 -605
  64. package/src/color.mjs +0 -71
  65. package/vendor/capabilities/capabilities/clusters.d.ts +0 -2
  66. package/vendor/capabilities/capabilities/fleet.d.ts +0 -8
  67. package/vendor/capabilities/capabilities/identity.d.ts +0 -3
  68. package/vendor/capabilities/capabilities/listings.d.ts +0 -51
  69. package/vendor/capabilities/capabilities/observability.d.ts +0 -5
  70. package/vendor/capabilities/capabilities/staff-access.d.ts +0 -29
  71. package/vendor/capabilities/capabilities/staff-access.js +0 -278
  72. package/vendor/capabilities/capabilities/staff-commerce.d.ts +0 -160
  73. package/vendor/capabilities/capabilities/staff-commerce.js +0 -689
  74. package/vendor/capabilities/capabilities/staff-fleet.d.ts +0 -140
  75. package/vendor/capabilities/capabilities/staff-fleet.js +0 -538
  76. package/vendor/capabilities/capabilities/staff-gtm.d.ts +0 -142
  77. package/vendor/capabilities/capabilities/staff-gtm.js +0 -521
  78. package/vendor/capabilities/capabilities/staff-shared.d.ts +0 -47
  79. package/vendor/capabilities/capabilities/staff-shared.js +0 -98
  80. package/vendor/capabilities/capabilities/tokens.d.ts +0 -10
  81. package/vendor/capabilities/capabilities/users.d.ts +0 -2
  82. package/vendor/capabilities/capability.d.ts +0 -93
  83. package/vendor/capabilities/catalog.d.ts +0 -3
  84. package/vendor/capabilities/catalog.js +0 -26
  85. package/vendor/capabilities/index.d.ts +0 -16
  86. package/vendor/capabilities/index.js +0 -16
  87. package/vendor/capabilities/names.d.ts +0 -6
  88. package/vendor/capabilities/role.d.ts +0 -6
  89. package/vendor/capabilities/role.js +0 -17
  90. package/vendor/capabilities/spec-catalog.d.ts +0 -78
@@ -0,0 +1,119 @@
1
+ const MODE_LABELS = {
2
+ "bare-metal": "Bare Metal",
3
+ kubernetes: "Kubernetes",
4
+ slurm: "Slurm",
5
+ vm: "VM",
6
+ };
7
+
8
+ const COMMON_OPTIONS = [
9
+ "$positionals",
10
+ "guided",
11
+ "idempotencyKey",
12
+ "mode",
13
+ "noWait",
14
+ "organization",
15
+ "reservation",
16
+ "timeout",
17
+ "wait",
18
+ "yes",
19
+ ];
20
+ const KEY_OPTIONS = ["key", "keyId", "label", "publicKey", "publicKeyFile"];
21
+ const MODE_OPTIONS = new Map([
22
+ ["vm", new Set([...COMMON_OPTIONS, ...KEY_OPTIONS, "username"])],
23
+ [
24
+ "bare-metal",
25
+ new Set([
26
+ ...COMMON_OPTIONS,
27
+ ...KEY_OPTIONS,
28
+ "mountAccess",
29
+ "network",
30
+ "resume",
31
+ "username",
32
+ "volume",
33
+ ]),
34
+ ],
35
+ ["kubernetes", new Set([...COMMON_OPTIONS, "machine", "machines", "network", "nodes"])],
36
+ [
37
+ "slurm",
38
+ new Set([...COMMON_OPTIONS, ...KEY_OPTIONS, "machine", "machines", "network", "nodes"]),
39
+ ],
40
+ ]);
41
+ const OPTION_FLAGS = {
42
+ idempotencyKey: "--idempotency-key",
43
+ keyId: "--key-id",
44
+ mountAccess: "--mount-access",
45
+ noWait: "--no-wait",
46
+ publicKey: "--public-key",
47
+ publicKeyFile: "--public-key-file",
48
+ };
49
+
50
+ function rejectUnsupportedModeOptions(state, mode) {
51
+ const allowed = MODE_OPTIONS.get(mode);
52
+ if (!allowed) return;
53
+ const unsupported = Object.entries(state.input).find(
54
+ ([key, value]) => value !== undefined && !allowed.has(key)
55
+ );
56
+ if (!unsupported) return;
57
+ const [field] = unsupported;
58
+ const flag =
59
+ OPTION_FLAGS[field] ?? `--${field.replace(/[A-Z]/g, (value) => `-${value.toLowerCase()}`)}`;
60
+ state.interaction.usage(`${MODE_LABELS[mode]} launch does not accept ${flag}.`, {
61
+ code: "unsupported_launch_option",
62
+ field,
63
+ });
64
+ }
65
+
66
+ /**
67
+ * The single `ornn launch` extension seam. Mode modules export a handler with:
68
+ *
69
+ * { mode, launch({ mode, input, context, interaction }) }
70
+ *
71
+ * A handler may supply `matches(state)` when one catalog mode needs more than
72
+ * an exact string match. The dispatcher owns explicit mode selection and never
73
+ * falls back from an unsupported mode to another lifecycle.
74
+ */
75
+ export async function dispatchLaunchWithHandlers(state, handlers = []) {
76
+ const valid = handlers.map((handler) => {
77
+ if (!handler || typeof handler.mode !== "string" || typeof handler.launch !== "function") {
78
+ throw new TypeError("Launch mode handlers require mode and launch(state).");
79
+ }
80
+ return handler;
81
+ });
82
+ const modes = [...new Set(valid.map((handler) => handler.mode))];
83
+ if (modes.length !== valid.length) {
84
+ throw new TypeError("Launch mode handlers must own unique modes.");
85
+ }
86
+ let mode = typeof state.input.mode === "string" ? state.input.mode.trim().toLowerCase() : "";
87
+ if (!mode) {
88
+ if (!state.interaction.eligibleHuman || state.interaction.machine) {
89
+ state.interaction.usage("--mode is required for launch automation.", {
90
+ code: "access_mode_required",
91
+ field: "mode",
92
+ });
93
+ }
94
+ mode = await state.interaction.ask({
95
+ label: "Access mode",
96
+ choices: modes.map((value) => ({ label: MODE_LABELS[value] ?? value, value })),
97
+ });
98
+ if (!mode) {
99
+ state.interaction.usage("Choose an explicit access mode before launch.", {
100
+ code: "access_mode_required",
101
+ field: "mode",
102
+ });
103
+ }
104
+ }
105
+ rejectUnsupportedModeOptions(state, mode);
106
+
107
+ const withMode = { ...state, mode };
108
+ for (const handler of valid) {
109
+ const matches =
110
+ typeof handler.matches === "function"
111
+ ? await handler.matches(withMode)
112
+ : handler.mode === mode;
113
+ if (matches) return handler.launch(withMode);
114
+ }
115
+ state.interaction.usage(
116
+ `Unsupported launch mode: ${mode}. Choose ${modes.join(", ") || "a registered mode"}.`,
117
+ { code: "unsupported_access_mode", field: "mode" }
118
+ );
119
+ }
@@ -0,0 +1,285 @@
1
+ import { gpuLabeler } from "./gpu-catalog.mjs";
2
+
3
+ const CANONICAL_ID = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
4
+ const PAGE_SIZE = 500;
5
+ const MAX_RESERVATION_PAGES = 20;
6
+ const TERMINAL_MACHINE_STATES = new Set([
7
+ "canceled",
8
+ "cancelled",
9
+ "deleted",
10
+ "ended",
11
+ "failed",
12
+ "released",
13
+ "terminated",
14
+ ]);
15
+
16
+ function record(value) {
17
+ return value && typeof value === "object" && !Array.isArray(value) ? value : {};
18
+ }
19
+
20
+ function text(value) {
21
+ return typeof value === "string" && value.trim() ? value.trim() : null;
22
+ }
23
+
24
+ function rows(payload, field) {
25
+ if (Array.isArray(payload)) return payload;
26
+ const value =
27
+ field === "machines"
28
+ ? (record(payload).nodes ?? record(payload).machines)
29
+ : record(payload)[field];
30
+ return Array.isArray(value) ? value : null;
31
+ }
32
+
33
+ function reservationId(value) {
34
+ const source = record(value);
35
+ return (
36
+ text(source.id) ??
37
+ text(source.ref) ??
38
+ text(source.reservation_id) ??
39
+ text(source.commerce_reservation_id)
40
+ );
41
+ }
42
+
43
+ function reservationMode(value) {
44
+ const source = record(value);
45
+ const mode = text(source.access_mode) ?? text(source.accessMode);
46
+ return mode?.toLowerCase() === "virtual-machine" ? "vm" : (mode?.toLowerCase() ?? null);
47
+ }
48
+
49
+ function reservationModeLabel(value) {
50
+ switch (reservationMode(value)) {
51
+ case "vm":
52
+ return "VM";
53
+ case "bare-metal":
54
+ return "Bare Metal";
55
+ case "kubernetes":
56
+ return "Kubernetes";
57
+ case "slurm":
58
+ return "Slurm";
59
+ default:
60
+ return null;
61
+ }
62
+ }
63
+
64
+ export function reservationChoiceLabel(value, gpuLabel) {
65
+ const reservation = record(value);
66
+ const gpu = text(reservation.gpu_type);
67
+ const hardware = gpu ? gpuLabel(gpu) : (text(reservation.hardware) ?? "GPU capacity");
68
+ const quantity = Number(reservation.node_count ?? reservation.machine_count ?? 1);
69
+ const machines = Number.isInteger(quantity) && quantity > 0 ? quantity : 1;
70
+ const location =
71
+ text(reservation.site_nickname) ??
72
+ text(reservation.facility) ??
73
+ text(reservation.location) ??
74
+ text(reservation.region) ??
75
+ "location unreported";
76
+ const start = text(reservation.start_at) ?? text(reservation.start_date);
77
+ const end = text(reservation.end_at) ?? text(reservation.end_date);
78
+ const createdAt = text(reservation.created_at);
79
+ return [
80
+ `${hardware}, ${machines} machine${machines === 1 ? "" : "s"}, ${location}`,
81
+ reservationModeLabel(reservation),
82
+ start && end ? `${start} to ${end}` : null,
83
+ createdAt ? `reserved ${createdAt}` : null,
84
+ ]
85
+ .filter(Boolean)
86
+ .join(" · ");
87
+ }
88
+
89
+ export async function reservationLabeler(fetch) {
90
+ const gpuLabel = await gpuLabeler(fetch);
91
+ return (reservation) => reservationChoiceLabel(reservation, gpuLabel);
92
+ }
93
+
94
+ function isActiveDirectReservation(value) {
95
+ const source = record(value);
96
+ const status = (text(source.status) ?? text(source.state) ?? "").toLowerCase();
97
+ const mode = reservationMode(source);
98
+ return status === "active" && (!mode || mode === "vm" || mode === "bare-metal");
99
+ }
100
+
101
+ export function canonicalMachineId(value) {
102
+ const source = record(value);
103
+ return (
104
+ text(source.id) ?? text(source.machine_id) ?? text(source.instance_id) ?? text(source.node_id)
105
+ );
106
+ }
107
+
108
+ export function machineHostname(value) {
109
+ return text(record(value).hostname);
110
+ }
111
+
112
+ function machineState(value) {
113
+ const source = record(value);
114
+ return (text(source.state) ?? text(source.status) ?? "unknown").toLowerCase();
115
+ }
116
+
117
+ export function isCanonicalMachineId(value) {
118
+ return CANONICAL_ID.test(text(value) ?? "");
119
+ }
120
+
121
+ export function isReferenceMiss(error) {
122
+ return error?.status === 404;
123
+ }
124
+
125
+ function invalidGateway(interaction, message) {
126
+ interaction.error(message, {
127
+ code: "invalid_gateway_response",
128
+ exitCode: 6,
129
+ retryable: true,
130
+ });
131
+ }
132
+
133
+ export async function listActiveDirectReservations(context, interaction) {
134
+ const reservations = [];
135
+ const seen = new Set();
136
+ let cursor = null;
137
+ for (let page = 0; page < MAX_RESERVATION_PAGES; page += 1) {
138
+ const query = new URLSearchParams({ limit: String(PAGE_SIZE), status: "active" });
139
+ if (cursor) query.set("cursor", cursor);
140
+ const payload = await context.fetch(`/v1/workflows/commerce/reservations?${query}`, {
141
+ method: "GET",
142
+ });
143
+ const pageRows = rows(payload, "reservations");
144
+ if (!pageRows)
145
+ invalidGateway(interaction, "The gateway returned an invalid capacity-reservation page.");
146
+ reservations.push(...pageRows.map(record).filter(isActiveDirectReservation));
147
+
148
+ const source = record(payload);
149
+ const next = text(source.next_cursor) ?? text(source.nextCursor);
150
+ if (!next || Array.isArray(payload)) return reservations;
151
+ if (seen.has(next)) {
152
+ invalidGateway(interaction, "Capacity-reservation pagination repeated a cursor.");
153
+ }
154
+ seen.add(next);
155
+ cursor = next;
156
+ }
157
+ interaction.error("Machine resolution exceeded its bounded capacity-reservation search.", {
158
+ code: "resource_search_incomplete",
159
+ exitCode: 6,
160
+ retryable: true,
161
+ });
162
+ }
163
+
164
+ export async function listOwnedActiveMachines(context, interaction) {
165
+ const reservations = await listActiveDirectReservations(context, interaction);
166
+ const pages = await Promise.all(
167
+ reservations.map(async (reservation) => {
168
+ const ref = reservationId(reservation);
169
+ if (!isCanonicalMachineId(ref)) {
170
+ invalidGateway(
171
+ interaction,
172
+ "The gateway returned a capacity reservation without a canonical identifier."
173
+ );
174
+ }
175
+ const payload = await context.fetch(
176
+ `/v1/workflows/compute/nodes/reservations/${encodeURIComponent(ref)}/machines`,
177
+ { method: "GET" }
178
+ );
179
+ const machineRows = rows(payload, "machines");
180
+ if (!machineRows)
181
+ invalidGateway(interaction, "The gateway returned an invalid machine collection.");
182
+ return machineRows
183
+ .map(record)
184
+ .filter((machine) => !TERMINAL_MACHINE_STATES.has(machineState(machine)))
185
+ .map((machine) => ({
186
+ ...machine,
187
+ reservation_id:
188
+ text(machine.reservation_id) ?? text(machine.commerce_reservation_id) ?? ref,
189
+ }));
190
+ })
191
+ );
192
+
193
+ const machines = new Map();
194
+ for (const machine of pages.flat()) {
195
+ const id = canonicalMachineId(machine);
196
+ if (!isCanonicalMachineId(id)) {
197
+ invalidGateway(interaction, "The gateway returned a machine without a canonical identifier.");
198
+ }
199
+ if (!machines.has(id)) machines.set(id, machine);
200
+ }
201
+ return [...machines.values()].sort(
202
+ (left, right) =>
203
+ (machineHostname(left) ?? "").localeCompare(machineHostname(right) ?? "") ||
204
+ canonicalMachineId(left).localeCompare(canonicalMachineId(right))
205
+ );
206
+ }
207
+
208
+ async function fetchCanonicalMachine(reference, context, interaction, { allowMiss = false } = {}) {
209
+ try {
210
+ const machine = record(
211
+ await context.fetch(`/v1/workflows/compute/nodes/${encodeURIComponent(reference)}`, {
212
+ method: "GET",
213
+ })
214
+ );
215
+ if (canonicalMachineId(machine)?.toLowerCase() !== reference.toLowerCase()) {
216
+ invalidGateway(
217
+ interaction,
218
+ "The gateway returned a machine with a mismatched canonical identifier."
219
+ );
220
+ }
221
+ return machine;
222
+ } catch (error) {
223
+ if (!isReferenceMiss(error)) throw error;
224
+ if (allowMiss) return null;
225
+ interaction.error(`Machine not found: ${reference}.`, { code: "machine_not_found" });
226
+ }
227
+ }
228
+
229
+ export async function resolveMachineReference(
230
+ reference,
231
+ context,
232
+ interaction,
233
+ { allowMiss = false } = {}
234
+ ) {
235
+ const requested = text(reference);
236
+ if (requested && isCanonicalMachineId(requested)) {
237
+ return fetchCanonicalMachine(requested, context, interaction, { allowMiss });
238
+ }
239
+
240
+ const machines = await listOwnedActiveMachines(context, interaction);
241
+ let selected;
242
+ if (requested) {
243
+ const normalized = requested.toLowerCase();
244
+ const matches = machines.filter(
245
+ (machine) => machineHostname(machine)?.toLowerCase() === normalized
246
+ );
247
+ if (matches.length > 1) {
248
+ interaction.usage(
249
+ `More than one active machine has hostname "${requested}". Use a canonical machine identifier.`,
250
+ { code: "machine_hostname_ambiguous", field: "machine" }
251
+ );
252
+ }
253
+ selected = matches[0];
254
+ } else {
255
+ if (!interaction.eligibleHuman || interaction.machine) {
256
+ if (allowMiss) return null;
257
+ interaction.usage("A machine reference is required outside an eligible terminal.", {
258
+ code: "missing_input",
259
+ field: "machine",
260
+ });
261
+ }
262
+ if (machines.length === 1) {
263
+ [selected] = machines;
264
+ } else if (machines.length > 1) {
265
+ const answer = await interaction.ask({
266
+ label: "Machine",
267
+ choices: machines.map((machine) => ({
268
+ label: `${machineHostname(machine) ?? "Unnamed machine"} (${machineState(machine)})`,
269
+ value: canonicalMachineId(machine),
270
+ })),
271
+ });
272
+ selected = machines.find((machine) => canonicalMachineId(machine) === answer);
273
+ if (!selected) interaction.usage("Choose one exact machine.", { code: "missing_input" });
274
+ }
275
+ }
276
+
277
+ if (!selected) {
278
+ if (allowMiss) return null;
279
+ interaction.error(
280
+ requested ? `Machine not found: ${requested}.` : "No active direct machine is available.",
281
+ { code: "machine_not_found" }
282
+ );
283
+ }
284
+ return fetchCanonicalMachine(canonicalMachineId(selected), context, interaction);
285
+ }