@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,1164 @@
1
+ import { createHash } from "node:crypto";
2
+ import { createReadStream } from "node:fs";
3
+ import { stat } from "node:fs/promises";
4
+ import { basename } from "node:path";
5
+ import { Readable } from "node:stream";
6
+
7
+ import {
8
+ CliApiError,
9
+ computeEndpoint,
10
+ loadRequiredSession,
11
+ raiseForFailedResponse,
12
+ resolveApiBaseUrl,
13
+ userCredentialHeaders,
14
+ } from "./api-client.mjs";
15
+ import {
16
+ assertAtomicOutputAvailable,
17
+ AtomicOutputExistsError,
18
+ writeAtomicOutput,
19
+ } from "./atomic-output.mjs";
20
+
21
+ const DEFAULT_PAGE_LIMIT = 50;
22
+ const MAX_PAGE_LIMIT = 500;
23
+ const STORAGE_PREFIX = "/nodes/storage-drives";
24
+ const UNSAFE_CONTENT_COMMANDS = new Set([
25
+ "storage.volumes.clear",
26
+ "storage.volumes.files.delete",
27
+ "storage.volumes.folders.delete",
28
+ ]);
29
+
30
+ /**
31
+ * Narrow CLI adapter expected by executeStorageCommand:
32
+ * - `interaction` is context.cli.interaction from interaction-runtime.mjs;
33
+ * - `fetch` is the authenticated customer CapabilityTransport;
34
+ * - `fetchImpl` and `env` are injected runner dependencies used only for the
35
+ * allowlisted signed upload and streamed gateway download.
36
+ *
37
+ * The shell installs this adapter as context.cli.interaction.storage(command,
38
+ * input). The capability package stays transport-agnostic and no owner service
39
+ * is called directly.
40
+ */
41
+
42
+ export class StorageCommandError extends Error {
43
+ constructor(
44
+ message,
45
+ { action, code = "usage_error", exitCode = 2, field, outcomeClass, retryable = false } = {}
46
+ ) {
47
+ super(message);
48
+ this.name = "StorageCommandError";
49
+ this.action = action;
50
+ this.code = code;
51
+ this.exitCode = exitCode;
52
+ this.field = field;
53
+ this.outcomeClass = outcomeClass;
54
+ this.retryable = retryable;
55
+ }
56
+ }
57
+
58
+ function usage(message, field) {
59
+ throw new StorageCommandError(message, { field });
60
+ }
61
+
62
+ function positionals(input, expected, usageText) {
63
+ const values = input.$positionals ?? [];
64
+ if (values.length !== expected) usage(usageText);
65
+ return values;
66
+ }
67
+
68
+ function requiredText(value, flag) {
69
+ const normalized = String(value ?? "")
70
+ .normalize("NFKC")
71
+ .trim();
72
+ if (!normalized) usage(`${flag} requires a value.`, flag.replace(/^--/, ""));
73
+ if (/\p{Cc}/u.test(normalized))
74
+ usage(`${flag} cannot contain control characters.`, flag.replace(/^--/, ""));
75
+ return normalized;
76
+ }
77
+
78
+ export function safeIdentifier(value) {
79
+ const id = String(value ?? "");
80
+ if (!/^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/.test(id)) {
81
+ throw new StorageCommandError("Ornn returned an unexpected storage identifier.", {
82
+ code: "invalid_gateway_response",
83
+ exitCode: 1,
84
+ });
85
+ }
86
+ return id;
87
+ }
88
+
89
+ export function safeTimestamp(value) {
90
+ if (typeof value !== "string") return null;
91
+ const timestamp = new Date(value);
92
+ return Number.isNaN(timestamp.getTime()) ? null : timestamp.toISOString();
93
+ }
94
+
95
+ function safeRemotePath(value) {
96
+ const path = String(value ?? "");
97
+ const segments = path.split("/");
98
+ if (
99
+ !path ||
100
+ path.startsWith("/") ||
101
+ path.endsWith("/") ||
102
+ segments.some(
103
+ (segment) => !segment || segment === "." || segment === ".." || /\p{Cc}/u.test(segment)
104
+ )
105
+ ) {
106
+ throw new StorageCommandError("Ornn returned an unexpected volume file path.", {
107
+ code: "invalid_gateway_response",
108
+ exitCode: 1,
109
+ });
110
+ }
111
+ return path;
112
+ }
113
+
114
+ function normalizeVolumeName(value) {
115
+ const name = requiredText(value, "--name").replace(/\s+/gu, " ");
116
+ if (name.length > 128) usage("--name must be at most 128 characters.", "name");
117
+ return name;
118
+ }
119
+
120
+ export function normalizedNameKey(value) {
121
+ return String(value ?? "")
122
+ .normalize("NFKC")
123
+ .trim()
124
+ .replace(/\s+/gu, " ")
125
+ .toLocaleLowerCase("en-US");
126
+ }
127
+
128
+ function normalizeRemotePath(value, { allowEmpty = false, field = "path" } = {}) {
129
+ const normalized = String(value ?? "")
130
+ .normalize("NFKC")
131
+ .trim()
132
+ .replaceAll("\\", "/");
133
+ if (allowEmpty && !normalized) return "";
134
+ const segments = normalized.split("/");
135
+ if (
136
+ !normalized ||
137
+ normalized.startsWith("/") ||
138
+ normalized.endsWith("/") ||
139
+ segments.some(
140
+ (segment) => !segment || segment === "." || segment === ".." || /\p{Cc}/u.test(segment)
141
+ )
142
+ ) {
143
+ usage(
144
+ "Remote paths must be relative, non-empty, and cannot contain empty, . or .. segments.",
145
+ field
146
+ );
147
+ }
148
+ return segments.join("/");
149
+ }
150
+
151
+ function encodedRemotePath(path) {
152
+ return path
153
+ .split("/")
154
+ .map((segment) => encodeURIComponent(segment))
155
+ .join("/");
156
+ }
157
+
158
+ function isManagedVolume(drive) {
159
+ const provider = String(drive?.provider ?? "").toLocaleLowerCase("en-US");
160
+ const sourceProvider = String(
161
+ drive?.source?.provider ?? drive?.source_provider ?? ""
162
+ ).toLocaleLowerCase("en-US");
163
+ return provider !== "nfs" && sourceProvider !== "external_gcs" && sourceProvider !== "s3";
164
+ }
165
+
166
+ export function volumeRows(payload) {
167
+ return Array.isArray(payload) ? payload : Array.isArray(payload?.drives) ? payload.drives : [];
168
+ }
169
+
170
+ function stableVolumePhase(status) {
171
+ const value = String(status ?? "").toLocaleLowerCase("en-US");
172
+ if (["active", "ready"].includes(value)) return "ready";
173
+ if (["failed", "error"].includes(value)) return "failed";
174
+ if (["deleted", "ended"].includes(value)) return "ended";
175
+ if (["creating", "copying", "importing", "updating"].includes(value)) return "working";
176
+ return "pending";
177
+ }
178
+
179
+ export function safeVolume(drive) {
180
+ const sourceId = drive?.source_drive_id ?? drive?.source?.source_drive_id;
181
+ const name = String(drive?.name ?? "")
182
+ .normalize("NFKC")
183
+ .trim();
184
+ if (!name || /\p{Cc}/u.test(name)) {
185
+ throw new StorageCommandError("Ornn returned an unexpected volume name.", {
186
+ code: "invalid_gateway_response",
187
+ exitCode: 1,
188
+ });
189
+ }
190
+ return {
191
+ id: safeIdentifier(drive?.id),
192
+ name,
193
+ phase: stableVolumePhase(drive?.status),
194
+ size_bytes: Number.isFinite(Number(drive?.size_bytes)) ? Number(drive.size_bytes) : 0,
195
+ source_volume_id: typeof sourceId === "string" ? safeIdentifier(sourceId) : null,
196
+ created_at: safeTimestamp(drive?.created_at),
197
+ updated_at: safeTimestamp(drive?.updated_at),
198
+ };
199
+ }
200
+
201
+ function safeFile(entry) {
202
+ return {
203
+ path: safeRemotePath(entry?.path ?? entry?.name),
204
+ size_bytes: Number.isFinite(Number(entry?.size_bytes)) ? Number(entry.size_bytes) : 0,
205
+ updated_at: safeTimestamp(entry?.updated_at),
206
+ };
207
+ }
208
+
209
+ function parseLimit(value) {
210
+ if (value === undefined) return DEFAULT_PAGE_LIMIT;
211
+ const limit = Number(value);
212
+ if (!Number.isInteger(limit) || limit < 1 || limit > MAX_PAGE_LIMIT) {
213
+ usage("--limit must be an integer from 1 through 500.", "limit");
214
+ }
215
+ return limit;
216
+ }
217
+
218
+ function encodeCursor(offset) {
219
+ return Buffer.from(`storage:${offset}`, "utf8").toString("base64url");
220
+ }
221
+
222
+ function decodeCursor(value) {
223
+ if (value === undefined) return 0;
224
+ try {
225
+ const decoded = Buffer.from(requiredText(value, "--cursor"), "base64url").toString("utf8");
226
+ const match = decoded.match(/^storage:(\d+)$/);
227
+ if (!match) throw new Error("invalid cursor");
228
+ const offset = Number(match[1]);
229
+ if (!Number.isSafeInteger(offset)) throw new Error("invalid cursor");
230
+ return offset;
231
+ } catch {
232
+ usage("--cursor is not a valid volume page cursor.", "cursor");
233
+ }
234
+ }
235
+
236
+ function boundedPage(rows, input) {
237
+ const limit = parseLimit(input.limit);
238
+ const offset = decodeCursor(input.cursor);
239
+ const items = rows.slice(offset, offset + limit);
240
+ const nextOffset = offset + items.length;
241
+ return {
242
+ items,
243
+ page: {
244
+ limit,
245
+ returned: items.length,
246
+ next_cursor: nextOffset < rows.length ? encodeCursor(nextOffset) : null,
247
+ },
248
+ };
249
+ }
250
+
251
+ export function detailOf(error) {
252
+ const value = error?.detail?.detail ?? error?.detail?.error ?? error?.detail;
253
+ return value && typeof value === "object" && !Array.isArray(value) ? value : null;
254
+ }
255
+
256
+ export function codeOf(error) {
257
+ const detail = detailOf(error);
258
+ if (typeof detail?.code === "string" && /^[a-z][a-z0-9_]*$/.test(detail.code)) return detail.code;
259
+ const nested = typeof error?.detail === "string" ? error.detail : null;
260
+ return nested && /^[a-z][a-z0-9_]*$/.test(nested) ? nested : null;
261
+ }
262
+
263
+ const SAFE_ERROR_MESSAGES = {
264
+ storage_content_busy: "The volume content is still changing. Inspect the volume and try again.",
265
+ storage_drive_attached_to_reservation:
266
+ "This volume still has a reservation relationship. Detach every volume mount before deleting it.",
267
+ storage_drive_mounted: "This volume is mounted. Detach it before changing its files.",
268
+ storage_drive_not_found: "The requested volume was not found.",
269
+ storage_file_not_found: "The requested volume file was not found.",
270
+ storage_folder_not_found: "The requested volume folder was not found.",
271
+ storage_object_file_api_unsupported: "This volume does not support nested file operations.",
272
+ storage_pending_uploads:
273
+ "This volume has an upload still being finalized. Inspect it and try again.",
274
+ };
275
+
276
+ function safeStorageError(error, fallback, { action } = {}) {
277
+ if (error instanceof StorageCommandError) return error;
278
+ if (error instanceof AtomicOutputExistsError) {
279
+ return new StorageCommandError(
280
+ "The output path already exists. Choose another path or pass --overwrite.",
281
+ { code: "destination_exists", field: "output" }
282
+ );
283
+ }
284
+ if (error instanceof CliApiError && [401, 403, 426].includes(error.status)) return error;
285
+ const code =
286
+ codeOf(error) ??
287
+ (error?.status >= 500 || error?.status === undefined
288
+ ? "service_unavailable"
289
+ : "storage_operation_failed");
290
+ const unavailable = code === "service_unavailable";
291
+ const ownerMessage = detailOf(error)?.message;
292
+ return new StorageCommandError(
293
+ SAFE_ERROR_MESSAGES[code] ??
294
+ (typeof ownerMessage === "string" && ownerMessage.trim() ? ownerMessage : fallback),
295
+ {
296
+ action,
297
+ code,
298
+ exitCode: unavailable ? 6 : 1,
299
+ retryable: unavailable,
300
+ }
301
+ );
302
+ }
303
+
304
+ export function idempotencyHeaders(input) {
305
+ const key = String(input.idempotencyKey ?? "").trim();
306
+ return key ? { "Idempotency-Key": key } : undefined;
307
+ }
308
+
309
+ function requireMachineMutationKey(input, runtime) {
310
+ if (!runtime.interaction.machine) return;
311
+ const key = String(input.idempotencyKey ?? "").trim();
312
+ if (!key) {
313
+ usage("Machine mutations require --idempotency-key <caller-stable-key>.", "idempotency-key");
314
+ }
315
+ }
316
+
317
+ export async function fetchDrivePage(runtime, { limit = 25, cursor, scope = "all" } = {}) {
318
+ if (!Number.isInteger(Number(limit)) || Number(limit) < 1 || Number(limit) > 100) {
319
+ usage("--limit must be an integer from 1 through 100.", "limit");
320
+ }
321
+ const params = new URLSearchParams({ limit: String(limit), scope });
322
+ if (cursor) params.set("cursor", cursor);
323
+ try {
324
+ const payload = await runtime.fetch(computeEndpoint(`${STORAGE_PREFIX}?${params}`), {
325
+ method: "GET",
326
+ });
327
+ return { drives: volumeRows(payload), next_cursor: payload?.next_cursor ?? null };
328
+ } catch (error) {
329
+ throw safeStorageError(error, "Volumes could not be loaded safely.");
330
+ }
331
+ }
332
+
333
+ // Exact-name resolution and write reconciliation need the complete catalog.
334
+ // Listing commands use fetchDrivePage directly and never take this path.
335
+ export async function fetchDrives(runtime) {
336
+ const drives = [];
337
+ const seen = new Set();
338
+ let cursor;
339
+ do {
340
+ const page = await fetchDrivePage(runtime, { limit: 100, cursor });
341
+ drives.push(...page.drives);
342
+ cursor = page.next_cursor;
343
+ if (cursor && (typeof cursor !== "string" || seen.has(cursor))) {
344
+ throw new StorageCommandError("Storage pagination did not advance. Try again.", {
345
+ code: "invalid_gateway_response",
346
+ });
347
+ }
348
+ seen.add(cursor);
349
+ } while (cursor);
350
+ return drives;
351
+ }
352
+
353
+ function resolveFromRows(rows, reference, { source = false } = {}) {
354
+ const ref = requiredText(reference, source ? "--source" : "volume");
355
+ const managed = rows.filter(isManagedVolume);
356
+ const idMatches = managed.filter((drive) => String(drive?.id) === ref);
357
+ if (idMatches.length === 1) return idMatches[0];
358
+ const key = normalizedNameKey(ref);
359
+ const nameMatches = managed.filter((drive) => normalizedNameKey(drive?.name) === key);
360
+ if (nameMatches.length === 1) return nameMatches[0];
361
+ if (nameMatches.length > 1) {
362
+ throw new StorageCommandError(
363
+ `More than one volume has the exact name “${ref}”. Use a canonical volume ID.`,
364
+ { code: "ambiguous_volume", field: source ? "source" : "volume" }
365
+ );
366
+ }
367
+ throw new StorageCommandError(`Volume not found: ${ref}`, {
368
+ code: source ? "source_volume_not_found" : "volume_not_found",
369
+ field: source ? "source" : "volume",
370
+ });
371
+ }
372
+
373
+ export async function resolveVolume(runtime, reference) {
374
+ if (/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(reference)) {
375
+ const drive = await runtime.fetch(
376
+ computeEndpoint(`${STORAGE_PREFIX}/${encodeURIComponent(reference)}`),
377
+ { method: "GET" }
378
+ );
379
+ return resolveFromRows([drive], reference);
380
+ }
381
+ return resolveFromRows(await fetchDrives(runtime), reference);
382
+ }
383
+
384
+ async function review(runtime, options) {
385
+ const accepted = await runtime.interaction.review(options);
386
+ if (accepted !== false) return null;
387
+ return runtime.interaction.result({
388
+ data: null,
389
+ exitCode: 1,
390
+ outcome: { class: "ended", phase: "declined" },
391
+ });
392
+ }
393
+
394
+ async function unsafeReview(command, input, runtime, reference, remotePath) {
395
+ if (!UNSAFE_CONTENT_COMMANDS.has(command)) return null;
396
+ const title =
397
+ command === "storage.volumes.clear"
398
+ ? "Clear volume contents"
399
+ : command === "storage.volumes.files.delete"
400
+ ? "Delete volume file"
401
+ : "Delete volume folder";
402
+ const terms = [
403
+ { label: "Volume", value: reference },
404
+ ...(remotePath ? [{ label: "Remote path", value: remotePath }] : []),
405
+ {
406
+ label: "Consequence",
407
+ value:
408
+ command === "storage.volumes.clear"
409
+ ? "Every file and folder will be permanently deleted"
410
+ : "The selected content will be permanently deleted",
411
+ },
412
+ ];
413
+ return review(runtime, {
414
+ prompt: "Continue?",
415
+ requireLive: true,
416
+ terms,
417
+ title,
418
+ });
419
+ }
420
+
421
+ async function createVolume(input, runtime) {
422
+ positionals(
423
+ input,
424
+ 0,
425
+ "Usage: ornn volumes create --name <name> [--source <volume>] [--yes] [--idempotency-key <key>]"
426
+ );
427
+ requireMachineMutationKey(input, runtime);
428
+ const name = normalizeVolumeName(input.name);
429
+ const rows = await fetchDrives(runtime);
430
+ const source =
431
+ input.source === undefined ? null : resolveFromRows(rows, input.source, { source: true });
432
+ const effective = { name, source_volume_id: source ? String(source.id) : null };
433
+ const matches = rows.filter(
434
+ (drive) => normalizedNameKey(drive?.name) === normalizedNameKey(name)
435
+ );
436
+ if (matches.length > 1) {
437
+ throw new StorageCommandError(
438
+ "Volume creation cannot be replayed because the normalized name is not unique.",
439
+ { code: "volume_name_ambiguous", field: "name" }
440
+ );
441
+ }
442
+ const declined = await review(runtime, {
443
+ prompt: "Create this volume?",
444
+ terms: [
445
+ { label: "Volume name", value: name },
446
+ { label: "Source", value: source ? String(source.id) : "empty volume" },
447
+ {
448
+ label: "Consequence",
449
+ value: source
450
+ ? "Create a durable managed volume copied from the selected source"
451
+ : "Create an empty durable managed volume",
452
+ },
453
+ ],
454
+ title: "Create volume",
455
+ });
456
+ if (declined) return declined;
457
+ if (matches.length === 1) {
458
+ const existing = matches[0];
459
+ const existingSource = existing?.source_drive_id ?? existing?.source?.source_drive_id ?? null;
460
+ if (
461
+ !isManagedVolume(existing) ||
462
+ String(existingSource ?? "") !== String(effective.source_volume_id ?? "")
463
+ ) {
464
+ throw new StorageCommandError(
465
+ "A volume with this normalized name already exists with different creation input.",
466
+ { code: "volume_creation_conflict", exitCode: 1, field: "name" }
467
+ );
468
+ }
469
+ return runtime.interaction.result({
470
+ data: { effective_input: effective, replayed: true, volume: safeVolume(existing) },
471
+ outcome: { class: "ready", phase: "replayed" },
472
+ resume: { kind: "volume", ref: String(existing.id) },
473
+ });
474
+ }
475
+
476
+ const body = { name, source_drive_id: source ? String(source.id) : null };
477
+ try {
478
+ const created = await runtime.fetch(computeEndpoint(STORAGE_PREFIX), {
479
+ body,
480
+ headers: idempotencyHeaders(input),
481
+ method: "POST",
482
+ });
483
+ return runtime.interaction.result({
484
+ data: { effective_input: effective, replayed: false, volume: safeVolume(created) },
485
+ outcome: { class: "ready", phase: "created" },
486
+ resume: { kind: "volume", ref: String(created?.id ?? "") },
487
+ });
488
+ } catch (error) {
489
+ if (error instanceof CliApiError && (error.status === undefined || error.status >= 500)) {
490
+ try {
491
+ const reconciled = resolveFromRows(await fetchDrives(runtime), name);
492
+ const reconciledSource =
493
+ reconciled?.source_drive_id ?? reconciled?.source?.source_drive_id ?? null;
494
+ if (String(reconciledSource ?? "") === String(effective.source_volume_id ?? "")) {
495
+ return runtime.interaction.result({
496
+ data: { effective_input: effective, replayed: true, volume: safeVolume(reconciled) },
497
+ outcome: { class: "ready", phase: "replayed" },
498
+ resume: { kind: "volume", ref: String(reconciled.id) },
499
+ });
500
+ }
501
+ } catch (reconciliationError) {
502
+ if (
503
+ reconciliationError instanceof StorageCommandError &&
504
+ reconciliationError.code !== "volume_not_found"
505
+ ) {
506
+ throw reconciliationError;
507
+ }
508
+ }
509
+ }
510
+ throw safeStorageError(error, "The volume could not be created safely.");
511
+ }
512
+ }
513
+
514
+ async function listVolumes(input, runtime) {
515
+ positionals(input, 0, "Usage: ornn volumes list [--limit <1-100>] [--cursor <cursor>]");
516
+ const limit = Number(input.limit ?? 25);
517
+ const page = await fetchDrivePage(runtime, { limit, cursor: input.cursor, scope: "managed" });
518
+ const rows = page.drives.filter(isManagedVolume).map(safeVolume);
519
+ return runtime.interaction.result({
520
+ data: { volumes: rows },
521
+ page: { limit, returned: rows.length, next_cursor: page.next_cursor },
522
+ outcome: { class: "ready", phase: rows.length ? "listed" : "empty" },
523
+ });
524
+ }
525
+
526
+ async function showVolume(input, runtime, { refresh = false } = {}) {
527
+ const [reference] = positionals(
528
+ input,
529
+ 1,
530
+ `Usage: ornn volumes ${refresh ? "refresh" : "show"} <volume>`
531
+ );
532
+ const drive = await resolveVolume(runtime, reference);
533
+ if (!refresh) {
534
+ return runtime.interaction.result({ data: { volume: safeVolume(drive) } });
535
+ }
536
+ try {
537
+ const refreshed = await runtime.fetch(
538
+ computeEndpoint(`${STORAGE_PREFIX}/${encodeURIComponent(String(drive.id))}/refresh`),
539
+ { method: "POST" }
540
+ );
541
+ return runtime.interaction.result({
542
+ data: { volume: safeVolume(refreshed) },
543
+ outcome: { class: "ready", phase: "refreshed" },
544
+ resume: { kind: "volume", ref: String(drive.id) },
545
+ });
546
+ } catch (error) {
547
+ throw safeStorageError(error, "The volume could not be refreshed safely.");
548
+ }
549
+ }
550
+
551
+ async function clearVolume(input, runtime) {
552
+ const [reference] = positionals(input, 1, "Usage: ornn volumes clear <volume>");
553
+ const declined = await unsafeReview("storage.volumes.clear", input, runtime, reference);
554
+ if (declined) return declined;
555
+ const drive = await resolveVolume(runtime, reference);
556
+ try {
557
+ const payload = await runtime.fetch(
558
+ computeEndpoint(`${STORAGE_PREFIX}/${encodeURIComponent(String(drive.id))}/contents`),
559
+ { method: "DELETE" }
560
+ );
561
+ return runtime.interaction.result({
562
+ data: {
563
+ cleared: true,
564
+ deleted_count: Number(payload?.contents?.deleted ?? 0),
565
+ volume: safeVolume(drive),
566
+ },
567
+ outcome: { class: "ready", phase: "cleared" },
568
+ });
569
+ } catch (error) {
570
+ const ambiguous =
571
+ error instanceof CliApiError && (error.status === undefined || error.status >= 500);
572
+ if (ambiguous) {
573
+ throw new StorageCommandError(
574
+ "The clear request had an ambiguous outcome. Inspect the volume before doing anything else.",
575
+ {
576
+ action: {
577
+ command: `ornn volumes files list ${String(drive.id)}`,
578
+ type: "command",
579
+ },
580
+ code: "content_mutation_outcome_unknown",
581
+ exitCode: 6,
582
+ retryable: false,
583
+ }
584
+ );
585
+ }
586
+ throw safeStorageError(error, "The volume could not be cleared safely.");
587
+ }
588
+ }
589
+
590
+ async function deleteVolume(input, runtime) {
591
+ const [reference] = positionals(input, 1, "Usage: ornn volumes delete <volume>");
592
+ requireMachineMutationKey(input, runtime);
593
+ const declined = await review(runtime, {
594
+ prompt: "Delete this volume?",
595
+ terms: [
596
+ { label: "Volume", value: reference },
597
+ { label: "Consequence", value: "The volume and all data in it will be permanently deleted" },
598
+ ],
599
+ title: "Delete volume",
600
+ });
601
+ if (declined) return declined;
602
+ const drive = await resolveVolume(runtime, reference);
603
+ try {
604
+ await runtime.fetch(
605
+ computeEndpoint(`${STORAGE_PREFIX}/${encodeURIComponent(String(drive.id))}`),
606
+ {
607
+ headers: idempotencyHeaders(input),
608
+ method: "DELETE",
609
+ }
610
+ );
611
+ return runtime.interaction.result({
612
+ data: { deleted: true, volume: safeVolume(drive) },
613
+ outcome: { class: "ended", phase: "deleted" },
614
+ });
615
+ } catch (error) {
616
+ if (error instanceof CliApiError && (error.status === undefined || error.status >= 500)) {
617
+ try {
618
+ const remaining = await fetchDrives(runtime);
619
+ if (!remaining.some((candidate) => String(candidate?.id) === String(drive.id))) {
620
+ return runtime.interaction.result({
621
+ data: { deleted: true, reconciled: true, volume: safeVolume(drive) },
622
+ outcome: { class: "ended", phase: "deleted" },
623
+ });
624
+ }
625
+ } catch {
626
+ // Preserve the original ambiguous delete error when reconciliation is unavailable.
627
+ }
628
+ }
629
+ const detail = detailOf(error);
630
+ const reservations = Array.isArray(detail?.reservation_ids)
631
+ ? detail.reservation_ids.filter(
632
+ (value) => typeof value === "string" && /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/.test(value)
633
+ )
634
+ : [];
635
+ throw safeStorageError(error, "The volume could not be deleted safely.", {
636
+ action:
637
+ codeOf(error) === "storage_drive_attached_to_reservation"
638
+ ? {
639
+ command: reservations[0]
640
+ ? `ornn volume-mounts detach ${reservations[0]}`
641
+ : `ornn volumes show ${String(drive.id)}`,
642
+ type: "command",
643
+ }
644
+ : undefined,
645
+ });
646
+ }
647
+ }
648
+
649
+ async function fetchFileRows(runtime, drive, prefix) {
650
+ const folder = prefix.includes("/") ? prefix.slice(0, prefix.lastIndexOf("/")) : "";
651
+ const entries = [];
652
+ let cursor = 0;
653
+ while (cursor !== null) {
654
+ const query = new URLSearchParams({ recursive: "true" });
655
+ if (folder) query.set("path", folder);
656
+ if (cursor) query.set("cursor", String(cursor));
657
+ let page;
658
+ try {
659
+ page = await runtime.fetch(
660
+ computeEndpoint(
661
+ `${STORAGE_PREFIX}/${encodeURIComponent(String(drive.id))}/contents?${query}`
662
+ ),
663
+ { method: "GET" }
664
+ );
665
+ } catch (error) {
666
+ throw safeStorageError(error, "Volume files could not be listed safely.");
667
+ }
668
+ entries.push(...(Array.isArray(page?.entries) ? page.entries : []));
669
+ const next = page?.next_cursor;
670
+ cursor = Number.isInteger(next) || (typeof next === "string" && next) ? next : null;
671
+ }
672
+ return entries
673
+ .filter((entry) => entry?.kind === "file")
674
+ .map(safeFile)
675
+ .filter((entry) => entry.path && entry.path.startsWith(prefix))
676
+ .sort(
677
+ (left, right) =>
678
+ (Date.parse(right.updated_at ?? "") || 0) - (Date.parse(left.updated_at ?? "") || 0) ||
679
+ left.path.localeCompare(right.path)
680
+ );
681
+ }
682
+
683
+ async function listFiles(input, runtime) {
684
+ const [reference] = positionals(
685
+ input,
686
+ 1,
687
+ "Usage: ornn volumes files list <volume> [--prefix <remote-path>] [--limit <1-500>] [--cursor <cursor>]"
688
+ );
689
+ const prefix = normalizeRemotePath(input.prefix, { allowEmpty: true, field: "prefix" });
690
+ const drive = await resolveVolume(runtime, reference);
691
+ const rows = await fetchFileRows(runtime, drive, prefix);
692
+ const page = boundedPage(rows, input);
693
+ return runtime.interaction.result({
694
+ data: { files: page.items, prefix, volume: safeVolume(drive) },
695
+ page: page.page,
696
+ outcome: { class: "ready", phase: rows.length ? "listed" : "empty" },
697
+ });
698
+ }
699
+
700
+ async function putSignedUpload({
701
+ contentType,
702
+ fetchImpl,
703
+ headers,
704
+ localFile,
705
+ sizeBytes,
706
+ uploadUrl,
707
+ }) {
708
+ let url;
709
+ try {
710
+ url = new URL(uploadUrl);
711
+ } catch {
712
+ throw new StorageCommandError("Ornn returned an invalid upload link. Start the upload again.", {
713
+ code: "unsafe_upload_link",
714
+ exitCode: 1,
715
+ });
716
+ }
717
+ if (
718
+ url.protocol !== "https:" ||
719
+ !(url.hostname === "storage.googleapis.com" || url.hostname.endsWith(".storage.googleapis.com"))
720
+ ) {
721
+ throw new StorageCommandError("Ornn returned an unsafe upload link. Start the upload again.", {
722
+ code: "unsafe_upload_link",
723
+ exitCode: 1,
724
+ });
725
+ }
726
+ const approved = new Headers();
727
+ for (const [name, value] of Object.entries(headers ?? {})) {
728
+ if (name.toLocaleLowerCase("en-US") === "content-type" && typeof value === "string") {
729
+ approved.set("content-type", value);
730
+ }
731
+ }
732
+ if (!approved.has("content-type")) approved.set("content-type", contentType);
733
+ approved.set("content-length", String(sizeBytes));
734
+ const hash = createHash("sha256");
735
+ let bytesHashed = 0;
736
+ const source = Readable.from(
737
+ (async function* () {
738
+ for await (const chunk of createReadStream(localFile)) {
739
+ hash.update(chunk);
740
+ bytesHashed += chunk.byteLength;
741
+ yield chunk;
742
+ }
743
+ })()
744
+ );
745
+ let response;
746
+ try {
747
+ response = await fetchImpl(url, {
748
+ body: source,
749
+ duplex: "half",
750
+ headers: approved,
751
+ method: "PUT",
752
+ redirect: "manual",
753
+ });
754
+ } catch {
755
+ return {
756
+ generation: null,
757
+ sha256: bytesHashed === sizeBytes ? hash.digest("hex") : null,
758
+ };
759
+ }
760
+ if (!response.ok) {
761
+ throw new StorageCommandError(
762
+ response.status === 403
763
+ ? "The upload link expired before the file finished uploading. Start the upload again."
764
+ : response.status === 409 || response.status === 412
765
+ ? "The destination changed while the upload was in progress. Refresh the volume and try again."
766
+ : "Storage could not accept the file. Start the upload again.",
767
+ {
768
+ code:
769
+ response.status === 403
770
+ ? "upload_link_expired"
771
+ : response.status === 409 || response.status === 412
772
+ ? "storage_file_changed"
773
+ : "upload_rejected",
774
+ exitCode: 1,
775
+ }
776
+ );
777
+ }
778
+ return { generation: response.headers.get("x-goog-generation"), sha256: hash.digest("hex") };
779
+ }
780
+
781
+ async function cancelUpload(runtime, driveId, uploadId) {
782
+ try {
783
+ await runtime.fetch(
784
+ computeEndpoint(
785
+ `${STORAGE_PREFIX}/${encodeURIComponent(driveId)}/file-uploads/${encodeURIComponent(uploadId)}`
786
+ ),
787
+ { method: "DELETE" }
788
+ );
789
+ } catch {
790
+ // Preserve the transfer error. Finalizing sessions are owner-reconciled.
791
+ }
792
+ }
793
+
794
+ async function uploadFile(input, runtime) {
795
+ const [reference, localFile] = positionals(
796
+ input,
797
+ 2,
798
+ "Usage: ornn volumes files upload <volume> <local-file> [--path <remote-path>] [--content-type <type>] [--yes] [--idempotency-key <key>]"
799
+ );
800
+ requireMachineMutationKey(input, runtime);
801
+ let fileInfo;
802
+ try {
803
+ fileInfo = await stat(localFile);
804
+ } catch {
805
+ throw new StorageCommandError("The local upload file was not found.", {
806
+ code: "local_file_not_found",
807
+ field: "local-file",
808
+ });
809
+ }
810
+ if (!fileInfo.isFile()) {
811
+ throw new StorageCommandError("The upload source must be a file, not a directory.", {
812
+ code: "local_file_not_regular",
813
+ field: "local-file",
814
+ });
815
+ }
816
+ const remotePath = normalizeRemotePath(input.path ?? basename(localFile));
817
+ const contentType = input.contentType
818
+ ? requiredText(input.contentType, "--content-type")
819
+ : "application/octet-stream";
820
+ const drive = await resolveVolume(runtime, reference);
821
+ const declined = await review(runtime, {
822
+ prompt: "Upload this file?",
823
+ terms: [
824
+ { label: "Volume", value: `${drive.name} (${drive.id})` },
825
+ { label: "Local file", value: localFile },
826
+ { label: "Remote path", value: remotePath },
827
+ { label: "Bytes", value: String(fileInfo.size) },
828
+ {
829
+ label: "Consequence",
830
+ value: "Create or replace the selected path in the managed volume",
831
+ },
832
+ ],
833
+ title: "Upload volume file",
834
+ });
835
+ if (declined) return declined;
836
+ const requestHash = createHash("sha256");
837
+ for await (const chunk of createReadStream(localFile)) requestHash.update(chunk);
838
+ const sha256 = requestHash.digest("hex");
839
+ let session;
840
+ try {
841
+ session = await runtime.fetch(
842
+ computeEndpoint(`${STORAGE_PREFIX}/${encodeURIComponent(String(drive.id))}/file-upload-url`),
843
+ {
844
+ body: { content_type: contentType, path: remotePath, size_bytes: fileInfo.size, sha256 },
845
+ headers: idempotencyHeaders(input),
846
+ method: "POST",
847
+ }
848
+ );
849
+ } catch (error) {
850
+ throw safeStorageError(error, "The upload could not be staged safely.");
851
+ }
852
+ const uploadId =
853
+ typeof session?.upload_id === "string" ? safeIdentifier(session.upload_id) : null;
854
+ if (uploadId && session.state === "complete") {
855
+ return runtime.interaction.result({
856
+ data: {
857
+ file: safeFile({ path: remotePath, size_bytes: fileInfo.size }),
858
+ integrity: { sha256 },
859
+ upload_id: uploadId,
860
+ volume: safeVolume(drive),
861
+ },
862
+ outcome: { class: "ready", phase: "uploaded" },
863
+ resume: { kind: "volume-upload", ref: uploadId },
864
+ });
865
+ }
866
+ if (!uploadId || typeof session?.upload_url !== "string") {
867
+ if (uploadId) await cancelUpload(runtime, String(drive.id), uploadId);
868
+ throw new StorageCommandError("Ornn returned an invalid upload session.", {
869
+ code: "invalid_upload_session",
870
+ exitCode: 1,
871
+ });
872
+ }
873
+ try {
874
+ const uploaded = await putSignedUpload({
875
+ contentType,
876
+ fetchImpl: runtime.fetchImpl,
877
+ headers: session.headers,
878
+ localFile,
879
+ sizeBytes: fileInfo.size,
880
+ uploadUrl: session.upload_url,
881
+ });
882
+ const completionPath = computeEndpoint(
883
+ `${STORAGE_PREFIX}/${encodeURIComponent(String(drive.id))}/file-uploads/${encodeURIComponent(uploadId)}/complete`
884
+ );
885
+ let completed;
886
+ try {
887
+ completed = await runtime.fetch(completionPath, {
888
+ body: { uploaded_generation: uploaded.generation },
889
+ headers: idempotencyHeaders(input),
890
+ method: "POST",
891
+ });
892
+ } catch (error) {
893
+ if (!(error instanceof CliApiError) || (error.status !== undefined && error.status < 500))
894
+ throw error;
895
+ completed = await runtime.fetch(completionPath, {
896
+ body: { uploaded_generation: uploaded.generation },
897
+ headers: idempotencyHeaders(input),
898
+ method: "POST",
899
+ });
900
+ }
901
+ const file = safeFile(completed?.file ?? { path: remotePath, size_bytes: fileInfo.size });
902
+ return runtime.interaction.result({
903
+ data: {
904
+ file,
905
+ integrity: { sha256: uploaded.sha256 },
906
+ upload_id: uploadId,
907
+ volume: safeVolume(drive),
908
+ },
909
+ outcome: { class: "ready", phase: "uploaded" },
910
+ resume: { kind: "volume-upload", ref: uploadId },
911
+ });
912
+ } catch (error) {
913
+ await cancelUpload(runtime, String(drive.id), uploadId);
914
+ throw safeStorageError(error, "The upload could not be finalized safely.");
915
+ }
916
+ }
917
+
918
+ async function downloadResponseError(response, runtime, url, accessToken) {
919
+ const text = await response.text().catch(() => "");
920
+ if (response.status === 401) {
921
+ await raiseForFailedResponse({
922
+ accessToken,
923
+ env: runtime.env,
924
+ fallbackMessage: "The CLI session expired. Run `ornn login` and try again.",
925
+ response,
926
+ text,
927
+ url,
928
+ });
929
+ }
930
+ let code;
931
+ try {
932
+ const parsed = JSON.parse(text);
933
+ const detail = parsed?.detail?.detail ?? parsed?.detail;
934
+ code =
935
+ typeof detail?.code === "string" ? detail.code : typeof detail === "string" ? detail : null;
936
+ } catch {
937
+ code = null;
938
+ }
939
+ throw new StorageCommandError(
940
+ code === "storage_file_not_found"
941
+ ? "The requested volume file was not found."
942
+ : "The volume file could not be downloaded safely.",
943
+ {
944
+ code: /^[a-z][a-z0-9_]*$/.test(code ?? "") ? code : "download_failed",
945
+ exitCode: response.status >= 500 ? 6 : 1,
946
+ retryable: response.status >= 500,
947
+ }
948
+ );
949
+ }
950
+
951
+ async function downloadFile(input, runtime) {
952
+ const [reference, rawRemotePath] = positionals(
953
+ input,
954
+ 2,
955
+ "Usage: ornn volumes files download <volume> <remote-path> --output <local-path> [--overwrite]"
956
+ );
957
+ const remotePath = normalizeRemotePath(rawRemotePath);
958
+ const output = requiredText(input.output, "--output");
959
+ try {
960
+ await assertAtomicOutputAvailable(output, { overwrite: input.overwrite === true });
961
+ } catch (error) {
962
+ throw safeStorageError(error, "The output path cannot be used safely.");
963
+ }
964
+ const drive = await resolveVolume(runtime, reference);
965
+ const session = await loadRequiredSession({ env: runtime.env });
966
+ const baseUrl = resolveApiBaseUrl({ env: runtime.env, session });
967
+ const endpoint = computeEndpoint(
968
+ `${STORAGE_PREFIX}/${encodeURIComponent(String(drive.id))}/files/${encodedRemotePath(remotePath)}`
969
+ );
970
+ const url = new URL(endpoint, `${baseUrl}/`);
971
+ let response;
972
+ try {
973
+ response = await runtime.fetchImpl(url, {
974
+ headers: userCredentialHeaders(session),
975
+ method: "GET",
976
+ });
977
+ } catch {
978
+ throw new StorageCommandError("The volume file could not be downloaded safely.", {
979
+ code: "service_unavailable",
980
+ exitCode: 6,
981
+ retryable: true,
982
+ });
983
+ }
984
+ if (!response.ok)
985
+ await downloadResponseError(response, runtime, url.toString(), session.accessToken);
986
+ if (!response.body) {
987
+ throw new StorageCommandError("Ornn returned an empty download stream.", {
988
+ code: "download_stream_missing",
989
+ exitCode: 1,
990
+ });
991
+ }
992
+ const hash = createHash("sha256");
993
+ const digesting = Readable.from(
994
+ (async function* () {
995
+ for await (const chunk of Readable.fromWeb(response.body)) {
996
+ hash.update(chunk);
997
+ yield chunk;
998
+ }
999
+ })()
1000
+ );
1001
+ try {
1002
+ await writeAtomicOutput(output, digesting, {
1003
+ mode: 0o644,
1004
+ overwrite: input.overwrite === true,
1005
+ });
1006
+ } catch (error) {
1007
+ throw safeStorageError(error, "The download could not be written safely.");
1008
+ }
1009
+ return runtime.interaction.result({
1010
+ data: {
1011
+ integrity: { sha256: hash.digest("hex") },
1012
+ output,
1013
+ path: remotePath,
1014
+ volume: safeVolume(drive),
1015
+ },
1016
+ outcome: { class: "ready", phase: "downloaded" },
1017
+ });
1018
+ }
1019
+
1020
+ async function deleteContent(command, input, runtime) {
1021
+ const kind = command === "storage.volumes.files.delete" ? "file" : "folder";
1022
+ const [reference, rawRemotePath] = positionals(
1023
+ input,
1024
+ 2,
1025
+ `Usage: ornn volumes ${kind === "file" ? "files" : "folders"} delete <volume> <remote-path>`
1026
+ );
1027
+ const remotePath = normalizeRemotePath(rawRemotePath);
1028
+ const declined = await unsafeReview(command, input, runtime, reference, remotePath);
1029
+ if (declined) return declined;
1030
+ const drive = await resolveVolume(runtime, reference);
1031
+ const endpoint = computeEndpoint(
1032
+ `${STORAGE_PREFIX}/${encodeURIComponent(String(drive.id))}/${kind}s/${encodedRemotePath(remotePath)}`
1033
+ );
1034
+ try {
1035
+ const payload = await runtime.fetch(endpoint, { method: "DELETE" });
1036
+ return runtime.interaction.result({
1037
+ data: {
1038
+ deleted: true,
1039
+ path: remotePath,
1040
+ ...(kind === "folder" ? { deleted_count: Number(payload?.folder?.deleted ?? 0) } : {}),
1041
+ volume: safeVolume(drive),
1042
+ },
1043
+ outcome: { class: "ready", phase: `${kind}_deleted` },
1044
+ });
1045
+ } catch (error) {
1046
+ const ambiguous =
1047
+ error instanceof CliApiError && (error.status === undefined || error.status >= 500);
1048
+ if (ambiguous) {
1049
+ throw new StorageCommandError(
1050
+ `The ${kind} deletion had an ambiguous outcome. Inspect the volume before doing anything else.`,
1051
+ {
1052
+ action: {
1053
+ command: `ornn volumes files list ${String(drive.id)}`,
1054
+ type: "command",
1055
+ },
1056
+ code: "content_mutation_outcome_unknown",
1057
+ exitCode: 6,
1058
+ retryable: false,
1059
+ }
1060
+ );
1061
+ }
1062
+ throw safeStorageError(error, `The volume ${kind} could not be deleted safely.`);
1063
+ }
1064
+ }
1065
+
1066
+ export async function executeStorageCommand(command, input, runtime) {
1067
+ switch (command) {
1068
+ case "storage.volumes.list":
1069
+ return listVolumes(input, runtime);
1070
+ case "storage.volumes.show":
1071
+ return showVolume(input, runtime);
1072
+ case "storage.volumes.create":
1073
+ return createVolume(input, runtime);
1074
+ case "storage.volumes.refresh":
1075
+ return showVolume(input, runtime, { refresh: true });
1076
+ case "storage.volumes.clear":
1077
+ return clearVolume(input, runtime);
1078
+ case "storage.volumes.delete":
1079
+ return deleteVolume(input, runtime);
1080
+ case "storage.volumes.files.list":
1081
+ return listFiles(input, runtime);
1082
+ case "storage.volumes.files.upload":
1083
+ return uploadFile(input, runtime);
1084
+ case "storage.volumes.files.download":
1085
+ return downloadFile(input, runtime);
1086
+ case "storage.volumes.files.delete":
1087
+ case "storage.volumes.folders.delete":
1088
+ return deleteContent(command, input, runtime);
1089
+ default:
1090
+ throw new StorageCommandError("Unknown storage command.", { code: "unknown_command" });
1091
+ }
1092
+ }
1093
+
1094
+ export function writeStorageResultHuman(stdout, capabilityId, execution) {
1095
+ const data = execution?.data;
1096
+ if (execution?.outcome?.phase === "declined") {
1097
+ stdout.write("No changes made.\n");
1098
+ return;
1099
+ }
1100
+ if (capabilityId === "storage.volumes.list") {
1101
+ const volumes = data?.volumes ?? [];
1102
+ if (!volumes.length) {
1103
+ stdout.write("No volumes found.\n");
1104
+ return;
1105
+ }
1106
+ for (const volume of volumes) {
1107
+ stdout.write(`${volume.name} ${volume.id} ${volume.phase} ${volume.size_bytes} bytes\n`);
1108
+ }
1109
+ if (execution.page?.next_cursor) {
1110
+ stdout.write(
1111
+ `Next: ornn volumes list --cursor ${execution.page.next_cursor} --format human\n`
1112
+ );
1113
+ }
1114
+ return;
1115
+ }
1116
+ if (capabilityId === "storage.volumes.files.list") {
1117
+ const files = data?.files ?? [];
1118
+ if (!files.length) {
1119
+ stdout.write("No files found.\n");
1120
+ return;
1121
+ }
1122
+ for (const file of files) stdout.write(`${file.path} ${file.size_bytes} bytes\n`);
1123
+ if (execution.page?.next_cursor) {
1124
+ stdout.write(
1125
+ `Next: ornn volumes files list ${data.volume.id} --cursor ${execution.page.next_cursor} --format human\n`
1126
+ );
1127
+ }
1128
+ return;
1129
+ }
1130
+ if (capabilityId === "storage.volumes.show" || capabilityId === "storage.volumes.refresh") {
1131
+ const volume = data?.volume;
1132
+ stdout.write(`${volume.name} ${volume.id} ${volume.phase} ${volume.size_bytes} bytes\n`);
1133
+ return;
1134
+ }
1135
+ if (capabilityId === "storage.volumes.create") {
1136
+ stdout.write(
1137
+ `${data.replayed ? "Found" : "Created"} volume ${data.volume.name} (${data.volume.id}).\n`
1138
+ );
1139
+ return;
1140
+ }
1141
+ if (capabilityId === "storage.volumes.clear") {
1142
+ stdout.write(`Cleared volume ${data.volume.name}.\n`);
1143
+ return;
1144
+ }
1145
+ if (capabilityId === "storage.volumes.delete") {
1146
+ stdout.write(`Deleted volume ${data.volume.name}.\n`);
1147
+ return;
1148
+ }
1149
+ if (capabilityId === "storage.volumes.files.upload") {
1150
+ stdout.write(`Uploaded ${data.file.path} to ${data.volume.name}.\n`);
1151
+ return;
1152
+ }
1153
+ if (capabilityId === "storage.volumes.files.download") {
1154
+ stdout.write(`Downloaded ${data.path} to ${data.output}.\n`);
1155
+ return;
1156
+ }
1157
+ if (capabilityId.endsWith("files.delete")) {
1158
+ stdout.write(`Deleted file ${data.path}.\n`);
1159
+ return;
1160
+ }
1161
+ if (capabilityId.endsWith("folders.delete")) {
1162
+ stdout.write(`Deleted folder ${data.path}.\n`);
1163
+ }
1164
+ }