@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,112 @@
1
+ import { constants } from "node:fs";
2
+ import { chmod, link, lstat, open, rename, rm } from "node:fs/promises";
3
+ import { dirname, join } from "node:path";
4
+ import { Readable } from "node:stream";
5
+ import { randomUUID } from "node:crypto";
6
+
7
+ export class AtomicOutputExistsError extends Error {
8
+ constructor(path) {
9
+ super(`Refusing to replace existing file: ${path}`);
10
+ this.name = "AtomicOutputExistsError";
11
+ this.code = "destination_exists";
12
+ this.path = path;
13
+ }
14
+ }
15
+
16
+ async function destinationExists(path) {
17
+ try {
18
+ await lstat(path);
19
+ return true;
20
+ } catch (error) {
21
+ if (error?.code === "ENOENT") return false;
22
+ throw error;
23
+ }
24
+ }
25
+
26
+ export async function assertAtomicOutputAvailable(destination, { overwrite = false } = {}) {
27
+ if (typeof destination !== "string" || !destination.trim()) {
28
+ throw new TypeError("An output path is required.");
29
+ }
30
+ if (!overwrite && (await destinationExists(destination))) {
31
+ throw new AtomicOutputExistsError(destination);
32
+ }
33
+ }
34
+
35
+ function nodeReadable(content) {
36
+ if (content && typeof content.getReader === "function") return Readable.fromWeb(content);
37
+ if (content && typeof content.pipe === "function") return content;
38
+ return null;
39
+ }
40
+ async function writeReadable(handle, readable) {
41
+ for await (const chunk of readable) {
42
+ const bytes =
43
+ typeof chunk === "string"
44
+ ? Buffer.from(chunk)
45
+ : chunk instanceof Uint8Array || chunk instanceof DataView
46
+ ? Buffer.from(chunk.buffer, chunk.byteOffset, chunk.byteLength)
47
+ : null;
48
+ if (!bytes) throw new TypeError("Readable atomic output chunks must be bytes or text.");
49
+ let offset = 0;
50
+ while (offset < bytes.byteLength) {
51
+ const result = await handle.write(bytes, offset, bytes.byteLength - offset);
52
+ if (result.bytesWritten === 0) throw new Error("Atomic output write made no progress.");
53
+ offset += result.bytesWritten;
54
+ }
55
+ }
56
+ }
57
+
58
+ /**
59
+ * Writes bytes through a unique same-directory temporary file and publishes
60
+ * them atomically. Without overwrite, hard-link publication provides an atomic
61
+ * no-replace operation; replacement uses an atomic rename.
62
+ */
63
+ export async function writeAtomicOutput(
64
+ destination,
65
+ content,
66
+ { mode = 0o644, overwrite = false } = {}
67
+ ) {
68
+ await assertAtomicOutputAvailable(destination, { overwrite });
69
+ if (!Number.isInteger(mode) || mode < 0 || mode > 0o777) {
70
+ throw new TypeError("Output mode must be an integer between 0000 and 0777.");
71
+ }
72
+
73
+ const directory = dirname(destination);
74
+ const temporary = join(directory, `.${process.pid}.${randomUUID()}.ornn-partial`);
75
+ let handle;
76
+ try {
77
+ handle = await open(temporary, constants.O_CREAT | constants.O_EXCL | constants.O_WRONLY, mode);
78
+ const readable = nodeReadable(content);
79
+ if (readable) {
80
+ await writeReadable(handle, readable);
81
+ } else if (
82
+ typeof content === "string" ||
83
+ content instanceof Uint8Array ||
84
+ content instanceof DataView
85
+ ) {
86
+ await handle.writeFile(content);
87
+ } else {
88
+ throw new TypeError("Atomic output content must be bytes, text, or a readable stream.");
89
+ }
90
+ await handle.sync();
91
+ await handle.close();
92
+ handle = undefined;
93
+ await chmod(temporary, mode);
94
+
95
+ if (overwrite) {
96
+ await rename(temporary, destination);
97
+ } else {
98
+ try {
99
+ await link(temporary, destination);
100
+ } catch (error) {
101
+ if (error?.code === "EEXIST") throw new AtomicOutputExistsError(destination);
102
+ throw error;
103
+ }
104
+ await rm(temporary);
105
+ }
106
+ return { mode, path: destination };
107
+ } catch (error) {
108
+ await handle?.close().catch(() => {});
109
+ await rm(temporary, { force: true }).catch(() => {});
110
+ throw error;
111
+ }
112
+ }
@@ -1,7 +1,9 @@
1
1
  import { readFileSync } from "node:fs";
