@xano/cli 1.3.4 → 1.3.5-beta.1

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 (105) hide show
  1. package/README.md +70 -3
  2. package/dist/base-command.d.ts +12 -1
  3. package/dist/base-command.js +36 -22
  4. package/dist/commands/branch/set_live/index.d.ts +14 -0
  5. package/dist/commands/branch/set_live/index.js +38 -0
  6. package/dist/commands/ephemeral/push/index.js +6 -1
  7. package/dist/commands/function/create/index.d.ts +15 -0
  8. package/dist/commands/function/create/index.js +39 -0
  9. package/dist/commands/function/edit/index.d.ts +15 -0
  10. package/dist/commands/function/edit/index.js +39 -0
  11. package/dist/commands/function/run/index.js +5 -11
  12. package/dist/commands/policy/catalogue/index.d.ts +16 -0
  13. package/dist/commands/policy/catalogue/index.js +60 -0
  14. package/dist/commands/{tenant/snapshot/create → policy/coverage}/index.d.ts +4 -4
  15. package/dist/commands/policy/coverage/index.js +33 -0
  16. package/dist/commands/policy/create/index.d.ts +18 -0
  17. package/dist/commands/policy/create/index.js +44 -0
  18. package/dist/commands/{tenant/snapshot → policy}/delete/index.d.ts +5 -5
  19. package/dist/commands/policy/delete/index.js +61 -0
  20. package/dist/commands/policy/evaluate/index.d.ts +32 -0
  21. package/dist/commands/policy/evaluate/index.js +71 -0
  22. package/dist/commands/{tenant/snapshot → policy}/list/index.d.ts +3 -5
  23. package/dist/commands/policy/list/index.js +21 -0
  24. package/dist/commands/policy/parse/index.d.ts +19 -0
  25. package/dist/commands/policy/parse/index.js +18 -0
  26. package/dist/commands/policy/publish/index.d.ts +20 -0
  27. package/dist/commands/policy/publish/index.js +38 -0
  28. package/dist/commands/policy/runs/index.d.ts +54 -0
  29. package/dist/commands/policy/runs/index.js +273 -0
  30. package/dist/commands/policy/status/index.d.ts +21 -0
  31. package/dist/commands/policy/status/index.js +54 -0
  32. package/dist/commands/release/create/index.js +4 -0
  33. package/dist/commands/release/deploy/index.d.ts +19 -0
  34. package/dist/commands/release/deploy/index.js +68 -6
  35. package/dist/commands/release/import/index.js +5 -1
  36. package/dist/commands/release/list/index.d.ts +7 -0
  37. package/dist/commands/release/list/index.js +46 -9
  38. package/dist/commands/release/push/index.js +19 -1
  39. package/dist/commands/sandbox/push/index.js +6 -1
  40. package/dist/commands/skills/pull/index.d.ts +30 -0
  41. package/dist/commands/skills/pull/index.js +118 -0
  42. package/dist/commands/tenant/create/index.d.ts +3 -0
  43. package/dist/commands/tenant/create/index.js +21 -0
  44. package/dist/commands/tenant/deploy_release/index.d.ts +40 -0
  45. package/dist/commands/tenant/deploy_release/index.js +182 -4
  46. package/dist/commands/tenant/edit/index.d.ts +2 -0
  47. package/dist/commands/tenant/edit/index.js +39 -4
  48. package/dist/commands/tenant_deploy_request/bypass/index.d.ts +31 -0
  49. package/dist/commands/tenant_deploy_request/bypass/index.js +109 -0
  50. package/dist/commands/tenant_deploy_request/create/index.d.ts +21 -0
  51. package/dist/commands/tenant_deploy_request/create/index.js +108 -0
  52. package/dist/commands/tenant_deploy_request/edit/index.d.ts +22 -0
  53. package/dist/commands/tenant_deploy_request/edit/index.js +103 -0
  54. package/dist/commands/tenant_deploy_request/get/index.d.ts +25 -0
  55. package/dist/commands/tenant_deploy_request/get/index.js +113 -0
  56. package/dist/commands/tenant_deploy_request/list/index.d.ts +20 -0
  57. package/dist/commands/tenant_deploy_request/list/index.js +122 -0
  58. package/dist/commands/{tenant/snapshot/swap → tenant_deploy_request/revision}/index.d.ts +8 -6
  59. package/dist/commands/tenant_deploy_request/revision/index.js +83 -0
  60. package/dist/commands/tenant_deploy_request/set_status/index.d.ts +38 -0
  61. package/dist/commands/tenant_deploy_request/set_status/index.js +159 -0
  62. package/dist/commands/workspace/pull/index.d.ts +10 -0
  63. package/dist/commands/workspace/pull/index.js +70 -7
  64. package/dist/commands/workspace/push/index.d.ts +19 -0
  65. package/dist/commands/workspace/push/index.js +135 -6
  66. package/dist/policy-command.d.ts +91 -0
  67. package/dist/policy-command.js +95 -0
  68. package/dist/utils/document-parser.d.ts +10 -0
  69. package/dist/utils/document-parser.js +21 -0
  70. package/dist/utils/multidoc-push.d.ts +57 -2
  71. package/dist/utils/multidoc-push.js +102 -33
  72. package/dist/utils/policy/catalogue.d.ts +85 -0
  73. package/dist/utils/policy/catalogue.js +196 -0
  74. package/dist/utils/policy/coverage.d.ts +19 -0
  75. package/dist/utils/policy/coverage.js +93 -0
  76. package/dist/utils/policy/errors.d.ts +20 -0
  77. package/dist/utils/policy/errors.js +93 -0
  78. package/dist/utils/policy/feedback.d.ts +90 -0
  79. package/dist/utils/policy/feedback.js +210 -0
  80. package/dist/utils/policy/findings.d.ts +29 -0
  81. package/dist/utils/policy/findings.js +82 -0
  82. package/dist/utils/policy/gate.d.ts +138 -0
  83. package/dist/utils/policy/gate.js +193 -0
  84. package/dist/utils/policy/permission.d.ts +7 -0
  85. package/dist/utils/policy/permission.js +55 -0
  86. package/dist/utils/policy/push-gate.d.ts +5 -0
  87. package/dist/utils/policy/push-gate.js +43 -0
  88. package/dist/utils/policy/release.d.ts +85 -0
  89. package/dist/utils/policy/release.js +117 -0
  90. package/dist/utils/policy/request.d.ts +55 -0
  91. package/dist/utils/policy/request.js +119 -0
  92. package/dist/utils/policy/runs.d.ts +30 -0
  93. package/dist/utils/policy/runs.js +101 -0
  94. package/dist/utils/policy/status.d.ts +55 -0
  95. package/dist/utils/policy/status.js +127 -0
  96. package/dist/utils/policy/template.d.ts +66 -0
  97. package/dist/utils/policy/template.js +152 -0
  98. package/dist/utils/policy/types.d.ts +295 -0
  99. package/dist/utils/policy/types.js +1 -0
  100. package/oclif.manifest.json +6829 -4006
  101. package/package.json +9 -1
  102. package/dist/commands/tenant/snapshot/create/index.js +0 -78
  103. package/dist/commands/tenant/snapshot/delete/index.js +0 -102
  104. package/dist/commands/tenant/snapshot/list/index.js +0 -100
  105. package/dist/commands/tenant/snapshot/swap/index.js +0 -103
