@xano/cli 1.2.0 → 1.3.1-beta.0

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 (192) hide show
  1. package/README.md +381 -4
  2. package/dist/base-command.d.ts +54 -36
  3. package/dist/base-command.js +246 -169
  4. package/dist/commands/auth/index.d.ts +1 -1
  5. package/dist/commands/auth/index.js +7 -7
  6. package/dist/commands/branch/create/index.d.ts +1 -1
  7. package/dist/commands/branch/create/index.js +1 -1
  8. package/dist/commands/branch/set_live/index.d.ts +22 -0
  9. package/dist/commands/branch/set_live/index.js +60 -1
  10. package/dist/commands/ephemeral/create/index.d.ts +18 -0
  11. package/dist/commands/ephemeral/create/index.js +103 -0
  12. package/dist/commands/ephemeral/delete/index.d.ts +18 -0
  13. package/dist/commands/ephemeral/delete/index.js +99 -0
  14. package/dist/commands/ephemeral/edit/index.d.ts +18 -0
  15. package/dist/commands/ephemeral/edit/index.js +109 -0
  16. package/dist/commands/ephemeral/get/index.d.ts +21 -0
  17. package/dist/commands/ephemeral/get/index.js +125 -0
  18. package/dist/commands/ephemeral/impersonate/index.d.ts +20 -0
  19. package/dist/commands/ephemeral/impersonate/index.js +120 -0
  20. package/dist/commands/ephemeral/list/index.d.ts +19 -0
  21. package/dist/commands/ephemeral/list/index.js +123 -0
  22. package/dist/commands/ephemeral/pull/index.d.ts +19 -0
  23. package/dist/commands/ephemeral/pull/index.js +163 -0
  24. package/dist/commands/ephemeral/push/index.d.ts +35 -0
  25. package/dist/commands/ephemeral/push/index.js +228 -0
  26. package/dist/commands/ephemeral/static_host/build/delete/index.d.ts +20 -0
  27. package/dist/commands/ephemeral/static_host/build/delete/index.js +121 -0
  28. package/dist/commands/ephemeral/static_host/build/get/index.d.ts +18 -0
  29. package/dist/commands/ephemeral/static_host/build/get/index.js +137 -0
  30. package/dist/commands/ephemeral/static_host/build/list/index.d.ts +18 -0
  31. package/dist/commands/ephemeral/static_host/build/list/index.js +137 -0
  32. package/dist/commands/ephemeral/static_host/build/pull/index.d.ts +53 -0
  33. package/dist/commands/ephemeral/static_host/build/pull/index.js +308 -0
  34. package/dist/commands/ephemeral/static_host/build/push/index.d.ts +24 -0
  35. package/dist/commands/ephemeral/static_host/build/push/index.js +250 -0
  36. package/dist/commands/ephemeral/static_host/create/index.d.ts +18 -0
  37. package/dist/commands/ephemeral/static_host/create/index.js +92 -0
  38. package/dist/commands/ephemeral/static_host/deploy/index.d.ts +19 -0
  39. package/dist/commands/ephemeral/static_host/deploy/index.js +112 -0
  40. package/dist/commands/ephemeral/static_host/edit/index.d.ts +24 -0
  41. package/dist/commands/ephemeral/static_host/edit/index.js +158 -0
  42. package/dist/commands/ephemeral/static_host/get/index.d.ts +19 -0
  43. package/dist/commands/ephemeral/static_host/get/index.js +101 -0
  44. package/dist/commands/ephemeral/static_host/list/index.d.ts +17 -0
  45. package/dist/commands/ephemeral/static_host/list/index.js +132 -0
  46. package/dist/commands/flatten/index.d.ts +34 -0
  47. package/dist/commands/flatten/index.js +140 -0
  48. package/dist/commands/function/create/index.d.ts +15 -0
  49. package/dist/commands/function/create/index.js +41 -1
  50. package/dist/commands/function/edit/index.d.ts +15 -0
  51. package/dist/commands/function/edit/index.js +66 -32
  52. package/dist/commands/function/get/index.js +2 -3
  53. package/dist/commands/function/run/index.d.ts +1 -0
  54. package/dist/commands/function/run/index.js +19 -6
  55. package/dist/commands/policy/catalogue/index.d.ts +15 -0
  56. package/dist/commands/policy/catalogue/index.js +29 -0
  57. package/dist/commands/policy/create/index.d.ts +18 -0
  58. package/dist/commands/policy/create/index.js +39 -0
  59. package/dist/commands/policy/delete/index.d.ts +19 -0
  60. package/dist/commands/policy/delete/index.js +61 -0
  61. package/dist/commands/policy/evaluate/index.d.ts +32 -0
  62. package/dist/commands/policy/evaluate/index.js +71 -0
  63. package/dist/commands/policy/list/index.d.ts +14 -0
  64. package/dist/commands/policy/list/index.js +21 -0
  65. package/dist/commands/policy/parse/index.d.ts +19 -0
  66. package/dist/commands/policy/parse/index.js +18 -0
  67. package/dist/commands/policy/publish/index.d.ts +20 -0
  68. package/dist/commands/policy/publish/index.js +38 -0
  69. package/dist/commands/policy/runs/index.d.ts +54 -0
  70. package/dist/commands/policy/runs/index.js +273 -0
  71. package/dist/commands/policy/status/index.d.ts +21 -0
  72. package/dist/commands/policy/status/index.js +54 -0
  73. package/dist/commands/profile/create/index.d.ts +1 -1
  74. package/dist/commands/profile/create/index.js +7 -7
  75. package/dist/commands/profile/me/index.js +1 -1
  76. package/dist/commands/profile/use/index.js +0 -4
  77. package/dist/commands/profile/wizard/index.d.ts +1 -1
  78. package/dist/commands/profile/wizard/index.js +12 -10
  79. package/dist/commands/profile/workspace/set/index.js +1 -1
  80. package/dist/commands/release/create/index.js +7 -0
  81. package/dist/commands/release/deploy/index.d.ts +28 -0
  82. package/dist/commands/release/deploy/index.js +90 -6
  83. package/dist/commands/release/edit/index.js +2 -2
  84. package/dist/commands/release/export/index.js +1 -2
  85. package/dist/commands/release/import/index.js +6 -2
  86. package/dist/commands/release/list/index.d.ts +7 -0
  87. package/dist/commands/release/list/index.js +54 -10
  88. package/dist/commands/release/pull/index.d.ts +0 -6
  89. package/dist/commands/release/pull/index.js +13 -99
  90. package/dist/commands/release/push/index.js +21 -3
  91. package/dist/commands/sandbox/env/get_all/index.js +1 -1
  92. package/dist/commands/sandbox/env/set_all/index.js +1 -1
  93. package/dist/commands/sandbox/get/index.js +1 -1
  94. package/dist/commands/sandbox/license/get/index.js +1 -1
  95. package/dist/commands/sandbox/license/set/index.js +1 -1
  96. package/dist/commands/sandbox/pull/index.d.ts +0 -1
  97. package/dist/commands/sandbox/pull/index.js +13 -81
  98. package/dist/commands/sandbox/push/index.d.ts +1 -1
  99. package/dist/commands/sandbox/push/index.js +11 -6
  100. package/dist/commands/skills/pull/index.d.ts +30 -0
  101. package/dist/commands/skills/pull/index.js +118 -0
  102. package/dist/commands/static_host/build/create/index.d.ts +1 -1
  103. package/dist/commands/static_host/build/create/index.js +2 -2
  104. package/dist/commands/static_host/build/pull/index.js +1 -1
  105. package/dist/commands/static_host/build/push/index.js +1 -1
  106. package/dist/commands/static_host/edit/index.js +1 -1
  107. package/dist/commands/tenant/backup/export/index.js +1 -2
  108. package/dist/commands/tenant/backup/import/index.js +1 -1
  109. package/dist/commands/tenant/cluster/create/index.js +1 -1
  110. package/dist/commands/tenant/cluster/license/get/index.js +1 -1
  111. package/dist/commands/tenant/cluster/license/set/index.js +1 -1
  112. package/dist/commands/tenant/create/index.d.ts +4 -1
  113. package/dist/commands/tenant/create/index.js +26 -5
  114. package/dist/commands/tenant/deploy_platform/index.js +1 -1
  115. package/dist/commands/tenant/deploy_release/index.d.ts +40 -0
  116. package/dist/commands/tenant/deploy_release/index.js +186 -4
  117. package/dist/commands/tenant/edit/index.d.ts +2 -0
  118. package/dist/commands/tenant/edit/index.js +46 -11
  119. package/dist/commands/tenant/env/get_all/index.js +1 -1
  120. package/dist/commands/tenant/env/set_all/index.js +1 -1
  121. package/dist/commands/tenant/impersonate/index.d.ts +1 -1
  122. package/dist/commands/tenant/impersonate/index.js +14 -14
  123. package/dist/commands/tenant/license/get/index.js +1 -1
  124. package/dist/commands/tenant/license/set/index.js +1 -1
  125. package/dist/commands/tenant/pull/index.d.ts +0 -6
  126. package/dist/commands/tenant/pull/index.js +14 -100
  127. package/dist/commands/tenant_deploy_request/bypass/index.d.ts +31 -0
  128. package/dist/commands/tenant_deploy_request/bypass/index.js +109 -0
  129. package/dist/commands/tenant_deploy_request/create/index.d.ts +21 -0
  130. package/dist/commands/tenant_deploy_request/create/index.js +108 -0
  131. package/dist/commands/tenant_deploy_request/edit/index.d.ts +22 -0
  132. package/dist/commands/tenant_deploy_request/edit/index.js +103 -0
  133. package/dist/commands/tenant_deploy_request/get/index.d.ts +25 -0
  134. package/dist/commands/tenant_deploy_request/get/index.js +116 -0
  135. package/dist/commands/tenant_deploy_request/list/index.d.ts +20 -0
  136. package/dist/commands/tenant_deploy_request/list/index.js +122 -0
  137. package/dist/commands/tenant_deploy_request/revision/index.d.ts +21 -0
  138. package/dist/commands/tenant_deploy_request/revision/index.js +83 -0
  139. package/dist/commands/tenant_deploy_request/set_status/index.d.ts +38 -0
  140. package/dist/commands/tenant_deploy_request/set_status/index.js +159 -0
  141. package/dist/commands/workspace/git/pull/index.d.ts +4 -4
  142. package/dist/commands/workspace/git/pull/index.js +158 -97
  143. package/dist/commands/workspace/pull/index.d.ts +9 -5
  144. package/dist/commands/workspace/pull/index.js +83 -106
  145. package/dist/commands/workspace/push/index.d.ts +19 -0
  146. package/dist/commands/workspace/push/index.js +124 -5
  147. package/dist/help.js +4 -2
  148. package/dist/policy-command.d.ts +90 -0
  149. package/dist/policy-command.js +94 -0
  150. package/dist/update-check.d.ts +1 -1
  151. package/dist/update-check.js +1 -1
  152. package/dist/utils/document-parser.d.ts +126 -0
  153. package/dist/utils/document-parser.js +332 -4
  154. package/dist/utils/flatten.d.ts +36 -0
  155. package/dist/utils/flatten.js +68 -0
  156. package/dist/utils/knowledge-sync.d.ts +1 -0
  157. package/dist/utils/knowledge-sync.js +11 -6
  158. package/dist/utils/microservice-wait.d.ts +53 -0
  159. package/dist/utils/microservice-wait.js +84 -0
  160. package/dist/utils/multidoc-push.d.ts +143 -2
  161. package/dist/utils/multidoc-push.js +479 -40
  162. package/dist/utils/paging.d.ts +2 -1
  163. package/dist/utils/paging.js +0 -1
  164. package/dist/utils/policy/catalogue.d.ts +7 -0
  165. package/dist/utils/policy/catalogue.js +72 -0
  166. package/dist/utils/policy/errors.d.ts +12 -0
  167. package/dist/utils/policy/errors.js +61 -0
  168. package/dist/utils/policy/feedback.d.ts +68 -0
  169. package/dist/utils/policy/feedback.js +165 -0
  170. package/dist/utils/policy/findings.d.ts +23 -0
  171. package/dist/utils/policy/findings.js +65 -0
  172. package/dist/utils/policy/gate.d.ts +161 -0
  173. package/dist/utils/policy/gate.js +201 -0
  174. package/dist/utils/policy/goal.d.ts +46 -0
  175. package/dist/utils/policy/goal.js +95 -0
  176. package/dist/utils/policy/permission.d.ts +23 -0
  177. package/dist/utils/policy/permission.js +84 -0
  178. package/dist/utils/policy/push-gate.d.ts +5 -0
  179. package/dist/utils/policy/push-gate.js +29 -0
  180. package/dist/utils/policy/release.d.ts +85 -0
  181. package/dist/utils/policy/release.js +115 -0
  182. package/dist/utils/policy/request.d.ts +55 -0
  183. package/dist/utils/policy/request.js +115 -0
  184. package/dist/utils/policy/runs.d.ts +30 -0
  185. package/dist/utils/policy/runs.js +101 -0
  186. package/dist/utils/policy/status.d.ts +47 -0
  187. package/dist/utils/policy/status.js +117 -0
  188. package/dist/utils/policy/types.d.ts +185 -0
  189. package/dist/utils/policy/types.js +1 -0
  190. package/dist/utils/reference-checker.js +14 -3
  191. package/oclif.manifest.json +8538 -3445
  192. package/package.json +16 -1
