@ornncompute/cli 1.0.35 → 1.0.62

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 (31) hide show
  1. package/README.md +92 -47
  2. package/package.json +2 -2
  3. package/src/admin-capacity-reservations.mjs +65 -153
  4. package/src/admin-command-handlers.mjs +2 -0
  5. package/src/admin-credits.mjs +57 -0
  6. package/src/admin-node-lifecycle.mjs +48 -7
  7. package/src/admin-shell.mjs +113 -27
  8. package/src/catalog-shell.mjs +13 -21
  9. package/src/evidence-commands.mjs +30 -12
  10. package/src/kubernetes-commands.mjs +58 -16
  11. package/src/launch-mode-handlers.mjs +2 -2
  12. package/src/storage-commands.mjs +9 -1
  13. package/src/vm-commands.mjs +3 -3
  14. package/vendor/capabilities/capabilities/admin-capacity-reservations.js +55 -57
  15. package/vendor/capabilities/capabilities/admin-enrollment.js +3 -3
  16. package/vendor/capabilities/capabilities/admin.js +9 -7
  17. package/vendor/capabilities/capabilities/commerce-market.js +179 -104
  18. package/vendor/capabilities/capabilities/compute.js +4 -2
  19. package/vendor/capabilities/capabilities/credits.js +78 -0
  20. package/vendor/capabilities/capabilities/fleet.js +6 -5
  21. package/vendor/capabilities/capabilities/listings.js +58 -38
  22. package/vendor/capabilities/capabilities/market-shared.js +2 -0
  23. package/vendor/capabilities/capabilities/markets.js +254 -221
  24. package/vendor/capabilities/capabilities/native-listing.js +63 -0
  25. package/vendor/capabilities/capabilities/organization-invites.js +53 -0
  26. package/vendor/capabilities/capabilities/reservation-list.js +68 -0
  27. package/vendor/capabilities/capabilities/staff-shared.js +64 -0
  28. package/vendor/capabilities/capabilities/users.js +141 -31
  29. package/vendor/capabilities/cli-catalog.js +8 -2
  30. package/vendor/capabilities/cli-migrations.js +13 -10
  31. package/vendor/capabilities/software-catalog.js +52 -6
@@ -339,9 +339,7 @@ async function resolveLiveMachine(fetch, node) {
339
339
  })
340
340
  );
341
341
  const refs = new Set([id, text(node?.id), text(node?.k8s_node_name)].filter(Boolean));
