@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,1354 @@
1
+ import { gpuLabeler } from "./gpu-catalog.mjs";
2
+ import { clusterFactLabel, reservationFactsForCluster } from "./kubernetes-commands.mjs";
3
+ import { listOwnedActiveMachines, resolveMachineReference } from "./machine-reference.mjs";
4
+ import { machineReachability } from "./vm-commands.mjs";
5
+
6
+ const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
7
+ const DEFAULT_HISTORY_MS = 15 * 60 * 1000;
8
+ const DEFAULT_LOG_LIMIT = 200;
9
+ const DEFAULT_MAX_POINTS = 240;
10
+ const DEFAULT_RETRY_WINDOW_MS = 5_000;
11
+ const DEFAULT_FOLLOW_INTERVAL_MS = 5_000;
12
+ const MAX_PAGE_SIZE = 500;
13
+ const MAX_RETRIES = 3;
14
+ const SAFE_METRICS = new Set([
15
+ "cpu_utilization",
16
+ "gpu_utilization",
17
+ "gpu_memory_utilization",
18
+ "gpu_power_w",
19
+ "gpu_tflops",
20
+ "memory_utilization",
21
+ "storage_utilization",
22
+ ]);
23
+ const TERMINAL_MACHINE_STATES = new Set(["deleted", "ended", "released", "terminated"]);
24
+ const UNSAFE_LOG_MESSAGE = [
25
+ /\b(?:provider(?:name|id|_name|_id)?|site_supplier|supplier(?:name|id|_name|_id|_organization_id|_organization_name)?|organization_?name)\s*[:=]/i,
26
+ /\b(?:authorization|api[_ -]?key|access[_ -]?token|refresh[_ -]?token|password|secret|private[_ -]?key)\s*[:=]/i,
27
+ /-----BEGIN [A-Z ]*PRIVATE KEY-----/i,
28
+ /\/(?:internal|v1\/(?:orchestrator|observability|cli\/operator))(?:\/|\b)/i,
29
+ /\b(?:staff_activity|staff activity|internal route|route)\s*[:=]/i,
30
+ ];
31
+ const UNSAFE_KEYS = new Set([
32
+ "authorization",
33
+ "clientsecret",
34
+ "organizationname",
35
+ "password",
36
+ "paymentsecret",
37
+ "privatekey",
38
+ "provider",
39
+ "providerid",
40
+ "providername",
41
+ "refreshtoken",
42
+ "sitesupplier",
43
+ "sitesupplierid",
44
+ "sitesuppliername",
45
+ "staffactivity",
46
+ "stripeclientsecret",
47
+ "supplier",
48
+ "supplierid",
49
+ "suppliername",
50
+ "supplierorganizationid",
51
+ "supplierorganizationname",
52
+ "token",
53
+ ]);
54
+ const STABLE_SOURCE_CODES = new Set([
55
+ "invalid_logs_response",
56
+ "invalid_metrics_response",
57
+ "logs_rejected",
58
+ "logs_unavailable",
59
+ "metrics_rejected",
60
+ "metrics_unavailable",
61
+ ]);
62
+
63
+ function record(value) {
64
+ return value && typeof value === "object" && !Array.isArray(value) ? value : {};
65
+ }
66
+
67
+ function text(value) {
68
+ return typeof value === "string" && value.trim() ? value.trim() : null;
69
+ }
70
+
71
+ function normalizedKey(value) {
72
+ return String(value)
73
+ .replaceAll(/[^a-z0-9]/gi, "")
74
+ .toLowerCase();
75
+ }
76
+
77
+ // A node, listing, or facility's organization is its supplier; a reservation's
78
+ // or drive's is the buyer, which the caller may see.
79
+ export function supplierOwnedRecord(value) {
80
+ const keys = new Set(Object.keys(record(value)).map(normalizedKey));
81
+ if (keys.has("driveid")) return false;
82
+ if (keys.has("nodeid")) return true;
83
+ if (keys.has("reservationid")) return false;
84
+ return keys.has("listingid") || keys.has("facilityid");
85
+ }
86
+
87
+ function containsUnsafeFields(value) {
88
+ if (Array.isArray(value)) return value.some(containsUnsafeFields);
89
+ if (!value || typeof value !== "object") return false;
90
+ const ownedBySupplier = supplierOwnedRecord(value);
91
+ return Object.entries(value).some(([key, child]) => {
92
+ const normalized = normalizedKey(key);
93
+ return (
94
+ UNSAFE_KEYS.has(normalized) ||
95
+ (ownedBySupplier && normalized === "organizationid") ||
96
+ normalized.includes("secret") ||
97
+ normalized.includes("token") ||
98
+ normalized.includes("password") ||
99
+ normalized.includes("privatekey") ||
100
+ normalized.includes("apikey") ||
101
+ normalized.includes("accesskey") ||
102
+ normalized.includes("credential") ||
103
+ containsUnsafeFields(child)
104
+ );
105
+ });
106
+ }
107
+
108
+ function machineId(value) {
109
+ const source = record(value);
110
+ return text(source.id) ?? text(source.machine_id) ?? text(source.instance_id);
111
+ }
112
+
113
+ function reservationId(value) {
114
+ const source = record(value);
115
+ return text(source.reservation_id) ?? text(source.reservationId);
116
+ }
117
+
118
+ function hostname(value) {
119
+ const source = record(value);
120
+ return text(source.hostname) ?? text(source.machine_hostname) ?? text(source.site_label);
121
+ }
122
+
123
+ function stateToken(value) {
124
+ return (
125
+ (text(value) ?? "unknown")
126
+ .toLowerCase()
127
+ .replaceAll(/[^a-z0-9]+/g, "_")
128
+ .replaceAll(/^_+|_+$/g, "") || "unknown"
129
+ );
130
+ }
131
+
132
+ function integerOption(interaction, value, name, fallback, maximum = MAX_PAGE_SIZE) {
133
+ if (value === undefined) return fallback;
134
+ if (!/^\d+$/.test(String(value))) {
135
+ interaction.usage(`--${name} must be an integer from 1 to ${maximum}.`, {
136
+ code: "invalid_input",
137
+ field: name.replace(/-([a-z])/g, (_, letter) => letter.toUpperCase()),
138
+ });
139
+ }
140
+ const parsed = Number(value);
141
+ if (parsed < 1 || parsed > maximum) {
142
+ interaction.usage(`--${name} must be an integer from 1 to ${maximum}.`, {
143
+ code: "invalid_input",
144
+ field: name.replace(/-([a-z])/g, (_, letter) => letter.toUpperCase()),
145
+ });
146
+ }
147
+ return parsed;
148
+ }
149
+
150
+ function durationMilliseconds(interaction, value, name) {
151
+ if (value === undefined) return null;
152
+ const match = String(value)
153
+ .trim()
154
+ .match(/^(\d+(?:\.\d+)?)(ms|s|m|h)$/i);
155
+ if (!match) {
156
+ interaction.usage(`--${name} must be a positive duration such as 500ms, 30s, 5m, or 1h.`, {
157
+ code: "invalid_input",
158
+ field: name,
159
+ });
160
+ }
161
+ const unit = match[2].toLowerCase();
162
+ const multiplier = unit === "ms" ? 1 : unit === "s" ? 1_000 : unit === "m" ? 60_000 : 3_600_000;
163
+ const milliseconds = Number(match[1]) * multiplier;
164
+ if (!Number.isFinite(milliseconds) || milliseconds < 1) {
165
+ interaction.usage(`--${name} must be a positive duration.`, {
166
+ code: "invalid_input",
167
+ field: name,
168
+ });
169
+ }
170
+ return Math.round(milliseconds);
171
+ }
172
+
173
+ function nowMilliseconds(now) {
174
+ const value = typeof now === "function" ? now() : Date.now();
175
+ if (value instanceof Date) return value.getTime();
176
+ return typeof value === "number" && Number.isFinite(value) ? value : Date.now();
177
+ }
178
+
179
+ function timestamp(interaction, value, field) {
180
+ const source = String(value);
181
+ const rfc3339 = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})$/i;
182
+ const milliseconds = rfc3339.test(source) ? Date.parse(source) : Number.NaN;
183
+ if (!Number.isFinite(milliseconds)) {
184
+ interaction.usage(`--${field} must be an RFC 3339 timestamp.`, {
185
+ code: "invalid_input",
186
+ field,
187
+ });
188
+ }
189
+ return milliseconds;
190
+ }
191
+
192
+ function evidenceWindow(input, interaction, now, { follow = false, logsOnly = false } = {}) {
193
+ if (input.before !== undefined && !logsOnly) {
194
+ interaction.usage("--before is available only for a logs command.", {
195
+ code: "invalid_input",
196
+ field: "before",
197
+ });
198
+ }
199
+ if (input.before !== undefined && (input.start !== undefined || input.end !== undefined)) {
200
+ interaction.usage("--before cannot be combined with --start or --end.", {
201
+ code: "invalid_input",
202
+ field: "before",
203
+ });
204
+ }
205
+ if ((input.start === undefined) !== (input.end === undefined)) {
206
+ interaction.usage("--start and --end must be supplied together.", {
207
+ code: "metric_window_incomplete",
208
+ field: input.start === undefined ? "start" : "end",
209
+ });
210
+ }
211
+ if (follow && input.end !== undefined) {
212
+ interaction.usage("--end cannot be combined with --follow.", {
213
+ code: "invalid_input",
214
+ field: "end",
215
+ });
216
+ }
217
+
218
+ const effectiveNow = nowMilliseconds(now);
219
+ const endMs =
220
+ input.before !== undefined
221
+ ? timestamp(interaction, input.before, "before")
222
+ : input.end !== undefined
223
+ ? timestamp(interaction, input.end, "end")
224
+ : effectiveNow;
225
+ const startMs =
226
+ input.start !== undefined
227
+ ? timestamp(interaction, input.start, "start")
228
+ : endMs - DEFAULT_HISTORY_MS;
229
+ if (startMs >= endMs) {
230
+ interaction.usage("The evidence window requires --start before --end.", {
231
+ code: "invalid_metric_window",
232
+ field: "start",
233
+ });
234
+ }
235
+ return {
236
+ start: new Date(startMs).toISOString(),
237
+ end: new Date(endMs).toISOString(),
238
+ startMs,
239
+ endMs,
240
+ };
241
+ }
242
+
243
+ function lifecycle(value, kind) {
244
+ const source = record(value);
245
+ const state = stateToken(source.state ?? source.status);
246
+ if (["deleted", "ended", "released", "terminated", "torn_down"].includes(state)) {
247
+ return { class: "ended", phase: kind === "cluster" ? "torn_down" : "ended" };
248
+ }
249
+ if (["failed", "error", "unavailable"].includes(state)) {
250
+ return { class: "failed", phase: "failed" };
251
+ }
252
+ if (["active", "online", "ready", "running", "provisioned"].includes(state)) {
253
+ return { class: "ready", phase: kind === "cluster" ? "access_ready" : "active" };
254
+ }
255
+ if (["creating", "pending", "provisioning", "starting", "switching"].includes(state)) {
256
+ return { class: "working", phase: "provisioning" };
257
+ }
258
+ return { class: "pending", phase: "unknown" };
259
+ }
260
+
261
+ function reachability(value) {
262
+ const source = record(value);
263
+ const explicit = source.reachable;
264
+ if (explicit === true) return { state: "reachable", observed_at: null };
265
+ if (explicit === false) return { state: "unreachable", observed_at: null };
266
+ const candidate = record(source.reachability);
267
+ const offGrid = record(source.off_grid_reachability);
268
+ const state = stateToken(candidate.state ?? candidate.status ?? offGrid.state ?? offGrid.status);
269
+ const phase = stateToken(candidate.phase ?? offGrid.phase);
270
+ const observedAt =
271
+ text(candidate.observed_at) ?? text(candidate.checked_at) ?? text(offGrid.last_checked_at);
272
+ if (["reachable", "live", "online"].includes(state) || phase === "ssh_ready")
273
+ return { state: "reachable", observed_at: observedAt };
274
+ if (
275
+ ["unreachable", "offline", "off_grid"].includes(state) ||
276
+ ["ssh_unreachable", "unreachable"].includes(phase)
277
+ )
278
+ return { state: "unreachable", observed_at: observedAt };
279
+ const inferred = machineReachability(source);
280
+ if (inferred.class === "ready") return { state: "reachable", observed_at: observedAt };
281
+ if (inferred.class === "failed") return { state: "unreachable", observed_at: observedAt };
282
+ return { state: "unknown", observed_at: observedAt };
283
+ }
284
+
285
+ function authoritativeHealth(value) {
286
+ const source = record(value);
287
+ const assessment = record(source.health_assessment ?? source.authoritative_health);
288
+ const explicitHealth = record(source.health);
289
+ const authoritative = Object.keys(assessment).length
290
+ ? assessment
291
+ : explicitHealth.authoritative === true
292
+ ? explicitHealth
293
+ : {};
294
+ const state = stateToken(authoritative.state ?? authoritative.status);
295
+ return {
296
+ state: ["healthy", "degraded", "unhealthy"].includes(state) ? state : "unknown",
297
+ assessed_at: text(authoritative.assessed_at) ?? text(authoritative.observed_at),
298
+ };
299
+ }
300
+
301
+ function projectMachine(value, fallbackReservationId = null) {
302
+ const source = record(record(value).machine ?? value);
303
+ const id = machineId(source);
304
+ const reservation = reservationId(source) ?? fallbackReservationId;
305
+ if (!id || !reservation) return null;
306
+ const mode = stateToken(source.access_mode ?? source.mode);
307
+ const accessMode =
308
+ mode === "bare_metal"
309
+ ? "bare-metal"
310
+ : ["virtual_machine", "vm"].includes(mode)
311
+ ? "vm"
312
+ : "unknown";
313
+ return {
314
+ id,
315
+ hostname: hostname(source),
316
+ reservation_id: reservation,
317
+ access_mode: accessMode,
318
+ lifecycle: lifecycle(source, "machine"),
319
+ reachability: reachability(source),
320
+ health: authoritativeHealth(source),
321
+ source,
322
+ };
323
+ }
324
+
325
+ function memberLifecycle(value) {
326
+ const state = stateToken(value);
327
+ if (["ready", "active", "running", "joined", "member"].includes(state)) {
328
+ return { class: "ready", phase: "member" };
329
+ }
330
+ if (["failed", "error", "cleanup_failed", "join_failed"].includes(state)) {
331
+ return { class: "failed", phase: "member_failed" };
332
+ }
333
+ if (["deleted", "removed", "detached"].includes(state)) {
334
+ return { class: "ended", phase: "removed" };
335
+ }
336
+ return { class: "working", phase: "membership_pending" };
337
+ }
338
+
339
+ function clusterLifecycle(source, machines) {
340
+ const state = stateToken(source.state ?? source.status);
341
+ const credentialState = stateToken(source.credential_state);
342
+ if (["deleted", "ended", "released", "terminated", "torn_down"].includes(state)) {
343
+ return { class: "ended", phase: "torn_down" };
344
+ }
345
+ if (state === "deleting") return { class: "working", phase: "tearing_down" };
346
+ if (["failed", "error", "unavailable"].includes(state)) {
347
+ return { class: machines.length ? "partial" : "failed", phase: "provisioning_failed" };
348
+ }
349
+ if (machines.some((machine) => machine.lifecycle.class === "failed")) {
350
+ return { class: "partial", phase: "member_failed" };
351
+ }
352
+ if (state === "active" && ["ready", "active", "available"].includes(credentialState)) {
353
+ return { class: "ready", phase: "access_ready" };
354
+ }
355
+ if (state === "active") return { class: "working", phase: "credentials_pending" };
356
+ if (["provisioning", "creating", "pending"].includes(state)) {
357
+ return { class: "working", phase: "provisioning" };
358
+ }
359
+ return { class: "pending", phase: "unknown" };
360
+ }
361
+
362
+ function projectCluster(value) {
363
+ const source = record(value);
364
+ const id = text(source.id) ?? text(source.cluster_id);
365
+ const reservation = reservationId(source);
366
+ if (!id || !reservation) return null;
367
+ const rawMachines = Array.isArray(source.machines)
368
+ ? source.machines
369
+ : Array.isArray(source.members)
370
+ ? source.members
371
+ : [];
372
+ const machines = rawMachines.flatMap((member) => {
373
+ const source = record(member);
374
+ const id = machineId(source) ?? text(source.node_id);
375
+ return id
376
+ ? [
377
+ {
378
+ id,
379
+ hostname: hostname(source),
380
+ lifecycle: memberLifecycle(source.state ?? source.membership_state),
381
+ },
382
+ ]
383
+ : [];
384
+ });
385
+ return {
386
+ id,
387
+ reservation_id: reservation,
388
+ access_mode: ["kubernetes", "slurm"].includes(stateToken(source.access_mode))
389
+ ? stateToken(source.access_mode)
390
+ : "unknown",
391
+ lifecycle: clusterLifecycle(source, machines),
392
+ gpu_type: text(source.gpu_type ?? source.hardware),
393
+ reachability: reachability(source),
394
+ health: authoritativeHealth(source),
395
+ machines,
396
+ source,
397
+ };
398
+ }
399
+
400
+ function payloadRows(payload, key) {
401
+ if (Array.isArray(payload)) return payload;
402
+ const rows = record(payload)[key];
403
+ return Array.isArray(rows) ? rows : null;
404
+ }
405
+
406
+ async function activeMachines(fetch, interaction) {
407
+ const rows = await listOwnedActiveMachines({ fetch }, interaction);
408
+ return rows.flatMap((row) => {
409
+ const projected = projectMachine(row);
410
+ return projected ? [projected] : [];
411
+ });
412
+ }
413
+
414
+ async function clusters(fetch, interaction) {
415
+ const payload = await fetch("/v1/workflows/compute/clusters");
416
+ const rows = payloadRows(payload, "clusters");
417
+ if (!rows)
418
+ interaction.error("The gateway returned an invalid cluster collection.", {
419
+ code: "invalid_gateway_response",
420
+ });
421
+ return rows
422
+ .flatMap((row) => {
423
+ const projected = projectCluster(row);
424
+ return projected ? [projected] : [];
425
+ })
426
+ .sort(
427
+ (left, right) =>
428
+ left.reservation_id.localeCompare(right.reservation_id) || left.id.localeCompare(right.id)
429
+ );
430
+ }
431
+
432
+ function resourceChoice(resource, gpuLabel) {
433
+ if (resource.kind === "machine") {
434
+ return {
435
+ label: `VM/Bare Metal · ${resource.machine.hostname ?? "Unnamed machine"} · ${resource.machine.lifecycle.class}`,
436
+ value: `machine:${resource.machine.id}`,
437
+ };
438
+ }
439
+ return {
440
+ label: clusterFactLabel(resource.cluster, resource.reservation_facts, gpuLabel),
441
+ value: `cluster:${resource.cluster.id}`,
442
+ };
443
+ }
444
+
445
+ async function resolveResource(command, input, fetch, interaction) {
446
+ const expectedKind = command.startsWith("machines.")
447
+ ? "machine"
448
+ : command.startsWith("clusters.")
449
+ ? "cluster"
450
+ : null;
451
+ const positionals = input.$positionals ?? [];
452
+ if (positionals.length > 1) interaction.usage("Supply exactly one machine or cluster reference.");
453
+ const reference = text(positionals[0]);
454
+
455
+ if (reference && expectedKind !== "cluster") {
456
+ const source = await resolveMachineReference(reference, { fetch }, interaction, {
457
+ allowMiss: expectedKind === null,
458
+ });
459
+ if (source) {
460
+ const machine = projectMachine(source);
461
+ if (!machine) {
462
+ interaction.error("The gateway returned an invalid machine.", {
463
+ code: "invalid_gateway_response",
464
+ });
465
+ }
466
+ return { kind: "machine", machine };
467
+ }
468
+ }
469
+ if (reference && UUID.test(reference) && expectedKind !== "machine") {
470
+ const matches = (await clusters(fetch, interaction)).filter(
471
+ (cluster) => cluster.id === reference
472
+ );
473
+ if (matches.length === 1) return { kind: "cluster", cluster: matches[0] };
474
+ if (matches.length > 1)
475
+ interaction.error("The gateway returned duplicate canonical cluster IDs.", {
476
+ code: "cluster_ambiguous",
477
+ });
478
+ }
479
+ if (reference && expectedKind === "cluster") {
480
+ interaction.usage(
481
+ "Clusters are referenced only by canonical ID or an invocation-local guided choice.",
482
+ {
483
+ code: "canonical_cluster_id_required",
484
+ field: "cluster",
485
+ }
486
+ );
487
+ }
488
+ if (reference) {
489
+ interaction.error(
490
+ `${expectedKind === "cluster" ? "Cluster" : "Machine or cluster"} not found: ${reference}.`,
491
+ {
492
+ code: expectedKind === "cluster" ? "cluster_not_found" : "resource_not_found",
493
+ }
494
+ );
495
+ }
496
+
497
+ if (expectedKind && input.guided !== true) {
498
+ interaction.usage(`A ${expectedKind} reference is required unless --guided is supplied.`, {
499
+ code: `${expectedKind}_reference_required`,
500
+ field: expectedKind,
501
+ });
502
+ }
503
+ if (!interaction.eligibleHuman || interaction.machine) {
504
+ interaction.usage("A machine or cluster reference is required outside an eligible terminal.", {
505
+ code: "resource_reference_required",
506
+ field: "resource",
507
+ });
508
+ }
509
+
510
+ const resources = [];
511
+ if (expectedKind !== "cluster") {
512
+ resources.push(
513
+ ...(await activeMachines(fetch, interaction)).map((machine) => ({ kind: "machine", machine }))
514
+ );
515
+ }
516
+ if (expectedKind !== "machine") {
517
+ resources.push(
518
+ ...(await clusters(fetch, interaction)).map((cluster) => ({ kind: "cluster", cluster }))
519
+ );
520
+ }
521
+ if (!resources.length)
522
+ interaction.error("No machine or cluster is available in this organization.", {
523
+ code: "resource_not_found",
524
+ });
525
+ if (resources.length === 1) return resources[0];
526
+ const enriched = await Promise.all(
527
+ resources.map(async (resource) => {
528
+ if (resource.kind === "machine") return resource;
529
+ return {
530
+ ...resource,
531
+ reservation_facts: await reservationFactsForCluster(fetch, interaction, resource.cluster),
532
+ };
533
+ })
534
+ );
535
+ const gpuLabel = await gpuLabeler(fetch);
536
+ const choices = enriched.map((resource) => resourceChoice(resource, gpuLabel));
537
+ const selected = await interaction.ask({ label: "Resource", choices });
538
+ const match = enriched.find((resource) => resourceChoice(resource, gpuLabel).value === selected);
539
+ if (!match)
540
+ interaction.usage("Choose one exact machine or cluster.", { code: "resource_ambiguous" });
541
+ return match;
542
+ }
543
+
544
+ function resourceBase(resource) {
545
+ const source = resource.kind === "machine" ? resource.machine : resource.cluster;
546
+ return {
547
+ resource: {
548
+ kind: resource.kind,
549
+ id: source.id,
550
+ reservation_id: source.reservation_id,
551
+ ...(resource.kind === "machine"
552
+ ? { hostname: source.hostname, access_mode: source.access_mode }
553
+ : { access_mode: source.access_mode, membership_count: source.machines.length }),
554
+ },
555
+ lifecycle: source.lifecycle,
556
+ reachability: source.reachability,
557
+ health: source.health,
558
+ metrics: null,
559
+ logs: null,
560
+ };
561
+ }
562
+
563
+ function sourceMachines(resource) {
564
+ return resource.kind === "machine"
565
+ ? [{ id: resource.machine.id, hostname: resource.machine.hostname }]
566
+ : resource.cluster.machines;
567
+ }
568
+
569
+ function errorDetail(error) {
570
+ const body = record(error?.detail);
571
+ const candidate = body.detail ?? body.error ?? body;
572
+ if (typeof candidate === "string") return { code: candidate };
573
+ return record(candidate);
574
+ }
575
+
576
+ function sourceFailure(error, source, resourceRef) {
577
+ const status = error?.status;
578
+ const detail = errorDetail(error);
579
+ const suppliedCode = text(detail.code);
580
+ const explicitRetryable = typeof detail.retryable === "boolean" ? detail.retryable : null;
581
+ const authorization = status === 401 || status === 403;
582
+ const retryable = authorization
583
+ ? false
584
+ : (explicitRetryable ??
585
+ (status === undefined || status >= 500 || [408, 425, 429].includes(status)));
586
+ return {
587
+ code: authorization
588
+ ? status === 401
589
+ ? "authentication_required"
590
+ : "authorization_denied"
591
+ : suppliedCode && STABLE_SOURCE_CODES.has(suppliedCode)
592
+ ? suppliedCode
593
+ : status !== undefined && status < 500
594
+ ? `${source}_rejected`
595
+ : `${source}_unavailable`,
596
+ message: authorization
597
+ ? status === 401
598
+ ? "The Ornn session expired while reading evidence."
599
+ : "The current session cannot read this evidence."
600
+ : `${source === "metrics" ? "Metrics" : "Logs"} are unavailable for this resource.`,
601
+ retryable,
602
+ resource_ref: resourceRef,
603
+ authorization,
604
+ };
605
+ }
606
+
607
+ async function sleepUntil(delay, deps, signal) {
608
+ if (signal?.aborted) return false;
609
+ if (!signal) {
610
+ await deps.sleep(delay);
611
+ return true;
612
+ }
613
+ return new Promise((resolve, reject) => {
614
+ let settled = false;
615
+ const finish = (value) => {
616
+ if (settled) return;
617
+ settled = true;
618
+ signal.removeEventListener("abort", aborted);
619
+ resolve(value);
620
+ };
621
+ const aborted = () => finish(false);
622
+ signal.addEventListener("abort", aborted, { once: true });
623
+ Promise.resolve(deps.sleep(delay)).then(() => finish(true), reject);
624
+ });
625
+ }
626
+
627
+ async function fetchEvidence(request, source, resourceRef, deps, deadline, signal) {
628
+ let attempt = 0;
629
+ for (;;) {
630
+ attempt += 1;
631
+ const remaining = deadline - nowMilliseconds(deps.now);
632
+ if (Number.isFinite(deadline) && remaining <= 0) {
633
+ return {
634
+ value: null,
635
+ error: sourceFailure(new Error("deadline elapsed"), source, resourceRef),
636
+ interrupted: false,
637
+ };
638
+ }
639
+ const timeoutSignal = Number.isFinite(deadline)
640
+ ? AbortSignal.timeout(Math.max(1, Math.min(remaining, 2_147_483_647)))
641
+ : null;
642
+ const requestSignal =
643
+ signal && timeoutSignal
644
+ ? AbortSignal.any([signal, timeoutSignal])
645
+ : (signal ?? timeoutSignal);
646
+ try {
647
+ return { value: await request(requestSignal), error: null, interrupted: false };
648
+ } catch (error) {
649
+ if (signal?.aborted) return { value: null, error: null, interrupted: true };
650
+ const failure = sourceFailure(error, source, resourceRef);
651
+ if (
652
+ timeoutSignal?.aborted ||
653
+ failure.authorization ||
654
+ !failure.retryable ||
655
+ attempt >= MAX_RETRIES
656
+ ) {
657
+ return { value: null, error: failure, interrupted: false };
658
+ }
659
+ const delay = Math.min(250 * 2 ** (attempt - 1), 1_000);
660
+ if (
661
+ nowMilliseconds(deps.now) + delay >= deadline ||
662
+ !(await sleepUntil(delay, deps, signal))
663
+ ) {
664
+ if (signal?.aborted) return { value: null, error: null, interrupted: true };
665
+ return { value: null, error: failure, interrupted: false };
666
+ }
667
+ }
668
+ }
669
+ }
670
+
671
+ function safeMetricName(name) {
672
+ const suffix = name.match(/_(?:min|max|median|std)$/)?.[0] ?? "";
673
+ const base = suffix ? name.slice(0, -suffix.length) : name;
674
+ return SAFE_METRICS.has(base);
675
+ }
676
+
677
+ function safeMetricValue(value) {
678
+ if (typeof value === "boolean") return value;
679
+ if (typeof value === "number") return Number.isFinite(value) ? value : null;
680
+ if (typeof value !== "string" || !/^-?(?:\d+\.?\d*|\.\d+)(?:e[+-]?\d+)?$/i.test(value.trim())) {
681
+ return null;
682
+ }
683
+ const numeric = Number(value);
684
+ return Number.isFinite(numeric) ? numeric : null;
685
+ }
686
+
687
+ function projectMetricPoint(value) {
688
+ const source = record(value);
689
+ const observedAt = text(source.observed_at) ?? text(source.timestamp) ?? text(source.date);
690
+ const rawMetrics = record(source.metrics ?? source.values ?? source.resource_metrics);
691
+ const metrics = {};
692
+ for (const [name, raw] of Object.entries(rawMetrics)) {
693
+ const metric = safeMetricValue(raw);
694
+ if (safeMetricName(name) && metric !== null) metrics[name] = metric;
695
+ }
696
+ if (!observedAt || !Object.keys(metrics).length) return null;
697
+ return { observed_at: observedAt, metrics };
698
+ }
699
+
700
+ function withinEvidenceWindow(observedAt, window) {
701
+ const milliseconds = Date.parse(observedAt);
702
+ return (
703
+ Number.isFinite(milliseconds) && milliseconds >= window.startMs && milliseconds <= window.endMs
704
+ );
705
+ }
706
+
707
+ function metricPoints(payload, maximum, window) {
708
+ const source = record(payload);
709
+ let raw;
710
+ if (Array.isArray(source.points)) {
711
+ raw = source.points;
712
+ } else if (Array.isArray(source.series)) {
713
+ raw = source.series.flatMap((series) =>
714
+ Array.isArray(record(series).points) ? series.points : []
715
+ );
716
+ } else if (source.machine && typeof source.machine === "object") {
717
+ raw = [
718
+ {
719
+ observed_at:
720
+ source.machine.observed_at ?? source.machine.resource_metrics?.last_heartbeat_at,
721
+ resource_metrics: source.machine.resource_metrics,
722
+ },
723
+ ];
724
+ } else {
725
+ return null;
726
+ }
727
+ return raw
728
+ .flatMap((point) => {
729
+ const projected = projectMetricPoint(point);
730
+ return projected && withinEvidenceWindow(projected.observed_at, window) ? [projected] : [];
731
+ })
732
+ .sort((left, right) => left.observed_at.localeCompare(right.observed_at))
733
+ .slice(0, maximum);
734
+ }
735
+
736
+ function safeLogLine(value) {
737
+ if (containsUnsafeFields(value)) return null;
738
+ const source = record(value);
739
+ const observedAt = text(source.date) ?? text(source.observed_at) ?? text(source.timestamp);
740
+ const message = text(source.message) ?? text(source.text);
741
+ if (!observedAt || !message || UNSAFE_LOG_MESSAGE.some((pattern) => pattern.test(message)))
742
+ return null;
743
+ const severity = stateToken(source.severity ?? "info");
744
+ return {
745
+ observed_at: observedAt,
746
+ severity: ["error", "warning", "info"].includes(severity) ? severity : "info",
747
+ message,
748
+ };
749
+ }
750
+
751
+ function logLines(payload, maximum, window) {
752
+ const raw = payloadRows(payload, "lines");
753
+ if (!raw) return null;
754
+ return raw
755
+ .flatMap((line) => {
756
+ const projected = safeLogLine(line);
757
+ return projected && withinEvidenceWindow(projected.observed_at, window) ? [projected] : [];
758
+ })
759
+ .sort(
760
+ (left, right) =>
761
+ left.observed_at.localeCompare(right.observed_at) ||
762
+ left.message.localeCompare(right.message)
763
+ )
764
+ .slice(0, maximum);
765
+ }
766
+
767
+ function invalidEvidenceResponse(code) {
768
+ const error = new Error("invalid evidence response");
769
+ error.status = 502;
770
+ error.detail = { code, retryable: true };
771
+ return error;
772
+ }
773
+
774
+ function query(params) {
775
+ return new URLSearchParams(
776
+ Object.entries(params).filter(([, value]) => value !== null && value !== undefined)
777
+ ).toString();
778
+ }
779
+
780
+ async function metricSource(machine, reservation, window, maximum, deps, deadline, signal, live) {
781
+ const path = live ? "live-metrics" : "telemetry-history";
782
+ const params = live
783
+ ? { machine_id: machine.id }
784
+ : { machine_id: machine.id, start: window.start, end: window.end, max_points: String(maximum) };
785
+ const response = await fetchEvidence(
786
+ async (requestSignal) => {
787
+ const payload = await deps.fetch(
788
+ `/v1/workflows/compute/nodes/reservations/${encodeURIComponent(reservation)}/${path}?${query(params)}`,
789
+ { signal: requestSignal }
790
+ );
791
+ const points = metricPoints(payload, maximum, window);
792
+ if (points === null) throw invalidEvidenceResponse("invalid_metrics_response");
793
+ return points;
794
+ },
795
+ "metrics",
796
+ machine.id,
797
+ deps,
798
+ deadline,
799
+ signal
800
+ );
801
+ if (response.interrupted) {
802
+ return {
803
+ machine_ref: machine.id,
804
+ state: "interrupted",
805
+ points: [],
806
+ error: null,
807
+ interrupted: true,
808
+ };
809
+ }
810
+ if (response.error) {
811
+ return { machine_ref: machine.id, state: "unavailable", points: [], error: response.error };
812
+ }
813
+ const points = response.value;
814
+ return {
815
+ machine_ref: machine.id,
816
+ state: points.length ? "available" : "no_data",
817
+ points,
818
+ error: null,
819
+ };
820
+ }
821
+
822
+ async function logSource(
823
+ machine,
824
+ reservation,
825
+ window,
826
+ stream,
827
+ limit,
828
+ cursor,
829
+ deps,
830
+ deadline,
831
+ signal
832
+ ) {
833
+ const response = await fetchEvidence(
834
+ async (requestSignal) => {
835
+ const payload = await deps.fetch(
836
+ `/v1/workflows/compute/nodes/reservations/${encodeURIComponent(reservation)}/logs?${query({
837
+ machine_id: machine.id,
838
+ stream,
839
+ start: window.start,
840
+ end: window.end,
841
+ limit: String(limit),
842
+ cursor,
843
+ })}`,
844
+ { signal: requestSignal }
845
+ );
846
+ const entries = logLines(payload, limit, window);
847
+ if (entries === null) throw invalidEvidenceResponse("invalid_logs_response");
848
+ return {
849
+ entries,
850
+ nextCursor: text(record(payload).next_cursor) ?? text(record(payload).nextCursor),
851
+ };
852
+ },
853
+ "logs",
854
+ machine.id,
855
+ deps,
856
+ deadline,
857
+ signal
858
+ );
859
+ if (response.interrupted) {
860
+ return {
861
+ machine_ref: machine.id,
862
+ state: "interrupted",
863
+ entries: [],
864
+ page: { limit, returned: 0, next_cursor: null },
865
+ error: null,
866
+ interrupted: true,
867
+ };
868
+ }
869
+ if (response.error) {
870
+ return {
871
+ machine_ref: machine.id,
872
+ state: "unavailable",
873
+ entries: [],
874
+ page: { limit, returned: 0, next_cursor: null },
875
+ error: response.error,
876
+ };
877
+ }
878
+ const { entries, nextCursor } = response.value;
879
+ return {
880
+ machine_ref: machine.id,
881
+ state: entries.length ? "available" : "no_data",
882
+ entries,
883
+ page: { limit, returned: entries.length, next_cursor: nextCursor },
884
+ error: null,
885
+ };
886
+ }
887
+
888
+ function aggregateState(sources, itemField) {
889
+ const unavailable = sources.filter((source) => source.state === "unavailable").length;
890
+ const useful = sources.some((source) => source[itemField].length > 0);
891
+ if (unavailable === sources.length && sources.length) return "unavailable";
892
+ if (unavailable) return "partial";
893
+ return useful ? "available" : "no_data";
894
+ }
895
+
896
+ function publicMetricSource(source) {
897
+ return {
898
+ machine_ref: source.machine_ref,
899
+ state: source.state,
900
+ points: source.points,
901
+ ...(source.error
902
+ ? {
903
+ error: {
904
+ code: source.error.code,
905
+ message: source.error.message,
906
+ retryable: source.error.retryable,
907
+ },
908
+ }
909
+ : {}),
910
+ };
911
+ }
912
+
913
+ function publicLogSource(source) {
914
+ return {
915
+ machine_ref: source.machine_ref,
916
+ state: source.state,
917
+ entries: source.entries,
918
+ page: source.page,
919
+ ...(source.error
920
+ ? {
921
+ error: {
922
+ code: source.error.code,
923
+ message: source.error.message,
924
+ retryable: source.error.retryable,
925
+ },
926
+ }
927
+ : {}),
928
+ };
929
+ }
930
+
931
+ async function collectEvidence(
932
+ resource,
933
+ requested,
934
+ window,
935
+ options,
936
+ deps,
937
+ signal,
938
+ { live = false } = {}
939
+ ) {
940
+ const machines = sourceMachines(resource);
941
+ const reservation =
942
+ resource.kind === "machine" ? resource.machine.reservation_id : resource.cluster.reservation_id;
943
+ const deadline = options.deadline;
944
+ const errors = [];
945
+ let metrics = null;
946
+ let logs = null;
947
+ let interrupted = false;
948
+
949
+ if (requested.metrics) {
950
+ const sources = [];
951
+ for (const machine of machines) {
952
+ const source = await metricSource(
953
+ machine,
954
+ reservation,
955
+ window,
956
+ options.maxPoints,
957
+ deps,
958
+ deadline,
959
+ signal,
960
+ live
961
+ );
962
+ sources.push(source);
963
+ if (source.interrupted) {
964
+ interrupted = true;
965
+ break;
966
+ }
967
+ if (source.error) errors.push(source.error);
968
+ if (source.error?.authorization) break;
969
+ }
970
+ metrics = {
971
+ state: aggregateState(sources, "points"),
972
+ window: { start: window.start, end: window.end, max_points: options.maxPoints },
973
+ series: sources.map(publicMetricSource),
974
+ };
975
+ }
976
+
977
+ if (requested.logs && !interrupted && !errors.some((error) => error.authorization)) {
978
+ const sources = [];
979
+ for (const machine of machines) {
980
+ const source = await logSource(
981
+ machine,
982
+ reservation,
983
+ window,
984
+ options.stream,
985
+ options.limit,
986
+ options.cursor,
987
+ deps,
988
+ deadline,
989
+ signal
990
+ );
991
+ sources.push(source);
992
+ if (source.interrupted) {
993
+ interrupted = true;
994
+ break;
995
+ }
996
+ if (source.error) errors.push(source.error);
997
+ if (source.error?.authorization) break;
998
+ }
999
+ logs = {
1000
+ state: aggregateState(sources, "entries"),
1001
+ stream: options.stream,
1002
+ window: { start: window.start, end: window.end },
1003
+ sources: sources.map(publicLogSource),
1004
+ };
1005
+ }
1006
+
1007
+ return { errors, interrupted, logs, metrics };
1008
+ }
1009
+
1010
+ function publicErrors(errors) {
1011
+ return errors.map(({ authorization: _authorization, ...error }) => error);
1012
+ }
1013
+
1014
+ function snapshotExit(errors, requestedCount) {
1015
+ if (!errors.length) return { exitCode: 0, outcome: null };
1016
+ if (errors.some((error) => error.authorization)) {
1017
+ return {
1018
+ exitCode: 5,
1019
+ outcome: { class: "failed", phase: errors.find((error) => error.authorization).code },
1020
+ };
1021
+ }
1022
+ if (errors.length < requestedCount) {
1023
+ return { exitCode: 4, outcome: { class: "partial", phase: "evidence_partial" } };
1024
+ }
1025
+ if (errors.every((error) => error.retryable)) {
1026
+ return { exitCode: 6, outcome: { class: "failed", phase: "evidence_unavailable" } };
1027
+ }
1028
+ return { exitCode: 1, outcome: { class: "failed", phase: "evidence_failed" } };
1029
+ }
1030
+
1031
+ function evidenceSourceLabel(data, index) {
1032
+ if (data.resource.kind === "machine") return data.resource.hostname ?? "Machine";
1033
+ return `Cluster member ${index + 1}`;
1034
+ }
1035
+
1036
+ function metricSummary(metrics) {
1037
+ return Object.entries(metrics)
1038
+ .sort(([left], [right]) => left.localeCompare(right))
1039
+ .map(([name, value]) => `${name.replaceAll("_", " ")}=${value}`)
1040
+ .join(", ");
1041
+ }
1042
+
1043
+ function evidenceLines(data) {
1044
+ const lines = [
1045
+ data.resource.kind === "machine"
1046
+ ? `Machine: ${data.resource.hostname ?? "selected machine"}`
1047
+ : `${data.resource.access_mode} cluster`,
1048
+ `Lifecycle: ${data.lifecycle.class} (${data.lifecycle.phase})`,
1049
+ `Reachability: ${data.reachability.state}`,
1050
+ `Health: ${data.health.state}`,
1051
+ ];
1052
+ if (data.metrics) {
1053
+ lines.push(`Metrics: ${data.metrics.state}`);
1054
+ for (const [index, source] of data.metrics.series.entries()) {
1055
+ const point = source.points.at(-1);
1056
+ if (point) {
1057
+ lines.push(
1058
+ ` ${evidenceSourceLabel(data, index)} · ${point.observed_at} · ${metricSummary(point.metrics)}`
1059
+ );
1060
+ }
1061
+ }
1062
+ }
1063
+ if (data.logs) {
1064
+ lines.push(`Logs (${data.logs.stream}): ${data.logs.state}`);
1065
+ for (const [index, source] of data.logs.sources.entries()) {
1066
+ for (const entry of source.entries) {
1067
+ lines.push(
1068
+ ` ${evidenceSourceLabel(data, index)} · ${entry.observed_at} · ${entry.severity} · ${entry.message}`
1069
+ );
1070
+ }
1071
+ }
1072
+ }
1073
+ return lines;
1074
+ }
1075
+
1076
+ function pageFor(resource, requested, logs) {
1077
+ return resource.kind === "machine" && requested.logs && !requested.metrics
1078
+ ? (logs?.sources?.[0]?.page ?? null)
1079
+ : null;
1080
+ }
1081
+
1082
+ async function snapshot(resource, requested, window, options, deps, interaction) {
1083
+ const collected = await collectEvidence(resource, requested, window, options, deps, null);
1084
+ const data = { ...resourceBase(resource), metrics: collected.metrics, logs: collected.logs };
1085
+ const requestedCount =
1086
+ sourceMachines(resource).length * (Number(requested.metrics) + Number(requested.logs));
1087
+ const status = snapshotExit(collected.errors, requestedCount);
1088
+ return interaction.result({
1089
+ data,
1090
+ errors: publicErrors(collected.errors),
1091
+ exitCode: status.exitCode,
1092
+ outcome: status.outcome ?? data.lifecycle,
1093
+ page: pageFor(resource, requested, data.logs),
1094
+ presentation: { lines: evidenceLines(data) },
1095
+ });
1096
+ }
1097
+
1098
+ function streamState(data, state) {
1099
+ return {
1100
+ ...data,
1101
+ metrics: data.metrics ? { ...data.metrics, state } : null,
1102
+ logs: data.logs ? { ...data.logs, state } : null,
1103
+ };
1104
+ }
1105
+
1106
+ function liveState(data) {
1107
+ const metricState = data.metrics?.state;
1108
+ const logState = data.logs?.state;
1109
+ return {
1110
+ ...data,
1111
+ metrics: data.metrics
1112
+ ? {
1113
+ ...data.metrics,
1114
+ state: ["partial", "unavailable"].includes(metricState)
1115
+ ? metricState
1116
+ : data.metrics.series.some((source) => source.points.length)
1117
+ ? "streaming"
1118
+ : "waiting_for_data",
1119
+ }
1120
+ : null,
1121
+ logs: data.logs
1122
+ ? {
1123
+ ...data.logs,
1124
+ state: ["partial", "unavailable"].includes(logState)
1125
+ ? logState
1126
+ : data.logs.sources.some((source) => source.entries.length)
1127
+ ? "streaming"
1128
+ : "waiting_for_data",
1129
+ }
1130
+ : null,
1131
+ };
1132
+ }
1133
+
1134
+ function writeHumanProgress(stdout, data) {
1135
+ if (data.metrics) {
1136
+ for (const [index, source] of data.metrics.series.entries()) {
1137
+ const point = source.points.at(-1);
1138
+ if (point) {
1139
+ stdout.write(
1140
+ `Metric ${evidenceSourceLabel(data, index)} ${point.observed_at} ${metricSummary(point.metrics)}\n`
1141
+ );
1142
+ }
1143
+ }
1144
+ }
1145
+ if (data.logs) {
1146
+ for (const [index, source] of data.logs.sources.entries()) {
1147
+ for (const line of source.entries) {
1148
+ stdout.write(
1149
+ `${evidenceSourceLabel(data, index)} ${line.observed_at} ${line.severity} ${line.message}\n`
1150
+ );
1151
+ }
1152
+ }
1153
+ }
1154
+ }
1155
+
1156
+ function streamResult(interaction, data, progress, exitCode, outcome, errors = []) {
1157
+ return interaction.result({
1158
+ data,
1159
+ errors: publicErrors(errors),
1160
+ exitCode,
1161
+ outcome,
1162
+ progress,
1163
+ presentation: { lines: evidenceLines(data) },
1164
+ });
1165
+ }
1166
+
1167
+ async function follow(resource, requested, initialWindow, options, deps, interaction) {
1168
+ if (interaction.format === "json") {
1169
+ interaction.usage("JSON cannot represent a follow stream. Use --format ndjson.", {
1170
+ code: "json_follow_unsupported",
1171
+ field: "format",
1172
+ });
1173
+ }
1174
+ const count = integerOption(interaction, interaction.options.count, "count", null, MAX_PAGE_SIZE);
1175
+ const timeoutMs = durationMilliseconds(interaction, interaction.options.timeout, "timeout");
1176
+ const intervalMs =
1177
+ durationMilliseconds(interaction, interaction.options.interval, "interval") ??
1178
+ DEFAULT_FOLLOW_INTERVAL_MS;
1179
+ if (interaction.machine && count === null && timeoutMs === null) {
1180
+ interaction.usage("Machine follow requires --count or --timeout so the stream is bounded.", {
1181
+ code: "follow_bound_required",
1182
+ field: "count",
1183
+ });
1184
+ }
1185
+
1186
+ const localController = deps.signal ? null : new AbortController();
1187
+ const signal = deps.signal ?? localController.signal;
1188
+ const onSigint = localController ? () => localController.abort() : null;
1189
+ if (onSigint) process.once("SIGINT", onSigint);
1190
+ const startedAt = nowMilliseconds(deps.now);
1191
+ const deadline = timeoutMs === null ? Number.POSITIVE_INFINITY : startedAt + timeoutMs;
1192
+ let current = resourceBase(resource);
1193
+ const progress = [];
1194
+ let iteration = 0;
1195
+ try {
1196
+ for (;;) {
1197
+ if (signal.aborted) {
1198
+ current = streamState(current, "interrupted");
1199
+ return streamResult(interaction, current, progress, 130, {
1200
+ class: "ended",
1201
+ phase: "interrupted",
1202
+ });
1203
+ }
1204
+ const observedAt = nowMilliseconds(deps.now);
1205
+ if (observedAt >= deadline) {
1206
+ current = streamState(current, "ended");
1207
+ return streamResult(interaction, current, progress, 124, {
1208
+ class: "ended",
1209
+ phase: "observation_timeout",
1210
+ });
1211
+ }
1212
+ iteration += 1;
1213
+ const window = {
1214
+ start: initialWindow.start,
1215
+ end: new Date(observedAt).toISOString(),
1216
+ startMs: initialWindow.startMs,
1217
+ endMs: observedAt,
1218
+ };
1219
+ const collected = await collectEvidence(
1220
+ resource,
1221
+ requested,
1222
+ window,
1223
+ { ...options, deadline },
1224
+ deps,
1225
+ signal,
1226
+ { live: true }
1227
+ );
1228
+ if (collected.interrupted || signal.aborted) {
1229
+ current = streamState(
1230
+ { ...resourceBase(resource), metrics: collected.metrics, logs: collected.logs },
1231
+ "interrupted"
1232
+ );
1233
+ return streamResult(interaction, current, progress, 130, {
1234
+ class: "ended",
1235
+ phase: "interrupted",
1236
+ });
1237
+ }
1238
+ current = liveState({
1239
+ ...resourceBase(resource),
1240
+ metrics: collected.metrics,
1241
+ logs: collected.logs,
1242
+ });
1243
+ if (collected.errors.length) {
1244
+ const requestedCount =
1245
+ sourceMachines(resource).length * (Number(requested.metrics) + Number(requested.logs));
1246
+ const status = snapshotExit(collected.errors, requestedCount);
1247
+ return streamResult(
1248
+ interaction,
1249
+ current,
1250
+ progress,
1251
+ status.exitCode,
1252
+ status.outcome,
1253
+ collected.errors
1254
+ );
1255
+ }
1256
+ const progressEntry = {
1257
+ data: current,
1258
+ message: `Observed ${requested.metrics && requested.logs ? "metrics and logs" : requested.metrics ? "metrics" : "logs"}.`,
1259
+ observedAt: new Date(observedAt).toISOString(),
1260
+ outcome: { class: "working", phase: "streaming" },
1261
+ sequence: iteration,
1262
+ };
1263
+ if (interaction.machine) progress.push(progressEntry);
1264
+ else writeHumanProgress(deps.stdout, current);
1265
+
1266
+ if (count !== null && iteration >= count) {
1267
+ current = streamState(current, "ended");
1268
+ return streamResult(interaction, current, progress, 0, {
1269
+ class: "ended",
1270
+ phase: "observation_complete",
1271
+ });
1272
+ }
1273
+ const remaining = deadline - nowMilliseconds(deps.now);
1274
+ if (remaining <= 0) continue;
1275
+ const continued = await sleepUntil(Math.min(intervalMs, remaining), deps, signal);
1276
+ if (!continued) {
1277
+ current = streamState(current, "interrupted");
1278
+ return streamResult(interaction, current, progress, 130, {
1279
+ class: "ended",
1280
+ phase: "interrupted",
1281
+ });
1282
+ }
1283
+ }
1284
+ } finally {
1285
+ if (onSigint) process.removeListener("SIGINT", onSigint);
1286
+ }
1287
+ }
1288
+
1289
+ export async function executeEvidenceCommand(command, input, deps) {
1290
+ const interaction = deps.interaction;
1291
+ if (input.follow === true && interaction.format === "json") {
1292
+ interaction.usage("JSON cannot represent a follow stream. Use --format ndjson.", {
1293
+ code: "json_follow_unsupported",
1294
+ field: "format",
1295
+ });
1296
+ }
1297
+ const requested = {
1298
+ metrics: command === "check" || command.endsWith(".metrics"),
1299
+ logs: command === "check" || command.endsWith(".logs"),
1300
+ };
1301
+ const logsOnly = requested.logs && !requested.metrics;
1302
+ const window = evidenceWindow(input, interaction, deps.now, {
1303
+ follow: input.follow === true,
1304
+ logsOnly,
1305
+ });
1306
+ const maxPoints = integerOption(interaction, input.maxPoints, "max-points", DEFAULT_MAX_POINTS);
1307
+ const limit = integerOption(interaction, input.limit, "limit", DEFAULT_LOG_LIMIT);
1308
+ const stream = input.stream ?? "kernel";
1309
+ if (
1310
+ !requested.logs &&
1311
+ (input.stream !== undefined ||
1312
+ input.limit !== undefined ||
1313
+ input.cursor !== undefined ||
1314
+ input.before !== undefined)
1315
+ ) {
1316
+ interaction.usage("Log options are not accepted by a metrics-only command.", {
1317
+ code: "invalid_input",
1318
+ field: "stream",
1319
+ });
1320
+ }
1321
+ if (!requested.metrics && input.maxPoints !== undefined) {
1322
+ interaction.usage("--max-points is not accepted by a logs-only command.", {
1323
+ code: "invalid_input",
1324
+ field: "maxPoints",
1325
+ });
1326
+ }
1327
+ if (
1328
+ input.follow !== true &&
1329
+ (input.count !== undefined || input.timeout !== undefined || input.interval !== undefined)
1330
+ ) {
1331
+ interaction.usage("--count, --timeout, and --interval require --follow.", {
1332
+ code: "invalid_input",
1333
+ field:
1334
+ input.count !== undefined ? "count" : input.timeout !== undefined ? "timeout" : "interval",
1335
+ });
1336
+ }
1337
+ if (input.cursor !== undefined && input.follow === true) {
1338
+ interaction.usage("--cursor cannot be combined with --follow.", {
1339
+ code: "invalid_input",
1340
+ field: "cursor",
1341
+ });
1342
+ }
1343
+
1344
+ const resource = await resolveResource(command, input, deps.fetch, interaction);
1345
+ const options = {
1346
+ cursor: text(input.cursor),
1347
+ deadline: nowMilliseconds(deps.now) + DEFAULT_RETRY_WINDOW_MS,
1348
+ limit,
1349
+ maxPoints,
1350
+ stream,
1351
+ };
1352
+ if (input.follow === true) return follow(resource, requested, window, options, deps, interaction);
1353
+ return snapshot(resource, requested, window, options, deps, interaction);
1354
+ }