@xano/cli 1.3.1-beta.1 → 1.3.2

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 (98) hide show
  1. package/README.md +1 -273
  2. package/dist/base-command.d.ts +1 -12
  3. package/dist/base-command.js +22 -36
  4. package/dist/commands/branch/set_live/index.d.ts +0 -22
  5. package/dist/commands/branch/set_live/index.js +1 -60
  6. package/dist/commands/ephemeral/push/index.js +1 -6
  7. package/dist/commands/function/create/index.d.ts +0 -15
  8. package/dist/commands/function/create/index.js +0 -40
  9. package/dist/commands/function/edit/index.d.ts +0 -15
  10. package/dist/commands/function/edit/index.js +0 -40
  11. package/dist/commands/function/run/index.d.ts +0 -1
  12. package/dist/commands/function/run/index.js +2 -10
  13. package/dist/commands/release/create/index.js +0 -7
  14. package/dist/commands/release/deploy/index.d.ts +0 -28
  15. package/dist/commands/release/deploy/index.js +6 -90
  16. package/dist/commands/release/import/index.js +1 -5
  17. package/dist/commands/release/list/index.d.ts +0 -7
  18. package/dist/commands/release/list/index.js +10 -54
  19. package/dist/commands/release/push/index.js +1 -19
  20. package/dist/commands/sandbox/push/index.js +1 -6
  21. package/dist/commands/tenant/create/index.d.ts +0 -3
  22. package/dist/commands/tenant/create/index.js +0 -21
  23. package/dist/commands/tenant/deploy_release/index.d.ts +0 -40
  24. package/dist/commands/tenant/deploy_release/index.js +4 -186
  25. package/dist/commands/tenant/edit/index.d.ts +0 -2
  26. package/dist/commands/tenant/edit/index.js +4 -39
  27. package/dist/commands/workspace/pull/index.d.ts +0 -10
  28. package/dist/commands/workspace/pull/index.js +7 -70
  29. package/dist/commands/workspace/push/index.d.ts +0 -19
  30. package/dist/commands/workspace/push/index.js +5 -124
  31. package/dist/utils/document-parser.d.ts +0 -10
  32. package/dist/utils/document-parser.js +0 -21
  33. package/dist/utils/multidoc-push.d.ts +2 -55
  34. package/dist/utils/multidoc-push.js +27 -103
  35. package/oclif.manifest.json +3570 -6579
  36. package/package.json +1 -7
  37. package/dist/commands/policy/catalogue/index.d.ts +0 -15
  38. package/dist/commands/policy/catalogue/index.js +0 -29
  39. package/dist/commands/policy/create/index.d.ts +0 -18
  40. package/dist/commands/policy/create/index.js +0 -39
  41. package/dist/commands/policy/delete/index.d.ts +0 -19
  42. package/dist/commands/policy/delete/index.js +0 -61
  43. package/dist/commands/policy/evaluate/index.d.ts +0 -32
  44. package/dist/commands/policy/evaluate/index.js +0 -71
  45. package/dist/commands/policy/list/index.d.ts +0 -14
  46. package/dist/commands/policy/list/index.js +0 -21
  47. package/dist/commands/policy/parse/index.d.ts +0 -19
  48. package/dist/commands/policy/parse/index.js +0 -18
  49. package/dist/commands/policy/publish/index.d.ts +0 -20
  50. package/dist/commands/policy/publish/index.js +0 -38
  51. package/dist/commands/policy/runs/index.d.ts +0 -54
  52. package/dist/commands/policy/runs/index.js +0 -273
  53. package/dist/commands/policy/status/index.d.ts +0 -21
  54. package/dist/commands/policy/status/index.js +0 -54
  55. package/dist/commands/skills/pull/index.d.ts +0 -30
  56. package/dist/commands/skills/pull/index.js +0 -118
  57. package/dist/commands/tenant_deploy_request/bypass/index.d.ts +0 -31
  58. package/dist/commands/tenant_deploy_request/bypass/index.js +0 -109
  59. package/dist/commands/tenant_deploy_request/create/index.d.ts +0 -21
  60. package/dist/commands/tenant_deploy_request/create/index.js +0 -108
  61. package/dist/commands/tenant_deploy_request/edit/index.d.ts +0 -22
  62. package/dist/commands/tenant_deploy_request/edit/index.js +0 -103
  63. package/dist/commands/tenant_deploy_request/get/index.d.ts +0 -25
  64. package/dist/commands/tenant_deploy_request/get/index.js +0 -116
  65. package/dist/commands/tenant_deploy_request/list/index.d.ts +0 -20
  66. package/dist/commands/tenant_deploy_request/list/index.js +0 -122
  67. package/dist/commands/tenant_deploy_request/revision/index.d.ts +0 -21
  68. package/dist/commands/tenant_deploy_request/revision/index.js +0 -83
  69. package/dist/commands/tenant_deploy_request/set_status/index.d.ts +0 -38
  70. package/dist/commands/tenant_deploy_request/set_status/index.js +0 -159
  71. package/dist/policy-command.d.ts +0 -90
  72. package/dist/policy-command.js +0 -94
  73. package/dist/utils/policy/catalogue.d.ts +0 -7
  74. package/dist/utils/policy/catalogue.js +0 -72
  75. package/dist/utils/policy/errors.d.ts +0 -12
  76. package/dist/utils/policy/errors.js +0 -61
  77. package/dist/utils/policy/feedback.d.ts +0 -68
  78. package/dist/utils/policy/feedback.js +0 -165
  79. package/dist/utils/policy/findings.d.ts +0 -23
  80. package/dist/utils/policy/findings.js +0 -65
  81. package/dist/utils/policy/gate.d.ts +0 -161
  82. package/dist/utils/policy/gate.js +0 -201
  83. package/dist/utils/policy/goal.d.ts +0 -46
  84. package/dist/utils/policy/goal.js +0 -95
  85. package/dist/utils/policy/permission.d.ts +0 -23
  86. package/dist/utils/policy/permission.js +0 -84
  87. package/dist/utils/policy/push-gate.d.ts +0 -5
  88. package/dist/utils/policy/push-gate.js +0 -29
  89. package/dist/utils/policy/release.d.ts +0 -85
  90. package/dist/utils/policy/release.js +0 -115
  91. package/dist/utils/policy/request.d.ts +0 -55
  92. package/dist/utils/policy/request.js +0 -115
  93. package/dist/utils/policy/runs.d.ts +0 -30
  94. package/dist/utils/policy/runs.js +0 -101
  95. package/dist/utils/policy/status.d.ts +0 -47
  96. package/dist/utils/policy/status.js +0 -117
  97. package/dist/utils/policy/types.d.ts +0 -185
  98. package/dist/utils/policy/types.js +0 -1
