primitive-admin 1.1.0-alpha.84 → 1.1.0-alpha.86

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 (132) hide show
  1. package/README.md +77 -1
  2. package/assets/skill/skills/primitive-platform/SKILL.md +8 -0
  3. package/dist/src/commands/admins.js +29 -32
  4. package/dist/src/commands/admins.js.map +1 -1
  5. package/dist/src/commands/analytics.js +15 -2
  6. package/dist/src/commands/analytics.js.map +1 -1
  7. package/dist/src/commands/apps.js +12 -5
  8. package/dist/src/commands/apps.js.map +1 -1
  9. package/dist/src/commands/blob-buckets.js +2 -1
  10. package/dist/src/commands/blob-buckets.js.map +1 -1
  11. package/dist/src/commands/catalog.js +26 -10
  12. package/dist/src/commands/catalog.js.map +1 -1
  13. package/dist/src/commands/collection-type-configs.js +2 -1
  14. package/dist/src/commands/collection-type-configs.js.map +1 -1
  15. package/dist/src/commands/collections.js +675 -19
  16. package/dist/src/commands/collections.js.map +1 -1
  17. package/dist/src/commands/connections.js +8 -9
  18. package/dist/src/commands/connections.js.map +1 -1
  19. package/dist/src/commands/cron-triggers.js +2 -1
  20. package/dist/src/commands/cron-triggers.js.map +1 -1
  21. package/dist/src/commands/database-type-configs.js +4 -3
  22. package/dist/src/commands/database-type-configs.js.map +1 -1
  23. package/dist/src/commands/databases.js +27 -10
  24. package/dist/src/commands/databases.js.map +1 -1
  25. package/dist/src/commands/documents.d.ts +20 -0
  26. package/dist/src/commands/documents.js +236 -28
  27. package/dist/src/commands/documents.js.map +1 -1
  28. package/dist/src/commands/email-templates.js +2 -1
  29. package/dist/src/commands/email-templates.js.map +1 -1
  30. package/dist/src/commands/env.js +11 -1
  31. package/dist/src/commands/env.js.map +1 -1
  32. package/dist/src/commands/feature-flags.js +2 -1
  33. package/dist/src/commands/feature-flags.js.map +1 -1
  34. package/dist/src/commands/functions.js +58 -6
  35. package/dist/src/commands/functions.js.map +1 -1
  36. package/dist/src/commands/group-type-configs.js +2 -1
  37. package/dist/src/commands/group-type-configs.js.map +1 -1
  38. package/dist/src/commands/groups.js +29 -8
  39. package/dist/src/commands/groups.js.map +1 -1
  40. package/dist/src/commands/guides.js +17 -11
  41. package/dist/src/commands/guides.js.map +1 -1
  42. package/dist/src/commands/integrations.js +30 -9
  43. package/dist/src/commands/integrations.js.map +1 -1
  44. package/dist/src/commands/locks.js +2 -1
  45. package/dist/src/commands/locks.js.map +1 -1
  46. package/dist/src/commands/metadata-category-configs.js +2 -1
  47. package/dist/src/commands/metadata-category-configs.js.map +1 -1
  48. package/dist/src/commands/metadata.js +8 -1
  49. package/dist/src/commands/metadata.js.map +1 -1
  50. package/dist/src/commands/prompts.js +28 -11
  51. package/dist/src/commands/prompts.js.map +1 -1
  52. package/dist/src/commands/rule-sets.js +2 -1
  53. package/dist/src/commands/rule-sets.js.map +1 -1
  54. package/dist/src/commands/scripts.js +15 -6
  55. package/dist/src/commands/scripts.js.map +1 -1
  56. package/dist/src/commands/secrets.js +2 -1
  57. package/dist/src/commands/secrets.js.map +1 -1
  58. package/dist/src/commands/sessions.js +10 -9
  59. package/dist/src/commands/sessions.js.map +1 -1
  60. package/dist/src/commands/sync.d.ts +125 -29
  61. package/dist/src/commands/sync.js +865 -88
  62. package/dist/src/commands/sync.js.map +1 -1
  63. package/dist/src/commands/tokens.js +2 -1
  64. package/dist/src/commands/tokens.js.map +1 -1
  65. package/dist/src/commands/users.js +19 -10
  66. package/dist/src/commands/users.js.map +1 -1
  67. package/dist/src/commands/vars.js +2 -1
  68. package/dist/src/commands/vars.js.map +1 -1
  69. package/dist/src/commands/waitlist.js +2 -1
  70. package/dist/src/commands/waitlist.js.map +1 -1
  71. package/dist/src/commands/webhooks.js +17 -10
  72. package/dist/src/commands/webhooks.js.map +1 -1
  73. package/dist/src/commands/workflows.js +36 -18
  74. package/dist/src/commands/workflows.js.map +1 -1
  75. package/dist/src/lib/api-client.d.ts +176 -8
  76. package/dist/src/lib/api-client.js +205 -68
  77. package/dist/src/lib/api-client.js.map +1 -1
  78. package/dist/src/lib/collection-export.d.ts +184 -0
  79. package/dist/src/lib/collection-export.js +252 -0
  80. package/dist/src/lib/collection-export.js.map +1 -0
  81. package/dist/src/lib/config-object-descriptor.js +44 -8
  82. package/dist/src/lib/config-object-descriptor.js.map +1 -1
  83. package/dist/src/lib/config-payload.d.ts +8 -1
  84. package/dist/src/lib/config-payload.js +49 -4
  85. package/dist/src/lib/config-payload.js.map +1 -1
  86. package/dist/src/lib/config-surface.d.ts +2 -1
  87. package/dist/src/lib/config-surface.js +54 -3
  88. package/dist/src/lib/config-surface.js.map +1 -1
  89. package/dist/src/lib/deprecation.d.ts +22 -0
  90. package/dist/src/lib/deprecation.js +43 -0
  91. package/dist/src/lib/deprecation.js.map +1 -0
  92. package/dist/src/lib/function-db-types.d.ts +14 -3
  93. package/dist/src/lib/function-db-types.js +55 -11
  94. package/dist/src/lib/function-db-types.js.map +1 -1
  95. package/dist/src/lib/function-trigger-listing.d.ts +27 -0
  96. package/dist/src/lib/function-trigger-listing.js +70 -0
  97. package/dist/src/lib/function-trigger-listing.js.map +1 -0
  98. package/dist/src/lib/generated-config-surfaces.d.ts +715 -4
  99. package/dist/src/lib/generated-config-surfaces.js +2424 -226
  100. package/dist/src/lib/generated-config-surfaces.js.map +1 -1
  101. package/dist/src/lib/generated-sdk-types.d.ts +1 -1
  102. package/dist/src/lib/generated-sdk-types.js +1 -1
  103. package/dist/src/lib/generated-sdk-types.js.map +1 -1
  104. package/dist/src/lib/list-options.d.ts +68 -0
  105. package/dist/src/lib/list-options.js +89 -0
  106. package/dist/src/lib/list-options.js.map +1 -0
  107. package/dist/src/lib/log-inspection.d.ts +34 -1
  108. package/dist/src/lib/log-inspection.js +28 -0
  109. package/dist/src/lib/log-inspection.js.map +1 -1
  110. package/dist/src/lib/paginate.d.ts +15 -0
  111. package/dist/src/lib/paginate.js +17 -0
  112. package/dist/src/lib/paginate.js.map +1 -1
  113. package/dist/src/lib/prompt-cost-format.d.ts +11 -0
  114. package/dist/src/lib/prompt-cost-format.js +41 -0
  115. package/dist/src/lib/prompt-cost-format.js.map +1 -0
  116. package/dist/src/lib/prompt-schema-codegen.d.ts +53 -0
  117. package/dist/src/lib/prompt-schema-codegen.js +253 -3
  118. package/dist/src/lib/prompt-schema-codegen.js.map +1 -1
  119. package/dist/src/lib/swift-codegen/agentGenerator.d.ts +42 -0
  120. package/dist/src/lib/swift-codegen/agentGenerator.js +118 -0
  121. package/dist/src/lib/swift-codegen/agentGenerator.js.map +1 -0
  122. package/dist/src/lib/swift-codegen/banners.d.ts +6 -0
  123. package/dist/src/lib/swift-codegen/banners.js +6 -0
  124. package/dist/src/lib/swift-codegen/banners.js.map +1 -1
  125. package/dist/src/lib/swift-codegen/dbGenerator.js +11 -3
  126. package/dist/src/lib/swift-codegen/dbGenerator.js.map +1 -1
  127. package/dist/src/lib/swift-codegen/functionGenerator.d.ts +7 -0
  128. package/dist/src/lib/swift-codegen/functionGenerator.js +41 -3
  129. package/dist/src/lib/swift-codegen/functionGenerator.js.map +1 -1
  130. package/dist/src/lib/swift-codegen/generator.js +8 -2
  131. package/dist/src/lib/swift-codegen/generator.js.map +1 -1
  132. package/package.json +2 -2
