@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.
- package/README.md +351 -426
- package/package.json +12 -6
- package/src/admin-capacity-reservations.mjs +305 -0
- package/src/admin-command-handlers.mjs +57 -0
- package/src/admin-enrollment.mjs +681 -0
- package/src/admin-mutations.mjs +187 -0
- package/src/admin-node-diagnostics.mjs +412 -0
- package/src/admin-node-lifecycle.mjs +905 -0
- package/src/admin-shell.mjs +456 -0
- package/src/api-client.mjs +81 -28
- package/src/atomic-output.mjs +112 -0
- package/src/auth-store.mjs +182 -11
- package/src/capability-catalog.mjs +7 -0
- package/src/catalog-shell.mjs +1363 -0
- package/src/cli.mjs +123 -10266
- package/src/connect-mode-handlers.mjs +170 -0
- package/src/device-auth.mjs +179 -62
- package/src/evidence-commands.mjs +1354 -0
- package/src/external-bucket-commands.mjs +1374 -0
- package/src/gpu-catalog.mjs +21 -0
- package/src/identity-file.mjs +28 -0
- package/src/interaction-runtime.mjs +310 -0
- package/src/kubernetes-commands.mjs +2235 -0
- package/src/launch-mode-handlers.mjs +119 -0
- package/src/machine-reference.mjs +285 -0
- package/src/mount-commands.mjs +1148 -0
- package/src/session-commands.mjs +565 -0
- package/src/slurm-commands.mjs +274 -0
- package/src/startup.mjs +34 -11
- package/src/storage-commands.mjs +1164 -0
- package/src/update.mjs +172 -24
- package/src/vm-commands.mjs +4308 -0
- package/src/vpn-secret-output.mjs +150 -0
- package/src/wait-runtime.mjs +231 -0
- package/vendor/capabilities/capabilities/admin-capacity-reservations.js +96 -0
- package/vendor/capabilities/capabilities/admin-enrollment.js +321 -0
- package/vendor/capabilities/capabilities/admin-node-diagnostics.js +88 -0
- package/vendor/capabilities/capabilities/admin-nodes-lifecycle.js +235 -0
- package/vendor/capabilities/capabilities/admin.js +89 -0
- package/vendor/capabilities/capabilities/cli.js +156 -0
- package/vendor/capabilities/capabilities/clusters.js +150 -1
- package/vendor/capabilities/capabilities/commerce-market.js +330 -0
- package/vendor/capabilities/capabilities/compute.js +522 -0
- package/vendor/capabilities/capabilities/evidence.js +145 -0
- package/vendor/capabilities/capabilities/external-buckets.js +191 -0
- package/vendor/capabilities/capabilities/fleet.js +77 -25
- package/vendor/capabilities/capabilities/identity.js +114 -3
- package/vendor/capabilities/capabilities/listings.js +141 -144
- package/vendor/capabilities/capabilities/market-shared.js +44 -0
- package/vendor/capabilities/capabilities/markets.js +449 -0
- package/vendor/capabilities/capabilities/mounts.js +176 -0
- package/vendor/capabilities/capabilities/networks.js +1264 -0
- package/vendor/capabilities/capabilities/observability.js +38 -8
- package/vendor/capabilities/capabilities/tokens.js +32 -17
- package/vendor/capabilities/capabilities/volumes.js +178 -0
- package/vendor/capabilities/capability.js +21 -14
- package/vendor/capabilities/cli-catalog.js +67 -0
- package/vendor/capabilities/cli-migrations.js +261 -0
- package/vendor/capabilities/cli.js +7 -0
- package/vendor/capabilities/software-catalog.js +36 -0
- package/vendor/capabilities/spec-catalog.js +28 -5
- package/vendor/capabilities/supplier-directory.js +20 -0
- package/src/catalog-dispatch.mjs +0 -605
- package/src/color.mjs +0 -71
- package/vendor/capabilities/capabilities/clusters.d.ts +0 -2
- package/vendor/capabilities/capabilities/fleet.d.ts +0 -8
- package/vendor/capabilities/capabilities/identity.d.ts +0 -3
- package/vendor/capabilities/capabilities/listings.d.ts +0 -51
- package/vendor/capabilities/capabilities/observability.d.ts +0 -5
- package/vendor/capabilities/capabilities/staff-access.d.ts +0 -29
- package/vendor/capabilities/capabilities/staff-access.js +0 -278
- package/vendor/capabilities/capabilities/staff-commerce.d.ts +0 -160
- package/vendor/capabilities/capabilities/staff-commerce.js +0 -689
- package/vendor/capabilities/capabilities/staff-fleet.d.ts +0 -140
- package/vendor/capabilities/capabilities/staff-fleet.js +0 -538
- package/vendor/capabilities/capabilities/staff-gtm.d.ts +0 -142
- package/vendor/capabilities/capabilities/staff-gtm.js +0 -521
- package/vendor/capabilities/capabilities/staff-shared.d.ts +0 -47
- package/vendor/capabilities/capabilities/staff-shared.js +0 -98
- package/vendor/capabilities/capabilities/tokens.d.ts +0 -10
- package/vendor/capabilities/capabilities/users.d.ts +0 -2
- package/vendor/capabilities/capability.d.ts +0 -93
- package/vendor/capabilities/catalog.d.ts +0 -3
- package/vendor/capabilities/catalog.js +0 -26
- package/vendor/capabilities/index.d.ts +0 -16
- package/vendor/capabilities/index.js +0 -16
- package/vendor/capabilities/names.d.ts +0 -6
- package/vendor/capabilities/role.d.ts +0 -6
- package/vendor/capabilities/role.js +0 -17
- package/vendor/capabilities/spec-catalog.d.ts +0 -78
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { gpuDisplay, HARDWARE_CATALOG_PATH, parseHardwareCatalog } from "../vendor/capabilities/cli.js";
|
|
2
|
+
|
|
3
|
+
const catalogs = new WeakMap();
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Renders GPU slugs by their catalog display name, reading the catalog once
|
|
7
|
+
* per fetch. An unreadable catalog renders GPUs as unknown instead of
|
|
8
|
+
* failing the command.
|
|
9
|
+
*/
|
|
10
|
+
export async function gpuLabeler(fetch, path = HARDWARE_CATALOG_PATH) {
|
|
11
|
+
let terms = catalogs.get(fetch);
|
|
12
|
+
if (!terms) {
|
|
13
|
+
terms = Promise.resolve()
|
|
14
|
+
.then(() => fetch(path, { method: "GET" }))
|
|
15
|
+
.then((payload) => parseHardwareCatalog(payload).terms)
|
|
16
|
+
.catch(() => []);
|
|
17
|
+
catalogs.set(fetch, terms);
|
|
18
|
+
}
|
|
19
|
+
const resolved = await terms;
|
|
20
|
+
return (slug) => gpuDisplay(resolved, slug);
|
|
21
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { stat as statDefault } from "node:fs/promises";
|
|
2
|
+
|
|
3
|
+
/** Quote one argument for a POSIX `sh` copy-paste command. */
|
|
4
|
+
export function shellSingleQuote(value) {
|
|
5
|
+
return `'${String(value).replaceAll("'", "'\"'\"'")}'`;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* OpenSSH refuses a private key that other users can read. Report that before
|
|
10
|
+
* handing the terminal to ssh so the fix is a single chmod. Windows file modes
|
|
11
|
+
* are not POSIX permissions, so the check only runs on POSIX systems; a
|
|
12
|
+
* missing file is left for ssh to report.
|
|
13
|
+
*/
|
|
14
|
+
export async function identityFilePermissionProblem(
|
|
15
|
+
path,
|
|
16
|
+
{ platform = process.platform, stat = statDefault } = {}
|
|
17
|
+
) {
|
|
18
|
+
if (!path || platform === "win32") return null;
|
|
19
|
+
let info;
|
|
20
|
+
try {
|
|
21
|
+
info = await stat(path);
|
|
22
|
+
} catch {
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
if ((info.mode & 0o077) === 0) return null;
|
|
26
|
+
const mode = (info.mode & 0o777).toString(8).padStart(3, "0");
|
|
27
|
+
return `SSH private key ${path} can be read by other users (mode ${mode}). Restrict it with: chmod 600 -- ${shellSingleQuote(path)}`;
|
|
28
|
+
}
|
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { createInterface } from "node:readline/promises";
|
|
3
|
+
|
|
4
|
+
import { resolveAuthBaseUrl } from "./device-auth.mjs";
|
|
5
|
+
|
|
6
|
+
const BOOLEAN_OPTIONS = new Set([
|
|
7
|
+
"all-nodes",
|
|
8
|
+
"erase-local-data",
|
|
9
|
+
"force",
|
|
10
|
+
"follow",
|
|
11
|
+
"major",
|
|
12
|
+
"guided",
|
|
13
|
+
"no-browser",
|
|
14
|
+
"no-wait",
|
|
15
|
+
"overwrite",
|
|
16
|
+
"print",
|
|
17
|
+
"read-only",
|
|
18
|
+
"read-write",
|
|
19
|
+
"recursive",
|
|
20
|
+
"wait",
|
|
21
|
+
"yes",
|
|
22
|
+
]);
|
|
23
|
+
|
|
24
|
+
const OPTIONAL_VALUE_OPTIONS = new Set(["resume"]);
|
|
25
|
+
|
|
26
|
+
function optionName(raw) {
|
|
27
|
+
return raw.replace(/-([a-z0-9])/g, (_, character) => character.toUpperCase());
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export class InteractionError extends Error {
|
|
31
|
+
constructor(
|
|
32
|
+
message,
|
|
33
|
+
{
|
|
34
|
+
action = null,
|
|
35
|
+
code = "command_failed",
|
|
36
|
+
exitCode = 1,
|
|
37
|
+
field,
|
|
38
|
+
outcomeClass = "failed",
|
|
39
|
+
retryable = false,
|
|
40
|
+
} = {}
|
|
41
|
+
) {
|
|
42
|
+
super(message);
|
|
43
|
+
this.name = "InteractionError";
|
|
44
|
+
this.action = action;
|
|
45
|
+
this.code = code;
|
|
46
|
+
this.exitCode = exitCode;
|
|
47
|
+
this.field = field;
|
|
48
|
+
this.outcomeClass = outcomeClass;
|
|
49
|
+
this.retryable = retryable;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function isRecoverableWaitFailure(error) {
|
|
54
|
+
const status = Number(error?.status);
|
|
55
|
+
if (status === 401 || status === 403 || (Number.isFinite(status) && status >= 500)) {
|
|
56
|
+
return true;
|
|
57
|
+
}
|
|
58
|
+
if (error?.retryable === true) return true;
|
|
59
|
+
const code = typeof error?.code === "string" ? error.code : "";
|
|
60
|
+
if (/^E(?:AI_|CONN|HOST|NET|PIPE|TIMEDOUT)/.test(code)) return true;
|
|
61
|
+
return (
|
|
62
|
+
error instanceof TypeError &&
|
|
63
|
+
/fetch|network|socket|connection|timed? ?out|lost response/i.test(error.message)
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function parseSemanticArguments(argv) {
|
|
68
|
+
const parsed = { $positionals: [] };
|
|
69
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
70
|
+
const argument = argv[index];
|
|
71
|
+
if (argument === "--" || /^-[^-]/.test(argument)) {
|
|
72
|
+
throw new InteractionError(`Unsupported option: ${argument}.`, {
|
|
73
|
+
code: "usage_error",
|
|
74
|
+
exitCode: 2,
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
if (!argument.startsWith("--")) {
|
|
78
|
+
parsed.$positionals.push(argument);
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const equals = argument.indexOf("=");
|
|
83
|
+
const rawName = argument.slice(2, equals === -1 ? undefined : equals);
|
|
84
|
+
if (!rawName) {
|
|
85
|
+
throw new InteractionError("An option name is required after --.", {
|
|
86
|
+
code: "usage_error",
|
|
87
|
+
exitCode: 2,
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
const name = optionName(rawName);
|
|
91
|
+
if (Object.hasOwn(parsed, name)) {
|
|
92
|
+
throw new InteractionError(`--${rawName} may be supplied only once.`, {
|
|
93
|
+
code: "usage_error",
|
|
94
|
+
exitCode: 2,
|
|
95
|
+
field: name,
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (OPTIONAL_VALUE_OPTIONS.has(rawName)) {
|
|
100
|
+
const value = equals === -1 ? argv[index + 1] : argument.slice(equals + 1);
|
|
101
|
+
if (equals !== -1 && value === "") {
|
|
102
|
+
throw new InteractionError(`--${rawName} requires a value after =.`, {
|
|
103
|
+
code: "usage_error",
|
|
104
|
+
exitCode: 2,
|
|
105
|
+
field: name,
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
if (equals === -1 && (value === undefined || value.startsWith("-"))) {
|
|
109
|
+
parsed[name] = true;
|
|
110
|
+
} else {
|
|
111
|
+
if (equals === -1) index += 1;
|
|
112
|
+
parsed[name] = value;
|
|
113
|
+
}
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (BOOLEAN_OPTIONS.has(rawName)) {
|
|
118
|
+
if (equals !== -1) {
|
|
119
|
+
throw new InteractionError(`--${rawName} does not accept a value.`, {
|
|
120
|
+
code: "usage_error",
|
|
121
|
+
exitCode: 2,
|
|
122
|
+
field: name,
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
parsed[name] = true;
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const value = equals === -1 ? argv[index + 1] : argument.slice(equals + 1);
|
|
130
|
+
if (value === undefined || (equals === -1 && value.startsWith("-")) || value === "") {
|
|
131
|
+
throw new InteractionError(`--${rawName} requires a value.`, {
|
|
132
|
+
code: "usage_error",
|
|
133
|
+
exitCode: 2,
|
|
134
|
+
field: name,
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
if (equals === -1) index += 1;
|
|
138
|
+
parsed[name] = value;
|
|
139
|
+
}
|
|
140
|
+
return parsed;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function execution({
|
|
144
|
+
action = null,
|
|
145
|
+
data = null,
|
|
146
|
+
errors = [],
|
|
147
|
+
exitCode = 0,
|
|
148
|
+
outcome = { class: "ready", phase: "complete" },
|
|
149
|
+
page = null,
|
|
150
|
+
progress = [],
|
|
151
|
+
presentation = null,
|
|
152
|
+
resume = null,
|
|
153
|
+
} = {}) {
|
|
154
|
+
return {
|
|
155
|
+
__ornnCliExecution: true,
|
|
156
|
+
action,
|
|
157
|
+
data,
|
|
158
|
+
errors,
|
|
159
|
+
exitCode,
|
|
160
|
+
outcome,
|
|
161
|
+
page,
|
|
162
|
+
progress,
|
|
163
|
+
presentation,
|
|
164
|
+
resume,
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
export function isInteractionExecution(value) {
|
|
169
|
+
return value?.__ornnCliExecution === true;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
export function createInteractionRuntime({
|
|
173
|
+
eligibleHuman,
|
|
174
|
+
extensions = {},
|
|
175
|
+
format,
|
|
176
|
+
formatProvided = false,
|
|
177
|
+
io,
|
|
178
|
+
options,
|
|
179
|
+
}) {
|
|
180
|
+
const machine = format === "json" || format === "ndjson";
|
|
181
|
+
const redirected = !eligibleHuman;
|
|
182
|
+
const stdin = io.stdin ?? process.stdin;
|
|
183
|
+
const stderr = io.stderr ?? process.stderr;
|
|
184
|
+
const sleep = io.sleep;
|
|
185
|
+
const now = io.now ?? (() => Date.now());
|
|
186
|
+
const progressIntervalMs = io.progressIntervalMs ?? 10_000;
|
|
187
|
+
let reader;
|
|
188
|
+
|
|
189
|
+
function readline() {
|
|
190
|
+
reader ??= createInterface({ input: stdin, output: stderr, terminal: false });
|
|
191
|
+
return reader;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function usage(message, { code = "usage_error", field } = {}) {
|
|
195
|
+
throw new InteractionError(message, { code, exitCode: 2, field });
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function error(message, init = {}) {
|
|
199
|
+
throw new InteractionError(message, init);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
async function ask({ choices = [], label }) {
|
|
203
|
+
if (!eligibleHuman || machine) {
|
|
204
|
+
usage(`${label} must be supplied explicitly outside an interactive terminal.`, {
|
|
205
|
+
code: "missing_input",
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
for (const [index, choice] of choices.entries()) {
|
|
209
|
+
stderr.write(` ${index + 1}) ${choice.label}\n`);
|
|
210
|
+
}
|
|
211
|
+
let answer;
|
|
212
|
+
try {
|
|
213
|
+
answer = await readline().question(`${label}: `);
|
|
214
|
+
} catch (caught) {
|
|
215
|
+
if (caught?.code === "ERR_USE_AFTER_CLOSE") return null;
|
|
216
|
+
throw caught;
|
|
217
|
+
}
|
|
218
|
+
if (!answer) return null;
|
|
219
|
+
if (choices.length) {
|
|
220
|
+
const numeric = Number(answer);
|
|
221
|
+
if (Number.isInteger(numeric) && numeric >= 1 && numeric <= choices.length) {
|
|
222
|
+
return choices[numeric - 1].value;
|
|
223
|
+
}
|
|
224
|
+
const exact = choices.filter(
|
|
225
|
+
(choice) => choice.label.toLowerCase() === answer.toLowerCase() || choice.value === answer
|
|
226
|
+
);
|
|
227
|
+
if (exact.length === 1) return exact[0].value;
|
|
228
|
+
usage(`Choose one exact ${label.toLowerCase()} value.`, { code: "ambiguous_input" });
|
|
229
|
+
}
|
|
230
|
+
return answer;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
async function review({ prompt = "Continue?", requireLive = false, terms, title }) {
|
|
234
|
+
if (machine) {
|
|
235
|
+
if (!requireLive && options.yes === true) return true;
|
|
236
|
+
usage(
|
|
237
|
+
requireLive
|
|
238
|
+
? "This operation is supported only in an eligible interactive terminal."
|
|
239
|
+
: "Consent is required. Review the terms and supply --yes to continue.",
|
|
240
|
+
{
|
|
241
|
+
code: requireLive ? "unsupported_in_machine_mode" : "consent_required",
|
|
242
|
+
...(!requireLive ? { field: "yes" } : {}),
|
|
243
|
+
}
|
|
244
|
+
);
|
|
245
|
+
}
|
|
246
|
+
if (requireLive && !eligibleHuman) {
|
|
247
|
+
usage("This operation is supported only in an eligible interactive terminal.", {
|
|
248
|
+
code: "unsupported_in_machine_mode",
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
stderr.write(`${title}\n`);
|
|
252
|
+
for (const term of terms) stderr.write(` ${term.label}: ${term.value}\n`);
|
|
253
|
+
|
|
254
|
+
if (!requireLive && options.yes === true) return true;
|
|
255
|
+
if (!eligibleHuman) {
|
|
256
|
+
usage("Consent is required. Review the terms and supply --yes to continue.", {
|
|
257
|
+
code: "consent_required",
|
|
258
|
+
field: "yes",
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
let answer;
|
|
262
|
+
try {
|
|
263
|
+
answer = await readline().question(`${prompt} [y/N] `);
|
|
264
|
+
} catch (caught) {
|
|
265
|
+
if (caught?.code === "ERR_USE_AFTER_CLOSE") return false;
|
|
266
|
+
throw caught;
|
|
267
|
+
}
|
|
268
|
+
return /^(y|yes)$/i.test(String(answer ?? "").trim());
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function webUrl(path) {
|
|
272
|
+
return new URL(path, `${resolveAuthBaseUrl({ env: io.env })}/`).toString();
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
const interaction = {
|
|
276
|
+
actionRequired: ({ action, data, resume }) =>
|
|
277
|
+
execution({
|
|
278
|
+
action,
|
|
279
|
+
data,
|
|
280
|
+
exitCode: 3,
|
|
281
|
+
outcome: { class: "action_required", phase: action.kind ?? "external_action" },
|
|
282
|
+
resume,
|
|
283
|
+
}),
|
|
284
|
+
ask,
|
|
285
|
+
close() {
|
|
286
|
+
reader?.close();
|
|
287
|
+
},
|
|
288
|
+
inform(lines) {
|
|
289
|
+
if (!eligibleHuman || machine) return;
|
|
290
|
+
for (const line of lines) stderr.write(`${line}\n`);
|
|
291
|
+
},
|
|
292
|
+
eligibleHuman,
|
|
293
|
+
error,
|
|
294
|
+
idempotencyKey(supplied) {
|
|
295
|
+
const stable = typeof supplied === "string" ? supplied.trim() : "";
|
|
296
|
+
return stable || `ornn-${randomUUID()}`;
|
|
297
|
+
},
|
|
298
|
+
format,
|
|
299
|
+
formatProvided,
|
|
300
|
+
machine,
|
|
301
|
+
options,
|
|
302
|
+
redirected,
|
|
303
|
+
result: execution,
|
|
304
|
+
review,
|
|
305
|
+
usage,
|
|
306
|
+
webUrl,
|
|
307
|
+
};
|
|
308
|
+
Object.assign(interaction, extensions);
|
|
309
|
+
return interaction;
|
|
310
|
+
}
|