@@ -1,7 +1,5 @@
1
1
  import { Args, Flags } from '@oclif/core';
2
2
  import BaseCommand from '../../../base-command.js';
3
- import { deployedGateLine, fetchPolicyGate, gateExitCode, gateLines, gateOverrideHint, gateRefusal, gateRefusalOutput, quoted, } from '../../../utils/policy/gate.js';
4
- import { policyPermissionGuidance } from '../../../utils/policy/permission.js';
5
3
  export default class TenantDeployRelease extends BaseCommand {
6
4
  static args = {
7
5
  tenant_name: Args.string({
@@ -9,26 +7,15 @@ export default class TenantDeployRelease extends BaseCommand {
9
7
  required: true,
10
8
  }),
11
9
  };
12
- static description = "[CRITICAL] STOP and confirm with the user before deploying a release to a tenant; this mutates the live tenant. Deploys a release to a tenant. A deploy is gated on the release's own policy check: any blocking finding in the release refuses it (exit 2) unless --override-reason is given, whatever the tenant runs today, so the release gets the same verdict on every tenant. --check previews that verdict without deploying.";
10
+ static description = '[CRITICAL] STOP and confirm with the user before deploying a release to a tenant; this mutates the live tenant. Deploys a release to a tenant.';
13
11
  static examples = [
14
12
  `$ xano tenant deploy_release t1234-abcd-xyz1 --release v1.0
15
13
  Deployed release "v1.0" to tenant: My Tenant (my-tenant)
16
14
  `,
17
15
  `$ xano tenant deploy_release t1234-abcd-xyz1 --release v1.0 -o json`,
18
- `$ xano tenant deploy_release prod --release v1.2 --check
19
- Checked release "v1.2" for tenant "prod"; nothing was deployed.
20
- Policy gate: blocked
21
- Deploy blocked: this release has 1 blocking policy finding.
22
- `,
23
- `$ xano tenant deploy_release prod --release v1.2 --override-reason "Hotfix; AUTH-001 finding tracked in JIRA-12"`,
24
16
  ];
25
17
  static flags = {
26
18
  ...BaseCommand.baseFlags,
27
- check: Flags.boolean({
28
- default: false,
29
- description: "Preview the release's policy gate on this tenant without deploying; exits 2 when the deploy would be refused",
30
- exclusive: ['override-reason'],
31
- }),
32
19
  output: Flags.string({
33
20
  char: 'o',
34
21
  default: 'summary',
@@ -36,10 +23,6 @@ Policy gate: blocked
36
23
  options: ['summary', 'json'],
37
24
  required: false,
38
25
  }),
39
- 'override-reason': Flags.string({
40
- description: 'Deploy past a blocking policy gate, recording this reason (needs the workspace:policy update permission; audited)',
41
- exclusive: ['check'],
42
- }),
43
26
  release: Flags.string({
44
27
  char: 'r',
45
28
  description: 'Release name to deploy',
@@ -51,14 +34,6 @@ Policy gate: blocked
51
34
  required: false,
52
35
  }),
53
36
  };
54
- /** Set once the policy gate refuses the deploy, the one failure that exits 2. */
55
- refusedByPolicyGate = false;
56
- /** Exit 2 is the policy gate's refusal; every other failure, flag errors included, exits 1. */
57
- async catch(error) {
58
- if (this.refusedByPolicyGate)
59
- return super.catch(error);
60
- return this.catchAsOperational(error);
61
- }
62
37
  async run() {
63
38
  const { args, flags } = await this.parse(TenantDeployRelease);
64
39
  const { profile } = this.resolveProfile(flags);
@@ -68,46 +43,12 @@ Policy gate: blocked
68
43
  }
69
44
  const releaseName = flags.release;
70
45
  const tenantName = args.tenant_name;
71
- if (flags.check) {
72
- await this.checkPolicyGate({ flags, profile, releaseName, tenantName, workspaceId });
73
- return;
74
- }
75
- const overrideReason = flags['override-reason']?.trim();
76
- if (flags['override-reason'] !== undefined && !overrideReason) {
77
- this.error('--override-reason needs a reason: say why this deploy may proceed past the policy gate.');
78
- }
79
- // Pre-flight: the "approval required" error must always be shown before any
80
- // permission/RBAC error. The backend's own deploy route checks RBAC first
81
- // and the approval gate second (inside mvp:tenant_deploy_release), so a
82
- // caller who lacks deploy permission would otherwise never learn a request
83
- // is what's actually needed. Checking here first, with an endpoint that
84
- // only needs read scope, guarantees the right error is shown regardless of
85
- // whether the caller can deploy at all.
86
- const requiresApproval = await this.tenantRequiresApproval(profile, workspaceId, tenantName, flags.verbose);
87
- if (requiresApproval) {
88
- const hasApprovedRequest = await this.hasApprovedDeployRequest({
89
- profile,
90
- releaseName,
91
- tenantName,
92
- verbose: flags.verbose,
93
- workspaceId,
94
- });
95
- if (!hasApprovedRequest) {
96
- this.error(`Tenant "${tenantName}" requires an approved deploy request before "${releaseName}" can be deployed.\n` +
97
- `Run: xano tenant_deploy_request create "Deploy ${releaseName} to ${tenantName}" --tenant ${tenantName} --release ${releaseName} --reviewers <id,id,...>`);
98
- }
99
- }
100
46
  const apiUrl = `${profile.instance_origin}/api:meta/workspace/${workspaceId}/tenant/${tenantName}/deploy`;
101
47
  this.warn('This may take a few minutes. Please be patient.');
102
48
  const startTime = Date.now();
103
- // A refusal by the policy gate exits 2 with its verdict; the catch that rewords other failures keeps it.
104
- let refused = null;
105
49
  try {
106
50
  const response = await this.verboseFetch(apiUrl, {
107
- body: JSON.stringify({
108
- release_name: releaseName,
109
- ...(overrideReason ? { override_policy: true, override_reason: overrideReason } : {}),
110
- }),
51
+ body: JSON.stringify({ release_name: releaseName }),
111
52
  headers: {
112
53
  accept: 'application/json',
113
54
  Authorization: `Bearer ${profile.access_token}`,
@@ -116,22 +57,8 @@ Policy gate: blocked
116
57
  method: 'POST',
117
58
  }, flags.verbose, profile.access_token);
118
59
  if (!response.ok) {
119
- refused = await gateRefusal(response);
120
- if (refused) {
121
- const override = `xano tenant deploy_release ${quoted(tenantName)} --release ${quoted(releaseName)} --override-reason "<why>"`;
122
- this.refuseByPolicyGate(refused, flags, override);
123
- }
124
- let message = await this.parseApiError(response, 'API request failed');
125
- // Only reached once the pre-flight above has already confirmed the
126
- // approval gate is satisfied or not applicable — so a permission error
127
- // here is genuinely about RBAC, not about a missing approval. If the
128
- // tenant supports deploy requests, mention that path as an alternative
129
- // to direct deploy, since opening a request doesn't require deploy
130
- // permission.
131
- if ((response.status === 401 || response.status === 403) && requiresApproval) {
132
- message += `\n\nYou don't have permission to deploy to this tenant directly, but it supports deploy requests: xano tenant_deploy_request create "Deploy ${releaseName} to ${tenantName}" --tenant ${tenantName} --release ${releaseName} --reviewers <id,id,...>`;
133
- }
134
- this.error(message);
60
+ const errorText = await response.text();
61
+ this.error(`API request failed with status ${response.status}: ${response.statusText}\n${errorText}`);
135
62
  }
136
63
  const tenant = (await response.json());
137
64
  if (flags.output === 'json') {
@@ -144,15 +71,10 @@ Policy gate: blocked
144
71
  this.log(` State: ${tenant.state}`);
145
72
  if (tenant.release?.name)
146
73
  this.log(` Release: ${tenant.release.name}`);
147
- if (tenant.policy_gate?.status)
148
- this.log(` ${deployedGateLine(tenant.policy_gate)}`);
149
74
  this.log(` Time: ${elapsed}s`);
150
75
  }
151
- this.warnPoliciesSkipped(tenant);
152
76
  }
153
77
  catch (error) {
154
- if (refused)
155
- throw error;
156
78
  if (error instanceof Error) {
157
79
  this.error(`Failed to deploy to tenant: ${error.message}`);
158
80
  }
@@ -161,108 +83,4 @@ Policy gate: blocked
161
83
  }
162
84
  }
163
85
  }
164
- /**
165
- * `--check`: what the policy gate would decide for this release on this tenant, without deploying.
166
- * Exits 2 when the deploy would be refused; a preview the platform cannot answer exits 1.
167
- */
168
- async checkPolicyGate(opts) {
169
- const { flags, profile, releaseName, tenantName, workspaceId } = opts;
170
- const result = await fetchPolicyGate({ verboseFetch: (...args) => this.verboseFetch(...args) }, { profile, release: releaseName, tenant: tenantName, verbose: flags.verbose, workspace: workspaceId });
171
- if ('unavailable' in result) {
172
- const { message, payload, status } = result.unavailable;
173
- this.error(`Policy gate preview failed (${status}): ${message}${policyPermissionGuidance(status, payload)}`);
174
- }
175
- const { answer } = result;
176
- const exit = gateExitCode(answer);
177
- if (flags.output === 'json') {
178
- this.log(JSON.stringify(answer, null, 2));
179
- }
180
- else {
181
- this.log(`Checked release "${releaseName}" for tenant "${tenantName}"; nothing was deployed.`);
182
- for (const line of gateLines(answer))
183
- this.log(line);
184
- if (exit) {
185
- this.log(answer.can_override
186
- ? `To deploy past the gate: xano tenant deploy_release ${quoted(tenantName)} --release ${quoted(releaseName)} --override-reason "<why>"`
187
- : 'A deploy is refused unless someone with the workspace:policy update permission overrides the gate.');
188
- }
189
- }
190
- if (exit)
191
- process.exitCode = exit;
192
- }
193
- /**
194
- * Does an approved tenant_deploy_request already exist for this (tenant, release)
195
- * pair? The list endpoint has no release filter, so this fetches approved
196
- * requests for the tenant and filters client-side by the resolved release name.
197
- */
198
- async hasApprovedDeployRequest(opts) {
199
- const { profile, releaseName, tenantName, verbose, workspaceId } = opts;
200
- const params = new URLSearchParams({ per_page: '100', status: 'approved', tenant_name: tenantName });
201
- const apiUrl = `${profile.instance_origin}/api:meta/workspace/${workspaceId}/approval_request?${params}`;
202
- const response = await this.verboseFetch(apiUrl, {
203
- headers: {
204
- accept: 'application/json',
205
- Authorization: `Bearer ${profile.access_token}`,
206
- },
207
- method: 'GET',
208
- }, verbose, profile.access_token);
209
- if (!response.ok) {
210
- // A read-scope failure here means something is more broadly wrong (e.g.
211
- // no workspace access at all) — surface it rather than silently treating
212
- // it as "no approval found", which would misreport as approval-required.
213
- const message = await this.parseApiError(response, 'Failed to check for an approved deploy request');
214
- this.error(message);
215
- }
216
- const data = (await response.json());
217
- const items = Array.isArray(data) ? data : (data.items ?? []);
218
- return items.some((item) => item.status === 'approved' && item._release?.name === releaseName);
219
- }
220
- /** A deploy the policy gate refused: its verdict, then the refusal and how to proceed, exiting 2. */
221
- refuseByPolicyGate(refused, flags, override) {
222
- for (const line of gateRefusalOutput(refused, flags.output === 'json'))
223
- this.log(line);
224
- this.refusedByPolicyGate = true;
225
- this.error(`${refused.message}${gateOverrideHint(refused.answer, override)}`, { exit: 2 });
226
- }
227
- /**
228
- * Does this tenant have the deploy approval gate enabled?
229
- *
230
- * Returns false if the tenant has `allow_quick_deploy` set — that flag lets a
231
- * tenant skip the approval gate entirely for quick deploys, regardless of
232
- * `required_reviewers`.
233
- *
234
- * Deliberately uses `tenant list` rather than `tenant get`: the single-tenant
235
- * GET route's output whitelist (tenant-detail.yaml) omits `deploy_settings`
236
- * (which holds `required_reviewers`, `allow_deploy_bypass`, and
237
- * `allow_quick_deploy`) even though the list route's output includes it
238
- * — confirmed live against a running instance. Filed as a backend bug; this
239
- * is the workaround until it's fixed. See TENANT_DEPLOY_REQUEST_TEST.md.
240
- */
241
- async tenantRequiresApproval(profile, workspaceId, tenantName, verbose) {
242
- const params = new URLSearchParams({ per_page: '100' });
243
- const apiUrl = `${profile.instance_origin}/api:meta/workspace/${workspaceId}/tenant?${params}`;
244
- const response = await this.verboseFetch(apiUrl, {
245
- headers: {
246
- accept: 'application/json',
247
- Authorization: `Bearer ${profile.access_token}`,
248
- },
249
- method: 'GET',
250
- }, verbose, profile.access_token);
251
- if (!response.ok) {
252
- const message = await this.parseApiError(response, 'Failed to look up tenant');
253
- this.error(message);
254
- }
255
- const data = (await response.json());
256
- const items = Array.isArray(data) ? data : (data.items ?? []);
257
- const tenant = items.find((item) => item.name === tenantName);
258
- if (tenant?.deploy_settings?.allow_quick_deploy)
259
- return false;
260
- return (tenant?.deploy_settings?.required_reviewers ?? 0) > 0;
261
- }
262
- /** A release policy the tenant's import could not land (its key is held by another policy) is never dropped silently. */
263
- warnPoliciesSkipped(tenant) {
264
- const message = tenant.policies_skipped?.message;
265
- if (message)
266
- this.warn(message);
267
- }
268
86
  }
