@ornncompute/cli 0.2.23 → 1.0.25

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 (85) hide show
  1. package/README.md +304 -420
  2. package/package.json +12 -6
  3. package/src/admin-capacity-reservations.mjs +280 -0
  4. package/src/admin-command-handlers.mjs +57 -0
  5. package/src/admin-enrollment.mjs +680 -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 +908 -0
  9. package/src/admin-shell.mjs +322 -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 +1350 -0
  15. package/src/cli.mjs +123 -10266
  16. package/src/connect-mode-handlers.mjs +170 -0
  17. package/src/device-auth.mjs +178 -61
  18. package/src/evidence-commands.mjs +1340 -0
  19. package/src/external-bucket-commands.mjs +1374 -0
  20. package/src/interaction-runtime.mjs +310 -0
  21. package/src/kubernetes-commands.mjs +2229 -0
  22. package/src/launch-mode-handlers.mjs +119 -0
  23. package/src/machine-reference.mjs +277 -0
  24. package/src/mount-commands.mjs +1146 -0
  25. package/src/session-commands.mjs +565 -0
  26. package/src/slurm-commands.mjs +263 -0
  27. package/src/startup.mjs +34 -11
  28. package/src/storage-commands.mjs +1144 -0
  29. package/src/update.mjs +172 -24
  30. package/src/vm-commands.mjs +4273 -0
  31. package/src/vpn-secret-output.mjs +150 -0
  32. package/src/wait-runtime.mjs +231 -0
  33. package/vendor/capabilities/capabilities/admin-capacity-reservations.js +126 -0
  34. package/vendor/capabilities/capabilities/admin-enrollment.js +321 -0
  35. package/vendor/capabilities/capabilities/admin-node-diagnostics.js +88 -0
  36. package/vendor/capabilities/capabilities/admin-nodes-lifecycle.js +235 -0
  37. package/vendor/capabilities/capabilities/admin.js +89 -0
  38. package/vendor/capabilities/capabilities/capacity-offers.js +368 -0
  39. package/vendor/capabilities/capabilities/cli.js +156 -0
  40. package/vendor/capabilities/capabilities/clusters.js +150 -1
  41. package/vendor/capabilities/capabilities/compute.js +522 -0
  42. package/vendor/capabilities/capabilities/evidence.js +145 -0
  43. package/vendor/capabilities/capabilities/external-buckets.js +191 -0
  44. package/vendor/capabilities/capabilities/fleet.js +21 -5
  45. package/vendor/capabilities/capabilities/identity.js +114 -3
  46. package/vendor/capabilities/capabilities/listings.js +14 -8
  47. package/vendor/capabilities/capabilities/mounts.js +176 -0
  48. package/vendor/capabilities/capabilities/networks.js +1257 -0
  49. package/vendor/capabilities/capabilities/observability.js +38 -8
  50. package/vendor/capabilities/capabilities/tokens.js +20 -8
  51. package/vendor/capabilities/capabilities/volumes.js +178 -0
  52. package/vendor/capabilities/capability.js +19 -13
  53. package/vendor/capabilities/cli-catalog.js +65 -0
  54. package/vendor/capabilities/cli-migrations.js +288 -0
  55. package/vendor/capabilities/cli.js +7 -0
  56. package/vendor/capabilities/software-catalog.js +36 -0
  57. package/vendor/capabilities/spec-catalog.js +2 -2
  58. package/src/catalog-dispatch.mjs +0 -605
  59. package/src/color.mjs +0 -71
  60. package/vendor/capabilities/capabilities/clusters.d.ts +0 -2
  61. package/vendor/capabilities/capabilities/fleet.d.ts +0 -8
  62. package/vendor/capabilities/capabilities/identity.d.ts +0 -3
  63. package/vendor/capabilities/capabilities/listings.d.ts +0 -51
  64. package/vendor/capabilities/capabilities/observability.d.ts +0 -5
  65. package/vendor/capabilities/capabilities/staff-access.d.ts +0 -29
  66. package/vendor/capabilities/capabilities/staff-access.js +0 -278
  67. package/vendor/capabilities/capabilities/staff-commerce.d.ts +0 -160
  68. package/vendor/capabilities/capabilities/staff-commerce.js +0 -689
  69. package/vendor/capabilities/capabilities/staff-fleet.d.ts +0 -140
  70. package/vendor/capabilities/capabilities/staff-fleet.js +0 -538
  71. package/vendor/capabilities/capabilities/staff-gtm.d.ts +0 -142
  72. package/vendor/capabilities/capabilities/staff-gtm.js +0 -521
  73. package/vendor/capabilities/capabilities/staff-shared.d.ts +0 -47
  74. package/vendor/capabilities/capabilities/staff-shared.js +0 -98
  75. package/vendor/capabilities/capabilities/tokens.d.ts +0 -10
  76. package/vendor/capabilities/capabilities/users.d.ts +0 -2
  77. package/vendor/capabilities/capability.d.ts +0 -93
  78. package/vendor/capabilities/catalog.d.ts +0 -3
  79. package/vendor/capabilities/catalog.js +0 -26
  80. package/vendor/capabilities/index.d.ts +0 -16
  81. package/vendor/capabilities/index.js +0 -16
  82. package/vendor/capabilities/names.d.ts +0 -6
  83. package/vendor/capabilities/role.d.ts +0 -6
  84. package/vendor/capabilities/role.js +0 -17
  85. package/vendor/capabilities/spec-catalog.d.ts +0 -78
