@knowttl/mg-axi 0.1.1 → 0.1.3

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 (48) hide show
  1. package/README.md +251 -77
  2. package/dist/api.js +253 -8
  3. package/dist/bin/mg-axi-hook.js +31 -0
  4. package/dist/catalogue.js +693 -2
  5. package/dist/cli.js +155 -17
  6. package/dist/docs.js +175 -46
  7. package/dist/doctor.js +2 -2
  8. package/dist/entra-access-reviews.js +11 -4
  9. package/dist/entra-apps.js +171 -14
  10. package/dist/entra-audit-logs.js +101 -16
  11. package/dist/entra-auth-methods.js +8 -4
  12. package/dist/entra-certificate-auth.js +12 -8
  13. package/dist/entra-conditional-access.js +425 -15
  14. package/dist/entra-contacts.js +19 -12
  15. package/dist/entra-contracts.js +7 -4
  16. package/dist/entra-custom-security-attributes.js +19 -12
  17. package/dist/entra-data-policy-operations.js +7 -4
  18. package/dist/entra-delegated-admin.js +55 -6
  19. package/dist/entra-deleted-items.js +6 -4
  20. package/dist/entra-directory-objects.js +6 -4
  21. package/dist/entra-directory.js +19 -12
  22. package/dist/entra-domains.js +13 -8
  23. package/dist/entra-entitlement-management.js +12 -4
  24. package/dist/entra-federation-configurations.js +7 -4
  25. package/dist/entra-grants.js +8 -4
  26. package/dist/entra-group-lifecycle.js +13 -8
  27. package/dist/entra-group-pim.js +431 -0
  28. package/dist/entra-groups.js +13 -8
  29. package/dist/entra-identity-providers.js +7 -4
  30. package/dist/entra-lifecycle-workflows.js +152 -13
  31. package/dist/entra-multi-tenant-organization.js +7 -4
  32. package/dist/entra-on-premises-synchronization.js +6 -4
  33. package/dist/entra-organization.js +13 -8
  34. package/dist/entra-risk-prevention.js +442 -0
  35. package/dist/entra-risk.js +224 -9
  36. package/dist/entra-roles.js +204 -12
  37. package/dist/entra-subscriptions.js +6 -4
  38. package/dist/entra-terms-of-use.js +13 -8
  39. package/dist/entra-users.js +7 -4
  40. package/dist/graph-session.js +30 -5
  41. package/dist/hook.js +84 -0
  42. package/dist/list-totals.js +52 -0
  43. package/dist/setup.js +9 -1
  44. package/docs/coverage.md +743 -610
  45. package/inventory/operations.json +2 -2
  46. package/inventory/schema.json +1 -1
  47. package/package.json +4 -3
  48. package/skills/mg-axi/SKILL.md +86 -1
@@ -1,4 +1,5 @@
1
1
  import { AxiError } from "axi-sdk-js";
2
+ import { listTotals } from "./list-totals.js";
2
3
  // EXT-04 partner contracts subfamily: the read mapping behind `mg-axi entra
3
4
  // contract list/show/count`. Operation construction stays beside its command;
4
5
  // the shared session owns URLs, credentials, paging, retries and error
@@ -198,20 +199,22 @@ export async function listContracts(session, flags, profile, operation, help, pr
198
199
  if (!result.complete) {
199
200
  return {
200
201
  contracts,
201
- count: { returned: contracts.length, complete: false, reason: result.reason },
202
+ ...listTotals(contracts.length, result.total, "contracts", false),
203
+ complete: false,
204
+ reason: result.reason,
202
205
  cursor: result.cursor,
203
206
  help: [...truncationHints, resumeHint(profileName), showHint, PARTNER_NOTE],
204
207
  };
205
208
  }
206
- const count = { returned: contracts.length, complete: true };
207
209
  if (!contracts.length) {
208
210
  return {
209
211
  contracts,
210
- count,
212
+ ...listTotals(contracts.length, result.total, "contracts", true),
213
+ complete: true,
211
214
  help: ["0 contracts matched; the absence of results is the answer, not an error", PARTNER_NOTE],
212
215
  };
213
216
  }
214
- return { contracts, count, help: [...truncationHints, showHint, PARTNER_NOTE] };
217
+ return { contracts, ...listTotals(contracts.length, result.total, "contracts", true), complete: true, help: [...truncationHints, showHint, PARTNER_NOTE] };
215
218
  }