@@ -1,5 +1,6 @@
1
1
  import { Args, Flags } from '@oclif/core';
2
2
  import BaseCommand from '../../../base-command.js';
3
+ import { releasePolicyRunLines } from '../../../utils/policy/release.js';
3
4
  export default class ReleaseCreate extends BaseCommand {
4
5
  static args = {
5
6
  name: Args.string({
@@ -11,6 +12,10 @@ export default class ReleaseCreate extends BaseCommand {
11
12
  static examples = [
12
13
  `$ xano release create "v1.0" --branch main
13
14
  Created release: v1.0 - ID: 10
15
+ Branch: main
16
+ Policy check: fail (run 1712), 2 blocking, 1 advisory findings
17
+ Findings: xano policy runs --release v1.0
18
+ Its blocking findings block a deploy to a standard or run tenant: xano tenant deploy_release <tenant> --release v1.0 --check
14
19
  `,
15
20
  `$ xano release create "v1.1-hotfix" --branch main --hotfix --description "Critical fix" -o json`,
16
21
  ];
@@ -90,6 +95,8 @@ Created release: v1.0 - ID: 10
90
95
  this.log(` Branch: ${release.branch}`);
91
96
  if (release.hotfix)
92
97
  this.log(` Hotfix: true`);
98
+ for (const line of releasePolicyRunLines(release.policy_run, release.name ?? args.name))
99
+ this.log(line);
93
100
  }
94
101
  }
95
102
  catch (error) {
@@ -9,12 +9,40 @@ export default class ReleaseDeploy extends BaseCommand {
9
9
  branch: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
10
10
  force: import("@oclif/core/interfaces").BooleanFlag<boolean>;
11
11
  output: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
12
+ 'policy-override': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
12
13
  set_live: import("@oclif/core/interfaces").BooleanFlag<boolean>;
13
14
  workspace: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
14
15
  config: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
15
16
  profile: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
16
17
  verbose: import("@oclif/core/interfaces").BooleanFlag<boolean>;
17
18
  };
19
+ /** Set once the platform refuses set live for a weakened mandatory policy: already explained, it exits 1 as it is. */
20
+ private refusedByPermission;
21
+ /** Set once the set-live policy gate refuses, the one failure that exits 2. */
22
+ private refusedByPolicyGate;
23
+ /** Exit 2 is the set-live gate's refusal; every other failure, flag errors included, exits 1. */
24
+ protected catch(error: Error & {
25
+ oclif?: {
26
+ exit?: number;
27
+ };
28
+ }): Promise<void>;
18
29
  run(): Promise<void>;
19
30
  private confirm;
31
+ /** The set-live verdict a successful deploy answered with --set_live (`pass`, `overridden`, ...). */
32
+ private logPolicyOutcome;
33
+ /** Stops the command when set live was refused after the branch was created: by the gate (exit 2) or for a weakened mandatory policy (exit 1). */
34
+ private refuseIfSetLiveRefused;
35
+ /**
36
+ * The set-live gate refused after the branch was created: the verdict, the refusal, and how to
37
+ * proceed, exiting 2. Under `-o json` the refusal is `{branch_created, set_live, branch, message,
38
+ * policy_gate}`.
39
+ */
40
+ private refuseSetLive;
41
+ /**
42
+ * Set live was refused after the branch was created because the release weakens (or lacks) a
43
+ * mandatory policy of the live branch, and the credential lacks `workspace:policy` update. That is a
44
+ * permission refusal, not a blocking finding: it exits 1 (under `-o json` as `{error}`) with the
45
+ * platform's sentence, which names the branch, and how to go on.
46
+ */
47
+ private refuseWeakening;
20
48
  }
@@ -1,5 +1,7 @@
1
1
  import { Args, Flags } from '@oclif/core';
2
2
  import BaseCommand from '../../../base-command.js';
3
+ import { BLANK_POLICY_OVERRIDE, blankPolicyOverride, gateOverrideHint, gateRefusal, liveGateLines, quoted, weakeningRefusal, } from '../../../utils/policy/gate.js';
4
+ import { policyWeakeningGuidance } from '../../../utils/policy/permission.js';
3
5
  export default class ReleaseDeploy extends BaseCommand {
4
6
  static args = {
5
7
  release_name: Args.string({
@@ -7,7 +9,7 @@ export default class ReleaseDeploy extends BaseCommand {
7
9
  required: true,
8
10
  }),
9
11
  };
10
- static description = '[IMPORTANT] ALWAYS confirm with the user before deploying a release. Deploys a release to its workspace as a new branch.';
12
+ static description = "[IMPORTANT] ALWAYS confirm with the user before deploying a release. Deploys a release to its workspace as a new branch. With --set_live the branch is then set live through the set-live policy gate: when blocking findings refuse it, the branch stays, set live is refused, and the command exits 2 unless --policy-override gives a reason. A release that weakens a mandatory policy of the live branch needs workspace:policy update to be set live; without it the branch stays and the command exits 1.";
11
13
  static examples = [
12
14
  `$ xano release deploy "v1.0"
13
15
  Are you sure you want to deploy release "v1.0"? (y/N) y
@@ -18,6 +20,7 @@ Deployed release "v1.0" to workspace 40 (branch: v1.0)
18
20
  `,
19
21
  `$ xano release deploy "v1.0" --branch "restore-v1" --no-set_live`,
20
22
  `$ xano release deploy "v1.0" -w 40 -o json --force`,
23
+ `$ xano release deploy "v1.0" --branch "rollback-v1" --set_live --policy-override "Rollback approved; finding tracked in JIRA-12"`,
21
24
  ];
22
25
  static flags = {
23
26
  ...BaseCommand.baseFlags,
@@ -39,6 +42,7 @@ Deployed release "v1.0" to workspace 40 (branch: v1.0)
39
42
  options: ['summary', 'json'],
40
43
  required: false,
41
44
  }),
45
+ 'policy-override': Flags.string({ description: 'With --set_live, set the branch live past a blocking set-live policy gate with an audited reason (requires workspace:policy update)' }),
42
46
  set_live: Flags.boolean({
43
47
  default: false,
44
48
  description: '[CRITICAL] STOP and confirm with the user before setting the deployed branch as live.',
@@ -50,8 +54,20 @@ Deployed release "v1.0" to workspace 40 (branch: v1.0)
50
54
  required: false,
51
55
  }),
52
56
  };
57
+ /** Set once the platform refuses set live for a weakened mandatory policy: already explained, it exits 1 as it is. */
58
+ refusedByPermission = false;
59
+ /** Set once the set-live policy gate refuses, the one failure that exits 2. */
60
+ refusedByPolicyGate = false;
61
+ /** Exit 2 is the set-live gate's refusal; every other failure, flag errors included, exits 1. */
62
+ async catch(error) {
63
+ if (this.refusedByPolicyGate)
64
+ return super.catch(error);
65
+ return this.catchAsOperational(error);
66
+ }
53
67
  async run() {
54
68
  const { args, flags } = await this.parse(ReleaseDeploy);
69
+ if (blankPolicyOverride(flags['policy-override']))
70
+ this.error(BLANK_POLICY_OVERRIDE);
55
71
  const { profile } = this.resolveProfile(flags);
56
72
  const workspaceId = flags.workspace || profile.workspace;
57
73
  if (!workspaceId) {
@@ -59,11 +75,10 @@ Deployed release "v1.0" to workspace 40 (branch: v1.0)
59
75
  }
60
76
  const releaseName = encodeURIComponent(args.release_name);
61
77
  const apiUrl = `${profile.instance_origin}/api:meta/workspace/${workspaceId}/release/${releaseName}/deploy`;
62
- const body = {
63
- set_live: flags.set_live,
64
- };
65
- if (flags.branch)
66
- body.branch = flags.branch;
78
+ const body = deployBody(flags);
79
+ if (body.override_reason && !flags.set_live) {
80
+ this.error('--policy-override only applies with --set_live: a branch that is not set live is not gated.');
81
+ }
67
82
  if (!flags.force) {
68
83
  const confirmed = await this.confirm(`Are you sure you want to deploy release "${args.release_name}" to workspace ${workspaceId}?`);
69
84
  if (!confirmed) {
@@ -84,6 +99,7 @@ Deployed release "v1.0" to workspace 40 (branch: v1.0)
84
99
  method: 'POST',
85
100
  }, flags.verbose, profile.access_token);
86
101
  if (!response.ok) {
102
+ await this.refuseIfSetLiveRefused(response, flags, args.release_name);
87
103
  const errorText = await response.text();
88
104
  this.error(`API request failed with status ${response.status}: ${response.statusText}\n${errorText}`);
89
105
  }
@@ -98,10 +114,17 @@ Deployed release "v1.0" to workspace 40 (branch: v1.0)
98
114
  this.log(`Deployed release "${release.name}" to workspace ${workspaceId} (branch: ${branchLabel}${liveStatus})`);
99
115
  if (release.description)
100
116
  this.log(` Description: ${release.description}`);
117
+ this.logPolicyOutcome(release);
101
118
  this.log(` Time: ${elapsed}s`);
102
119
  }
120
+ // A policy of the release the new branch could not take is never dropped silently.
121
+ const skipped = release.policies_skipped?.message;
122
+ if (skipped)
123
+ this.warn(skipped);
103
124
  }
104
125
  catch (error) {
126
+ if (this.refusedByPolicyGate || this.refusedByPermission)
127
+ throw error;
105
128
  if (error instanceof Error) {
106
129
  this.error(`Failed to deploy release: ${error.message}`);
107
130
  }
@@ -123,4 +146,65 @@ Deployed release "v1.0" to workspace 40 (branch: v1.0)
123
146
  });
124
147
  });
