@ornncompute/cli 0.2.23 → 1.0.25

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