@@ -6,7 +6,6 @@ export default class TenantEdit extends BaseCommand {
6
6
  static description: string;
7
7
  static examples: string[];
8
8
  static flags: {
9
- allow_quick_deploy: import("@oclif/core/interfaces").BooleanFlag<boolean>;
10
9
  description: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
11
10
  display: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
12
11
  domain: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
@@ -14,7 +13,6 @@ export default class TenantEdit extends BaseCommand {
14
13
  output: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
15
14
  proxy: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
16
15
  rbac: import("@oclif/core/interfaces").BooleanFlag<boolean>;
17
- required_reviewers: import("@oclif/core/interfaces").OptionFlag<number | undefined, import("@oclif/core/interfaces").CustomOptions>;
18
16
  tasks: import("@oclif/core/interfaces").BooleanFlag<boolean>;
19
17
  workspace: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
20
18
  config: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
@@ -13,15 +13,9 @@ export default class TenantEdit extends BaseCommand {
13
13
  Updated tenant: New Name (my-tenant) - ID: 42
14
14
  `,
15
15
  `$ xano tenant edit t1234-abcd-xyz1 --no-tasks --no-ingress -o json`,
16
- `$ xano tenant edit t1234-abcd-xyz1 --required_reviewers 1`,
17
16
  ];
18
17
  static flags = {
19
18
  ...BaseCommand.baseFlags,
20
- allow_quick_deploy: Flags.boolean({
21
- allowNo: true,
22
- description: 'Allow this tenant to skip the deploy approval gate entirely for quick deploys, regardless of --required_reviewers.',
23
- required: false,
24
- }),
25
19
  description: Flags.string({
26
20
  char: 'd',
27
21
  description: 'New description',
@@ -56,10 +50,6 @@ Updated tenant: New Name (my-tenant) - ID: 42
56
50
  description: '[CRITICAL] NEVER disable RBAC without explicit user confirmation; this removes role-based access controls on the tenant. Enables/disables RBAC.',
57
51
  required: false,
58
52
  }),
59
- required_reviewers: Flags.integer({
60
- description: 'When greater than 0, that many distinct reviewers must approve a tenant_deploy_request before a release may be deployed to this tenant. 0 means not gated.',
61
- required: false,
62
- }),
63
53
  tasks: Flags.boolean({
64
54
  allowNo: true,
65
55
  description: '[IMPORTANT] ALWAYS confirm with the user before disabling background tasks; this stops scheduled jobs on the tenant. Enables/disables background tasks.',
@@ -86,17 +76,8 @@ Updated tenant: New Name (my-tenant) - ID: 42
86
76
  'Content-Type': 'application/json',
87
77
  };
88
78
  try {
89
- // Fetch current tenant state (PUT requires all fields). Deliberately uses
90
- // `tenant list` rather than `tenant get`: the single-tenant GET route's
91
- // output whitelist (tenant-detail.yaml) omits `deploy_settings` (which
92
- // holds `required_reviewers`, `allow_deploy_bypass`, and
93
- // `allow_quick_deploy`) even though the list route's output includes it
94
- // — confirmed live against a running instance. Using GET here would
95
- // silently reset those fields to their defaults on every edit that doesn't
96
- // explicitly pass them. Filed as a backend bug; this is the workaround
97
- // until it's fixed. See TENANT_DEPLOY_REQUEST_TEST.md.
98
- const listParams = new URLSearchParams({ per_page: '100' });
99
- const getResponse = await this.verboseFetch(`${profile.instance_origin}/api:meta/workspace/${workspaceId}/tenant?${listParams}`, {
79
+ // Fetch current tenant state (PUT requires all fields)
80
+ const getResponse = await this.verboseFetch(baseUrl, {
100
81
  headers: {
101
82
  'accept': 'application/json',
102
83
  'Authorization': `Bearer ${profile.access_token}`,
@@ -107,25 +88,9 @@ Updated tenant: New Name (my-tenant) - ID: 42
107
88
  const errorText = await getResponse.text();
108
89
  this.error(`Failed to fetch tenant: ${getResponse.status} ${getResponse.statusText}\n${errorText}`);
109
90
  }
110
- const listData = (await getResponse.json());
111
- const listItems = Array.isArray(listData) ? listData : (listData.items ?? []);
112
- const current = listItems.find((item) => item.name === tenantName);
113
- if (!current) {
114
- this.error(`Tenant "${tenantName}" not found`);
115
- }
116
- // Merge in user-provided values. allow_deploy_bypass has no edit flag (it's
117
- // fixed at `tenant create` time), but the PUT still requires the full body,
118
- // so its current value is preserved here rather than reset.
91
+ const current = await getResponse.json();
92
+ // Merge in user-provided values
119
93
  const body = {
120
- deploy_settings: {
121
- allow_deploy_bypass: current.deploy_settings?.allow_deploy_bypass ?? false,
122
- allow_quick_deploy: flags.allow_quick_deploy === undefined
123
- ? (current.deploy_settings?.allow_quick_deploy ?? false)
124
- : flags.allow_quick_deploy,
125
- required_reviewers: flags.required_reviewers === undefined
126
- ? (current.deploy_settings?.required_reviewers ?? 0)
127
- : flags.required_reviewers,
128
- },
129
94
  description: flags.description === undefined ? (current.description ?? '') : flags.description,
130
95
  display: flags.display === undefined ? (current.display ?? current.name) : flags.display,
131
96
  domain: flags.domain === undefined ? (current.domain ?? '') : flags.domain,
@@ -14,14 +14,4 @@ export default class Pull extends BaseCommand {
14
14
  verbose: import("@oclif/core/interfaces").BooleanFlag<boolean>;
15
15
  };
16
16
  run(): Promise<void>;
17
- /**
18
- * Policy keys are unique on the server without regard to case, but an older export or a local file
19
- * can still differ only in case, and file names are not case-sensitive on every checkout. The
20
- * policies whose file name differs only in case from another exported policy, or from a local file
21
- * it would overwrite, are left out with a warning; everything else is written. Local policy files
22
- * absent from the export are kept. They are reported as stale only when this credential reads the
23
- * branch's policies, because an export leaves out the policies its credential cannot read, and
24
- * every policy while the Policies feature is off.
25
- */
26
- private policyClashes;
27
17
  }
@@ -3,9 +3,8 @@ import snakeCase from 'lodash.snakecase';
3
3
  import * as fs from 'node:fs';
4
4
  import path from 'node:path';
5
5
  import BaseCommand from '../../../base-command.js';
6
- import { buildApiGroupFolderResolver, buildChannelServerResolver, parseDocument, policyBaseName, resolveDocumentPath, } from '../../../utils/document-parser.js';
6
+ import { buildApiGroupFolderResolver, buildChannelServerResolver, parseDocument, resolveDocumentPath, } from '../../../utils/document-parser.js';
7
7
  import { fetchKnowledge, writeKnowledge } from '../../../utils/knowledge-sync.js';
8
- import { policyListing } from '../../../utils/policy/request.js';
9
8
  export default class Pull extends BaseCommand {
10
9
  static description = 'Pull a workspace multidoc from the Xano Metadata API and split into individual files';
11
10
  static examples = [
@@ -28,7 +27,7 @@ Pulled 58 documents
28
27
  ...BaseCommand.baseFlags,
29
28
  branch: Flags.string({
30
29
  char: 'b',
31
- description: "Branch name (defaults to profile branch or live; -b '' selects live)",
30
+ description: 'Branch name (optional if set in profile, defaults to live)',
32
31
  required: false,
33
32
  }),
34
33
  directory: Flags.string({
@@ -74,8 +73,8 @@ Pulled 58 documents
74
73
  ` 1. Provide it as a flag: xano workspace pull -w <workspace_id>\n` +
75
74
  ` 2. Set it in your profile using: xano profile:edit ${profileName} -w <workspace_id>`);
76
75
  }
77
- // Determine branch from flag or profile; `-b ''` selects live whatever the profile's branch, as in push.
78
- const branch = flags.branch ?? profile.branch ?? '';
76
+ // Determine branch from flag or profile
77
+ const branch = flags.branch || profile.branch || '';
79
78
  // Build query parameters
80
79
  const queryParams = new URLSearchParams({
81
80
  branch,
@@ -124,13 +123,12 @@ Pulled 58 documents
124
123
  documents.push(parsed);
125
124
  }
126
125
  }
127
- // Resolve the output directory
128
- const outputDir = path.resolve(flags.directory);
129
- const clashing = await this.policyClashes(documents, outputDir, () => policyListing({ logToStderr: (message) => this.logToStderr(message), verboseFetch: (...args) => this.verboseFetch(...args) }, { branch, profile, verbose: flags.verbose, workspace: workspaceId }));
130
126
  if (documents.length === 0) {
131
127
  this.log('No documents found in response');
132
128
  return;
133
129
  }
130
+ // Resolve the output directory
131
+ const outputDir = path.resolve(flags.directory);
134
132
  // Create the output directory if it doesn't exist
135
133
  fs.mkdirSync(outputDir, { recursive: true });
136
134
  // Resolve api_group names to unique folder names, disambiguating collisions
@@ -142,7 +140,7 @@ Pulled 58 documents
142
140
  // Track filenames per type to handle duplicates
143
141
  const filenameCounters = new Map();
144
142
  let writtenCount = 0;
145
- for (const doc of documents.filter((document) => !clashing.has(document))) {
143
+ for (const doc of documents) {
146
144
  const { baseName, typeDir } = resolveDocumentPath(doc, outputDir, {
147
145
  getApiGroupFolder,
148
146
  getChannelServer,
@@ -175,66 +173,5 @@ Pulled 58 documents
175
173
  if (knowledgeCount > 0)
176
174
  parts.push(`${knowledgeCount} knowledge file${knowledgeCount === 1 ? '' : 's'}`);
177
175
  this.log(`Pulled ${parts.join(' + ')} to ${flags.directory}`);
178
- // Policy files state the rules; the skill the instance generates tells an agent how to follow them.
179
- // Only a pull that carried policy files says so: an instance with the Policies feature off exports
180
- // none, and its skill route answers 403.
181
- if (documents.some((doc) => doc.type === 'policy')) {
182
- this.log('Run `xano skills pull` to install the policies skill for your coding agent.');
183
- }
184
- }
185
- /**
186
- * Policy keys are unique on the server without regard to case, but an older export or a local file
187
- * can still differ only in case, and file names are not case-sensitive on every checkout. The
188
- * policies whose file name differs only in case from another exported policy, or from a local file
189
- * it would overwrite, are left out with a warning; everything else is written. Local policy files
190
- * absent from the export are kept. They are reported as stale only when this credential reads the
191
- * branch's policies, because an export leaves out the policies its credential cannot read, and
192
- * every policy while the Policies feature is off.
193
- */
194
- async policyClashes(documents, outputDir, readsPolicies) {
195
- const exported = new Map();
196
- for (const doc of documents.filter(doc => doc.type === 'policy')) {
197
- const filename = `${policyBaseName(doc.name)}.xs`;
198
- exported.set(filename.toLowerCase(), [...(exported.get(filename.toLowerCase()) ?? []), filename]);
199
- }
200
- const clashes = new Map();
201
- for (const [normalized, names] of exported) {
202
- if (names.length > 1)
203
- clashes.set(normalized, names.map(name => `policies/${name}`).join(' and '));
204
- }
205
- const policyDir = path.join(outputDir, 'policies');
206
- const localFiles = fs.existsSync(policyDir)
207
- ? fs.readdirSync(policyDir, { withFileTypes: true })
208
- .filter(entry => entry.isFile() && entry.name.toLowerCase().endsWith('.xs'))
209
- .map(entry => entry.name)
210
- .sort()
211
- : [];
212
- const stale = [];
213
- for (const filename of localFiles) {
214
- const names = exported.get(filename.toLowerCase());
215
- if (!names)
216
- stale.push(`policies/${filename}`);
217
- else if (!names.includes(filename) && !clashes.has(filename.toLowerCase())) {
218
- clashes.set(filename.toLowerCase(), `policies/${names[0]} (local policies/${filename})`);
219
- }
220
- }
221
- if (clashes.size > 0) {
222
- this.warn(`Policy files that differ only in case are left out of this pull; everything else is written:\n ${[...clashes.values()].join('\n ')}\n` +
223
- 'Rename one of the policies, or move the local file aside, then pull again.');
224
- }
225
- if (stale.length > 0) {
226
- // An export that carries a policy was made by a credential that reads them.
227
- const listing = exported.size > 0 ? 'listed' : await readsPolicies();
228
- if (listing === 'listed') {
229
- this.warn(`Stale local policy files are absent from this export and were kept:\n ${stale.join('\n ')}\nReview them before pushing; a push can publish these policies again.`);
230
- }
231
- else if (listing === 'feature_off') {
232
- this.log('Local policy files were kept: Policies are not enabled on this instance, so the export carries none.');
233
- }
234
- else {
235
- this.log(`Local policy files were kept: this credential cannot list this workspace's policies, and the export carries none.`);
236
- }
237
- }
238
- return new Set(documents.filter(doc => doc.type === 'policy' && clashes.has(`${doc.name}.xs`.toLowerCase())));
239
176
  }
240
177
  }