216
219
  export async function showContract(session, flags, profile, operation, help, profileName) {
217
220
  const { select, fields } = selectedFields(flags, KNOWN_CONTRACTS, KNOWN_CONTRACT_FIELDS, DEFAULT_CONTRACT_SHOW_SELECT, help);
@@ -1,4 +1,5 @@
1
1
  import { AxiError } from "axi-sdk-js";
2
+ import { listTotals } from "./list-totals.js";
2
3
  // EXT-01 custom-security-attributes subfamily: the read mapping behind
3
4
  // `mg-axi entra attribute-set list/show/count`,
4
5
  // `mg-axi entra custom-security-attribute-definition list/show/count` and
@@ -265,20 +266,22 @@ export async function listAttributeSets(session, flags, profile, operation, help
265
266
  if (!result.complete) {
266
267
  return {
267
268
  attributeSets: sets,
268
- count: { returned: sets.length, complete: false, reason: result.reason },
269
+ ...listTotals(sets.length, result.total, "attribute sets", false),
270
+ complete: false,
271
+ reason: result.reason,
269
272
  cursor: result.cursor,
270
273
  help: [...truncationHints, resumeHint(profileName), showHint, filterNote],
271
274
  };
272
275
  }
273
- const count = { returned: sets.length, complete: true };
274
276
  if (!sets.length) {
275
277
  return {
276
278
  attributeSets: sets,
277
- count,
279
+ ...listTotals(sets.length, result.total, "attribute sets", true),
280
+ complete: true,
278
281
  help: [filterNote, "0 attribute sets matched; the absence of results is the answer, not an error"],
279
282
  };
280
283
  }
281
- return { attributeSets: sets, count, help: [...truncationHints, showHint, filterNote] };
284
+ return { attributeSets: sets, ...listTotals(sets.length, result.total, "attribute sets", true), complete: true, help: [...truncationHints, showHint, filterNote] };
282
285
  }
283
286
  export async function showAttributeSet(session, flags, profile, operation, help, profileName) {
284
287
  const { select, fields } = selectedFields(flags, KNOWN_ATTRIBUTE_SETS, KNOWN_ATTRIBUTE_SET_FIELDS, DEFAULT_ATTRIBUTE_SET_SHOW_SELECT, help, "attribute-set");
@@ -331,20 +334,22 @@ export async function listCustomSecurityAttributeDefinitions(session, flags, pro
331
334
  if (!result.complete) {
332
335
  return {
333
336
  customSecurityAttributeDefinitions: definitions,
334
- count: { returned: definitions.length, complete: false, reason: result.reason },
337
+ ...listTotals(definitions.length, result.total, "custom security attribute definitions", false),
338
+ complete: false,
339
+ reason: result.reason,
335
340
  cursor: result.cursor,
336
341
  help: [...truncationHints, resumeHint(profileName), showHint],
337
342
  };
338
343
  }
339
- const count = { returned: definitions.length, complete: true };
340
344
  if (!definitions.length) {
341
345
  return {
342
346
  customSecurityAttributeDefinitions: definitions,
343
- count,
347
+ ...listTotals(definitions.length, result.total, "custom security attribute definitions", true),
348
+ complete: true,
344
349
  help: ["0 custom security attribute definitions matched; the absence of results is the answer, not an error"],
345
350
  };
346
351
  }
347
- return { customSecurityAttributeDefinitions: definitions, count, help: [...truncationHints, showHint] };
352
+ return { customSecurityAttributeDefinitions: definitions, ...listTotals(definitions.length, result.total, "custom security attribute definitions", true), complete: true, help: [...truncationHints, showHint] };
348
353
  }
349
354
  export async function showCustomSecurityAttributeDefinition(session, flags, profile, operation, help, profileName) {
350
355
  const { select, fields } = selectedFields(flags, KNOWN_DEFINITIONS, KNOWN_CUSTOM_SECURITY_DEFINITION_FIELDS, DEFAULT_DEFINITION_SHOW_SELECT, help, "definition");
@@ -407,20 +412,22 @@ export async function listAllowedValues(session, flags, profile, operation, help
407
412
  if (!result.complete) {
408
413
  return {
409
414
  allowedValues: values,
410
- count: { returned: values.length, complete: false, reason: result.reason },
415
+ ...listTotals(values.length, result.total, "allowed values", false),
416
+ complete: false,
417
+ reason: result.reason,
411
418
  cursor: result.cursor,
412
419
  help: [...truncationHints, resumeHint(profileName), showHint, filterNote],
413
420
  };
414
421
  }
415
- const count = { returned: values.length, complete: true };
416
422
  if (!values.length) {
417
423
  return {
418
424
  allowedValues: values,
419
- count,
425
+ ...listTotals(values.length, result.total, "allowed values", true),
426
+ complete: true,
420
427
  help: [filterNote, "0 allowed values matched; the definition may allow free-form values instead of predefined ones"],
421
428
  };
422
429
  }
423
- return { allowedValues: values, count, help: [...truncationHints, showHint, filterNote] };
430
+ return { allowedValues: values, ...listTotals(values.length, result.total, "allowed values", true), complete: true, help: [...truncationHints, showHint, filterNote] };
424
431
  }
425
432
  export async function showAllowedValue(session, flags, profile, operation, help, profileName) {
426
433
  const { select, fields } = selectedFields(flags, KNOWN_ALLOWED_VALUES, KNOWN_ALLOWED_VALUE_FIELDS, DEFAULT_ALLOWED_VALUE_SHOW_SELECT, help, "allowed-value");
@@ -1,4 +1,5 @@
1
1
  import { AxiError } from "axi-sdk-js";
2
+ import { listTotals } from "./list-totals.js";
2
3
  import { REDACTED } from "./graph-session.js";
3
4
  // EXT-03 data-policy-operations subfamily: the read mapping behind
4
5
  // `mg-axi entra data-policy-operation list/show/count`. Operation
@@ -224,20 +225,22 @@ export async function listDataPolicyOperations(session, flags, profile, operatio
224
225
  if (!result.complete) {
225
226
  return {
226
227
  dataPolicyOperations: operations,
227
- count: { returned: operations.length, complete: false, reason: result.reason },
228
+ ...listTotals(operations.length, result.total, "data-policy operations", false),
229
+ complete: false,
230
+ reason: result.reason,
228
231
  cursor: result.cursor,
229
232
  help: [...truncationHints, resumeHint(profileName), showHint, WORKFORCE_NOTE],
230
233
  };
231
234
  }
232
- const count = { returned: operations.length, complete: true };
233
235
  if (!operations.length) {
234
236
  return {
235
237
  dataPolicyOperations: operations,
236
- count,
238
+ ...listTotals(operations.length, result.total, "data-policy operations", true),
239
+ complete: true,
237
240
  help: ["0 data-policy operations matched; the absence of results is the answer, not an error", WORKFORCE_NOTE],
238
241
  };
239
242
  }
240
- return { dataPolicyOperations: operations, count, help: [...truncationHints, showHint, WORKFORCE_NOTE] };
243
+ return { dataPolicyOperations: operations, ...listTotals(operations.length, result.total, "data-policy operations", true), complete: true, help: [...truncationHints, showHint, WORKFORCE_NOTE] };
241
244
  }
242
245
  export async function showDataPolicyOperation(session, flags, profile, operation, help, profileName) {
243
246
  const { select, fields } = selectedFields(flags, DEFAULT_SHOW_SELECT, help);
@@ -1,4 +1,5 @@
1
1
  import { AxiError } from "axi-sdk-js";
2
+ import { listTotals } from "./list-totals.js";
2
3
  // EXT-04 delegated-admin subfamily: the read mapping behind
3
4
  // `mg-axi entra delegated-admin-customer list/show`,
4
5
  // `mg-axi entra delegated-admin-relationship list/show` and the navigation
@@ -35,8 +36,8 @@ import { AxiError } from "axi-sdk-js";
35
36
  // --filter passes through as plain $filter with no $count or ConsistencyLevel
36
37
  // contract. serviceManagementDetails documents no query parameters, so both
37
38
  // service-management-detail reads run queryless and project locally. The
38
- // $count scalars and the tenantRelationship container root stay scheduled
39
- // for later EXT-04 subfamilies, as do the multi-tenant-organization reads
39
+ // six $count scalars ship in this module; the tenantRelationship container
40
+ // root stays scheduled for a later EXT-04 subfamily, as do the multi-tenant-organization reads
40
41
  // (shipped separately) and the tenant-lookup functions (shipped as
41
42
  // tenant-information show under mg-ext-04e). Beta stays out. No delegated-admin mutation exists in this slice:
42
43
  // relationship creation, approval and termination are writes.
@@ -389,20 +390,22 @@ async function listResource(session, flags, profile, operation, help, profileNam
389
390
  if (!result.complete) {
390
391
  return {
391
392
  [collectionKey]: rows,
392
- count: { returned: rows.length, complete: false, reason: result.reason },
393
+ ...listTotals(rows.length, result.total, `${resource.noun}s`, false),
394
+ complete: false,
395
+ reason: result.reason,
393
396
  cursor: result.cursor,
394
397
  help: [...truncationHints, resumeHint(profileName), showHint, partnerNote(resource)],
395
398
  };
396
399
  }
397
- const count = { returned: rows.length, complete: true };
398
400
  if (!rows.length) {
399
401
  return {
400
402
  [collectionKey]: rows,
401
- count,
403
+ ...listTotals(rows.length, result.total, `${resource.noun}s`, true),
404
+ complete: true,
402
405
  help: [`0 ${resource.noun}s matched; the absence of results is the answer, not an error`, partnerNote(resource)],
403
406
  };
404
407
  }
405
- return { [collectionKey]: rows, count, help: [...truncationHints, showHint, partnerNote(resource)] };
408
+ return { [collectionKey]: rows, ...listTotals(rows.length, result.total, `${resource.noun}s`, true), complete: true, help: [...truncationHints, showHint, partnerNote(resource)] };
406
409
  }
407
410
  async function showResource(session, flags, profile, operation, help, profileName, resource, rowKey, showCommand) {
408
411
  const queryless = resource.queryless === true;
@@ -466,3 +469,49 @@ export async function listDelegatedAdminServiceManagementDetails(session, flags,
466
469
  export async function showDelegatedAdminServiceManagementDetail(session, flags, profile, operation, help, profileName) {
467
470
  return showResource(session, flags, profile, operation, help, profileName, SERVICE_MANAGEMENT_DETAIL, "delegatedAdminServiceManagementDetail", "entra delegated-admin-customer show-service-management-detail");
468
471
  }
472
+ // The $count routes return a text/plain integer scalar rather than a JSON
473
+ // collection, so each leaf reads it through session.execute and accepts only
474
+ // a non-negative integer. There is no --filter/--select/--limit contract on
475
+ // the counts: the catalogue declares no such flags and strict input
476
+ // validation refuses them before credentials. All six share the parent
477
+ // list/show read scope DelegatedAdminRelationship.Read.All, so no new
478
+ // consent is needed.
479
+ function countParentId(flags, label, help) {
480
+ const id = String(flags.id);
481
+ if (!id.trim())
482
+ throw new AxiError(`--id needs the delegated-admin ${label} identifier`, "VALIDATION_ERROR", [help]);
483
+ return id;
484
+ }
485
+ async function countScalar(session, flags, profile, operation, help, profileName, params, rowKey, noun) {
486
+ void profileName;
487
+ const scopes = scopesFor(flags, DEFAULT_DELEGATED_ADMIN_SCOPES, profile, help);
488
+ const raw = await withGuidance(DELEGATED_ADMIN_DENIAL_HINTS, () => session.execute({ profile, operation, params, scopes }));
489
+ if (typeof raw !== "number" || !Number.isInteger(raw) || raw < 0) {
490
+ throw new AxiError(`Graph returned a malformed delegated-admin ${noun} count body`, "GRAPH_ERROR", [
491
+ "Delegated-admin counts carry one non-negative integer scalar; treat anything else as unknown, not empty",
492
+ ]);
493
+ }
494
+ return { [rowKey]: raw };
495
+ }
496
+ export async function countDelegatedAdminCustomers(session, flags, profile, operation, help, profileName) {
497
+ return countScalar(session, flags, profile, operation, help, profileName, {}, "delegatedAdminCustomerCount", "customer");
498
+ }
499
+ export async function countDelegatedAdminServiceManagementDetails(session, flags, profile, operation, help, profileName) {
500
+ const customer = countParentId(flags, "customer", help);
501
+ return countScalar(session, flags, profile, operation, help, profileName, { "delegatedAdminCustomer-id": customer }, "delegatedAdminServiceManagementDetailCount", "service-management detail");
502
+ }
503
+ export async function countDelegatedAdminRelationships(session, flags, profile, operation, help, profileName) {
504
+ return countScalar(session, flags, profile, operation, help, profileName, {}, "delegatedAdminRelationshipCount", "relationship");
505
+ }
506
+ export async function countDelegatedAdminAccessAssignments(session, flags, profile, operation, help, profileName) {
507
+ const relationship = countParentId(flags, "relationship", help);
508
+ return countScalar(session, flags, profile, operation, help, profileName, { "delegatedAdminRelationship-id": relationship }, "delegatedAdminAccessAssignmentCount", "access assignment");
509
+ }
510
+ export async function countDelegatedAdminOperations(session, flags, profile, operation, help, profileName) {
511
+ const relationship = countParentId(flags, "relationship", help);
512
+ return countScalar(session, flags, profile, operation, help, profileName, { "delegatedAdminRelationship-id": relationship }, "delegatedAdminRelationshipOperationCount", "relationship operation");
513
+ }
514
+ export async function countDelegatedAdminRequests(session, flags, profile, operation, help, profileName) {
515
+ const relationship = countParentId(flags, "relationship", help);
516
+ return countScalar(session, flags, profile, operation, help, profileName, { "delegatedAdminRelationship-id": relationship }, "delegatedAdminRelationshipRequestCount", "relationship request");
517
+ }
@@ -1,4 +1,5 @@
1
1
  import { AxiError } from "axi-sdk-js";
2
+ import { listTotals } from "./list-totals.js";
2
3
  // EXT-01 deleted-items subfamily: the read mapping behind
3
4
  // `mg-axi entra deleted-user|deleted-group|deleted-application|
4
5
  // deleted-service-principal|deleted-administrative-unit list/count` and
@@ -402,16 +403,17 @@ export async function listDeletedItems(session, flags, profile, operation, help,
402
403
  if (!result.complete) {
403
404
  return {
404
405
  deletedItems: rows,
405
- count: { returned: rows.length, complete: false, reason: result.reason },
406
+ ...listTotals(rows.length, result.total, "deleted items", false),
407
+ complete: false,
408
+ reason: result.reason,
406
409
  cursor: result.cursor,
407
410
  help: [...truncationHints, resumeHint(profileName), showHint, ...groupNote, ...appNote],
408
411
  };
409
412
  }
410
- const count = { returned: rows.length, complete: true };
411
413
  if (!rows.length) {
412
- return { deletedItems: rows, count, help: [emptyNote, ...groupNote, ...appNote] };
414
+ return { deletedItems: rows, ...listTotals(rows.length, result.total, "deleted items", true), complete: true, help: [emptyNote, ...groupNote, ...appNote] };
413
415
  }
414
- return { deletedItems: rows, count, help: [...truncationHints, showHint, ...groupNote, ...appNote] };
416
+ return { deletedItems: rows, ...listTotals(rows.length, result.total, "deleted items", true), complete: true, help: [...truncationHints, showHint, ...groupNote, ...appNote] };
415
417
  }
416
418
  export async function showDeletedItem(session, flags, profile, operation, help, profileName) {
417
419
  checkShowOperation(operation, help);
@@ -1,4 +1,5 @@
1
1
  import { AxiError } from "axi-sdk-js";
2
+ import { listTotals } from "./list-totals.js";
2
3
  // EXT-01 directory-objects subfamily: the read mapping behind
3
4
  // `mg-axi entra directory-object list/show/count`. Operation construction
4
5
  // stays beside its command; the shared session owns URLs, credentials,
@@ -225,16 +226,17 @@ export async function listDirectoryObjects(session, flags, profile, operation, h
225
226
  if (!result.complete) {
226
227
  return {
227
228
  directoryObjects,
228
- count: { returned: directoryObjects.length, complete: false, reason: result.reason },
229
+ ...listTotals(directoryObjects.length, result.total, "directory objects", false),
230
+ complete: false,
231
+ reason: result.reason,
229
232
  cursor: result.cursor,
230
233
  help: [...truncationHints, resumeHint(profileName), showHint],
231
234
  };
232
235
  }
233
- const count = { returned: directoryObjects.length, complete: true };
234
236
  if (!directoryObjects.length) {
235
- return { directoryObjects, count, help: [EMPTY_NOTE] };
237
+ return { directoryObjects, ...listTotals(directoryObjects.length, result.total, "directory objects", true), complete: true, help: [EMPTY_NOTE] };
236
238
  }
237
- return { directoryObjects, count, help: [...truncationHints, showHint] };
239
+ return { directoryObjects, ...listTotals(directoryObjects.length, result.total, "directory objects", true), complete: true, help: [...truncationHints, showHint] };
238
240
  }
239
241
  export async function showDirectoryObject(session, flags, profile, operation, help, profileName) {
240
242
  checkReadOperation(operation, help);
@@ -1,4 +1,5 @@
1
1
  import { AxiError } from "axi-sdk-js";
2
+ import { listTotals } from "./list-totals.js";
2
3
  // READ-10: the directory-device and administrative-unit read mapping behind
3
4
  // `mg-axi entra device list/show`, `mg-axi entra administrative-unit
4
5
  // list/show` and `mg-axi entra administrative-unit member list`. Operation
@@ -260,16 +261,18 @@ export async function listDevices(session, flags, profile, operation, help, prof
260
261
  if (!result.complete) {
261
262
  return {
262
263
  devices,
263
- count: { returned: devices.length, complete: false, reason: result.reason },
264
+ ...listTotals(devices.length, result.total, "devices", false),
265
+ complete: false,
266
+ reason: result.reason,
264
267
  cursor: result.cursor,
265
268
  help: [...truncationHints, resumeHint(profileName), showHint, scopeHint],
266
269
  };
267
270
  }
268
- const count = { returned: devices.length, complete: true };
269
271
  if (!devices.length) {
270
272
  return {
271
273
  devices,
272
- count,
274
+ ...listTotals(devices.length, result.total, "devices", true),
275
+ complete: true,
273
276
  help: [
274
277
  `mg-axi entra device list --filter <odata-filter> ${profileHint(profileName)}`,
275
278
  "0 devices matched; the absence of results is the answer, not an error",
@@ -277,7 +280,7 @@ export async function listDevices(session, flags, profile, operation, help, prof
277
280
  ],
278
281
  };
279
282
  }
280
- return { devices, count, help: [...truncationHints, showHint, scopeHint] };
283
+ return { devices, ...listTotals(devices.length, result.total, "devices", true), complete: true, help: [...truncationHints, showHint, scopeHint] };
281
284
  }
282
285
  export async function showDevice(session, flags, profile, operation, help, profileName) {
283
286
  const { select, fields } = selectedFields(flags, KNOWN_DEVICES, KNOWN_DEVICE_FIELDS, DEFAULT_DEVICE_SHOW_SELECT, help);
@@ -318,23 +321,25 @@ export async function listAdministrativeUnits(session, flags, profile, operation
318
321
  if (!result.complete) {
319
322
  return {
320
323
  administrativeUnits: units,
321
- count: { returned: units.length, complete: false, reason: result.reason },
324
+ ...listTotals(units.length, result.total, "administrative units", false),
325
+ complete: false,
326
+ reason: result.reason,
322
327
  cursor: result.cursor,
323
328
  help: [...truncationHints, resumeHint(profileName), showHint],
324
329
  };
325
330
  }
326
- const count = { returned: units.length, complete: true };
327
331
  if (!units.length) {
328
332
  return {
329
333
  administrativeUnits: units,
330
- count,
334
+ ...listTotals(units.length, result.total, "administrative units", true),
335
+ complete: true,
331
336
  help: [
332
337
  `mg-axi entra administrative-unit list --filter <odata-filter> ${profileHint(profileName)}`,
333
338
  "0 administrative units matched; the absence of results is the answer, not an error",
334
339
  ],
335
340
  };
336
341
  }
337
- return { administrativeUnits: units, count, help: [...truncationHints, showHint] };
342
+ return { administrativeUnits: units, ...listTotals(units.length, result.total, "administrative units", true), complete: true, help: [...truncationHints, showHint] };
338
343
  }
339
344
  export async function showAdministrativeUnit(session, flags, profile, operation, help, profileName) {
340
345
  const { select, fields } = selectedFields(flags, KNOWN_AUS, KNOWN_AU_FIELDS, DEFAULT_AU_SHOW_SELECT, help);
@@ -390,16 +395,18 @@ export async function listAdministrativeUnitMembers(session, flags, profile, ope
390
395
  if (!result.complete) {
391
396
  return {
392
397
  members: rows,
393
- count: { returned: rows.length, complete: false, reason: result.reason },
398
+ ...listTotals(rows.length, result.total, "members", false),
399
+ complete: false,
400
+ reason: result.reason,
394
401
  cursor: result.cursor,
395
402
  help: [...truncationHints, ...limitedHints, hiddenHint, resumeHint(profileName)],
396
403
  };
397
404
  }
398
- const count = { returned: rows.length, complete: true };
399
405
  if (!rows.length) {
400
406
  return {
401
407
  members: rows,
402
- count,
408
+ ...listTotals(rows.length, result.total, "members", true),
409
+ complete: true,
403
410
  help: [
404
411
  ...limitedHints,
405
412
  hiddenHint,
@@ -407,5 +414,5 @@ export async function listAdministrativeUnitMembers(session, flags, profile, ope
407
414
  ],
408
415
  };
409
416
  }
410
- return { members: rows, count, help: [...truncationHints, ...limitedHints, hiddenHint] };
417
+ return { members: rows, ...listTotals(rows.length, result.total, "members", true), complete: true, help: [...truncationHints, ...limitedHints, hiddenHint] };
411
418
  }
@@ -1,4 +1,5 @@
1
1
  import { AxiError } from "axi-sdk-js";
2
+ import { listTotals } from "./list-totals.js";
2
3
  // EXT-01 domains subfamily: the read mapping behind `mg-axi entra domain
3
4
  // list/show`, the per-domain verification and service-configuration DNS
4
5
  // record reads, and the top-level domain DNS record reads. Operation
@@ -254,20 +255,22 @@ export async function listDomains(session, flags, profile, operation, help, prof
254
255
  if (!result.complete) {
255
256
  return {
256
257
  domains,
257
- count: { returned: domains.length, complete: false, reason: result.reason },
258
+ ...listTotals(domains.length, result.total, "domains", false),
259
+ complete: false,
260
+ reason: result.reason,
258
261
  cursor: result.cursor,
259
262
  help: [...truncationHints, resumeHint(profileName), showHint, filterNote],
260
263
  };
261
264
  }
262
- const count = { returned: domains.length, complete: true };
263
265
  if (!domains.length) {
264
266
  return {
265
267
  domains,
266
- count,
268
+ ...listTotals(domains.length, result.total, "domains", true),
269
+ complete: true,
267
270
  help: [filterNote, "0 domains matched; the absence of results is the answer, not an error"],
268
271
  };
269
272
  }
270
- return { domains, count, help: [...truncationHints, showHint, filterNote] };
273
+ return { domains, ...listTotals(domains.length, result.total, "domains", true), complete: true, help: [...truncationHints, showHint, filterNote] };
271
274
  }
272
275
  export async function showDomain(session, flags, profile, operation, help, profileName) {
273
276
  const { select, fields } = selectedFields(flags, KNOWN_DOMAINS, KNOWN_DOMAIN_FIELDS, DEFAULT_DOMAIN_SHOW_SELECT, help);
@@ -321,20 +324,22 @@ async function listDnsRecords(session, flags, profile, operation, help, profileN
321
324
  if (!result.complete) {
322
325
  return {
323
326
  [command.collectionKey]: records,
324
- count: { returned: records.length, complete: false, reason: result.reason },
327
+ ...listTotals(records.length, result.total, "DNS records", false),
328
+ complete: false,
329
+ reason: result.reason,
325
330
  cursor: result.cursor,
326
331
  help: [...truncationHints, resumeHint(profileName), showHint, kindHint],
327
332
  };
328
333
  }
329
- const count = { returned: records.length, complete: true };
330
334
  if (!records.length) {
331
335
  return {
332
336
  [command.collectionKey]: records,
333
- count,
337
+ ...listTotals(records.length, result.total, "DNS records", true),
338
+ complete: true,
334
339
  help: [kindHint, "0 DNS records matched; the absence of results is the answer, not an error"],
335
340
  };
336
341
  }
337
- return { [command.collectionKey]: records, count, help: [...truncationHints, showHint, kindHint] };
342
+ return { [command.collectionKey]: records, ...listTotals(records.length, result.total, "DNS records", true), complete: true, help: [...truncationHints, showHint, kindHint] };
338
343
  }
339
344
  async function showDnsRecord(session, flags, profile, operation, help, profileName, command) {
340
345
  const { select, fields } = selectedFields(flags, KNOWN_DNS, KNOWN_DNS_FIELDS, DEFAULT_DNS_SHOW_SELECT, help);
@@ -1,4 +1,5 @@
1
1
  import { AxiError } from "axi-sdk-js";
2
+ import { listTotals } from "./list-totals.js";
2
3
  // EXT-02 entitlement-management reads: the read-only catalog and
3
4
  // access-package mapping plus the assignment and request reads. See
4
5
  // README.md for the supported commands and usage. Operation construction
@@ -348,6 +349,7 @@ function flattenRequestLinks(source) {
348
349
  const CATALOG_LIST = {
349
350
  command: "entra entitlement catalog list",
350
351
  key: "catalogs",
352
+ noun: "catalogs",
351
353
  known: CATALOG_KNOWN,
352
354
  knownList: KNOWN_CATALOG_FIELDS,
353
355
  defaultSelect: DEFAULT_CATALOG_LIST_SELECT,
@@ -360,6 +362,7 @@ const CATALOG_LIST = {
360
362
  const ACCESS_PACKAGE_LIST = {
361
363
  command: "entra entitlement access-package list",
362
364
  key: "accessPackages",
365
+ noun: "access packages",
363
366
  known: ACCESS_PACKAGE_KNOWN,
364
367
  knownList: KNOWN_ACCESS_PACKAGE_FIELDS,
365
368
  defaultSelect: DEFAULT_ACCESS_PACKAGE_LIST_SELECT,
@@ -372,6 +375,7 @@ const ACCESS_PACKAGE_LIST = {
372
375
  const ASSIGNMENT_POLICY_LIST = {
373
376
  command: "entra entitlement assignment-policy list",
374
377
  key: "assignmentPolicies",
378
+ noun: "assignment policies",
375
379
  known: ASSIGNMENT_POLICY_KNOWN,
376
380
  knownList: KNOWN_ASSIGNMENT_POLICY_FIELDS,
377
381
  defaultSelect: DEFAULT_ASSIGNMENT_POLICY_LIST_SELECT,
@@ -384,6 +388,7 @@ const ASSIGNMENT_POLICY_LIST = {
384
388
  const RESOURCE_ROLE_SCOPE_LIST = {
385
389
  command: "entra entitlement resource-role-scope list",
386
390
  key: "resourceRoleScopes",
391
+ noun: "resource-role scopes",
387
392
  known: RESOURCE_ROLE_SCOPE_KNOWN,
388
393
  knownList: KNOWN_RESOURCE_ROLE_SCOPE_FIELDS,
389
394
  defaultSelect: DEFAULT_RESOURCE_ROLE_SCOPE_LIST_SELECT,
@@ -396,6 +401,7 @@ const RESOURCE_ROLE_SCOPE_LIST = {
396
401
  const ASSIGNMENT_LIST = {
397
402
  command: "entra entitlement assignment list",
398
403
  key: "assignments",
404
+ noun: "assignments",
399
405
  known: ASSIGNMENT_KNOWN,
400
406
  knownList: KNOWN_ASSIGNMENT_FIELDS,
401
407
  defaultSelect: DEFAULT_ASSIGNMENT_LIST_SELECT,
@@ -413,6 +419,7 @@ const ASSIGNMENT_LIST = {
413
419
  const ASSIGNMENT_REQUEST_LIST = {
414
420
  command: "entra entitlement assignment-request list",
415
421
  key: "assignmentRequests",
422
+ noun: "assignment requests",
416
423
  known: ASSIGNMENT_REQUEST_KNOWN,
417
424
  knownList: KNOWN_ASSIGNMENT_REQUEST_FIELDS,
418
425
  defaultSelect: DEFAULT_ASSIGNMENT_REQUEST_LIST_SELECT,
@@ -501,16 +508,17 @@ async function listCollection(shape, session, flags, profile, operation, help, p
501
508
  if (!result.complete) {
502
509
  return {
503
510
  [shape.key]: rows,
504
- count: { returned: rows.length, complete: false, reason: result.reason },
511
+ ...listTotals(rows.length, result.total, shape.noun, false),
512
+ complete: false,
513
+ reason: result.reason,
505
514
  cursor: result.cursor,
506
515
  help: [...truncationHints, resumeHint, ...standing],
507
516
  };
508
517
  }
509
- const count = { returned: rows.length, complete: true };
510
518
  if (!rows.length) {
511
- return { [shape.key]: rows, count, help: [shape.emptyNote, ...standing] };
519
+ return { [shape.key]: rows, ...listTotals(rows.length, result.total, shape.noun, true), complete: true, help: [shape.emptyNote, ...standing] };
512
520
  }
513
- return { [shape.key]: rows, count, help: [...truncationHints, ...standing] };
521
+ return { [shape.key]: rows, ...listTotals(rows.length, result.total, shape.noun, true), complete: true, help: [...truncationHints, ...standing] };
514
522
  }
515
523
  async function showOne(key, command, known, knownList, defaultSelect, session, flags, profile, operation, help, profileName, params, malformed, extra) {
516
524
  const { select, fields, fetch } = selectedFields(flags, defaultSelect, known, knownList, help, extra?.synthetic);
@@ -1,4 +1,5 @@
1
1
  import { AxiError } from "axi-sdk-js";
2
+ import { listTotals } from "./list-totals.js";
2
3
  // EXT-01 directory federation-configurations subfamily: the read mapping
3
4
  // behind `mg-axi entra federation-configuration list/show/count/available-types`.
4
5
  // Operation construction stays beside its command; the shared session owns
@@ -203,20 +204,22 @@ export async function listFederationConfigurations(session, flags, profile, oper
203
204
  if (!result.complete) {
204
205
  return {
205
206
  federationConfigurations: configurations,
206
- count: { returned: configurations.length, complete: false, reason: result.reason },
207
+ ...listTotals(configurations.length, result.total, "federation configurations", false),
208
+ complete: false,
209
+ reason: result.reason,
207
210
  cursor: result.cursor,
208
211
  help: [...truncationHints, `Resume losslessly with the same flags plus --cursor <cursor-from-output> ${profileHint(profileName)}`, showHint, CERT_BLOB_NOTE, WORKFORCE_NOTE],
209
212
  };
210
213
  }
211
- const count = { returned: configurations.length, complete: true };
212
214
  if (!configurations.length) {
213
215
  return {
214
216
  federationConfigurations: configurations,
215
- count,
217
+ ...listTotals(configurations.length, result.total, "federation configurations", true),
218
+ complete: true,
216
219
  help: ["0 federation configurations matched; no SAML or WS-Fed federation is configured for the tenant, or no row passed --filter", CERT_BLOB_NOTE, WORKFORCE_NOTE],
217
220
  };
218
221
  }
219
- return { federationConfigurations: configurations, count, help: [...truncationHints, showHint, CERT_BLOB_NOTE, WORKFORCE_NOTE] };
222
+ return { federationConfigurations: configurations, ...listTotals(configurations.length, result.total, "federation configurations", true), complete: true, help: [...truncationHints, showHint, CERT_BLOB_NOTE, WORKFORCE_NOTE] };
220
223
  }
221
224
  export async function showFederationConfiguration(session, flags, profile, operation, help, profileName) {
222
225
  const { select, fields } = selectedFields(flags, DEFAULT_SHOW_SELECT, help);
@@ -1,4 +1,5 @@
1
1
  import { AxiError } from "axi-sdk-js";
2
+ import { listTotals } from "./list-totals.js";
2
3
  // READ-08: the Entra consent-grant read mapping behind
3
4
  // `mg-axi entra service-principal oauth2-grant list` and
4
5
  // `mg-axi entra service-principal app-role-assignment list` for a named
@@ -181,6 +182,7 @@ const APP_ROLE_DENIAL_HINTS = [
181
182
  const OAUTH2_GRANT_LIST = {
182
183
  command: "entra service-principal oauth2-grant list",
183
184
  key: "oauth2PermissionGrants",
185
+ noun: "oauth2 grants",
184
186
  known: GRANT_KNOWN,
185
187
  knownList: KNOWN_GRANT_FIELDS,
186
188
  defaultSelect: DEFAULT_GRANT_SELECT,
@@ -192,6 +194,7 @@ const OAUTH2_GRANT_LIST = {
192
194
  const APP_ROLE_LIST = {
193
195
  command: "entra service-principal app-role-assignment list",
194
196
  key: "appRoleAssignments",
197
+ noun: "app role assignments",
195
198
  known: APP_ROLE_KNOWN,
196
199
  knownList: KNOWN_APP_ROLE_FIELDS,
197
200
  defaultSelect: DEFAULT_APP_ROLE_SELECT,
@@ -253,16 +256,17 @@ async function listCollection(shape, session, flags, profile, operation, help, p
253
256
  if (!result.complete) {
254
257
  return {
255
258
  [shape.key]: rows,
256
- count: { returned: rows.length, complete: false, reason: result.reason },
259
+ ...listTotals(rows.length, result.total, shape.noun, false),
260
+ complete: false,
261
+ reason: result.reason,
257
262
  cursor: result.cursor,
258
263
  help: [...truncationHints, `Resume losslessly with the same flags plus --cursor <cursor-from-output> ${profileHint(profileName)}`, ...standing],
259
264
  };
260
265
  }
261
- const count = { returned: rows.length, complete: true };
262
266
  if (!rows.length) {
263
- return { [shape.key]: rows, count, help: [shape.emptyNote, ...standing] };
267
+ return { [shape.key]: rows, ...listTotals(rows.length, result.total, shape.noun, true), complete: true, help: [shape.emptyNote, ...standing] };
264
268
  }
265
- return { [shape.key]: rows, count, help: [...truncationHints, ...standing] };
269
+ return { [shape.key]: rows, ...listTotals(rows.length, result.total, shape.noun, true), complete: true, help: [...truncationHints, ...standing] };
266
270
  }
267
271
  export async function listOAuth2Grants(session, flags, profile, operation, help, profileName) {
268
272
  return listCollection(OAUTH2_GRANT_LIST, session, flags, profile, operation, help, profileName);