125
148
  }
149
+ /** The set-live verdict a successful deploy answered with --set_live (`pass`, `overridden`, ...). */
150
+ logPolicyOutcome(release) {
151
+ const status = release.policy_gate?.status;
152
+ if (status)
153
+ this.log(` Policy gate: ${status}`);
154
+ }
155
+ /** Stops the command when set live was refused after the branch was created: by the gate (exit 2) or for a weakened mandatory policy (exit 1). */
156
+ async refuseIfSetLiveRefused(response, flags, releaseName) {
157
+ const refused = await gateRefusal(response);
158
+ if (refused)
159
+ this.refuseSetLive(refused, flags, releaseName);
160
+ const weakening = await weakeningRefusal(response);
161
+ if (weakening)
162
+ this.refuseWeakening(weakening, flags);
163
+ }
164
+ /**
165
+ * The set-live gate refused after the branch was created: the verdict, the refusal, and how to
166
+ * proceed, exiting 2. Under `-o json` the refusal is `{branch_created, set_live, branch, message,
167
+ * policy_gate}`.
168
+ */
169
+ refuseSetLive(refused, flags, releaseName) {
170
+ const answer = refused.answer;
171
+ const label = answer.branch?.label || flags.branch || 'the release branch';
172
+ if (flags.output === 'json') {
173
+ this.log(JSON.stringify({ branch: answer.branch ?? null, branch_created: true, message: refused.message, policy_gate: answer, set_live: false }, null, 2));
174
+ }
175
+ else {
176
+ for (const line of liveGateLines(answer, 'release'))
177
+ this.log(line);
178
+ }
179
+ const workspace = flags.workspace ? ` -w ${quoted(flags.workspace)}` : '';
180
+ const rerun = `xano branch delete ${quoted(label)}${workspace} && xano release deploy ${quoted(releaseName)}${workspace}${flags.branch ? ` --branch ${quoted(flags.branch)}` : ''} --set_live --policy-override "<why>"`;
181
+ const hint = gateOverrideHint(answer, rerun);
182
+ this.refusedByPolicyGate = true;
183
+ this.error(`The branch was created; set live was refused. ${refused.message}${hint}${answer.can_override && !answer.override_denied ? `\nOr set "${label}" live from Studio's Branches panel, which asks for the reason.` : ''}`, { exit: 2 });
184
+ }
185
+ /**
186
+ * Set live was refused after the branch was created because the release weakens (or lacks) a
187
+ * mandatory policy of the live branch, and the credential lacks `workspace:policy` update. That is a
188
+ * permission refusal, not a blocking finding: it exits 1 (under `-o json` as `{error}`) with the
189
+ * platform's sentence, which names the branch, and how to go on.
190
+ */
191
+ refuseWeakening(refused, flags) {
192
+ const label = refused.payload.branch?.label || flags.branch;
193
+ const workspace = flags.workspace ? ` -w ${quoted(flags.workspace)}` : '';
194
+ const ways = label
195
+ ? [`The branch stays. To remove it: xano branch delete ${quoted(label)}${workspace}`, `Someone with that permission can set it live: xano branch set_live ${quoted(label)}${workspace}`]
196
+ : [];
197
+ this.refusedByPermission = true;
198
+ this.error(`${refused.message}${policyWeakeningGuidance(refused.payload, ways, true)}`);
199
+ }
200
+ }
201
+ /** The deploy request's body: the new branch's label, set_live, and the override reason when one is given. */
202
+ function deployBody(flags) {
203
+ const body = { set_live: flags.set_live };
204
+ if (flags.branch)
205
+ body.branch = flags.branch;
206
+ const overrideReason = flags['policy-override']?.trim();
207
+ if (overrideReason)
208
+ body.override_reason = overrideReason;
209
+ return body;
126
210
  }