@@ -6,6 +6,8 @@ import { resolveAppId } from "../lib/config.js";
6
6
  import { success, error, info, formatTable, formatId, formatDate, json, keyValue, result as printResult, warn, flushOutput, } from "../lib/output.js";
7
7
  import { confirmPrompt } from "../lib/confirm-prompt.js";
8
8
  import { resolveOwnerUserId } from "../lib/resolve-owner.js";
9
+ import { pageCursorOption, pageLimitOption, parsePageLimit, printEmptyPage, printPageHint, } from "../lib/list-options.js";
10
+ import { normalizeCliListEnvelope, wholeListEnvelope, } from "../lib/paginate.js";
9
11
  import { parseDataOption } from "../lib/data-input.js";
10
12
  import { parseFilterOptions } from "../lib/record-filter.js";
11
13
  import { buildPermissionsExport } from "../lib/document-export-permissions.js";
@@ -33,6 +35,23 @@ function printIngestSession(session) {
33
35
  printResult(label, value);
34
36
  }
35
37
  }
38
+ /**
39
+ * One refused records call, as one line for an operator (#3764).
40
+ *
41
+ * The same composition `describeBulkFailure` makes for the atomic blob, on the
42
+ * verbs that have no `--json` envelope to fill: the server's sentence, the
43
+ * stable code a script branches on, and the status. `statusCode` FIRST —
44
+ * `cli/src/lib/api-client.ts`'s `ApiError` is where the status lives, and a
45
+ * reader that asks for `status` alone prints nothing at all (#3597 paid for
46
+ * that once already).
47
+ */
48
+ export function describeRecordsFailure(err) {
49
+ const message = (typeof err?.message === "string" && err.message) || String(err);
50
+ const code = typeof err?.code === "string" && err.code ? err.code : undefined;
51
+ const raw = err?.statusCode ?? err?.status;
52
+ const status = Number.isFinite(Number(raw)) ? Number(raw) : undefined;
53
+ return `${message}${code ? ` [${code}]` : ""}${status !== undefined ? ` (${status})` : ""}`;
54
+ }
36
55
  /**
37
56
  * Report a document's format (#2816) — but only when it is a large document.
38
57
  *
@@ -47,6 +66,30 @@ function printDocumentFormat(doc) {
47
66
  keyValue("Format", "2 (large document)");
48
67
  }
49
68
  }
69
+ /**
70
+ * The `--document-format` an operator stated, or the refusal (#3764, E10).
71
+ *
72
+ * Refused HERE rather than by the server, because an operator CAN act on it
73
+ * (principle 6) and a round trip to be told what they typed wrong is a round
74
+ * trip they do not need to spend. Commander hands the raw string over, so the
75
+ * digits are what is read; an unset flag states nothing.
76
+ */
77
+ export function parseDocumentFormatOption(value) {
78
+ if (value === undefined || value === null)
79
+ return undefined;
80
+ const text = String(value).trim();
81
+ if (text === "1")
82
+ return 1;
83
+ if (text === "2")
84
+ return 2;
85
+ throw new Error(`Invalid --document-format "${String(value)}". Expected 1 (an ordinary ` +
86
+ `document) or 2 (a large document).`);
87
+ }
88
+ /** The `--document-format <1|2>` flag, said once for every records verb. */
89
+ const DOCUMENT_FORMAT_FLAG = "--document-format <1|2>";
90
+ const DOCUMENT_FORMAT_HELP = "The document format you expect: 1 (ordinary) or 2 (large). A disagreement " +
91
+ "with the platform's own answer is refused with DOCUMENT_FORMAT_MISMATCH " +
92
+ "rather than answered out of the wrong tables";
50
93
  /**
51
94
  * What `documents records bulk` reports when the write is refused (#3619).
52
95
  *
@@ -164,18 +207,25 @@ Examples:
164
207
  .description("List a user's documents")
165
208
  .requiredOption("--user-id <id>", "User ID whose documents to list")
166
209
  .option("--app <app-id>", "App ID")
210
+ .addOption(pageLimitOption())
211
+ .addOption(pageCursorOption())
167
212
  .option("--json", "Output as JSON")
168
213
  .action(async (options) => {
214
+ const limit = parsePageLimit(options.limit);
169
215
  const resolvedAppId = resolveAppId(undefined, options);
170
216
  const client = new ApiClient();
171
217
  try {
172
- const list = await client.listAdminDocuments(resolvedAppId, options.userId);
218
+ const page = normalizeCliListEnvelope(await client.listAdminDocumentsPage(resolvedAppId, options.userId, {
219
+ limit,
220
+ cursor: options.cursor,
221
+ }));
222
+ const list = page.items;
173
223
  if (options.json) {
174
- json(list);
224
+ json(page);
175
225
  return;
176
226
  }
177
227
  if (list.length === 0) {
178
- info("No documents found.");
228
+ printEmptyPage(page, "documents");
179
229
  return;
180
230
  }
181
231
  console.log(formatTable(list, [
@@ -188,6 +238,7 @@ Examples:
188
238
  { header: "PERMISSION", key: "permission" },
189
239
  { header: "GRANTED", key: "grantedAt", format: formatDate },
190
240
  ]));
241
+ printPageHint(page);
191
242
  }
192
243
  catch (err) {
193
244
  error(err.message);
@@ -378,7 +429,7 @@ Permissions (enforced by the server, not the CLI):
378
429
  const result = await client.listDocumentPermissions(resolvedAppId, documentId);
379
430
  const list = Array.isArray(result) ? result : result?.permissions ?? [];
380
431
  if (options.json) {
381
- json(list);
432
+ json(wholeListEnvelope(list));
382
433
  return;
383
434
  }
384
435
  if (list.length === 0) {
@@ -516,7 +567,9 @@ Permissions (enforced by the server, not the CLI):
516
567
  }
517
568
  }
518
569
  catch (err) {
519
- error(err.message);
570
+ // #3764 — the server's sentence, the stable code and the status, so a
571
+ // refused records call is scriptable rather than only readable.
572
+ error(describeRecordsFailure(err));
520
573
  process.exit(1);
521
574
  }
522
575
  });
@@ -567,7 +620,9 @@ Permissions (enforced by the server, not the CLI):
567
620
  ]));
568
621
  }
569
622
  catch (err) {
570
- error(err.message);
623
+ // #3764 — the server's sentence, the stable code and the status, so a
624
+ // refused records call is scriptable rather than only readable.
625
+ error(describeRecordsFailure(err));
571
626
  process.exit(1);
572
627
  }
573
628
  });
@@ -582,13 +637,24 @@ Permissions (enforced by the server, not the CLI):
582
637
  .option("--filter-file <path>", "Read filter from a JSON or TOML file")
583
638
  .option("--limit <n>", "Maximum number of records to return (max 100)", parseInt)
584
639
  .option("--cursor <cursor>", "Pagination cursor from a previous query")
640
+ .option(DOCUMENT_FORMAT_FLAG, DOCUMENT_FORMAT_HELP)
585
641
  .option("--json", "Output as JSON")
586
642
  .action(async (documentId, modelName, options) => {
587
643
  const resolvedAppId = resolveAppId(undefined, options);
644
+ let documentFormat;
645
+ try {
646
+ documentFormat = parseDocumentFormatOption(options.documentFormat);
647
+ }
648
+ catch (err) {
649
+ // #3764 — the server's sentence, the stable code and the status, so a
650
+ // refused records call is scriptable rather than only readable.
651
+ error(describeRecordsFailure(err));
652
+ process.exit(1);
653
+ }
588
654
  const filter = parseFilterOptions(options);
589
655
  const client = new ApiClient();
590
656
  try {
591
- const result = await client.queryDocumentRecords(resolvedAppId, documentId, modelName, { filter, limit: options.limit, cursor: options.cursor });
657
+ const result = await client.queryDocumentRecords(resolvedAppId, documentId, modelName, { filter, limit: options.limit, cursor: options.cursor, documentFormat });
592
658
  if (options.json) {
593
659
  json(result);
594
660
  return;
@@ -626,7 +692,9 @@ Permissions (enforced by the server, not the CLI):
626
692
  }
627
693
  }
628
694
  catch (err) {
629
- error(err.message);
695
+ // #3764 — the server's sentence, the stable code and the status, so a
696
+ // refused records call is scriptable rather than only readable.
697
+ error(describeRecordsFailure(err));
630
698
  process.exit(1);
631
699
  }
632
700
  });
@@ -638,15 +706,26 @@ Permissions (enforced by the server, not the CLI):
638
706
  .argument("<model-name>", "Model name")
639
707
  .argument("<record-id>", "Record ID")
640
708
  .option("--app <app-id>", "App ID")
709
+ .option(DOCUMENT_FORMAT_FLAG, DOCUMENT_FORMAT_HELP)
641
710
  .option("--json", "Output as JSON")
642
711
  .action(async (documentId, modelName, recordId, options) => {
643
712
  const resolvedAppId = resolveAppId(undefined, options);
713
+ let documentFormat;
714
+ try {
715
+ documentFormat = parseDocumentFormatOption(options.documentFormat);
716
+ }
717
+ catch (err) {
718
+ // #3764 — the server's sentence, the stable code and the status, so a
719
+ // refused records call is scriptable rather than only readable.
720
+ error(describeRecordsFailure(err));
721
+ process.exit(1);
722
+ }
644
723
  const client = new ApiClient();
645
724
  try {
646
725
  // A single-record fetch is a query filtered by primary-key id (there is
647
726
  // no dedicated get endpoint — reuse queryDocumentRecords, exactly as
648
727
  // the `databases records get` twin reuses queryDatabaseRecords).
649
- const result = await client.queryDocumentRecords(resolvedAppId, documentId, modelName, { filter: { id: recordId }, limit: 1 });
728
+ const result = await client.queryDocumentRecords(resolvedAppId, documentId, modelName, { filter: { id: recordId }, limit: 1, documentFormat });
650
729
  const record = result.items[0];
651
730
  if (options.json) {
652
731
  json(record ?? null);
@@ -670,7 +749,9 @@ Permissions (enforced by the server, not the CLI):
670
749
  }
671
750
  }
672
751
  catch (err) {
673
- error(err.message);
752
+ // #3764 — the server's sentence, the stable code and the status, so a
753
+ // refused records call is scriptable rather than only readable.
754
+ error(describeRecordsFailure(err));
674
755
  process.exit(1);
675
756
  }
676
757
  });
@@ -682,14 +763,25 @@ Permissions (enforced by the server, not the CLI):
682
763
  .argument("<model-name>", "Model name to count")
683
764
  .option("--app <app-id>", "App ID")
684
765
  .option("--filter <json>", "Filter as JSON (e.g. '{\"status\":\"open\"}')")
766
+ .option(DOCUMENT_FORMAT_FLAG, DOCUMENT_FORMAT_HELP)
685
767
  .option("--filter-file <path>", "Read filter from a JSON or TOML file")
686
768
  .option("--json", "Output as JSON")
687
769
  .action(async (documentId, modelName, options) => {
688
770
  const resolvedAppId = resolveAppId(undefined, options);
771
+ let documentFormat;
772
+ try {
773
+ documentFormat = parseDocumentFormatOption(options.documentFormat);
774
+ }
775
+ catch (err) {
776
+ // #3764 — the server's sentence, the stable code and the status, so a
777
+ // refused records call is scriptable rather than only readable.
778
+ error(describeRecordsFailure(err));
779
+ process.exit(1);
780
+ }
689
781
  const filter = parseFilterOptions(options);
690
782
  const client = new ApiClient();
691
783
  try {
692
- const result = await client.countDocumentRecords(resolvedAppId, documentId, modelName, { filter });
784
+ const result = await client.countDocumentRecords(resolvedAppId, documentId, modelName, { filter, documentFormat });
693
785
  if (options.json) {
694
786
  json(result);
695
787
  return;
@@ -697,7 +789,9 @@ Permissions (enforced by the server, not the CLI):
697
789
  console.log(` ${result.count}`);
698
790
  }
699
791
  catch (err) {
700
- error(err.message);
792
+ // #3764 — the server's sentence, the stable code and the status, so a
793
+ // refused records call is scriptable rather than only readable.
794
+ error(describeRecordsFailure(err));
701
795
  process.exit(1);
702
796
  }
703
797
  });
@@ -717,9 +811,20 @@ Permissions (enforced by the server, not the CLI):
717
811
  .option("--group-by <field>", "Group results by this plain field (repeatable; StringSet fields are not supported)", (value, previous = []) => previous.concat(value), [])
718
812
  .option("--filter <json>", "Filter as JSON")
719
813
  .option("--filter-file <path>", "Read filter from a JSON or TOML file")
814
+ .option(DOCUMENT_FORMAT_FLAG, DOCUMENT_FORMAT_HELP)
720
815
  .option("--json", "Output as JSON")
721
816
  .action(async (documentId, modelName, options) => {
722
817
  const resolvedAppId = resolveAppId(undefined, options);
818
+ let documentFormat;
819
+ try {
820
+ documentFormat = parseDocumentFormatOption(options.documentFormat);
821
+ }
822
+ catch (err) {
823
+ // #3764 — the server's sentence, the stable code and the status, so a
824
+ // refused records call is scriptable rather than only readable.
825
+ error(describeRecordsFailure(err));
826
+ process.exit(1);
827
+ }
723
828
  if (!AGGREGATE_OPS.includes(options.op)) {
724
829
  error(`Invalid --op "${options.op}". Expected one of: ${AGGREGATE_OPS.join(", ")}.`);
725
830
  process.exit(1);
@@ -742,7 +847,7 @@ Permissions (enforced by the server, not the CLI):
742
847
  const groupBy = options.groupBy || [];
743
848
  const client = new ApiClient();
744
849
  try {
745
- const response = await client.aggregateDocumentRecords(resolvedAppId, documentId, modelName, { operations: [operation], groupBy, filter });
850
+ const response = await client.aggregateDocumentRecords(resolvedAppId, documentId, modelName, { operations: [operation], groupBy, filter }, { documentFormat });
746
851
  if (options.json) {
747
852
  json(response);
748
853
  return;
@@ -779,7 +884,9 @@ Permissions (enforced by the server, not the CLI):
779
884
  }
780
885
  }
781
886
  catch (err) {
782
- error(err.message);
887
+ // #3764 — the server's sentence, the stable code and the status, so a
888
+ // refused records call is scriptable rather than only readable.
889
+ error(describeRecordsFailure(err));
783
890
  process.exit(1);
784
891
  }
785
892
  });
@@ -802,15 +909,26 @@ Permissions (enforced by the server, not the CLI):
802
909
  .option("--data <json>", "Record fields as JSON (e.g. '{\"qty\":10}')")
803
910
  .option("--data-file <file>", "Read record fields from a JSON file")
804
911
  .option("--upsert-on <field>", "Update the record whose <field> matches instead of creating")
912
+ .option(DOCUMENT_FORMAT_FLAG, DOCUMENT_FORMAT_HELP)
805
913
  .option("--json", "Output as JSON")
806
914
  .action(async (documentId, modelName, options) => {
807
915
  const data = parseDataOption(options, "record fields");
808
916
  const resolvedAppId = resolveAppId(undefined, options);
917
+ let documentFormat;
918
+ try {
919
+ documentFormat = parseDocumentFormatOption(options.documentFormat);
920
+ }
921
+ catch (err) {
922
+ // #3764 — the server's sentence, the stable code and the status, so a
923
+ // refused records call is scriptable rather than only readable.
924
+ error(describeRecordsFailure(err));
925
+ process.exit(1);
926
+ }
809
927
  const recordId = options.id || ulid();
810
928
  const writeOptions = options.upsertOn ? { upsertOn: options.upsertOn } : undefined;
811
929
  const client = new ApiClient();
812
930
  try {
813
- const result = await client.saveDocumentRecord(resolvedAppId, documentId, modelName, { id: recordId, data, ...(writeOptions ? { options: writeOptions } : {}) });
931
+ const result = await client.saveDocumentRecord(resolvedAppId, documentId, modelName, { id: recordId, data, ...(writeOptions ? { options: writeOptions } : {}) }, { documentFormat });
814
932
  if (options.json) {
815
933
  json(result);
816
934
  return;
@@ -818,7 +936,9 @@ Permissions (enforced by the server, not the CLI):
818
936
  success(`Record saved: ${result.record?.id ?? recordId}`);
819
937
  }
820
938
  catch (err) {
821
- error(err.message);
939
+ // #3764 — the server's sentence, the stable code and the status, so a
940
+ // refused records call is scriptable rather than only readable.
941
+ error(describeRecordsFailure(err));
822
942
  process.exit(1);
823
943
  }
824
944
  });
@@ -831,14 +951,25 @@ Permissions (enforced by the server, not the CLI):
831
951
  .argument("<record-id>", "Record ID to patch")
832
952
  .option("--app <app-id>", "App ID")
833
953
  .option("--data <json>", "Fields to merge as JSON (e.g. '{\"status\":\"closed\"}')")
954
+ .option(DOCUMENT_FORMAT_FLAG, DOCUMENT_FORMAT_HELP)
834
955
  .option("--data-file <file>", "Read fields from a JSON file")
835
956
  .option("--json", "Output as JSON")
836
957
  .action(async (documentId, modelName, recordId, options) => {
837
958
  const data = parseDataOption(options, "fields to merge");
838
959
  const resolvedAppId = resolveAppId(undefined, options);
960
+ let documentFormat;
961
+ try {
962
+ documentFormat = parseDocumentFormatOption(options.documentFormat);
963
+ }
964
+ catch (err) {
965
+ // #3764 — the server's sentence, the stable code and the status, so a
966
+ // refused records call is scriptable rather than only readable.
967
+ error(describeRecordsFailure(err));
968
+ process.exit(1);
969
+ }
839
970
  const client = new ApiClient();
840
971
  try {
841
- const result = await client.patchDocumentRecord(resolvedAppId, documentId, modelName, recordId, { data });
972
+ const result = await client.patchDocumentRecord(resolvedAppId, documentId, modelName, recordId, { data }, { documentFormat });
842
973
  if (options.json) {
843
974
  json(result);
844
975
  return;
@@ -846,7 +977,9 @@ Permissions (enforced by the server, not the CLI):
846
977
  success(`Record patched: ${recordId}`);
847
978
  }
848
979
  catch (err) {
849
- error(err.message);
980
+ // #3764 — the server's sentence, the stable code and the status, so a
981
+ // refused records call is scriptable rather than only readable.
982
+ error(describeRecordsFailure(err));
850
983
  process.exit(1);
851
984
  }
852
985
  });
@@ -858,10 +991,21 @@ Permissions (enforced by the server, not the CLI):
858
991
  .argument("<model-name>", "Model name")
859
992
  .argument("<record-id>", "Record ID to delete")
860
993
  .option("--app <app-id>", "App ID")
994
+ .option(DOCUMENT_FORMAT_FLAG, DOCUMENT_FORMAT_HELP)
861
995
  .option("-y, --yes", "Skip confirmation prompt")
862
996
  .option("--json", "Output as JSON")
863
997
  .action(async (documentId, modelName, recordId, options) => {
864
998
  const resolvedAppId = resolveAppId(undefined, options);
999
+ let documentFormat;
1000
+ try {
1001
+ documentFormat = parseDocumentFormatOption(options.documentFormat);
1002
+ }
1003
+ catch (err) {
1004
+ // #3764 — the server's sentence, the stable code and the status, so a
1005
+ // refused records call is scriptable rather than only readable.
1006
+ error(describeRecordsFailure(err));
1007
+ process.exit(1);
1008
+ }
865
1009
  if (!options.yes) {
866
1010
  const confirmed = await confirmPrompt(`Delete record ${recordId} from ${modelName} in document ${documentId}?`);
867
1011
  if (!confirmed) {
@@ -871,7 +1015,7 @@ Permissions (enforced by the server, not the CLI):
871
1015
  }
872
1016
  const client = new ApiClient();
873
1017
  try {
874
- const result = await client.deleteDocumentRecord(resolvedAppId, documentId, modelName, recordId);
1018
+ const result = await client.deleteDocumentRecord(resolvedAppId, documentId, modelName, recordId, { documentFormat });
875
1019
  if (options.json) {
876
1020
  json(result);
877
1021
  return;
@@ -881,7 +1025,9 @@ Permissions (enforced by the server, not the CLI):
881
1025
  success(`Record deleted: ${recordId}`);
882
1026
  }
883
1027
  catch (err) {
884
- error(err.message);
1028
+ // #3764 — the server's sentence, the stable code and the status, so a
1029
+ // refused records call is scriptable rather than only readable.
1030
+ error(describeRecordsFailure(err));
885
1031
  process.exit(1);
886
1032
  }
887
1033
  });
@@ -896,9 +1042,20 @@ Permissions (enforced by the server, not the CLI):
896
1042
  "create/patch, not allowed on delete; a create `id` must be a 26-char " +
897
1043
  "uppercase Crockford ULID")
898
1044
  .option("--app <app-id>", "App ID")
1045
+ .option(DOCUMENT_FORMAT_FLAG, DOCUMENT_FORMAT_HELP)
899
1046
  .option("-y, --yes", "Skip confirmation prompt")
900
1047
  .option("--json", "Output as JSON")
901
1048
  .action(async (documentId, options) => {
1049
+ let documentFormat;
1050
+ try {
1051
+ documentFormat = parseDocumentFormatOption(options.documentFormat);
1052
+ }
1053
+ catch (err) {
1054
+ // #3764 — the server's sentence, the stable code and the status, so a
1055
+ // refused records call is scriptable rather than only readable.
1056
+ error(describeRecordsFailure(err));
1057
+ process.exit(1);
1058
+ }
902
1059
  const parsed = parseDataOption(options, "operations blob", {
903
1060
  allowArray: true,
904
1061
  });
@@ -917,7 +1074,7 @@ Permissions (enforced by the server, not the CLI):
917
1074
  }
918
1075
  const client = new ApiClient();
919
1076
  try {
920
- const result = await client.bulkDocumentRecords(resolvedAppId, documentId, operations);
1077
+ const result = await client.bulkDocumentRecords(resolvedAppId, documentId, operations, { documentFormat });
921
1078
  if (options.json) {
922
1079
  json(result);
923
1080
  return;
@@ -1152,13 +1309,14 @@ Permissions (enforced by the server, not the CLI):
1152
1309
  .option("--cursor <cursor>", "Continue from a previous page's nextCursor")
1153
1310
  .option("--json", "Output as JSON")
1154
1311
  .action(async (documentId, options) => {
1312
+ const limit = parsePageLimit(options.limit);
1155
1313
  const resolvedAppId = resolveAppId(undefined, options);
1156
1314
  const client = new ApiClient();
1157
1315
  try {
1158
- const page = await client.listDocumentIngests(resolvedAppId, documentId, {
1159
- limit: Number(options.limit),
1316
+ const page = normalizeCliListEnvelope(await client.listDocumentIngests(resolvedAppId, documentId, {
1317
+ limit,
1160
1318
  ...(options.cursor ? { cursor: options.cursor } : {}),
1161
- });
1319
+ }));
1162
1320
  if (options.json) {
1163
1321
  json(page);
1164
1322
  return;
@@ -1226,13 +1384,14 @@ Permissions (enforced by the server, not the CLI):
1226
1384
  .option("--cursor <cursor>", "Continue from a previous page's nextCursor")
1227
1385
  .option("--json", "Output as JSON")
1228
1386
  .action(async (documentId, options) => {
1387
+ const limit = parsePageLimit(options.limit);
1229
1388
  const resolvedAppId = resolveAppId(undefined, options);
1230
1389
  const client = new ApiClient();
1231
1390
  try {
1232
- const page = await client.listDocumentSnapshots(resolvedAppId, documentId, {
1233
- limit: Number(options.limit),
1391
+ const page = normalizeCliListEnvelope(await client.listDocumentSnapshots(resolvedAppId, documentId, {
1392
+ limit,
1234
1393
  ...(options.cursor ? { cursor: options.cursor } : {}),
1235
- });
1394
+ }));
1236
1395
  if (options.json) {
1237
1396
  json(page);
1238
1397
  return;
@@ -1540,7 +1699,7 @@ Permissions (enforced by the server, not the CLI):
1540
1699
  const result = await client.listDocumentGroupPermissions(resolvedAppId, documentId);
1541
1700
  const list = Array.isArray(result) ? result : result?.permissions ?? [];
1542
1701
  if (options.json) {
1543
- json(list);
1702
+ json(wholeListEnvelope(list));
1544
1703
  return;
1545
1704
  }
1546
1705
  if (list.length === 0) {
@@ -1589,6 +1748,55 @@ Permissions (enforced by the server, not the CLI):
1589
1748
  process.exit(1);
1590
1749
  }
1591
1750
  });
1751
+ // ---- Effective access for a named user (#3658) ----
1752
+ const access = documents
1753
+ .command("access")
1754
+ .description("Read a user's effective access to a document");
1755
+ access
1756
+ .command("get")
1757
+ .description("Show what a user may do with a document, across direct, group, " +
1758
+ "collection and link access")
1759
+ .argument("<document-id>", "Document ID")
1760
+ .requiredOption("--user <user-id>", "The user whose access to resolve")
1761
+ .option("--app <app-id>", "App ID")
1762
+ .option("--json", "Output as JSON")
1763
+ .action(async (documentId, options) => {
1764
+ const resolvedAppId = resolveAppId(undefined, options);
1765
+ const client = new ApiClient();
1766
+ try {
1767
+ const result = await client.validateDocumentAccessForUser(resolvedAppId, documentId, options.user);
1768
+ const row = {
1769
+ documentId,
1770
+ userId: options.user,
1771
+ hasAccess: result?.hasAccess === true,
1772
+ permission: result?.permission ?? null,
1773
+ accessSource: result?.accessSource ?? null,
1774
+ appRole: result?.appRole ?? null,
1775
+ };
1776
+ if (options.json) {
1777
+ json(row);
1778
+ return;
1779
+ }
1780
+ console.log(formatTable([row], [
1781
+ { header: "DOCUMENT", key: "documentId" },
1782
+ { header: "USER", key: "userId" },
1783
+ { header: "ACCESS", key: "hasAccess" },
1784
+ { header: "PERMISSION", key: "permission" },
1785
+ { header: "SOURCE", key: "accessSource" },
1786
+ { header: "APP_ROLE", key: "appRole" },
1787
+ ]));
1788
+ if (!row.hasAccess) {
1789
+ // The role is not a grant: only the tag routes admit an
1790
+ // administrator without one, so an operator reading "admin" here
1791
+ // should not conclude the user may write content.
1792
+ info("No document grant. An app role admits tag changes only, never a content write.");
1793
+ }
1794
+ }
1795
+ catch (err) {
1796
+ error(err.message);
1797
+ process.exit(1);
1798
+ }
1799
+ });
1592
1800
  // ---- Link access ("anyone with the link") ----
1593
1801
  const linkAccess = documents
1594
1802
  .command("link-access")