@@ -1,4 +1,4 @@
1
- import { ux } from '@oclif/core';
1
+ import { Errors, ux } from '@oclif/core';
2
2
  import { minimatch } from 'minimatch';
3
3
  import * as fs from 'node:fs';
4
4
  import { join, relative } from 'node:path';
@@ -7,9 +7,21 @@ import { compareLocalToExport, indexLocalDocuments, parseExportDocuments, resolv
7
7
  import { flattenBundleFile } from './flatten.js';
8
8
  import { collectKnowledgeObjects, fetchKnowledge, knowledgePreview, pushKnowledge, syncGuidToFrontmatter, toPushItems, } from './knowledge-sync.js';
9
9
  import { fetchFormattedObjects, } from './object-fetch.js';
10
+ import { PushPolicyGateError } from './policy/push-gate.js';
10
11
  import { checkReferences, checkTableIndexes } from './reference-checker.js';
11
12
  import { invalidateSandboxMarkerForOrigin, readMarkerToken, sandboxMarkerExists, writeMarkerToken, } from './sandbox-marker.js';
12
13
  import { hashBytes, hashFile, instanceHost, loadSyncState, recordEntriesForContents, resolveTarget, sandboxTargetIdentity, saveSyncState, syncStateExists, toRelPath, } from './sync-state.js';
14
+ /**
15
+ * A push that failed after its multidoc import landed. The import is not rolled back, so the error
16
+ * carries what it did for the calling command to report beside the failure.
17
+ */
18
+ export class FailedAfterImportError extends Errors.CLIError {
19
+ imported;
20
+ constructor(message, imported) {
21
+ super(message);
22
+ this.imported = imported;
23
+ }
24
+ }
13
25
  /**
14
26
  * Filter document entries down to the ones the dry-run preview reports as changed,
15
27
  * used to send only the changed documents during a partial (non-`--sync`) push.
@@ -712,7 +724,9 @@ function mergeKnowledgePreview(preview, knowledge) {
712
724
  */
713
725
  export async function executePush(ctx, target, flags) {
714
726
  const { accessToken, command, inputDir, verboseFetch } = ctx;
715
- const log = command.log.bind(command);
727
+ const log = ctx.log ?? command.log.bind(command);
728
+ let dryRunPreview = null;
729
+ const stop = (stopped) => ({ knowledge: { deleted: 0, imported: 0 }, preview: dryRunPreview, sent: [], stopped });
716
730
  if (flags.fast && flags.sync) {
717
731
  command.error('--fast cannot be used with --sync');
718
732
  }
@@ -781,7 +795,7 @@ export async function executePush(ctx, target, flags) {
781
795
  const doFlatten = await confirm('Flatten these file(s) in place and continue the push?');
782
796
  if (!doFlatten) {
783
797
  log('Push cancelled. Run `xano flatten <file>` yourself, or re-run to be prompted again.');
784
- return;
798
+ return stop('cancelled');
785
799
  }
786
800
  }
787
801
  else {
@@ -949,7 +963,7 @@ export async function executePush(ctx, target, flags) {
949
963
  }
950
964
  log('');
951
965
  log('No changes to push.');
952
- return;
966
+ return stop('no-changes');
953
967
  }
954
968
  documentEntries = candidates;
955
969
  recordSourceEntries = [...documentEntries];
@@ -994,7 +1008,7 @@ export async function executePush(ctx, target, flags) {
994
1008
  recordSourceEntries = [...documentEntries];
995
1009
  if (documentEntries.length === 0 && knowledgeObjects.length === 0) {
996
1010
  log('No local changes since last sync');
997
- return;
1011
+ return stop('no-changes');
998
1012
  }
999
1013
  multidoc = documentEntries.map((entry) => entry.content).join('\n---\n');
1000
1014
  documentFileMap.clear();
@@ -1048,7 +1062,6 @@ export async function executePush(ctx, target, flags) {
1048
1062
  'Content-Type': 'text/x-xanoscript',
1049
1063
  };
1050
1064
  // ── Dry-run / Preview ─────────────────────────────────────────────────
1051
- let dryRunPreview = null;
1052
1065
  const dryRunUrl = knowledgeOnly ? null : target.buildDryRunUrl(queryParams);
1053
1066
  if (dryRunUrl && (flags['dry-run'] || !flags.force)) {
1054
1067
  const dryRunParams = new URLSearchParams(queryParams);
@@ -1140,7 +1153,7 @@ export async function executePush(ctx, target, flags) {
1140
1153
  log('');
1141
1154
  log(ux.colorize('red', `Push blocked: ${criticalOps.length} critical error(s) found.`));
1142
1155
  if (!flags.force) {
1143
- return;
1156
+ return stop('blocked');
1144
1157
  }
1145
1158
  log(ux.colorize('yellow', 'Proceeding anyway due to --force flag.'));
1146
1159
  }
@@ -1173,10 +1186,10 @@ export async function executePush(ctx, target, flags) {
1173
1186
  if (persistRecord) {
1174
1187
  commitSyncRecordWithSandbox(ctx, inputDir, resolvedTarget, recordEntriesFor(recordSourceEntries.map((entry) => entry.filePath)), { pruneMissing, replace: replaceRecord() }, (message) => command.warn(message));
1175
1188
  }
1176
- return;
1189
+ return stop('no-changes');
1177
1190
  }
1178
1191
  if (flags['dry-run']) {
1179
- return;
1192
+ return stop('dry-run');
1180
1193
  }
1181
1194
  // Warn when the sandbox currently holds a different source workspace than the one being
1182
1195
  // pushed. The backend compares the source workspace id we sent against the id stored on
@@ -1194,7 +1207,7 @@ export async function executePush(ctx, target, flags) {
1194
1207
  const proceed = await confirm('Continue with push anyway?');
1195
1208
  if (!proceed) {
1196
1209
  log('Push cancelled. Run `xano sandbox reset` then retry.');
1197
- return;
1210
+ return stop('cancelled');
1198
1211
  }
1199
1212
  mismatchConfirmed = true;
1200
1213
  }
@@ -1215,7 +1228,7 @@ export async function executePush(ctx, target, flags) {
1215
1228
  const confirmed = await confirm(message);
1216
1229
  if (!confirmed) {
1217
1230
  log('Push cancelled.');
1218
- return;
1231
+ return stop('cancelled');
1219
1232
  }
1220
1233
  }
1221
1234
  else {
@@ -1232,15 +1245,17 @@ export async function executePush(ctx, target, flags) {
1232
1245
  }
1233
1246
  }
1234
1247
  else {
1235
- await handleDryRunError(dryRunResponse, command, flags, target);
1248
+ await handleDryRunError(dryRunResponse, command, target, { log, verbose: flags.verbose });
1236
1249
  // If we get here, the user confirmed to proceed without preview
1237
1250
  }
1238
1251
  }
1239
1252
  catch (error) {
1240
1253
  // Ctrl+C or SIGINT
1254
+ if (error instanceof PushPolicyGateError)
1255
+ throw error;
1241
1256
  if (error.name === 'AbortError' || error.code === 'ERR_USE_AFTER_CLOSE') {
1242
1257
  log('\nPush cancelled.');
1243
- return;
1258
+ return stop('cancelled');
1244
1259
  }
1245
1260
  // Re-throw oclif errors
1246
1261
  if (error instanceof Error && 'oclif' in error) {
@@ -1271,16 +1286,16 @@ export async function executePush(ctx, target, flags) {
1271
1286
  if (!hasChanges) {
1272
1287
  log('');
1273
1288
  log('No changes to push.');
1274
- return;
1289
+ return stop('no-changes');
1275
1290
  }
1276
1291
  if (flags['dry-run']) {
1277
- return;
1292
+ return stop('dry-run');
1278
1293
  }
1279
1294
  if (process.stdin.isTTY) {
1280
1295
  const confirmed = await confirm('Proceed with push?');
1281
1296
  if (!confirmed) {
1282
1297
  log('Push cancelled.');
1283
- return;
1298
+ return stop('cancelled');
1284
1299
  }
1285
1300
  }
1286
1301
  else {
@@ -1296,24 +1311,24 @@ export async function executePush(ctx, target, flags) {
1296
1311
  }
1297
1312
  }
1298
1313
  // ── Partial push: filter to changed documents only ────────────────────
1299
- let pushedEntries = documentEntries;
1314
+ let sent = documentEntries;
1300
1315
  if (!knowledgeOnly && isPartial && dryRunPreview) {
1301
- pushedEntries = filterChangedEntries(documentEntries, dryRunPreview.operations, flags.records);
1302
- if (pushedEntries.length === 0 && knowledgeObjects.length === 0) {
1316
+ sent = filterChangedEntries(documentEntries, dryRunPreview.operations, flags.records);
1317
+ if (sent.length === 0 && knowledgeObjects.length === 0) {
1303
1318
  log('No changes to push.');
1304
1319
  if (persistRecord) {
1305
1320
  commitSyncRecordWithSandbox(ctx, inputDir, resolvedTarget, recordEntriesFor(recordSourceEntries.map((entry) => entry.filePath)), { pruneMissing: false, replace: replaceRecord() }, (message) => command.warn(message));
1306
1321
  }
1307
- return;
1322
+ return stop('no-changes');
1308
1323
  }
1309
- multidoc = pushedEntries.length > 0 ? pushedEntries.map((d) => d.content).join('\n---\n') : '';
1324
+ multidoc = sent.map((d) => d.content).join('\n---\n');
1310
1325
  }
1311
1326
  const persist = () => {
1312
1327
  if (!persistRecord || !ctx.syncTarget)
1313
1328
  return;
1314
1329
  const paths = [
1315
1330
  ...recordSourceEntries.map((entry) => entry.filePath),
1316
- ...pushedEntries.map((entry) => entry.filePath),
1331
+ ...sent.map((entry) => entry.filePath),
1317
1332
  ];
1318
1333
  commitSyncRecordWithSandbox(ctx, inputDir, resolvedTarget, recordEntriesFor(paths), { pruneMissing, replace: replaceRecord() }, (message) => command.warn(message));
1319
1334
  };
@@ -1325,6 +1340,7 @@ export async function executePush(ctx, target, flags) {
1325
1340
  }
1326
1341
  const startTime = Date.now();
1327
1342
  let pushedDocCount = 0;
1343
+ let pushResponse;
1328
1344
  if (!knowledgeOnly && multidoc) {
1329
1345
  const apiUrl = target.buildPushUrl(queryParams);
1330
1346
  try {
@@ -1334,14 +1350,19 @@ export async function executePush(ctx, target, flags) {
1334
1350
  method: 'POST',
1335
1351
  }, flags.verbose, accessToken);
1336
1352
  if (!response.ok) {
1337
- handlePushError(response, await response.text(), documentEntries, inputDir, command);
1353
+ const errorText = await response.text();
1354
+ refuseIfExplained(command, target, response.status, errorText);
1355
+ handlePushError(response, errorText, documentEntries, inputDir, command);
1338
1356
  }
1339
1357
  // Parse response for GUID map
1340
1358
  const responseText = await response.text();
1341
1359
  let guidMap = [];
1360
+ pushResponse = {};
1342
1361
  if (responseText && responseText !== 'null') {
1343
1362
  try {
1344
1363
  const responseJson = JSON.parse(responseText);
1364
+ if (responseJson && typeof responseJson === 'object')
1365
+ pushResponse = responseJson;
1345
1366
  if (responseJson?.guid_map && Array.isArray(responseJson.guid_map)) {
1346
1367
  guidMap = responseJson.guid_map;
1347
1368
  }
@@ -1367,7 +1388,7 @@ export async function executePush(ctx, target, flags) {
1367
1388
  let updatedCount = 0;
1368
1389
  let canonicalCount = 0;
1369
1390
  for (const entry of guidMap) {
1370
- if (!entry.guid)
1391
+ if (!entry.guid || entry.type === 'policy')
1371
1392
  continue;
1372
1393
  const key = buildDocumentKey(entry.type, entry.name, entry.verb, entry.api_group);
1373
1394
  let filePath = documentFileMap.get(key);
@@ -1416,6 +1437,8 @@ export async function executePush(ctx, target, flags) {
1416
1437
  pushedDocCount = multidoc.split('\n---\n').length;
1417
1438
  }
1418
1439
  catch (error) {
1440
+ if (error instanceof PushPolicyGateError)
1441
+ throw error;
1419
1442
  if (error instanceof Error && 'oclif' in error)
1420
1443
  throw error;
1421
1444
  const elapsedMs = Date.now() - startTime;
@@ -1435,8 +1458,9 @@ export async function executePush(ctx, target, flags) {
1435
1458
  includeRecords: flags.records,
1436
1459
  inputDir,
1437
1460
  isLiveBranch: resolved.isLiveBranch,
1461
+ log,
1438
1462
  pushedBytes,
1439
- pushedEntries,
1463
+ pushedEntries: sent,
1440
1464
  resolvedTarget,
1441
1465
  syncState: loadedState,
1442
1466
  target,
@@ -1455,6 +1479,12 @@ export async function executePush(ctx, target, flags) {
1455
1479
  // ── Push knowledge ────────────────────────────────────────────────────
1456
1480
  let knowledgeImported = 0;
1457
1481
  let knowledgeDeleted = 0;
1482
+ const pushed = () => ({
1483
+ knowledge: { deleted: knowledgeDeleted, imported: knowledgeImported },
1484
+ preview: dryRunPreview,
1485
+ response: pushResponse,
1486
+ sent: pushResponse ? sent : [],
1487
+ });
1458
1488
  if (ctx.knowledge && (knowledgeObjects.length > 0 || shouldDelete)) {
1459
1489
  const listUrl = ctx.knowledge.listUrl();
1460
1490
  try {
@@ -1492,7 +1522,10 @@ export async function executePush(ctx, target, flags) {
1492
1522
  if (error instanceof Error && 'oclif' in error)
1493
1523
  throw error;
1494
1524
  const elapsedMs = Date.now() - startTime;
1495
- command.error(`Failed to push knowledge: ${describeNetworkError(error, listUrl, elapsedMs)}`);
1525
+ const message = `Failed to push knowledge: ${describeNetworkError(error, listUrl, elapsedMs)}`;
1526
+ if (pushResponse)
1527
+ throw new FailedAfterImportError(message, pushed());
1528
+ command.error(message);
1496
1529
  }
1497
1530
  }
1498
1531
  const elapsed = (Math.max(0, Date.now() - startTime - writebackMs) / 1000).toFixed(1);
@@ -1507,11 +1540,12 @@ export async function executePush(ctx, target, flags) {
1507
1540
  }
1508
1541
  persist();
1509
1542
  log(`Pushed ${parts.join(' + ')} to ${target.label} from ${relative(process.cwd(), inputDir) || inputDir} in ${elapsed}s`);
1543
+ return pushed();
1510
1544
  }
1511
1545
  export async function writeBackFormattedDocuments(args) {
1512
1546
  const started = Date.now();
1513
1547
  const { accessToken, branch, command, includeEnv, includeRecords, inputDir, isLiveBranch = !branch, pushedBytes, resolvedTarget, syncState, target, verbose, verboseFetch, workspaceId, writebackDiff, writebackObjectLimit, } = args;
1514
- const log = command.log.bind(command);
1548
+ const log = args.log ?? command.log.bind(command);
1515
1549
  const elapsedLabel = () => ` (${((Date.now() - started) / 1000).toFixed(1)}s)`;
1516
1550
  const ids = {};
1517
1551
  const written = [];
@@ -1774,10 +1808,37 @@ function formatFailureDuration(elapsedMs) {
1774
1808
  ? ` (failed after ~${human}, near a common ${boundaries.find((b) => Math.abs(totalSeconds - b) <= b * 0.05)}s timeout — likely a server or proxy cutoff)`
1775
1809
  : ` (failed after ${human})`;
1776
1810
  }
1777
- async function handleDryRunError(response, command, flags, target) {
1778
- const log = command.log.bind(command);
1811
+ /** Stop with the target's own guidance when it recognises this refusal. */
1812
+ function refuseIfExplained(command, target, status, body) {
1813
+ let message = body;
1814
+ let payload;
1815
+ try {
1816
+ const parsed = JSON.parse(body);
1817
+ if (typeof parsed?.message === 'string')
1818
+ message = parsed.message;
1819
+ payload = parsed?.payload;
1820
+ }
1821
+ catch {
1822
+ // Not JSON
1823
+ }
1824
+ const refusal = payload;
1825
+ if (status === 403 && refusal?.code === 'policy_gate')
1826
+ throw new PushPolicyGateError(refusal, message === body ? undefined : message);
1827
+ // The plan's policy cap: the platform's message names the plan, its cap and the remedy, and the
1828
+ // push itself would be refused the same way, so the preview stops here rather than skipping.
1829
+ if (status === 403 && refusal?.code === 'policy_plan_limit')
1830
+ command.error(`Push refused (${status}): ${message}`);
1831
+ if (refusal?.code === 'policy_gate_transaction_required') {
1832
+ command.error('Push refused: the live branch policy gate requires a transaction. Remove --no-transaction and retry. Nothing was imported.');
1833
+ }
1834
+ const guidance = target.explainRefusal?.(status, payload);
1835
+ if (guidance)
1836
+ command.error(`Push refused (${status}): ${message}${guidance}`);
1837
+ }
1838
+ async function handleDryRunError(response, command, target, { log, verbose }) {
1839
+ const errorText = await response.text();
1840
+ refuseIfExplained(command, target, response.status, errorText);
1779
1841
  if (response.status === 404) {
1780
- const errorText = await response.text();
1781
1842
  try {
1782
1843
  const errorJson = JSON.parse(errorText);
1783
1844
  if (errorJson.message) {
@@ -1795,7 +1856,6 @@ async function handleDryRunError(response, command, flags, target) {
1795
1856
  log('');
1796
1857
  }
1797
1858
  else {
1798
- const errorText = await response.text();
1799
1859
  // Check if push is disabled
1800
1860
  try {
1801
1861
  const errorJson = JSON.parse(errorText);
@@ -1818,8 +1878,17 @@ async function handleDryRunError(response, command, flags, target) {
1818
1878
  catch {
1819
1879
  // Not JSON, fall through
1820
1880
  }
1821
- command.warn(`Push preview failed (${response.status}). Skipping preview.`);
1822
- if (flags.verbose) {
1881
+ let reason = '';
1882
+ try {
1883
+ const errorJson = JSON.parse(errorText);
1884
+ if (typeof errorJson?.message === 'string' && errorJson.message.trim())
1885
+ reason = `: ${errorJson.message.trim().replace(/\.$/, '')}`;
1886
+ }
1887
+ catch {
1888
+ // Not JSON
1889
+ }
1890
+ command.warn(`Push preview failed (${response.status})${reason}. Skipping preview.`);
1891
+ if (verbose) {
1823
1892
  log(ux.colorize('dim', errorText));
1824
1893
  }
1825
1894
  }
@@ -0,0 +1,85 @@
1
+ import type { PolicyTemplate } from './template.js';
2
+ import type { PolicyCatalogueEntry } from './types.js';
3
+ /**
4
+ * `--check` narrows the catalogue to one check. An id that is not in it names the closest matches
5
+ * instead of reprinting the whole list: the usual mistake is a typo one character from a real id.
6
+ */
7
+ export declare function selectCatalogueCheck(checks: PolicyCatalogueEntry[], id: string): PolicyCatalogueEntry[];
8
+ /** `--recipe` narrows the catalogue's recipes to one, refusing an unknown id as `--check` does. */
9
+ export declare function selectCatalogueRecipe(recipes: PolicyRecipe[], id: string): PolicyRecipe[];
10
+ /**
11
+ * The checks as a table of id, label and description, object kinds and required params, each row
12
+ * followed by the check's `uses`: a goal, the params that achieve it (one line of JSON to copy) and
13
+ * its note.
14
+ */
15
+ export declare function policyCatalogueSummary(checks: PolicyCatalogueEntry[]): string[];
16
+ /** The catalogue's `template_categories`: the groups templates are filed under, in the order to offer them. */
17
+ export interface PolicyTemplateCategory {
18
+ id: string;
19
+ label: string;
20
+ }
21
+ /** The catalogue's `template_frameworks`: the framework tags a template can carry, in order, with their labels. */
22
+ export interface PolicyTemplateFramework {
23
+ id: string;
24
+ label: string;
25
+ }
26
+ /**
27
+ * The catalogue's templates, grouped under their `category` in the order `template_categories`
28
+ * lists them, each group named by its label. A category the list does not name follows in the order
29
+ * first seen, named by its id; a template without one (an older instance) is listed under "Other".
30
+ * Each row names the id `policy create --template` takes, the title, what it would create and its
31
+ * framework tags by label (an unknown id as itself). When any row has a tag, the catalogue's
32
+ * `template_frameworks_note` follows the heading, worded by the platform rather than here; an
33
+ * instance that serves no note gets no tags, since a tag must never show without it.
34
+ */
35
+ export declare function policyTemplateSummary(templates: PolicyTemplate[], categories?: PolicyTemplateCategory[], frameworks?: PolicyTemplateFramework[], frameworksNote?: string): string[];
36
+ /**
37
+ * One of the catalogue's `recipes`: a goal spanning several templates, served as data. There is no
38
+ * recipe object and no create action: an author creates its `templates` in order, one policy each,
39
+ * then its `rules` together in a policy of their own.
40
+ */
41
+ export interface PolicyRecipe {
42
+ /** Framework tag ids, as a template's: topics it relates to, never a compliance claim. */
43
+ frameworks?: string[];
44
+ id: string;
45
+ keywords?: string[];
46
+ /** `not_checkable` ids: what no rule in the recipe can see. */
47
+ limits?: string[];
48
+ /** What the author does first: the tags, fields and functions the rules depend on. */
49
+ prerequisites?: string[];
50
+ /** Rules no template covers, shaped like a template's; `needs` are the params the author fills. */
51
+ rules?: Array<{
52
+ check: string;
53
+ needs?: string[];
54
+ params?: Record<string, unknown> | unknown[];
55
+ title: string;
56
+ }>;
57
+ summary?: string;
58
+ /** Template ids, in the order to create them. */
59
+ templates?: string[];
60
+ /** The goal, never a framework name. */
61
+ title: string;
62
+ }
63
+ /** One of the catalogue's `not_checkable` entries: a topic no check can see, and why. */
64
+ export interface PolicyNotCheckable {
65
+ id: string;
66
+ keywords?: string[];
67
+ topic: string;
68
+ why: string;
69
+ }
70
+ /** What the recipe section reads beside the recipes: templates by id, tags, and the not-checkable topics. */
71
+ export interface PolicyRecipeContext {
72
+ frameworks?: PolicyTemplateFramework[];
73
+ frameworksNote?: string;
74
+ notCheckable?: PolicyNotCheckable[];
75
+ templates?: PolicyTemplate[];
76
+ }
77
+ /**
78
+ * The catalogue's recipes, each with its title, tags, summary, what to do first, its templates in
79
+ * order (each with its key and title, so `policy create --template` can take it from here), the rules
80
+ * no template covers (their params as JSON and what the author fills in) and the topics no rule in it
81
+ * can see. The framework note follows the heading when any recipe shows a tag, as for templates.
82
+ */
83
+ export declare function policyRecipeSummary(recipes: PolicyRecipe[], context?: PolicyRecipeContext): string[];
84
+ /** The catalogue's `not_checkable`: each topic no check can see, by id, with why. */
85
+ export declare function policyNotCheckableSummary(entries: PolicyNotCheckable[]): string[];
@@ -0,0 +1,196 @@
1
+ /**
2
+ * `--check` narrows the catalogue to one check. An id that is not in it names the closest matches
3
+ * instead of reprinting the whole list: the usual mistake is a typo one character from a real id.
4
+ */
5
+ export function selectCatalogueCheck(checks, id) {
6
+ return selectById(checks, id, 'a policy check');
7
+ }
8
+ /** `--recipe` narrows the catalogue's recipes to one, refusing an unknown id as `--check` does. */
9
+ export function selectCatalogueRecipe(recipes, id) {
10
+ return selectById(recipes, id, 'a recipe');
11
+ }
12
+ function selectById(entries, id, noun) {
13
+ const wanted = id.trim().toLowerCase();
14
+ const exact = entries.filter(entry => entry.id.toLowerCase() === wanted);
15
+ if (exact.length > 0)
16
+ return exact;
17
+ const near = entries.map(entry => entry.id).filter(known => {
18
+ const lower = known.toLowerCase();
19
+ return lower.includes(wanted) || wanted.includes(lower) || editDistanceWithin(lower, wanted, 2);
20
+ }).sort();
21
+ throw new Error(`"${id}" is not ${noun}.${near.length > 0
22
+ ? ` Did you mean ${near.join(', ')}?`
23
+ : ' Run `xano policy catalogue` for the full list.'}`);
24
+ }
25
+ /** Cheap bounded Levenshtein: true when `a` and `b` are at most `max` edits apart. */
26
+ function editDistanceWithin(a, b, max) {
27
+ if (Math.abs(a.length - b.length) > max)
28
+ return false;
29
+ let previous = Array.from({ length: b.length + 1 }, (_, index) => index);
30
+ for (let i = 1; i <= a.length; i++) {
31
+ const current = [i];
32
+ for (let j = 1; j <= b.length; j++) {
33
+ current[j] = a[i - 1] === b[j - 1]
34
+ ? previous[j - 1]
35
+ : 1 + Math.min(previous[j - 1], previous[j], current[j - 1]);
36
+ }
37
+ if (Math.min(...current) > max)
38
+ return false;
39
+ previous = current;
40
+ }
41
+ return previous[b.length] <= max;
42
+ }
43
+ /** How wide a full-width line (a use, a recipe) is wrapped; the check table is its own columns. */
44
+ const LINE_WIDTH = 120;
45
+ /**
46
+ * The checks as a table of id, label and description, object kinds and required params, each row
47
+ * followed by the check's `uses`: a goal, the params that achieve it (one line of JSON to copy) and
48
+ * its note.
49
+ */
50
+ export function policyCatalogueSummary(checks) {
51
+ if (checks.length === 0)
52
+ return ['No policy checks found.'];
53
+ const rows = [
54
+ ['Check ID', 'Label / Description', 'Object kinds', 'Required params'],
55
+ ...checks.map(check => [
56
+ check.id,
57
+ [check.label.trim(), check.description, check.fix_hint && `Fix hint: ${check.fix_hint}`].filter(Boolean).join('\n'),
58
+ (check.object_kinds ?? []).join(', ') || '—',
59
+ requiredParams(check),
60
+ ]),
61
+ ];
62
+ const widths = rows[0].map((_, column) => Math.min([36, 46, 24, 32][column], Math.max(...rows.map(row => row[column].length))));
63
+ return rows.flatMap((row, index) => {
64
+ const cells = row.map((cell, column) => cell.split('\n').flatMap(paragraph => wrap(paragraph, widths[column])));
65
+ const lines = Array.from({ length: Math.max(...cells.map(cell => cell.length)) }, (_, line) => cells.map((cell, column) => (cell[line] ?? '').padEnd(widths[column])).join(' ').trimEnd());
66
+ return index === 0 ? lines : [...lines, ...useLines(checks[index - 1].uses)];
67
+ });
68
+ }
69
+ /** A check's uses under its row: `Use: goal`, the params as JSON, then `Note: …` when it has one. */
70
+ function useLines(uses) {
71
+ if (!Array.isArray(uses))
72
+ return [];
73
+ return uses.flatMap(use => [
74
+ ...hanging(' Use: ', use.goal),
75
+ ` ${JSON.stringify(Array.isArray(use.params) ? {} : use.params ?? {})}`,
76
+ ...(use.note ? hanging(' Note: ', use.note, ' ') : []),
77
+ ]);
78
+ }
79
+ /** Words wrapped at `width`; a word longer than the width keeps a line of its own. */
80
+ function wrap(text, width) {
81
+ const lines = [''];
82
+ for (const word of text.split(/\s+/)) {
83
+ const last = lines.length - 1;
84
+ if (lines[last] && lines[last].length + word.length + 1 > width)
85
+ lines.push(word);
86
+ else
87
+ lines[last] += `${lines[last] ? ' ' : ''}${word}`;
88
+ }
89
+ return lines;
90
+ }
91
+ /** `text` after `first`, wrapped at the line width, its later lines indented as `rest` (default: as wide as `first`). */
92
+ function hanging(first, text, rest = ' '.repeat(first.length)) {
93
+ return wrap(text, LINE_WIDTH - first.length).map((line, index) => `${index === 0 ? first : rest}${line}`);
94
+ }
95
+ /** The params a rule must set: each required one, and any "one of" group. */
96
+ function requiredParams(check) {
97
+ const required = Object.entries(check.params ?? {}).filter(([, schema]) => schema.required)
98
+ .map(([name, schema]) => `${name}: ${schema.type ?? 'any'}`);
99
+ const oneOf = check.requires_one_of ?? [];
100
+ if (oneOf.length > 0)
101
+ required.push(`one of: ${oneOf.join(' | ')}`);
102
+ return required.join(', ') || 'none';
103
+ }
104
+ /**
105
+ * The catalogue's templates, grouped under their `category` in the order `template_categories`
106
+ * lists them, each group named by its label. A category the list does not name follows in the order
107
+ * first seen, named by its id; a template without one (an older instance) is listed under "Other".
108
+ * Each row names the id `policy create --template` takes, the title, what it would create and its
109
+ * framework tags by label (an unknown id as itself). When any row has a tag, the catalogue's
110
+ * `template_frameworks_note` follows the heading, worded by the platform rather than here; an
111
+ * instance that serves no note gets no tags, since a tag must never show without it.
112
+ */
113
+ export function policyTemplateSummary(templates, categories = [], frameworks = [], frameworksNote = '') {
114
+ if (templates.length === 0)
115
+ return [];
116
+ const labels = new Map(categories.map(category => [category.id, category.label]));
117
+ const tags = frameworkTags(frameworks, frameworksNote);
118
+ const note = templates.some(template => tags(template).length > 0) ? [frameworksNote.trim()] : [];
119
+ const groups = new Map(categories.map(category => [category.id, []]));
120
+ for (const template of templates) {
121
+ const category = template.category?.trim() || '';
122
+ groups.set(category, [...(groups.get(category) ?? []), template]);
123
+ }
124
+ const width = Math.min(36, Math.max(...templates.map(template => template.id.length)));
125
+ return [
126
+ 'Templates (xano policy create --template <id>):',
127
+ ...note,
128
+ ...[...groups].filter(([, members]) => members.length > 0).flatMap(([category, members]) => [
129
+ '',
130
+ labels.get(category) ?? (category || 'Other'),
131
+ ...members.map(template => {
132
+ const rules = template.rules.length === 1 ? '1 rule' : `${template.rules.length} rules`;
133
+ const tagged = tags(template).length > 0 ? ` [${tags(template).join(', ')}]` : '';
134
+ return ` ${template.id.padEnd(width)} ${template.title} (${template.key}, ${template.severity}, ${rules})${tagged}`;
135
+ }),
136
+ ]),
137
+ ];
138
+ }
139
+ /**
140
+ * Framework tag labels for a template or recipe (an unknown id as itself). Tags are shown only
141
+ * beside the platform's sentence on what they mean: an instance that serves no note gets none.
142
+ */
143
+ function frameworkTags(frameworks, frameworksNote) {
144
+ const labels = new Map(frameworks.map(framework => [framework.id, framework.label]));
145
+ return entry => frameworksNote.trim() === '' || !Array.isArray(entry.frameworks) ? [] : entry.frameworks.map(id => labels.get(id) ?? id);
146
+ }
147
+ /**
148
+ * The catalogue's recipes, each with its title, tags, summary, what to do first, its templates in
149
+ * order (each with its key and title, so `policy create --template` can take it from here), the rules
150
+ * no template covers (their params as JSON and what the author fills in) and the topics no rule in it
151
+ * can see. The framework note follows the heading when any recipe shows a tag, as for templates.
152
+ */
153
+ export function policyRecipeSummary(recipes, context = {}) {
154
+ if (recipes.length === 0)
155
+ return [];
156
+ const { frameworks = [], frameworksNote = '', notCheckable = [], templates = [] } = context;
157
+ const tags = frameworkTags(frameworks, frameworksNote);
158
+ const note = recipes.some(recipe => tags(recipe).length > 0) ? [frameworksNote.trim()] : [];
159
+ const byId = new Map(templates.map(template => [template.id, template]));
160
+ const topics = new Map(notCheckable.map(entry => [entry.id, entry.topic]));
161
+ return [
162
+ 'Recipes (goals that span several templates; nothing creates a recipe: create its templates, then its rules):',
163
+ ...note,
164
+ ...recipes.flatMap(recipe => {
165
+ const tagged = tags(recipe).length > 0 ? ` [${tags(recipe).join(', ')}]` : '';
166
+ const steps = recipe.templates ?? [];
167
+ const rules = recipe.rules ?? [];
168
+ const limits = recipe.limits ?? [];
169
+ return [
170
+ '',
171
+ ` ${recipe.id} ${recipe.title}${tagged}`,
172
+ ...(recipe.summary ? hanging(' ', recipe.summary) : []),
173
+ ...((recipe.prerequisites ?? []).length > 0 ? [' First:', ...(recipe.prerequisites ?? []).flatMap(step => hanging(' - ', step))] : []),
174
+ ...(steps.length > 0 ? [' Templates, in order (xano policy create --template <id>, one policy each):', ...steps.map((id, index) => {
175
+ const template = byId.get(id);
176
+ return ` ${index + 1}. ${id}${template ? ` ${template.title} (${template.key})` : ''}`;
177
+ })] : []),
178
+ ...(rules.length > 0 ? [' Rules no template covers, together in one policy of your own:', ...rules.flatMap(rule => [
179
+ ...hanging(' - ', `${rule.title}: ${rule.check}`),
180
+ ` ${JSON.stringify(Array.isArray(rule.params) ? {} : rule.params ?? {})}`,
181
+ ...((rule.needs ?? []).length > 0 ? [` You fill in: ${(rule.needs ?? []).join(', ')}`] : []),
182
+ ])] : []),
183
+ ...(limits.length > 0 ? hanging(' No rule here can see: ', `${limits.map(id => topics.get(id) ?? id).join('; ')}.`, ' ') : []),
184
+ ];
185
+ }),
186
+ ];
187
+ }
188
+ /** The catalogue's `not_checkable`: each topic no check can see, by id, with why. */
189
+ export function policyNotCheckableSummary(entries) {
190
+ if (entries.length === 0)
191
+ return [];
192
+ return [
193
+ 'What no check can see (recipes name these under "No rule here can see"):',
194
+ ...entries.flatMap(entry => ['', ` ${entry.id} ${entry.topic}`, ...hanging(' ', entry.why)]),
195
+ ];
196
+ }
@@ -0,0 +1,19 @@
1
+ import type { ObjectCoverage } from './types.js';
2
+ /** An object as findings name it, `type:id` (`query:42`), or `null` when the text is not one. */
3
+ export declare function parseObjectRef(text: string): null | {
4
+ id: number;
5
+ type: string;
6
+ };
7
+ /** The object coverage chip, worded as Studio's object panel words it. */
8
+ export declare function coverageChip(summary: ObjectCoverage['summary']): string;
9
+ /**
10
+ * The policies that apply with no finding here, counted from the summary as Studio's panel words its
11
+ * line under the findings: "6 passing · 1 couldn't check · 1 not checked yet". Empty when it counts none.
12
+ */
13
+ export declare function coverageCounts(summary: ObjectCoverage['summary']): string;
14
+ /**
15
+ * An object's coverage on stdout: the chip, the run it reads, the findings Blocking then Advisory,
16
+ * each with its fix, the policies that apply (counted, then each with its status and why), and the
17
+ * platform's note.
18
+ */
19
+ export declare function coverageLines(answer: ObjectCoverage): string[];