@@ -69,8 +69,8 @@ Updated release: v1.0-final - ID: 10
69
69
  const current = await getResponse.json();
70
70
  // Merge in user-provided values
71
71
  const body = {
72
- description: flags.description !== undefined ? flags.description : (current.description ?? ''),
73
- name: flags.name !== undefined ? flags.name : current.name,
72
+ description: flags.description === undefined ? (current.description ?? '') : flags.description,
73
+ name: flags.name === undefined ? current.name : flags.name,
74
74
  };
75
75
  // Update release
76
76
  const putResponse = await this.verboseFetch(baseUrl, {
@@ -1,6 +1,6 @@
1
1
  import { Args, Flags } from '@oclif/core';
2
2
  import * as fs from 'node:fs';
3
- import * as path from 'node:path';
3
+ import path from 'node:path';
4
4
  import BaseCommand, { buildUserAgent } from '../../../base-command.js';
5
5
  export default class ReleaseExport extends BaseCommand {
6
6
  static args = {
@@ -79,7 +79,6 @@ Downloaded release 'v1.0' to ./release-v1.0.tar.gz
79
79
  const fileStream = fs.createWriteStream(resolvedPath);
80
80
  const reader = downloadResponse.body.getReader();
81
81
  let totalBytes = 0;
82
- // eslint-disable-next-line no-constant-condition
83
82
  while (true) {
84
83
  // eslint-disable-next-line no-await-in-loop
85
84
  const { done, value } = await reader.read();
@@ -1,7 +1,8 @@
1
1
  import { Flags } from '@oclif/core';
2
2
  import * as fs from 'node:fs';
3
- import * as path from 'node:path';
3
+ import path from 'node:path';
4
4
  import BaseCommand from '../../../base-command.js';
5
+ import { releaseImportRefusal, releasePolicyRunLines } from '../../../utils/policy/release.js';
5
6
  export default class ReleaseImport extends BaseCommand {
6
7
  static args = {};
7
8
  static description = '[IMPORTANT] ALWAYS confirm with the user before importing a release. Imports a release file into a workspace.';
@@ -58,7 +59,8 @@ Imported release as #15
58
59
  }, flags.verbose, profile.access_token);
59
60
  if (!response.ok) {
60
61
  const errorText = await response.text();
61
- this.error(`API request failed with status ${response.status}: ${response.statusText}\n${errorText}`);
62
+ this.error(releaseImportRefusal(errorText, response.status, apiUrl) ??
63
+ `API request failed with status ${response.status}: ${response.statusText}\n${errorText}`);
62
64
  }
63
65
  const result = await response.json();
64
66
  if (flags.output === 'json') {
@@ -67,6 +69,8 @@ Imported release as #15
67
69
  else {
68
70
  const sizeMb = (fileBuffer.length / 1024 / 1024).toFixed(2);
69
71
  this.log(`Imported release as #${result.id} (${sizeMb} MB)`);
72
+ for (const line of releasePolicyRunLines(result.policy_run, result.name, result.id))
73
+ this.log(line);
70
74
  }
71
75
  }
72
76
  catch (error) {
@@ -10,4 +10,11 @@ export default class ReleaseList extends BaseCommand {
10
10
  verbose: import("@oclif/core/interfaces").BooleanFlag<boolean>;
11
11
  };
12
12
  run(): Promise<void>;
13
+ /**
14
+ * The releases' stored policy checks, read once for the whole list. The list never fails or
15
+ * changes because of this read: it answers `null` when the checks cannot be read (the Policies
16
+ * feature is off, the credential lacks `workspace:policy` read, or the request failed), and the
17
+ * list then prints as it would without policies.
18
+ */
19
+ private policyChecks;
13
20
  }
@@ -1,13 +1,21 @@
1
1
  import { Flags } from '@oclif/core';
2
2
  import BaseCommand from '../../../base-command.js';
3
3
  import { buildPagingJson } from '../../../utils/paging.js';
4
+ import { readReleasePolicyChecks, releasePolicyCheckTag } from '../../../utils/policy/release.js';
4
5
  export default class ReleaseList extends BaseCommand {
5
- static description = 'List all releases in a workspace';
6
+ static description = "List all releases in a workspace, each with its stored policy check when the credential can read policies (the release's own check, as Studio's Policies column shows it)";
6
7
  static examples = [
7
8
  `$ xano release list
8
9
  Releases in workspace 5:
9
10
  - v1.0 (ID: 10) - main
10
11
  - v1.1-hotfix (ID: 11) - main [hotfix]
12
+ `,
13
+ `$ xano release list
14
+ Releases in workspace 5:
15
+ - v1.2 (ID: 12) - main (9/28/2026, 4:12:03 PM PDT) [policies: 2 blocking, 1 advisory]
16
+ - v1.1-hotfix (ID: 11) - main [hotfix] (9/27/2026, 9:40:15 AM PDT) [policies: passed]
17
+ - v1.0 (ID: 10) - main (9/20/2026, 1:05:44 PM PDT) [policies: not checked]
18
+ Policy check details: xano policy runs --release <name>
11
19
  `,
12
20
  `$ xano release list -w 5 --output json`,
13
21
  ];
@@ -57,22 +65,19 @@ Releases in workspace 5:
57
65
  else {
58
66
  this.error('Unexpected API response format');
59
67
  }
68
+ const checks = await this.policyChecks(profile, String(workspaceId), releases, flags.verbose);
60
69
  if (flags.output === 'json') {
61
- this.log(JSON.stringify(buildPagingJson({ items: releases }, { tier: 'none' }), null, 2));
70
+ this.log(JSON.stringify(buildPagingJson({ items: withPolicyChecks(releases, checks) }, { tier: 'none' }), null, 2));
62
71
  }
63
72
  else if (releases.length === 0) {
64
73
  this.log('No releases found');
65
74
  }
66
75
  else {
67
76
  this.log(`Releases in workspace ${workspaceId}:`);
68
- for (const release of releases) {
69
- const branch = release.branch ? ` - ${release.branch}` : '';
70
- const hotfix = release.hotfix ? ' [hotfix]' : '';
71
- const createdAt = release.created_at
72
- ? ` (${new Date(release.created_at).toLocaleString(undefined, { timeZoneName: 'short' })})`
73
- : '';
74
- this.log(` - ${release.name} (ID: ${release.id})${branch}${hotfix}${createdAt}`);
75
- }
77
+ for (const release of releases)
78
+ this.log(releaseLine(release, checks));
79
+ if (checks)
80
+ this.log('Policy check details: xano policy runs --release <name>');
76
81
  }
77
82
  }
78
83
  catch (error) {
@@ -84,4 +89,43 @@ Releases in workspace 5:
84
89
  }
85
90
  }
86
91
  }
92
+ /**
93
+ * The releases' stored policy checks, read once for the whole list. The list never fails or
94
+ * changes because of this read: it answers `null` when the checks cannot be read (the Policies
95
+ * feature is off, the credential lacks `workspace:policy` read, or the request failed), and the
96
+ * list then prints as it would without policies.
97
+ */
98
+ async policyChecks(profile, workspace, releases, verbose) {
99
+ if (releases.length === 0)
100
+ return null;
101
+ return readReleasePolicyChecks({ logToStderr: (message) => this.logToStderr(message), verboseFetch: (...args) => this.verboseFetch(...args) }, { branch: '', profile, verbose, workspace }, releases.map(release => release.id));
102
+ }
103
+ }
104
+ /** A release's summary line, tagged with its stored check when the checks were read. */
105
+ function releaseLine(release, checks) {
106
+ const branch = release.branch ? ` - ${release.branch}` : '';
107
+ const hotfix = release.hotfix ? ' [hotfix]' : '';
108
+ const createdAt = release.created_at
109
+ ? ` (${new Date(release.created_at).toLocaleString(undefined, { timeZoneName: 'short' })})`
110
+ : '';
111
+ const policies = checks ? ` ${releasePolicyCheckTag(checks.get(release.id))}` : '';
112
+ return ` - ${release.name} (ID: ${release.id})${branch}${hotfix}${createdAt}${policies}`;
113
+ }
114
+ /**
115
+ * The releases for `-o json`: each gains `policy_check` (its stored check as the platform lists
116
+ * it, without the release id it is keyed by, or `null` for a release with no stored check) when
117
+ * the checks were read, and stays as served when they could not be, so a reader can tell an
118
+ * unchecked release (`null`) from checks the CLI could not read (absent).
119
+ */
120
+ function withPolicyChecks(releases, checks) {
121
+ if (!checks)
122
+ return releases;
123
+ return releases.map(release => {
124
+ const check = checks.get(release.id);
125
+ if (!check)
126
+ return { ...release, policy_check: null };
127
+ const stored = { ...check };
128
+ delete stored.release_id;
129
+ return { ...release, policy_check: stored };
130
+ });
87
131
  }
@@ -14,10 +14,4 @@ export default class ReleasePull extends BaseCommand {
14
14
  };
15
15
  run(): Promise<void>;
16
16
  private resolveReleaseName;
17
- /**
18
- * Sanitize a document name for use as a filename.
19
- * Strips quotes, replaces spaces with underscores, and removes
20
- * characters that are unsafe in filenames.
21
- */
22
- private sanitizeFilename;
23
17
  }