@@ -0,0 +1,263 @@
1
+ import { spawn as spawnProcessDefault } from "node:child_process";
2
+ import { homedir } from "node:os";
3
+
4
+ import {
5
+ clusterModeConnectCandidates,
6
+ launchClusterMode,
7
+ readClusterModeConnectContext,
8
+ resolveClusterModeConnectTarget,
9
+ } from "./kubernetes-commands.mjs";
10
+
11
+ const SAFE_HOST = /^[A-Za-z0-9][A-Za-z0-9.:-]*$/;
12
+ const SAFE_USER = /^[A-Za-z_][A-Za-z0-9_.-]*[$]?$/;
13
+ const connectCache = new WeakMap();
14
+
15
+ function text(value) {
16
+ return typeof value === "string" && value.trim() ? value.trim() : null;
17
+ }
18
+
19
+ function expandHome(path) {
20
+ if (path === "~") return homedir();
21
+ return path.startsWith("~/") ? `${homedir()}${path.slice(1)}` : path;
22
+ }
23
+
24
+ function shellQuote(value) {
25
+ const rendered = String(value);
26
+ return /^[A-Za-z0-9_./:@%+=,-]+$/.test(rendered)
27
+ ? rendered
28
+ : `'${rendered.replaceAll("'", `'\\''`)}'`;
29
+ }
30
+
31
+ function commandText(invocation) {
32
+ return [invocation.command, ...invocation.args].map(shellQuote).join(" ");
33
+ }
34
+
35
+ function safeLogin(runtime, credentials, input) {
36
+ const credentialType = text(credentials.credential_type);
37
+ if (credentialType && credentialType !== "login-ssh") {
38
+ runtime.error("The gateway returned a non-Slurm native-access credential.", {
39
+ code: "invalid_slurm_access",
40
+ });
41
+ }
42
+ const host = text(credentials.login_host);
43
+ if (!host) return null;
44
+ if (!SAFE_HOST.test(host)) {
45
+ runtime.error("The gateway returned an unsafe Slurm SSH host.", {
46
+ code: "invalid_slurm_access",
47
+ });
48
+ }
49
+ const user = text(input.user) ?? text(credentials.login_user);
50
+ if (user && !SAFE_USER.test(user)) {
51
+ runtime.usage("--user must be a valid SSH login name.", {
52
+ code: "invalid_ssh_user",
53
+ field: "user",
54
+ });
55
+ }
56
+ const rawPort = credentials.login_port ?? 22;
57
+ const port = Number(rawPort);
58
+ if (!Number.isInteger(port) || port < 1 || port > 65_535) {
59
+ runtime.error("The gateway returned an invalid Slurm SSH port.", {
60
+ code: "invalid_slurm_access",
61
+ });
62
+ }
63
+ const identityFile = text(input.identityFile);
64
+ if (identityFile && /[\u0000-\u001f\u007f]/.test(identityFile)) {
65
+ runtime.usage("--identity-file contains an invalid path.", {
66
+ code: "invalid_identity_file",
67
+ field: "identityFile",
68
+ });
69
+ }
70
+ const args = [];
71
+ if (identityFile) args.push("-i", expandHome(identityFile));
72
+ if (port !== 22) args.push("-p", String(port));
73
+ args.push("--", user ? `${user}@${host}` : host);
74
+ return {
75
+ args,
76
+ command: "ssh",
77
+ host,
78
+ port,
79
+ user,
80
+ };
81
+ }
82
+
83
+ function privateTransport(state) {
84
+ if (!state.privateNetwork) return null;
85
+ return {
86
+ private_network: {
87
+ id: text(state.privateNetwork.id),
88
+ name: text(state.privateNetwork.name) ?? text(state.privateNetwork.label),
89
+ },
90
+ vpn_connection: state.vpnConnection
91
+ ? {
92
+ id: text(state.vpnConnection.id),
93
+ name: text(state.vpnConnection.name) ?? text(state.vpnConnection.label),
94
+ status: text(state.vpnConnection.lifecycle?.class) ?? text(state.vpnConnection.status),
95
+ }
96
+ : null,
97
+ };
98
+ }
99
+
100
+ function retryConnectAction(cluster) {
101
+ return {
102
+ command: `ornn connect ${cluster.id}`,
103
+ description: "Retry this exact Slurm cluster after its SSH login becomes ready.",
104
+ type: "command",
105
+ };
106
+ }
107
+
108
+ async function spawnSsh(invocation, spawnProcess) {
109
+ return new Promise((resolve, reject) => {
110
+ const child = spawnProcess(invocation.command, invocation.args, { stdio: "inherit" });
111
+ child.on("error", reject);
112
+ child.on("exit", (code, signal) => {
113
+ resolve({ code: signal ? 130 : (code ?? 0), signal: signal ?? null });
114
+ });
115
+ });
116
+ }
117
+
118
+ export function createSlurmConnectModeHandler({ spawnProcess = spawnProcessDefault } = {}) {
119
+ return {
120
+ mode: "slurm",
121
+ candidates(state) {
122
+ return clusterModeConnectCandidates(state, "slurm");
123
+ },
124
+ resolve(state) {
125
+ return resolveClusterModeConnectTarget(state, "slurm");
126
+ },
127
+ async matches(state) {
128
+ if (state.mode && state.mode !== "slurm" && state.cluster?.access_mode !== "slurm") {
129
+ return false;
130
+ }
131
+ const prepared = await readClusterModeConnectContext(state, "slurm");
132
+ if (!prepared) return false;
133
+ connectCache.set(state, prepared);
134
+ return true;
135
+ },
136
+ async connect(state) {
137
+ const runtime = state.interaction;
138
+ const prepared =
139
+ connectCache.get(state) ?? (await readClusterModeConnectContext(state, "slurm"));
140
+ if (!prepared) {
141
+ runtime.error("The selected Slurm cluster could not be rediscovered.", {
142
+ code: "cluster_not_found",
143
+ });
144
+ }
145
+ if (state.input.output || state.input.overwrite) {
146
+ runtime.usage(
147
+ "Slurm connect uses native SSH and does not write a kubeconfig or other output file.",
148
+ {
149
+ code: "slurm_output_unsupported",
150
+ field: "output",
151
+ }
152
+ );
153
+ }
154
+ const login = safeLogin(runtime, prepared.credentials, state.input);
155
+ const transport = privateTransport(state);
156
+ if (!login || ["partial", "failed", "ended"].includes(prepared.cluster.lifecycle.class)) {
157
+ const unavailable = prepared.credentialError === "unavailable";
158
+ return {
159
+ action: retryConnectAction(prepared.cluster),
160
+ data: {
161
+ capacity_reservation: prepared.capacityReservation,
162
+ cluster: prepared.cluster,
163
+ private_transport: transport,
164
+ ssh: null,
165
+ },
166
+ errors: [
167
+ {
168
+ code: unavailable ? "slurm_access_unavailable" : "slurm_access_not_ready",
169
+ message: unavailable
170
+ ? "Slurm SSH access could not be loaded; the cluster remains unchanged."
171
+ : "The Slurm cluster exists, but its SSH login is not usable yet.",
172
+ retryable: true,
173
+ resource_ref: prepared.cluster.id,
174
+ },
175
+ ],
176
+ exitCode: 4,
177
+ outcome: {
178
+ class: unavailable ? "partial" : "working",
179
+ phase: unavailable ? "access_unavailable" : "ssh_pending",
180
+ },
181
+ presentation: {
182
+ lines: [
183
+ unavailable
184
+ ? "Slurm SSH access is temporarily unavailable for the selected cluster."
185
+ : "The selected Slurm cluster is not SSH-ready yet.",
186
+ ],
187
+ next: "ornn connect",
188
+ },
189
+ resume: prepared.resume,
190
+ };
191
+ }
192
+ const ssh = {
193
+ command: commandText(login),
194
+ host: login.host,
195
+ port: login.port,
196
+ transport: "ssh",
197
+ user: login.user,
198
+ };
199
+ const data = {
200
+ capacity_reservation: prepared.capacityReservation,
201
+ cluster: prepared.cluster,
202
+ private_transport: transport,
203
+ ssh,
204
+ };
205
+ if (runtime.eligibleHuman && !runtime.machine && state.input.print !== true) {
206
+ const launched = await spawnSsh(login, spawnProcess);
207
+ if (launched.code === 130) {
208
+ return {
209
+ data,
210
+ errors: [
211
+ {
212
+ code: "interrupted",
213
+ message:
214
+ "The local SSH process was interrupted; the Slurm cluster was not changed.",
215
+ retryable: true,
216
+ resource_ref: prepared.cluster.id,
217
+ },
218
+ ],
219
+ exitCode: 130,
220
+ outcome: { class: "ended", phase: "interrupted" },
221
+ resume: prepared.resume,
222
+ };
223
+ }
224
+ if (launched.code !== 0) {
225
+ return {
226
+ data,
227
+ errors: [
228
+ {
229
+ code: "ssh_failed",
230
+ message: `SSH exited with status ${launched.code}.`,
231
+ retryable: false,
232
+ resource_ref: prepared.cluster.id,
233
+ },
234
+ ],
235
+ exitCode: 1,
236
+ outcome: { class: "failed", phase: "ssh_failed" },
237
+ resume: prepared.resume,
238
+ };
239
+ }
240
+ }
241
+ return {
242
+ data,
243
+ outcome: { class: "ready", phase: "ssh_ready" },
244
+ presentation: {
245
+ lines: [
246
+ runtime.eligibleHuman && state.input.print !== true
247
+ ? `Connected to Slurm cluster ${prepared.cluster.id}.`
248
+ : ssh.command,
249
+ "Use standard Slurm tools such as sinfo, srun, sbatch, and sacct after login.",
250
+ ],
251
+ },
252
+ resume: prepared.resume,
253
+ };
254
+ },
255
+ };
256
+ }
257
+
258
+ export const slurmConnectModeHandler = createSlurmConnectModeHandler();
259
+
260
+ export const slurmLaunchModeHandler = {
261
+ mode: "slurm",
262
+ launch: (state) => launchClusterMode(state, "slurm"),
263
+ };
package/src/startup.mjs CHANGED
@@ -6,6 +6,7 @@ import { pathToFileURL } from "node:url";
6
6
  import { forwardSignals } from "./signals.mjs";
7
7
  import {
8
8
  acquireUpdateLock,
9
+ CLI_MIGRATION_GUIDE_URL,
9
10
  compareVersions,
10
11
  detectInstallContext,
11
12
  installVersion,
@@ -31,6 +32,8 @@ process.exitCode = await entry.execute(process.argv.slice(3), {
31
32
  function hasHumanOnlyArgument(argv) {
32
33
  return argv.some(
33
34
  (arg) =>
35
+ arg === "--format" ||
36
+ arg.startsWith("--format=") ||
34
37
  arg === "--json" ||
35
38
  arg.startsWith("--json=") ||
36
39
  arg === "--raw" ||
@@ -39,7 +42,7 @@ function hasHumanOnlyArgument(argv) {
39
42
  arg === "help" ||
40
43
  arg === "--version" ||
41
44
  arg === "-v" ||
42
- arg === "version",
45
+ arg === "version"
43
46
  );
44
47
  }
45
48
 
@@ -73,7 +76,7 @@ function failedProbe(stderr) {
73
76
  function unavailableLock(stderr) {
74
77
  warning(
75
78
  stderr,
76
- "Automatic update unavailable; retry after any other ornn update finishes. If no updater or package installer is running, remove the stale update-install lock from your Ornn config directory or check that directory's permissions.",
79
+ "Automatic update unavailable; retry after any other ornn update finishes. If no updater or package installer is running, remove the stale update-install lock from your Ornn config directory or check that directory's permissions."
77
80
  );
78
81
  return { kind: "exit", code: 1 };
79
82
  }
@@ -88,7 +91,12 @@ function entryUrl(binPath) {
88
91
 
89
92
  export async function probeInstalledCli(
90
93
  binPath,
91
- { spawnProcess = spawn, env = process.env, signalSource = process, timeoutMs = PROBE_TIMEOUT_MS } = {},
94
+ {
95
+ spawnProcess = spawn,
96
+ env = process.env,
97
+ signalSource = process,
98
+ timeoutMs = PROBE_TIMEOUT_MS,
99
+ } = {}
92
100
  ) {
93
101
  return await new Promise((resolve) => {
94
102
  let output = "";
@@ -96,11 +104,15 @@ export async function probeInstalledCli(
96
104
  let timedOut = false;
97
105
  let forceKillTimer;
98
106
  let timeoutTimer;
99
- const child = spawnProcess(process.execPath, ["--input-type=module", "--eval", PROBE_PROGRAM, entryUrl(binPath)], {
100
- env,
101
- shell: false,
102
- stdio: ["ignore", "pipe", "ignore"],
103
- });
107
+ const child = spawnProcess(
108
+ process.execPath,
109
+ ["--input-type=module", "--eval", PROBE_PROGRAM, entryUrl(binPath)],
110
+ {
111
+ env,
112
+ shell: false,
113
+ stdio: ["ignore", "pipe", "ignore"],
114
+ }
115
+ );
104
116
  const scheduleForceKill = () => {
105
117
  forceKillTimer ??= setTimeout(() => child.kill?.("SIGKILL"), PROBE_KILL_GRACE_MS);
106
118
  };
@@ -136,14 +148,14 @@ export async function probeInstalledCli(
136
148
  export async function restartCli(
137
149
  binPath,
138
150
  argv,
139
- { spawnProcess = spawn, env = process.env, signalSource = process } = {},
151
+ { spawnProcess = spawn, env = process.env, signalSource = process } = {}
140
152
  ) {
141
153
  return await new Promise((resolve, reject) => {
142
154
  let parentSignal = null;
143
155
  const child = spawnProcess(
144
156
  process.execPath,
145
157
  ["--input-type=module", "--eval", RESTART_PROGRAM, entryUrl(binPath), binPath, ...argv],
146
- { env, shell: false, stdio: "inherit" },
158
+ { env, shell: false, stdio: "inherit" }
147
159
  );
148
160
  const removeSignals = forwardSignals(signalSource, child, (signal) => {
149
161
  parentSignal ??= signal;
@@ -165,7 +177,10 @@ function unexpectedVersion(stderr, version, currentVersion, target) {
165
177
  ? `The installer left ornn CLI at ${version}, expected ${target}.`
166
178
  : "The installer left an invalid ornn CLI version.";
167
179
  const downgrade = isValidReleaseVersion(version) && compareVersions(version, currentVersion) < 0;
168
- warning(stderr, `${detail}${downgrade ? ` It is older than ${currentVersion}.` : ""} ${REINSTALL_GUIDANCE}`);
180
+ warning(
181
+ stderr,
182
+ `${detail}${downgrade ? ` It is older than ${currentVersion}.` : ""} ${REINSTALL_GUIDANCE}`
183
+ );
169
184
  return { kind: "exit", code: 1 };
170
185
  }
171
186
 
@@ -220,6 +235,14 @@ export async function prepareStartup(argv, options = {}) {
220
235
  return { kind: "continue" };
221
236
  }
222
237
 
238
+ if (Number(target.split(".")[0]) > Number(options.currentVersion.split(".")[0])) {
239
+ warning(
240
+ stderr,
241
+ `Major update available: ${target}. Review ${CLI_MIGRATION_GUIDE_URL}, then run ornn update --major.`
242
+ );
243
+ return { kind: "continue" };
244
+ }
245
+
223
246
  warning(stderr, `Updating ornn CLI ${options.currentVersion} -> ${target}...`);
224
247
  let stableBin;
225
248
  try {