@xano/cli 1.3.1-beta.0 → 1.3.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.
- package/README.md +5 -269
- package/dist/base-command.d.ts +1 -12
- package/dist/base-command.js +22 -36
- package/dist/commands/branch/set_live/index.d.ts +0 -22
- package/dist/commands/branch/set_live/index.js +1 -60
- package/dist/commands/ephemeral/push/index.js +1 -6
- package/dist/commands/function/create/index.d.ts +0 -15
- package/dist/commands/function/create/index.js +0 -40
- package/dist/commands/function/edit/index.d.ts +0 -15
- package/dist/commands/function/edit/index.js +0 -40
- package/dist/commands/function/run/index.d.ts +1 -0
- package/dist/commands/function/run/index.js +10 -2
- package/dist/commands/release/create/index.js +0 -7
- package/dist/commands/release/deploy/index.d.ts +0 -28
- package/dist/commands/release/deploy/index.js +6 -90
- package/dist/commands/release/import/index.js +1 -5
- package/dist/commands/release/list/index.d.ts +0 -7
- package/dist/commands/release/list/index.js +10 -54
- package/dist/commands/release/push/index.js +1 -19
- package/dist/commands/sandbox/push/index.js +1 -6
- package/dist/commands/tenant/create/index.d.ts +0 -3
- package/dist/commands/tenant/create/index.js +0 -21
- package/dist/commands/tenant/deploy_release/index.d.ts +0 -40
- package/dist/commands/tenant/deploy_release/index.js +4 -186
- package/dist/commands/tenant/edit/index.d.ts +0 -2
- package/dist/commands/tenant/edit/index.js +4 -39
- package/dist/commands/workspace/pull/index.d.ts +0 -10
- package/dist/commands/workspace/pull/index.js +7 -70
- package/dist/commands/workspace/push/index.d.ts +0 -19
- package/dist/commands/workspace/push/index.js +5 -124
- package/dist/utils/document-parser.d.ts +0 -10
- package/dist/utils/document-parser.js +0 -21
- package/dist/utils/multidoc-push.d.ts +2 -55
- package/dist/utils/multidoc-push.js +27 -103
- package/oclif.manifest.json +3658 -6647
- package/package.json +1 -7
- package/dist/commands/policy/catalogue/index.d.ts +0 -15
- package/dist/commands/policy/catalogue/index.js +0 -29
- package/dist/commands/policy/create/index.d.ts +0 -18
- package/dist/commands/policy/create/index.js +0 -39
- package/dist/commands/policy/delete/index.d.ts +0 -19
- package/dist/commands/policy/delete/index.js +0 -61
- package/dist/commands/policy/evaluate/index.d.ts +0 -32
- package/dist/commands/policy/evaluate/index.js +0 -71
- package/dist/commands/policy/list/index.d.ts +0 -14
- package/dist/commands/policy/list/index.js +0 -21
- package/dist/commands/policy/parse/index.d.ts +0 -19
- package/dist/commands/policy/parse/index.js +0 -18
- package/dist/commands/policy/publish/index.d.ts +0 -20
- package/dist/commands/policy/publish/index.js +0 -38
- package/dist/commands/policy/runs/index.d.ts +0 -54
- package/dist/commands/policy/runs/index.js +0 -273
- package/dist/commands/policy/status/index.d.ts +0 -21
- package/dist/commands/policy/status/index.js +0 -54
- package/dist/commands/skills/pull/index.d.ts +0 -30
- package/dist/commands/skills/pull/index.js +0 -118
- package/dist/commands/tenant_deploy_request/bypass/index.d.ts +0 -31
- package/dist/commands/tenant_deploy_request/bypass/index.js +0 -109
- package/dist/commands/tenant_deploy_request/create/index.d.ts +0 -21
- package/dist/commands/tenant_deploy_request/create/index.js +0 -108
- package/dist/commands/tenant_deploy_request/edit/index.d.ts +0 -22
- package/dist/commands/tenant_deploy_request/edit/index.js +0 -103
- package/dist/commands/tenant_deploy_request/get/index.d.ts +0 -25
- package/dist/commands/tenant_deploy_request/get/index.js +0 -116
- package/dist/commands/tenant_deploy_request/list/index.d.ts +0 -20
- package/dist/commands/tenant_deploy_request/list/index.js +0 -122
- package/dist/commands/tenant_deploy_request/revision/index.d.ts +0 -21
- package/dist/commands/tenant_deploy_request/revision/index.js +0 -83
- package/dist/commands/tenant_deploy_request/set_status/index.d.ts +0 -38
- package/dist/commands/tenant_deploy_request/set_status/index.js +0 -159
- package/dist/policy-command.d.ts +0 -90
- package/dist/policy-command.js +0 -94
- package/dist/utils/policy/catalogue.d.ts +0 -7
- package/dist/utils/policy/catalogue.js +0 -72
- package/dist/utils/policy/errors.d.ts +0 -12
- package/dist/utils/policy/errors.js +0 -61
- package/dist/utils/policy/feedback.d.ts +0 -68
- package/dist/utils/policy/feedback.js +0 -165
- package/dist/utils/policy/findings.d.ts +0 -23
- package/dist/utils/policy/findings.js +0 -65
- package/dist/utils/policy/gate.d.ts +0 -161
- package/dist/utils/policy/gate.js +0 -201
- package/dist/utils/policy/goal.d.ts +0 -46
- package/dist/utils/policy/goal.js +0 -95
- package/dist/utils/policy/permission.d.ts +0 -23
- package/dist/utils/policy/permission.js +0 -84
- package/dist/utils/policy/push-gate.d.ts +0 -5
- package/dist/utils/policy/push-gate.js +0 -29
- package/dist/utils/policy/release.d.ts +0 -85
- package/dist/utils/policy/release.js +0 -115
- package/dist/utils/policy/request.d.ts +0 -55
- package/dist/utils/policy/request.js +0 -115
- package/dist/utils/policy/runs.d.ts +0 -30
- package/dist/utils/policy/runs.js +0 -101
- package/dist/utils/policy/status.d.ts +0 -47
- package/dist/utils/policy/status.js +0 -117
- package/dist/utils/policy/types.d.ts +0 -185
- 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 =
|
|
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
|
-
|
|
120
|
-
|
|
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)
|
|
90
|
-
|
|
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
|
|
111
|
-
|
|
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,
|
|
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:
|
|
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
|
|
78
|
-
const branch = flags.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
|
|
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
|
}
|