@@ -1,9 +1,9 @@
1
1
  import { Flags } from '@oclif/core';
2
- import * as fs from 'node:fs';
3
- import * as path from 'node:path';
4
2
  import snakeCase from 'lodash.snakecase';
3
+ import * as fs from 'node:fs';
4
+ import path from 'node:path';
5
5
  import BaseCommand from '../../../base-command.js';
6
- import { buildApiGroupFolderResolver, parseDocument } from '../../../utils/document-parser.js';
6
+ import { buildApiGroupFolderResolver, buildChannelServerResolver, parseDocument, resolveDocumentPath, } from '../../../utils/document-parser.js';
7
7
  export default class ReleasePull extends BaseCommand {
8
8
  static description = 'Pull a release multidoc from the Xano Metadata API and split into individual files';
9
9
  static examples = [
@@ -123,96 +123,19 @@ Pulled 58 documents from release 'v1.0'
123
123
  fs.mkdirSync(outputDir, { recursive: true });
124
124
  // Resolve api_group names to unique folder names, disambiguating collisions
125
125
  const getApiGroupFolder = buildApiGroupFolderResolver(documents, snakeCase);
126
+ // Resolve a realtime v2 channel path -> owning realtime_server name, so
127
+ // messages can nest under their channel's server (see resolver docs).
128
+ const getChannelServer = buildChannelServerResolver(documents);
126
129
  // Track filenames per type to handle duplicates
127
130
  const filenameCounters = new Map();
128
131
  let writtenCount = 0;
129
132
  for (const doc of documents) {
130
- let typeDir;
131
- let baseName;
132
- if (doc.type === 'workspace') {
133
- // workspace → workspace/{name}.xs
134
- typeDir = path.join(outputDir, 'workspace');
135
- baseName = this.sanitizeFilename(doc.name);
136
- }
137
- else if (doc.type === 'workspace_trigger') {
138
- // workspace_trigger → workspace/trigger/{name}.xs
139
- typeDir = path.join(outputDir, 'workspace', 'trigger');
140
- baseName = this.sanitizeFilename(doc.name);
141
- }
142
- else if (doc.type === 'error_trigger') {
143
- // error_trigger → workspace/trigger/{name}.xs (singleton, colocated with workspace triggers)
144
- typeDir = path.join(outputDir, 'workspace', 'trigger');
145
- baseName = this.sanitizeFilename(doc.name);
146
- }
147
- else if (doc.type === 'agent') {
148
- // agent → ai/agent/{name}.xs
149
- typeDir = path.join(outputDir, 'ai', 'agent');
150
- baseName = this.sanitizeFilename(doc.name);
151
- }
152
- else if (doc.type === 'mcp_server') {
153
- // mcp_server → ai/mcp_server/{name}.xs
154
- typeDir = path.join(outputDir, 'ai', 'mcp_server');
155
- baseName = this.sanitizeFilename(doc.name);
156
- }
157
- else if (doc.type === 'tool') {
158
- // tool → ai/tool/{name}.xs
159
- typeDir = path.join(outputDir, 'ai', 'tool');
160
- baseName = this.sanitizeFilename(doc.name);
161
- }
162
- else if (doc.type === 'agent_trigger') {
163
- // agent_trigger → ai/agent/trigger/{name}.xs
164
- typeDir = path.join(outputDir, 'ai', 'agent', 'trigger');
165
- baseName = this.sanitizeFilename(doc.name);
166
- }
167
- else if (doc.type === 'mcp_server_trigger') {
168
- // mcp_server_trigger → ai/mcp_server/trigger/{name}.xs
169
- typeDir = path.join(outputDir, 'ai', 'mcp_server', 'trigger');
170
- baseName = this.sanitizeFilename(doc.name);
171
- }
172
- else if (doc.type === 'table_trigger') {
173
- // table_trigger → table/trigger/{name}.xs
174
- typeDir = path.join(outputDir, 'table', 'trigger');
175
- baseName = this.sanitizeFilename(doc.name);
176
- }
177
- else if (doc.type === 'realtime_channel') {
178
- // realtime_channel → realtime/channel/{name}.xs
179
- typeDir = path.join(outputDir, 'realtime', 'channel');
180
- baseName = this.sanitizeFilename(doc.name);
181
- }
182
- else if (doc.type === 'realtime_trigger') {
183
- // realtime_trigger → realtime/trigger/{name}.xs
184
- typeDir = path.join(outputDir, 'realtime', 'trigger');
185
- baseName = this.sanitizeFilename(doc.name);
186
- }
187
- else if (doc.type === 'api_group') {
188
- // api_group "test" → api/{resolved_folder}/{name}.xs
189
- const groupFolder = getApiGroupFolder(doc.name);
190
- typeDir = path.join(outputDir, 'api', groupFolder);
191
- baseName = this.sanitizeFilename(doc.name);
192
- }
193
- else if (doc.type === 'query' && doc.apiGroup) {
194
- // query in group "test" → api/{resolved_folder}/{query_name}.xs
195
- const groupFolder = getApiGroupFolder(doc.apiGroup);
196
- const nameParts = doc.name.split('/');
197
- const leafName = nameParts.pop();
198
- const folderParts = nameParts.map((part) => snakeCase(part));
199
- typeDir = path.join(outputDir, 'api', groupFolder, ...folderParts);
200
- baseName = this.sanitizeFilename(leafName);
201
- if (doc.verb) {
202
- baseName = `${baseName}_${doc.verb}`;
203
- }
204
- }
205
- else {
206
- // Default: split folder path from name
207
- const nameParts = doc.name.split('/');
208
- const leafName = nameParts.pop();
209
- const folderParts = nameParts.map((part) => snakeCase(part));
210
- typeDir = path.join(outputDir, doc.type, ...folderParts);
211
- baseName = this.sanitizeFilename(leafName);
212
- if (doc.verb) {
213
- baseName = `${baseName}_${doc.verb}`;
214
- }
215
- }
133
+ const { baseName, typeDir } = resolveDocumentPath(doc, outputDir, {
134
+ getApiGroupFolder,
135
+ getChannelServer,
136
+ join: path.join,
137
+ snakeCase,
138
+ });
216
139
  fs.mkdirSync(typeDir, { recursive: true });
217
140
  // Track duplicates per directory
218
141
  const dirKey = path.relative(outputDir, typeDir);
@@ -223,8 +146,7 @@ Pulled 58 documents from release 'v1.0'
223
146
  const count = typeCounters.get(baseName) || 0;
224
147
  typeCounters.set(baseName, count + 1);
225
148
  // Append numeric suffix for duplicates
226
- let filename;
227
- filename = count === 0 ? `${baseName}.xs` : `${baseName}_${count + 1}.xs`;
149
+ const filename = count === 0 ? `${baseName}.xs` : `${baseName}_${count + 1}.xs`;
228
150
  const filePath = path.join(typeDir, filename);
229
151
  fs.writeFileSync(filePath, doc.content, 'utf8');
230
152
  writtenCount++;
@@ -257,12 +179,4 @@ Pulled 58 documents from release 'v1.0'
257
179
  }
258
180
  return match.id;
259
181
  }
260
- /**
261
- * Sanitize a document name for use as a filename.
262
- * Strips quotes, replaces spaces with underscores, and removes
263
- * characters that are unsafe in filenames.
264
- */
265
- sanitizeFilename(name) {
266
- return snakeCase(name.replaceAll('"', ''));
267
- }
268
182
  }