2
- import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
3
- import { dirname, join, resolve } from "node:path";
2
+ import { mkdir, readFile, rm } from "node:fs/promises";
4
3
  import { homedir } from "node:os";
4
+ import { join, resolve } from "node:path";
5
+
6
+ import { writeAtomicOutput } from "./atomic-output.mjs";
5
7
 
6
8
  const APP_DIR = "ornn";
7
9
  const AUTH_FILE = "auth.json";
@@ -46,6 +48,15 @@ export function configuredApiBaseUrl(env = process.env) {
46
48
  }
47
49
  }
48
50
 
51
+ export function configuredInstallerBaseUrl(env = process.env) {
52
+ try {
53
+ const value = JSON.parse(readFileSync(getCliConfigPath(env), "utf8"))?.installerBaseUrl;
54
+ return typeof value === "string" ? value.trim() : "";
55
+ } catch {
56
+ return "";
57
+ }
58
+ }
59
+
49
60
  export async function loadAuthSession({ env = process.env } = {}) {
50
61
  const path = getAuthConfigPath(env);
51
62
 
@@ -60,20 +71,180 @@ export async function loadAuthSession({ env = process.env } = {}) {
60
71
  }
61
72
  }
62
73
 
63
- export async function saveAuthSession(session, { env = process.env, now = new Date() } = {}) {
74
+ async function writeAuthSession(session, env) {
64
75
  const path = getAuthConfigPath(env);
76
+ await mkdir(getAuthConfigDir(env), { recursive: true, mode: 0o700 });
77
+ await writeAtomicOutput(path, `${JSON.stringify(session, null, 2)}\n`, {
78
+ mode: 0o600,
79
+ overwrite: true,
80
+ });
81
+ return session;
82
+ }
83
+
84
+ export async function saveAuthSession(session, { env = process.env, now = new Date() } = {}) {
85
+ const existing = await loadAuthSession({ env });
86
+ const payload = { ...session };
87
+ delete payload.pendingPurchase;
88
+ delete payload.pendingWorkspace;
89
+ delete payload.recoveryUserId;
90
+ const userId = pendingString(session?.user, "id");
91
+ const existingOwnerId = recoveryOwnerId(existing);
92
+ const existingRecovery =
93
+ userId && existingOwnerId === userId
94
+ ? durableRecoveryFields(existing)
95
+ : !userId && existingOwnerId
96
+ ? durableRecoveryFields(existing)
97
+ : {};
98
+ const suppliedRecovery = userId ? durableRecoveryFields(session) : {};
99
+ const retainedRecoveryOwnerId =
100
+ !userId && Object.keys(existingRecovery).length > 0 ? existingOwnerId : null;
101
+ return writeAuthSession(
102
+ {
103
+ ...payload,
104
+ ...existingRecovery,
105
+ ...suppliedRecovery,
106
+ ...(retainedRecoveryOwnerId ? { recoveryUserId: retainedRecoveryOwnerId } : {}),
107
+ savedAt: now.toISOString(),
108
+ },
109
+ env
110
+ );
111
+ }
112
+
113
+ export async function bindAuthUser(user, { env = process.env, expectedAccessToken } = {}) {
114
+ const userId = pendingString(user, "id");
115
+ if (!userId) return false;
116
+ const existing = await loadAuthSession({ env });
117
+ if (
118
+ !existing?.accessToken ||
119
+ (expectedAccessToken && existing.accessToken !== expectedAccessToken)
120
+ ) {
121
+ return false;
122
+ }
65
123
  const payload = {
66
- ...session,
67
- savedAt: now.toISOString(),
124
+ ...existing,
125
+ user: { ...(existing.user ?? {}), ...user, id: userId },
68
126
  };
127
+ const ownerId = recoveryOwnerId(existing);
128
+ if (ownerId && ownerId !== userId) {
129
+ delete payload.pendingPurchase;
130
+ delete payload.pendingWorkspace;
131
+ }
132
+ delete payload.recoveryUserId;
133
+ await writeAuthSession(payload, env);
134
+ return true;
135
+ }
69
136
 
70
- await mkdir(dirname(path), { recursive: true, mode: 0o700 });
71
- await writeFile(path, `${JSON.stringify(payload, null, 2)}\n`, {
72
- encoding: "utf8",
73
- mode: 0o600,
74
- });
137
+ export async function saveOrganizationHint(
138
+ selectedOrganizationId,
139
+ { env = process.env, expectedAccessToken } = {}
140
+ ) {
141
+ const session = await loadAuthSession({ env });
142
+ if (
143
+ !session?.accessToken ||
144
+ (expectedAccessToken && session.accessToken !== expectedAccessToken)
145
+ ) {
146
+ return null;
147
+ }
148
+ const payload = { ...session };
149
+ if (typeof selectedOrganizationId === "string" && selectedOrganizationId.trim()) {
150
+ payload.selectedOrganizationId = selectedOrganizationId.trim();
151
+ } else {
152
+ delete payload.selectedOrganizationId;
153
+ }
154
+ return writeAuthSession(payload, env);
155
+ }
156
+
157
+ function pendingString(value, field) {
158
+ return typeof value?.[field] === "string" ? value[field].trim() : "";
159
+ }
75
160
 
76
- return payload;
161
+ function pendingWorkspaceValue(value) {
162
+ const pending = value && typeof value === "object" ? value : {};
163
+ const required = [
164
+ "id",
165
+ "organizationId",
166
+ "reservationId",
167
+ "keyId",
168
+ "networkId",
169
+ "volumeId",
170
+ "mountAccess",
171
+ ];
172
+ const normalized = Object.fromEntries(
173
+ required.map((field) => [
174
+ field,
175
+ typeof pending[field] === "string" ? pending[field].trim() : "",
176
+ ])
177
+ );
178
+ if (required.some((field) => !normalized[field])) return null;
179
+ return {
180
+ ...normalized,
181
+ username:
182
+ typeof pending.username === "string" && pending.username.trim()
183
+ ? pending.username.trim()
184
+ : null,
185
+ };
186
+ }
187
+
188
+ function durableRecoveryFields(value) {
189
+ const pendingWorkspace = pendingWorkspaceValue(value?.pendingWorkspace);
190
+ return {
191
+ ...(pendingWorkspace ? { pendingWorkspace } : {}),
192
+ };
193
+ }
194
+
195
+ function recoveryOwnerId(value) {
196
+ return pendingString(value?.user, "id") || pendingString(value, "recoveryUserId");
197
+ }
198
+
199
+ export async function loadPendingWorkspace({ env = process.env, session: suppliedSession } = {}) {
200
+ const session = suppliedSession === undefined ? await loadAuthSession({ env }) : suppliedSession;
201
+ return pendingWorkspaceValue(session?.pendingWorkspace);
202
+ }
203
+
204
+ export async function savePendingWorkspace(value, { env = process.env, expectedAccessToken } = {}) {
205
+ const pendingWorkspace = pendingWorkspaceValue(value);
206
+ if (!pendingWorkspace) return false;
207
+ const session = await loadAuthSession({ env });
208
+ if (
209
+ !session?.accessToken ||
210
+ (expectedAccessToken && session.accessToken !== expectedAccessToken)
211
+ ) {
212
+ return false;
213
+ }
214
+ const existing = pendingWorkspaceValue(session.pendingWorkspace);
215
+ if (existing && existing.id !== pendingWorkspace.id) return false;
216
+ await writeAuthSession({ ...session, pendingWorkspace }, env);
217
+ return true;
218
+ }
219
+
220
+ export async function clearPendingWorkspace(id, { env = process.env, expectedAccessToken } = {}) {
221
+ const session = await loadAuthSession({ env });
222
+ if (!session?.pendingWorkspace) return false;
223
+ if (expectedAccessToken && session.accessToken !== expectedAccessToken) return false;
224
+ if (id && session.pendingWorkspace.id !== id) return false;
225
+ const payload = { ...session };
226
+ delete payload.pendingWorkspace;
227
+ await writeAuthSession(payload, env);
228
+ return true;
229
+ }
230
+
231
+ export async function clearAuthCredentials({ env = process.env, expectedAccessToken } = {}) {
232
+ const existing = await loadAuthSession({ env });
233
+ if (
234
+ expectedAccessToken &&
235
+ existing?.accessToken &&
236
+ existing.accessToken !== expectedAccessToken
237
+ ) {
238
+ return false;
239
+ }
240
+ const recovery = durableRecoveryFields(existing);
241
+ const recoveryUserId = recoveryOwnerId(existing);
242
+ if (Object.keys(recovery).length === 0 || !recoveryUserId) {
243
+ await rm(getAuthConfigPath(env), { force: true });
244
+ return true;
245
+ }
246
+ await writeAuthSession({ ...recovery, recoveryUserId }, env);
247
+ return true;
77
248
  }
78
249
 
79
250
  export async function clearAuthSession({ env = process.env } = {}) {
@@ -0,0 +1,7 @@
1
+ export async function loadCapabilities() {
2
+ try {
3
+ return await import("@ornn/capabilities/cli");
4
+ } catch {
5
+ return await import("../vendor/capabilities/cli.js");
6
+ }
7
+ }