@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.
- package/README.md +92 -47
- package/package.json +2 -2
- package/src/admin-capacity-reservations.mjs +65 -153
- package/src/admin-command-handlers.mjs +2 -0
- package/src/admin-credits.mjs +57 -0
- package/src/admin-node-lifecycle.mjs +48 -7
- package/src/admin-shell.mjs +113 -27
- package/src/catalog-shell.mjs +13 -21
- package/src/evidence-commands.mjs +30 -12
- package/src/kubernetes-commands.mjs +58 -16
- package/src/launch-mode-handlers.mjs +2 -2
- package/src/storage-commands.mjs +9 -1
- package/src/vm-commands.mjs +3 -3
- package/vendor/capabilities/capabilities/admin-capacity-reservations.js +55 -57
- package/vendor/capabilities/capabilities/admin-enrollment.js +3 -3
- package/vendor/capabilities/capabilities/admin.js +9 -7
- package/vendor/capabilities/capabilities/commerce-market.js +179 -104
- package/vendor/capabilities/capabilities/compute.js +4 -2
- package/vendor/capabilities/capabilities/credits.js +78 -0
- package/vendor/capabilities/capabilities/fleet.js +6 -5
- package/vendor/capabilities/capabilities/listings.js +58 -38
- package/vendor/capabilities/capabilities/market-shared.js +2 -0
- package/vendor/capabilities/capabilities/markets.js +254 -221
- package/vendor/capabilities/capabilities/native-listing.js +63 -0
- package/vendor/capabilities/capabilities/organization-invites.js +53 -0
- package/vendor/capabilities/capabilities/reservation-list.js +68 -0
- package/vendor/capabilities/capabilities/staff-shared.js +64 -0
- package/vendor/capabilities/capabilities/users.js +141 -31
- package/vendor/capabilities/cli-catalog.js +8 -2
- package/vendor/capabilities/cli-migrations.js +13 -10
- 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 (
|
|
680
|
-
|
|
681
|
-
|
|
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
|
|
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
|
|
package/src/admin-shell.mjs
CHANGED
|
@@ -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
|
|
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.
|
|
178
|
-
if (options.
|
|
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(
|
|
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 [
|
|
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
|
|
342
|
-
if (
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
["
|
|
445
|
-
["
|
|
446
|
-
["
|
|
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)
|
package/src/catalog-shell.mjs
CHANGED
|
@@ -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
|
-
//
|
|
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", "/
|
|
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(
|
|
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)
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
});
|