@@ -1,8 +1,10 @@
1
1
  import { Flags } from '@oclif/core';
2
2
  import * as fs from 'node:fs';
3
- import * as path from 'node:path';
3
+ import path from 'node:path';
4
4
  import BaseCommand from '../../../base-command.js';
5
- import { findFilesWithGuid } from '../../../utils/document-parser.js';
5
+ import { findFilesWithGuid, parseDocument } from '../../../utils/document-parser.js';
6
+ import { policiesSkippedNotice } from '../../../utils/policy/feedback.js';
7
+ import { releasePolicyRunLines } from '../../../utils/policy/release.js';
6
8
  export default class ReleasePush extends BaseCommand {
7
9
  static description = '[IMPORTANT] ALWAYS confirm with the user before creating a release from local files. Creates a new release from local XanoScript files via the multidoc endpoint.';
8
10
  static examples = [
@@ -72,7 +74,7 @@ Output release details as JSON
72
74
  };
73
75
  async run() {
74
76
  const { flags } = await this.parse(ReleasePush);
75
- const { profileName, profile } = this.resolveProfile(flags);
77
+ const { profile, profileName } = this.resolveProfile(flags);
76
78
  // Determine workspace_id from flag or profile
77
79
  let workspaceId;
78
80
  if (flags.workspace) {
@@ -110,6 +112,12 @@ Output release details as JSON
110
112
  if (documentEntries.length === 0) {
111
113
  this.error(`All .xs files in ${flags.directory} are empty`);
112
114
  }
115
+ // A release carries its policy files like its code.
116
+ const policyKeys = documentEntries
117
+ .map((entry) => parseDocument(entry.content))
118
+ .filter((document) => document?.type === 'policy')
119
+ .map((document) => document.name)
120
+ .sort();
113
121
  const multidoc = documentEntries.map((d) => d.content).join('\n---\n');
114
122
  // Construct the API URL with query params
115
123
  const queryParams = new URLSearchParams({
@@ -158,6 +166,10 @@ Output release details as JSON
158
166
  this.error(errorMessage);
159
167
  }
160
168
  const release = (await response.json());
169
+ // The platform says which policy files it left out: all of them while the Policies feature is
170
+ // off, and those of a caller who may not author policies, whose release carries the live
171
+ // branch's policies instead.
172
+ const skipped = policiesSkippedNotice(release);
161
173
  if (flags.output === 'json') {
162
174
  this.log(JSON.stringify(release, null, 2));
163
175
  }
@@ -171,8 +183,14 @@ Output release details as JSON
171
183
  this.log(` Description: ${release.description}`);
172
184
  const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
173
185
  this.log(` Documents: ${documentEntries.length}`);
186
+ if (policyKeys.length > 0 && !skipped)
187
+ this.log(` Policy documents: ${policyKeys.length} (${policyKeys.join(', ')})`);
188
+ for (const line of releasePolicyRunLines(release.policy_run, release.name ?? flags.name))
189
+ this.log(line);
174
190
  this.log(` Time: ${elapsed}s`);
175
191
  }
192
+ if (skipped)
193
+ this.warn(skipped);
176
194
  }
177
195
  catch (error) {
178
196
  if (error instanceof Error) {
@@ -1,7 +1,7 @@
1
1
  import { Flags } from '@oclif/core';
2
2
  import * as yaml from 'js-yaml';
3
3
  import * as fs from 'node:fs';
4
- import * as path from 'node:path';
4
+ import path from 'node:path';
5
5
  import BaseCommand from '../../../../base-command.js';
6
6
  export default class SandboxEnvGetAll extends BaseCommand {
7
7
  static description = 'Get all environment variables for a sandbox environment and save to a YAML file';
@@ -1,7 +1,7 @@
1
1
  import { Flags } from '@oclif/core';
2
2
  import * as yaml from 'js-yaml';
3
3
  import * as fs from 'node:fs';
4
- import * as path from 'node:path';
4
+ import path from 'node:path';
5
5
  import BaseCommand from '../../../../base-command.js';
6
6
  export default class SandboxEnvSetAll extends BaseCommand {
7
7
  static description = '[CRITICAL] STOP and confirm with the user; this replaces all environment variables with the imported file. Sets all environment variables for a sandbox environment from a YAML file.';
@@ -36,7 +36,7 @@ Sandbox Environment: (tc24-abcd-x1y2)
36
36
  this.log(` Domain: ${tenant.xano_domain}`);
37
37
  if (tenant.sandbox_expires_at) {
38
38
  const expiresAt = new Date(tenant.sandbox_expires_at);
39
- if (!isNaN(expiresAt.getTime())) {
39
+ if (!Number.isNaN(expiresAt.getTime())) {
40
40
  const msLeft = expiresAt.getTime() - Date.now();
41
41
  if (msLeft > 0) {
42
42
  const minsLeft = Math.ceil(msLeft / 60_000);
@@ -1,6 +1,6 @@
1
1
  import { Flags } from '@oclif/core';
2
2
  import * as fs from 'node:fs';
3
- import * as path from 'node:path';
3
+ import path from 'node:path';
4
4
  import BaseCommand from '../../../../base-command.js';
5
5
  export default class SandboxLicenseGet extends BaseCommand {
6
6
  static description = 'Get the license for a sandbox environment';