342
- const matches = rows.filter((machine) =>
343
- refs.has(text(machine?.node_id))
344
- );
342
+ const matches = rows.filter((machine) => refs.has(text(machine?.node_id)));
345
343
  if (matches.length > 1) {
346
344
  throw mutationError(`Multiple live machines resolve to node ${id}; no reboot was submitted.`, {
347
345
  code: "ambiguous_live_machine",
@@ -676,14 +674,39 @@ function goalSatisfied(command, node, input) {
676
674
  if (command === "suspend-management") return isOffGrid(node);
677
675
  if (command === "resume-management") return !isOffGrid(node);
678
676
  if (command === "assign") return reservationId(node) === input.reservation_id;
679
- if (["pending", "rebooting"].includes(text(node?.cleanup_state).toLowerCase())) return false;
680
- const readiness = node?.health_readiness;
681
- return (
677
+ if (cleanupState(node) === "failed") return false;
678
+ return !restartInProgress(node);
679
+ }
680
+
681
+ function cleanupState(node) {
682
+ return text(node?.cleanup_state).toLowerCase();
683
+ }
684
+
685
+ function restartInProgress(node) {
686
+ if (["pending", "rebooting"].includes(cleanupState(node))) return true;
687
+ const readiness = node.health_readiness;
688
+ return !(
682
689
  readiness?.ready === true ||
683
690
  ["ready", "running"].includes(text(readiness?.status).toLowerCase())
684
691
  );
685
692
  }
686
693
 
694
+ // Before the restart starts, a healthy node already looks "done", so reboot
695
+ // and hard-reset only count as complete after the restart has been observed.
696
+ function restartGoal(command, input) {
697
+ if (command !== "reboot" && command !== "hard-reset") {
698
+ return { isReady: (node) => goalSatisfied(command, node, input), isTerminal: () => false };
699
+ }
700
+ let restartSeen = false;
701
+ return {
702
+ isReady: (node) => {
703
+ if (node && restartInProgress(node)) restartSeen = true;
704
+ return restartSeen && goalSatisfied(command, node, input);
705
+ },
706
+ isTerminal: (node) => restartSeen && cleanupState(node) === "failed",
707
+ };
708
+ }
709
+
687
710
  function waitResult(kind, command, id, accepted, lastState) {
688
711
  const timeout = kind === "timeout";
689
712
  return {
@@ -711,13 +734,15 @@ function waitResult(kind, command, id, accepted, lastState) {
711
734
  async function waitForGoal({ command, fetch, input, nodeId: id, runtime, accepted }) {
712
735
  let lastState = null;
713
736
  let outcome;
737
+ const { isReady, isTerminal } = restartGoal(command, input);
714
738
  try {
715
739
  outcome = await waitForReadGoal(
716
740
  {
717
741
  command: `ornn admin nodes ${command}`,
718
742
  initial: null,
719
743
  intervalMs: POLL_INTERVAL_MS,
720
- isReady: (node) => goalSatisfied(command, node, input),
744
+ isReady,
745
+ isTerminal,
721
746
  message: () => `Waiting for node ${id} to reach the requested state.`,
722
747
  onObserve: (node) => {
723
748
  lastState = node;
@@ -790,6 +815,22 @@ async function waitForGoal({ command, fetch, input, nodeId: id, runtime, accepte
790
815
  errors: [],
791
816
  };
792
817
  }
818
+ if (outcome.kind === "terminal") {
819
+ return {
820
+ data: { ...accepted, node_id: id, state: safeNodeState(outcome.value) },
821
+ exitCode: 1,
822
+ outcome: { class: "failed", phase: "cleanup_failed" },
823
+ resume: resumeReference(id),
824
+ action: inspectionAction(id),
825
+ errors: [
826
+ {
827
+ code: "cleanup_failed",
828
+ message: "The node reported the operation as failed.",
829
+ retryable: false,
830
+ },
831
+ ],
832
+ };
833
+ }
793
834
  return waitResult(outcome.kind, command, id, accepted, outcome.value);
794
835
  }
795
836
 
@@ -156,6 +156,24 @@ function positiveInteger(value, name, maximum) {
156
156
  return parsed;
157
157
  }
158
158
 
159
+ function nonnegativeInteger(value, name) {
160
+ if (value === undefined) return undefined;
161
+ const parsed = Number(value);
162
+ if (!Number.isSafeInteger(parsed) || parsed < 0) {
163
+ throw new AdminUsageError(`--${name} must be a non-negative integer.`, {
164
+ field: name,
165
+ });
166
+ }
167
+ return parsed;
168
+ }
169
+
170
+ function booleanOption(value, name) {
171
+ if (value === undefined) return undefined;
172
+ if (value === "true") return true;
173
+ if (value === "false") return false;
174
+ throw new AdminUsageError(`--${name} must be true or false.`, { field: name });
175
+ }
176
+
159
177
  function parseAdminInput(capability, argv) {
160
178
  const command = vnextCommand(capability).join(" ");
161
179
  const help = capability.cli.vnext;
@@ -167,22 +185,37 @@ function parseAdminInput(capability, argv) {
167
185
  throw new AdminUsageError(usage);
168
186
  }
169
187
 
170
- const limit = positiveInteger(options.limit, "limit", MAX_PAGE_LIMIT) ?? DEFAULT_PAGE_LIMIT;
188
+ const offsetPagination = ["users.list", "organization.list"].includes(capability.id);
189
+ const reservationList = capability.id === "reservation.list";
190
+ const limit = positiveInteger(
191
+ options.limit,
192
+ "limit",
193
+ offsetPagination || reservationList ? 100 : MAX_PAGE_LIMIT
194
+ ) ?? (offsetPagination ? 25 : DEFAULT_PAGE_LIMIT);
171
195
  const input = {};
172
196
  if (nodeCommand) input.node_id = positionals[0];
173
197
  if (options.supplier) input.organization_id = options.supplier;
198
+ if (options.organization && ["users.list", "organization.invite"].includes(capability.id)) {
199
+ input.organization_id = options.organization;
200
+ }
174
201
  if (options.facility) input.facility_id = options.facility;
175
202
  if (options.status) input.status = options.status;
176
203
  if (options.email) input.email = options.email;
177
- if (options.organization) input.organization = options.organization;
178
- if (options.name) input.name = options.name;
204
+ if (options.reservation) input.reservation_id = options.reservation;
205
+ if (options.customer) input.customerId = options.customer;
206
+ if (options.listing) input.listingId = options.listing;
207
+ if (options["listing-term"]) input.listingTermId = options["listing-term"];
208
+ if (options.query) input.q = options.query;
209
+ if (options.role) input.role = options.role;
210
+ const approved = booleanOption(options.approved, "approved");
211
+ if (approved !== undefined) input.approved = approved;
212
+ if (options.ids) input.ids = options.ids.split(",");
179
213
  if (options.span) input.span = options.span;
180
214
  if (options.start) input.start = options.start;
181
215
  if (options.end) input.end = options.end;
182
216
  if (options.before) input.before = options.before;
183
217
  if (options.stream) input.stream = options.stream;
184
218
  if (options.cursor && command === "admin nodes logs") input.cursor = options.cursor;
185
- if (command === "admin users search") input.max = 1000;
186
219
  const maxPoints = positiveInteger(options["max-points"], "max-points", 500);
187
220
  if (maxPoints !== undefined) input.max_points = maxPoints;
188
221
  const count = positiveInteger(options.count, "count", 500);
@@ -190,11 +223,23 @@ function parseAdminInput(capability, argv) {
190
223
 
191
224
  if (command === "admin nodes logs") return parseLogPage(input);
192
225
 
193
- const ownerPagination = ["facilities.list", "nodes.list"].includes(capability.id);
226
+ const ownerPagination = ["facilities.list", "nodes.list", "reservation.list"].includes(
227
+ capability.id
228
+ );
229
+ const explicitOffset = nonnegativeInteger(options.offset, "offset");
194
230
  if (ownerPagination) {
195
231
  input.limit = limit;
196
232
  if (options.cursor) input.cursor = options.cursor;
197
233
  }
234
+ if (offsetPagination) {
235
+ if (explicitOffset !== undefined && options.cursor) {
236
+ throw new AdminUsageError("--offset and --cursor cannot be used together.", {
237
+ field: "offset",
238
+ });
239
+ }
240
+ input.limit = limit;
241
+ input.offset = explicitOffset ?? decodeCursor(command, options.cursor);
242
+ }
198
243
 
199
244
  return {
200
245
  input,
@@ -202,6 +247,8 @@ function parseAdminInput(capability, argv) {
202
247
  cursor: options.cursor,
203
248
  limit,
204
249
  ownerPagination,
250
+ ownerOffsetPagination: offsetPagination,
251
+ offset: input.offset,
205
252
  },
206
253
  };
207
254
  }
@@ -281,7 +328,16 @@ function collectionKey(capability) {
281
328
  if (root === "nodes" && leaf === "logs") return "logs";
282
329
  if (!["list", "search"].includes(leaf)) return null;
283
330
  if (root === "enrollment-tokens") return "enrollment_tokens";
284
- return ["facilities", "nodes", "reservations", "suppliers", "users"].includes(root) ? root : null;
331
+ return [
332
+ "facilities",
333
+ "nodes",
334
+ "organizations",
335
+ "reservations",
336
+ "suppliers",
337
+ "users",
338
+ ].includes(root)
339
+ ? root
340
+ : null;
285
341
  }
286
342
 
287
343
  function isSensitiveAdminField(key) {
@@ -337,21 +393,36 @@ function encodeCursor(command, offset) {
337
393
 
338
394
  function decodeCursor(command, cursor) {
339
395
  if (!cursor) return 0;
396
+ let decoded;
340
397
  try {
341
- const decoded = JSON.parse(Buffer.from(cursor, "base64url").toString("utf8"));
342
- if (decoded.command === command && Number.isInteger(decoded.offset) && decoded.offset >= 0) {
343
- return decoded.offset;
344
- }
345
- if (decoded.command !== undefined || decoded.offset !== undefined) {
346
- throw new AdminUsageError("The pagination cursor belongs to a different admin command.", {
347
- code: "invalid_cursor",
348
- field: "cursor",
349
- });
350
- }
351
- } catch (error) {
352
- if (error instanceof AdminUsageError) throw error;
398
+ const bytes = Buffer.from(cursor, "base64url");
399
+ if (bytes.toString("base64url") !== cursor) throw new Error("invalid base64url");
400
+ decoded = JSON.parse(bytes.toString("utf8"));
401
+ } catch {
402
+ throw new AdminUsageError("The pagination cursor is invalid.", {
403
+ code: "invalid_cursor",
404
+ field: "cursor",
405
+ });
406
+ }
407
+ if (!decoded || typeof decoded !== "object" || Array.isArray(decoded)) {
408
+ throw new AdminUsageError("The pagination cursor is invalid.", {
409
+ code: "invalid_cursor",
410
+ field: "cursor",
411
+ });
412
+ }
413
+ if (decoded.command !== command) {
414
+ throw new AdminUsageError("The pagination cursor belongs to a different admin command.", {
415
+ code: "invalid_cursor",
416
+ field: "cursor",
417
+ });
418
+ }
419
+ if (!Number.isSafeInteger(decoded.offset) || decoded.offset < 0) {
420
+ throw new AdminUsageError("The pagination cursor has an invalid offset.", {
421
+ code: "invalid_cursor",
422
+ field: "cursor",
423
+ });
353
424
  }
354
- return 0;
425
+ return decoded.offset;
355
426
  }
356
427
 
357
428
  export function finalizeAdminResult(capability, payload, pagination) {
@@ -362,18 +433,27 @@ export function finalizeAdminResult(capability, payload, pagination) {
362
433
  const rows = collectionRows(payload, key);
363
434
  const offset = pagination.ownerPagination
364
435
  ? 0
365
- : pagination.logQuery
436
+ : pagination.logQuery || pagination.ownerOffsetPagination
366
437
  ? pagination.offset
367
438
  : decodeCursor(command, pagination.cursor);
368
439
  const selected = (
369
- pagination.ownerPagination ? rows : rows.slice(offset, offset + pagination.limit)
440
+ pagination.ownerPagination || pagination.ownerOffsetPagination
441
+ ? rows
442
+ : rows.slice(offset, offset + pagination.limit)
370
443
  ).map(sanitizeAdminValue);
444
+ const ownerNextOffset =
445
+ pagination.ownerOffsetPagination && payload && !Array.isArray(payload)
446
+ ? payload.next_offset
447
+ : undefined;
371
448
  const ownerNextCursor =
372
449
  payload && !Array.isArray(payload)
373
450
  ? (payload.next_cursor ?? payload.nextCursor ?? payload.page?.next_cursor ?? null)
374
451
  : null;
375
- let nextCursor =
376
- !pagination.ownerPagination && offset + selected.length < rows.length
452
+ let nextCursor = pagination.ownerOffsetPagination
453
+ ? Number.isSafeInteger(ownerNextOffset) && ownerNextOffset > offset
454
+ ? encodeCursor(command, ownerNextOffset)
455
+ : null
456
+ : !pagination.ownerPagination && offset + selected.length < rows.length
377
457
  ? encodeCursor(command, offset + selected.length)
378
458
  : (ownerNextCursor ?? null);
379
459
  if (pagination.ownerPagination && nextCursor !== null && nextCursor === pagination.cursor) {
@@ -438,12 +518,18 @@ export function writeAdminResultHuman(stdout, command, result, input = {}) {
438
518
  if (result.page.next_cursor) {
439
519
  const target = command === "admin nodes logs" ? ` ${shellSingleQuote(input.node_id)}` : "";
440
520
  const filters = [
441
- ["supplier", input.organization_id],
521
+ ["supplier", command === "admin users list" ? undefined : input.organization_id],
522
+ ["organization", command === "admin users list" ? input.organization_id : undefined],
442
523
  ["facility", input.facility_id],
443
524
  ["status", input.status],
444
- ["email", input.email],
445
- ["organization", input.organization],
446
- ["name", input.name],
525
+ ["reservation", input.reservation_id],
526
+ ["customer", input.customerId],
527
+ ["listing", input.listingId],
528
+ ["listing-term", input.listingTermId],
529
+ ["query", input.q],
530
+ ["role", input.role],
531
+ ["approved", input.approved],
532
+ ["ids", Array.isArray(input.ids) ? input.ids.join(",") : undefined],
447
533
  ["limit", input.limit],
448
534
  ]
449
535
  .filter(([, value]) => value !== undefined)
@@ -1,5 +1,3 @@
1
- import { HARDWARE_CATALOG_PATH } from "../vendor/capabilities/cli.js";
2
-
3
1
  import {
4
2
  CliApiError,
5
3
  cliRequest,
@@ -793,7 +791,7 @@ const CUSTOMER_FORBIDDEN_FIELDS = {
793
791
  };
794
792
  const SUPPLIER_OWNER_FIELDS = { organizationId: true, organization_id: true };
795
793
 
796
- // The caller's own organization on a listing it relisted is not supplier identity.
794
+ // A listing owner may receive its own organization ID.
797
795
  function assertCustomerResultSafe(value, ownOrganizationId) {
798
796
  if (Array.isArray(value)) {
799
797
  for (const item of value) assertCustomerResultSafe(item, ownOrganizationId);
@@ -802,6 +800,7 @@ function assertCustomerResultSafe(value, ownOrganizationId) {
802
800
  if (!value || typeof value !== "object") return;
803
801
  const ownedBySupplier = supplierOwnedRecord(value);
804
802
  for (const [key, child] of Object.entries(value)) {
803
+ if (key === "supplierId" && value.capacityListingId && child === ownOrganizationId) continue;
805
804
  if (
806
805
  CUSTOMER_FORBIDDEN_FIELDS[key] === true ||
807
806
  (ownedBySupplier &&
@@ -835,11 +834,13 @@ function parseCapabilityInput(capability, rawInput) {
835
834
 
836
835
  // Commerce's market routes and Markets serve every caller on one path and
837
836
  // decide from the session what it may do; a staff session is an admin there.
838
- const ROLE_AWARE_PREFIXES = ["/commerce/listings", "/commerce/reservations", "/commerce/fees", "/v1/markets"];
837
+ const ROLE_AWARE_PREFIXES = ["/commerce/v2/listings", "/commerce/reservations", "/v1/markets"];
839
838
 
840
839
  function isRoleAwarePath(path) {
841
840
  const pathname = String(path).split("?")[0];
842
- return ROLE_AWARE_PREFIXES.some((prefix) => pathname === prefix || pathname.startsWith(`${prefix}/`));
841
+ return ROLE_AWARE_PREFIXES.some(
842
+ (prefix) => pathname === prefix || pathname.startsWith(`${prefix}/`)
843
+ );
843
844
  }
844
845
 
845
846
  function adminFetch(io, path, init = {}) {
@@ -852,7 +853,9 @@ function adminFetch(io, path, init = {}) {
852
853
  ...(init.headers !== undefined ? { headers: init.headers } : {}),
853
854
  ...((init.signal ?? io.signal) !== undefined ? { signal: init.signal ?? io.signal } : {}),
854
855
  };
855
- return isRoleAwarePath(path) ? cliRequest(request) : operatorRequest(request);
856
+ return isRoleAwarePath(path) || (path === "/v1/staff/invites" && request.method === "POST")
857
+ ? cliRequest(request)
858
+ : operatorRequest(request);
856
859
  }
857
860
 
858
861
  async function customerCapabilityFetch(io, path, init = {}, sessionSnapshot) {
@@ -992,20 +995,6 @@ async function executeCapability(
992
995
  role,
993
996
  fetch: (path, init = {}) => {
994
997
  if (executionRuntime?.transport) return executionRuntime.transport(path, init);
995
- if (
996
- audience === "admin" &&
997
- capability.id === "listings.create" &&
998
- path === HARDWARE_CATALOG_PATH
999
- ) {
1000
- return operatorRequest({
1001
- endpoint: "/catalog/hardware",
1002
- env: io.env,
1003
- fetchImpl: io.fetchImpl,
1004
- method: init.method ?? "GET",
1005
- ...(init.headers !== undefined ? { headers: init.headers } : {}),
1006
- ...(init.signal !== undefined ? { signal: init.signal } : {}),
1007
- });
1008
- }
1009
998
  if (audience === "admin") {
1010
999
  // Shared observability capabilities use their owner namespace;
1011
1000
  // the CLI reaches those same node-scoped routes through staff auth.
@@ -1173,9 +1162,12 @@ export async function runCatalogCli(argv, io, { version }) {
1173
1162
  eligibleHuman,
1174
1163
  machine,
1175
1164
  });
1176
- const role = await resolveAdminRole(io);
1177
1165
  const adminRequest = resolveAdminRequest(catalog, capabilities.ADMIN_CLI_ROOTS, request.argv);
1178
1166
  activeCommand = adminRequest.command;
1167
+ if (adminRequest.kind === "command" && !adminRequest.handler) {
1168
+ adminRequest.input = parseCapabilityInput(adminRequest.capability, adminRequest.input);
1169
+ }
1170
+ const role = await resolveAdminRole(io);
1179
1171
  if (adminRequest.kind === "root") {
1180
1172
  const data = adminRootData(catalog, capabilities.ADMIN_CLI_ROOTS, adminRequest.root);
1181
1173
  if (machine) writeMachine(stdout, format, resultEnvelope(adminRequest.command, data));
@@ -1040,7 +1040,7 @@ function metricSummary(metrics) {
1040
1040
  .join(", ");
1041
1041
  }
1042
1042
 
1043
- function evidenceLines(data) {
1043
+ function evidenceLines(data, printedLogs = new Map()) {
1044
1044
  const lines = [
1045
1045
  data.resource.kind === "machine"
1046
1046
  ? `Machine: ${data.resource.hostname ?? "selected machine"}`
@@ -1063,7 +1063,7 @@ function evidenceLines(data) {
1063
1063
  if (data.logs) {
1064
1064
  lines.push(`Logs (${data.logs.stream}): ${data.logs.state}`);
1065
1065
  for (const [index, source] of data.logs.sources.entries()) {
1066
- for (const entry of source.entries) {
1066
+ for (const entry of unprintedLogLines(source, printedLogs.get(index)).fresh) {
1067
1067
  lines.push(
1068
1068
  ` ${evidenceSourceLabel(data, index)} · ${entry.observed_at} · ${entry.severity} · ${entry.message}`
1069
1069
  );
@@ -1131,7 +1131,21 @@ function liveState(data) {
1131
1131
  };
1132
1132
  }
1133
1133
 
1134
- function writeHumanProgress(stdout, data) {
1134
+ // Follow re-reads the whole live window, so count each line's occurrences
1135
+ // rather than trusting timestamps: identical records still print once each,
1136
+ // and a late older line is still new.
1137
+ function unprintedLogLines(source, printed = new Map()) {
1138
+ const counts = new Map();
1139
+ const fresh = source.entries.filter((line) => {
1140
+ const key = `${line.observed_at} ${line.severity} ${line.message}`;
1141
+ counts.set(key, (counts.get(key) ?? 0) + 1);
1142
+ return counts.get(key) > (printed.get(key) ?? 0);
1143
+ });
1144
+ for (const [key, count] of counts) counts.set(key, Math.max(count, printed.get(key) ?? 0));
1145
+ return { fresh, seen: counts };
1146
+ }
1147
+
1148
+ function writeHumanProgress(stdout, data, printedLogs) {
1135
1149
  if (data.metrics) {
1136
1150
  for (const [index, source] of data.metrics.series.entries()) {
1137
1151
  const point = source.points.at(-1);
@@ -1144,7 +1158,9 @@ function writeHumanProgress(stdout, data) {
1144
1158
  }
1145
1159
  if (data.logs) {
1146
1160
  for (const [index, source] of data.logs.sources.entries()) {
1147
- for (const line of source.entries) {
1161
+ const { fresh, seen } = unprintedLogLines(source, printedLogs.get(index));
1162
+ printedLogs.set(index, seen);
1163
+ for (const line of fresh) {
1148
1164
  stdout.write(
1149
1165
  `${evidenceSourceLabel(data, index)} ${line.observed_at} ${line.severity} ${line.message}\n`
1150
1166
  );
@@ -1153,14 +1169,14 @@ function writeHumanProgress(stdout, data) {
1153
1169
  }
1154
1170
  }
1155
1171
 
1156
- function streamResult(interaction, data, progress, exitCode, outcome, errors = []) {
1172
+ function streamResult(interaction, data, progress, printedLogs, exitCode, outcome, errors = []) {
1157
1173
  return interaction.result({
1158
1174
  data,
1159
1175
  errors: publicErrors(errors),
1160
1176
  exitCode,
1161
1177
  outcome,
1162
1178
  progress,
1163
- presentation: { lines: evidenceLines(data) },
1179
+ presentation: { lines: evidenceLines(data, printedLogs) },
1164
1180
  });
1165
1181
  }
1166
1182
 
@@ -1191,12 +1207,13 @@ async function follow(resource, requested, initialWindow, options, deps, interac
1191
1207
  const deadline = timeoutMs === null ? Number.POSITIVE_INFINITY : startedAt + timeoutMs;
1192
1208
  let current = resourceBase(resource);
1193
1209
  const progress = [];
1210
+ const printedLogs = new Map();
1194
1211
  let iteration = 0;
1195
1212
  try {
1196
1213
  for (;;) {
1197
1214
  if (signal.aborted) {
1198
1215
  current = streamState(current, "interrupted");
1199
- return streamResult(interaction, current, progress, 130, {
1216
+ return streamResult(interaction, current, progress, printedLogs, 130, {
1200
1217
  class: "ended",
1201
1218
  phase: "interrupted",
1202
1219
  });
@@ -1204,7 +1221,7 @@ async function follow(resource, requested, initialWindow, options, deps, interac
1204
1221
  const observedAt = nowMilliseconds(deps.now);
1205
1222
  if (observedAt >= deadline) {
1206
1223
  current = streamState(current, "ended");
1207
- return streamResult(interaction, current, progress, 124, {
1224
+ return streamResult(interaction, current, progress, printedLogs, 124, {
1208
1225
  class: "ended",
1209
1226
  phase: "observation_timeout",
1210
1227
  });
@@ -1230,7 +1247,7 @@ async function follow(resource, requested, initialWindow, options, deps, interac
1230
1247
  { ...resourceBase(resource), metrics: collected.metrics, logs: collected.logs },
1231
1248
  "interrupted"
1232
1249
  );
1233
- return streamResult(interaction, current, progress, 130, {
1250
+ return streamResult(interaction, current, progress, printedLogs, 130, {
1234
1251
  class: "ended",
1235
1252
  phase: "interrupted",
1236
1253
  });
@@ -1248,6 +1265,7 @@ async function follow(resource, requested, initialWindow, options, deps, interac
1248
1265
  interaction,
1249
1266
  current,
1250
1267
  progress,
1268
+ printedLogs,
1251
1269
  status.exitCode,
1252
1270
  status.outcome,
1253
1271
  collected.errors
@@ -1261,11 +1279,11 @@ async function follow(resource, requested, initialWindow, options, deps, interac
1261
1279
  sequence: iteration,
1262
1280
  };
1263
1281
  if (interaction.machine) progress.push(progressEntry);
1264
- else writeHumanProgress(deps.stdout, current);
1282
+ else writeHumanProgress(deps.stdout, current, printedLogs);
1265
1283
 
1266
1284
  if (count !== null && iteration >= count) {
1267
1285
  current = streamState(current, "ended");
1268
- return streamResult(interaction, current, progress, 0, {
1286
+ return streamResult(interaction, current, progress, printedLogs, 0, {
1269
1287
  class: "ended",
1270
1288
  phase: "observation_complete",
1271
1289
  });
@@ -1275,7 +1293,7 @@ async function follow(resource, requested, initialWindow, options, deps, interac
1275
1293
  const continued = await sleepUntil(Math.min(intervalMs, remaining), deps, signal);
1276
1294
  if (!continued) {
1277
1295
  current = streamState(current, "interrupted");
1278
- return streamResult(interaction, current, progress, 130, {
1296
+ return streamResult(interaction, current, progress, printedLogs, 130, {
1279
1297
  class: "ended",
1280
1298
  phase: "interrupted",
1281
1299
  });