@@ -12,9 +12,6 @@ export default class Push extends BaseCommand {
12
12
  force: import("@oclif/core/interfaces").BooleanFlag<boolean>;
13
13
  guids: import("@oclif/core/interfaces").BooleanFlag<boolean>;
14
14
  include: import("@oclif/core/interfaces").OptionFlag<string[] | undefined, import("@oclif/core/interfaces").CustomOptions>;
15
- message: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
16
- output: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
17
- 'policy-override': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
18
15
  records: import("@oclif/core/interfaces").BooleanFlag<boolean>;
19
16
  sync: import("@oclif/core/interfaces").BooleanFlag<boolean>;
20
17
  transaction: import("@oclif/core/interfaces").BooleanFlag<boolean>;
@@ -24,21 +21,5 @@ export default class Push extends BaseCommand {
24
21
  profile: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
25
22
  verbose: import("@oclif/core/interfaces").BooleanFlag<boolean>;
26
23
  };
27
- protected catch(error: Error & {
28
- oclif?: {
29
- exit?: number;
30
- };
31
- }): Promise<void>;
32
24
  run(): Promise<void>;
33
- /**
34
- * A failure after the import landed. The import stands, so its policy feedback is reported as for
35
- * any push and a blocking finding still exits 2; otherwise the failure exits 1. Under `-o json`
36
- * stdout holds the import's document with the failure as `error`.
37
- */
38
- private catchAfterImport;
39
- /**
40
- * What happened to the policy documents, then the policy check the import answered with. When the
41
- * platform left the policy files out, its notice says so instead of a count of what was sent.
42
- */
43
- private reportPolicyFeedback;
44
25
  }