@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.
- package/README.md +1 -273
- 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 +0 -1
- package/dist/commands/function/run/index.js +2 -10
- 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 +3570 -6579
- 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,273 +0,0 @@
|
|
|
1
|
-
import { Args, Flags } from '@oclif/core';
|
|
2
|
-
import PolicyCommand from '../../../policy-command.js';
|
|
3
|
-
import { quoted } from '../../../utils/policy/gate.js';
|
|
4
|
-
import { releasePolicyRunLines, releaseRunLines } from '../../../utils/policy/release.js';
|
|
5
|
-
import { listItems } from '../../../utils/policy/request.js';
|
|
6
|
-
import { policyRunSummary, policyRunTable } from '../../../utils/policy/runs.js';
|
|
7
|
-
/** A branch retains this many runs, and the run list serves at most this many. */
|
|
8
|
-
const RUNS_RETAINED = 20;
|
|
9
|
-
/** How many findings a run read prints unless `--limit` says otherwise, and the most a page holds. */
|
|
10
|
-
const FINDINGS_LIMIT = 100;
|
|
11
|
-
const FINDINGS_MAX = 500;
|
|
12
|
-
/** The findings route's list filters, each a repeatable flag of the same name. */
|
|
13
|
-
const LIST_FILTERS = ['policy', 'rule', 'severity', 'kind', 'object', 'tag'];
|
|
14
|
-
/** The flags that page or filter one run's findings, so a run list refuses them. */
|
|
15
|
-
const FINDING_FLAGS = ['offset', 'all', 'advisory', 'blocking', ...LIST_FILTERS, 'search'];
|
|
16
|
-
/** How many releases a lookup by name reads a request, and the most requests it makes. */
|
|
17
|
-
const RELEASES_PAGE = 100;
|
|
18
|
-
const RELEASE_PAGES = 50;
|
|
19
|
-
export default class PolicyRuns extends PolicyCommand {
|
|
20
|
-
static args = {
|
|
21
|
-
run_id: Args.string({
|
|
22
|
-
description: 'Run ID to read; omit to list the retained runs, newest first',
|
|
23
|
-
required: false,
|
|
24
|
-
}),
|
|
25
|
-
};
|
|
26
|
-
static description = "List the stored policy check runs, or read one of them a page of findings at a time. --release reads a release's check instead: the run stored when the release was cut, evaluated on what it ships against the policies it ships";
|
|
27
|
-
static examples = [
|
|
28
|
-
'$ xano policy runs',
|
|
29
|
-
'$ xano policy runs --limit 5',
|
|
30
|
-
'$ xano policy runs 1674 --run-detail',
|
|
31
|
-
'$ xano policy runs 1674 --offset 100 --limit 100',
|
|
32
|
-
'$ xano policy runs 1674 --all -o json',
|
|
33
|
-
'$ xano policy runs 1674 --blocking --policy AUTH-001 --severity critical --severity high',
|
|
34
|
-
'$ xano policy runs 1674 -o json',
|
|
35
|
-
'$ xano policy runs --release v1.2',
|
|
36
|
-
'$ xano policy runs --release v1.2 --blocking --all -o json',
|
|
37
|
-
'$ xano policy runs --release v1.2 --recheck',
|
|
38
|
-
'$ xano policy runs --release-id 12',
|
|
39
|
-
];
|
|
40
|
-
static flags = {
|
|
41
|
-
...PolicyCommand.policyFlags,
|
|
42
|
-
advisory: Flags.boolean({
|
|
43
|
-
default: false,
|
|
44
|
-
description: 'With a run ID, only the advisory findings',
|
|
45
|
-
exclusive: ['blocking'],
|
|
46
|
-
}),
|
|
47
|
-
all: Flags.boolean({
|
|
48
|
-
default: false,
|
|
49
|
-
description: 'With a run ID, read every page and print every finding that matches, instead of one page',
|
|
50
|
-
exclusive: ['offset', 'limit'],
|
|
51
|
-
}),
|
|
52
|
-
blocking: Flags.boolean({
|
|
53
|
-
default: false,
|
|
54
|
-
description: "With a run ID, only the blocking findings (an active mandatory policy's)",
|
|
55
|
-
exclusive: ['advisory'],
|
|
56
|
-
}),
|
|
57
|
-
kind: Flags.string({
|
|
58
|
-
description: 'With a run ID, only findings on this object kind (query, function, table, ...); repeatable',
|
|
59
|
-
multiple: true,
|
|
60
|
-
}),
|
|
61
|
-
limit: Flags.integer({
|
|
62
|
-
description: `How many runs to list, newest first (1-${RUNS_RETAINED}, default ${RUNS_RETAINED}: a branch retains at most ${RUNS_RETAINED}); with a run ID, how many findings to print (1-${FINDINGS_MAX}, default ${FINDINGS_LIMIT})`,
|
|
63
|
-
min: 1,
|
|
64
|
-
}),
|
|
65
|
-
object: Flags.string({
|
|
66
|
-
description: 'With a run ID, only findings on this object, written type:id as a finding names it (query:18); repeatable',
|
|
67
|
-
multiple: true,
|
|
68
|
-
}),
|
|
69
|
-
offset: Flags.integer({
|
|
70
|
-
description: "With a run ID, how many findings to skip, in the platform's order (blocking first)",
|
|
71
|
-
min: 0,
|
|
72
|
-
}),
|
|
73
|
-
policy: Flags.string({
|
|
74
|
-
description: 'With a run ID, only findings of this policy key; repeatable',
|
|
75
|
-
multiple: true,
|
|
76
|
-
}),
|
|
77
|
-
recheck: Flags.boolean({
|
|
78
|
-
default: false,
|
|
79
|
-
description: "With --release, check the release again from its archive and store the new run, then read it (a read-only credential is refused)",
|
|
80
|
-
}),
|
|
81
|
-
release: Flags.string({
|
|
82
|
-
description: "Read this release's policy check (by release name) instead of a branch's run; the finding flags page and filter it as they do a run's",
|
|
83
|
-
exclusive: ['branch', 'release-id'],
|
|
84
|
-
}),
|
|
85
|
-
'release-id': Flags.integer({
|
|
86
|
-
description: 'Read a release policy check by release ID, without looking up its name',
|
|
87
|
-
exclusive: ['branch', 'release'],
|
|
88
|
-
min: 1,
|
|
89
|
-
}),
|
|
90
|
-
rule: Flags.string({
|
|
91
|
-
description: 'With a run ID, only findings of this rule ID (AUTH-001.R1); repeatable',
|
|
92
|
-
multiple: true,
|
|
93
|
-
}),
|
|
94
|
-
'run-detail': Flags.boolean({
|
|
95
|
-
default: false,
|
|
96
|
-
description: 'With a run ID, also print the policy descriptions and rule settings that run recorded',
|
|
97
|
-
}),
|
|
98
|
-
search: Flags.string({
|
|
99
|
-
description: 'With a run ID, only findings whose policy key or title, rule title, object name or message contains this text',
|
|
100
|
-
}),
|
|
101
|
-
severity: Flags.string({
|
|
102
|
-
description: 'With a run ID, only findings of this severity (critical, high, medium, low); repeatable',
|
|
103
|
-
multiple: true,
|
|
104
|
-
}),
|
|
105
|
-
tag: Flags.string({
|
|
106
|
-
description: 'With a run ID, only findings whose policy or object carries this tag; repeatable',
|
|
107
|
-
multiple: true,
|
|
108
|
-
}),
|
|
109
|
-
};
|
|
110
|
-
async run() {
|
|
111
|
-
const { args, flags } = await this.parse(PolicyRuns);
|
|
112
|
-
if (flags.release !== undefined || flags['release-id'] !== undefined)
|
|
113
|
-
return this.readRelease(flags, args.run_id);
|
|
114
|
-
if (flags.recheck)
|
|
115
|
-
this.error('--recheck checks a release again: `xano policy runs --release <name> --recheck`.');
|
|
116
|
-
const { request } = this.policyTarget(flags);
|
|
117
|
-
const wanted = args.run_id?.trim() ?? '';
|
|
118
|
-
if (wanted === '')
|
|
119
|
-
return this.listRuns(request, flags);
|
|
120
|
-
// Runs older than the one `policy status` reads are reachable here by id.
|
|
121
|
-
if (!/^\d+$/.test(wanted))
|
|
122
|
-
this.error(`"${wanted}" is not a run ID. Run \`xano policy runs\` for the retained runs.`);
|
|
123
|
-
const limit = findingsLimit(this, flags);
|
|
124
|
-
// The run's summary and its findings a page at a time: neither reads the whole run, which can hold tens of thousands.
|
|
125
|
-
const head = (await request(`/run/${wanted}/summary`));
|
|
126
|
-
await this.reportRun({ flags, head, limit, path: `/run/${wanted}/findings`, reading: `run ${wanted}`, request });
|
|
127
|
-
}
|
|
128
|
-
/**
|
|
129
|
-
* `--all`: every finding that matches, read a page of 500 at a time in the platform's order, as
|
|
130
|
-
* one page. A run of many pages says on stderr what it is reading.
|
|
131
|
-
*/
|
|
132
|
-
async everyFinding(request, path, reading, query) {
|
|
133
|
-
const items = [];
|
|
134
|
-
let total = Number.POSITIVE_INFINITY;
|
|
135
|
-
while (items.length < total) {
|
|
136
|
-
// eslint-disable-next-line no-await-in-loop -- one page at a time, each starting where the last ended
|
|
137
|
-
const page = (await request(path, 'GET', undefined, { ...query, limit: String(FINDINGS_MAX), offset: String(items.length) }));
|
|
138
|
-
if (items.length === 0 && page.total > FINDINGS_MAX)
|
|
139
|
-
this.logToStderr(`Reading ${page.total} findings of ${reading}, ${FINDINGS_MAX} a request (${Math.ceil(page.total / FINDINGS_MAX)} requests)...`);
|
|
140
|
-
total = page.total ?? 0;
|
|
141
|
-
if ((page.items ?? []).length === 0)
|
|
142
|
-
break;
|
|
143
|
-
items.push(...page.items);
|
|
144
|
-
}
|
|
145
|
-
return { items, limit: items.length, offset: 0, total: Number.isFinite(total) ? total : items.length };
|
|
146
|
-
}
|
|
147
|
-
/** The retained runs, newest first; the flags that read one run's findings are refused. */
|
|
148
|
-
async listRuns(request, flags) {
|
|
149
|
-
if (flags['run-detail'])
|
|
150
|
-
this.error('--run-detail reads one run: `xano policy runs <id> --run-detail`.');
|
|
151
|
-
const paging = FINDING_FLAGS.find(name => flags[name] !== undefined && flags[name] !== false);
|
|
152
|
-
if (paging)
|
|
153
|
-
this.error(`--${paging} pages one run's findings: \`xano policy runs <id> --${paging} ...\`.`);
|
|
154
|
-
const limit = flags.limit ?? RUNS_RETAINED;
|
|
155
|
-
if (limit > RUNS_RETAINED)
|
|
156
|
-
this.error(`--limit lists at most ${RUNS_RETAINED} runs: a branch retains only its newest ${RUNS_RETAINED}.`);
|
|
157
|
-
const result = await request('/run', 'GET', undefined, { limit: String(limit) });
|
|
158
|
-
if (flags.output === 'json') {
|
|
159
|
-
this.log(JSON.stringify(result, null, 2));
|
|
160
|
-
return;
|
|
161
|
-
}
|
|
162
|
-
const runs = listItems(result);
|
|
163
|
-
if (runs.length === 0) {
|
|
164
|
-
this.log('No policy runs retained on this branch.');
|
|
165
|
-
return;
|
|
166
|
-
}
|
|
167
|
-
for (const line of policyRunTable(runs))
|
|
168
|
-
this.log(line);
|
|
169
|
-
this.log('Only the most recent runs are retained per branch. Read one with `xano policy runs <id> --run-detail`.');
|
|
170
|
-
}
|
|
171
|
-
/**
|
|
172
|
-
* `--release <name>`: the release's stored check (`GET release/{id}/policy_run`) and a page of its
|
|
173
|
-
* findings, as a run is read. `--recheck` checks the release again from its archive first. A
|
|
174
|
-
* release with no check says so; under `-o json` it is `{run: null}`.
|
|
175
|
-
*/
|
|
176
|
-
async readRelease(flags, runId) {
|
|
177
|
-
if (runId?.trim())
|
|
178
|
-
this.error('Read one run by its ID, or a release\'s check with --release, not both.');
|
|
179
|
-
const name = flags.release?.trim() ?? String(flags['release-id'] ?? '');
|
|
180
|
-
if (!name)
|
|
181
|
-
this.error('--release needs a release name.');
|
|
182
|
-
const limit = findingsLimit(this, flags);
|
|
183
|
-
const { request } = this.policyTarget(flags, { label: 'Release policy check', path: '' });
|
|
184
|
-
const releaseId = flags['release-id'] ?? await this.releaseId(request, name);
|
|
185
|
-
const route = `/release/${releaseId}/policy_run`;
|
|
186
|
-
const answer = (flags.recheck ? await request(route, 'POST', {}) : await request(route));
|
|
187
|
-
const check = flags.recheck ? { check: answer.check ?? null } : {};
|
|
188
|
-
// The recheck's outcome in a line: recorded with its verdict, or why nothing was recorded.
|
|
189
|
-
const rechecked = flags.recheck ? releasePolicyRunLines(answer.check, name).slice(0, 1).map(line => `Rechecked. ${line.trim()}`) : [];
|
|
190
|
-
const head = answer.run ?? null;
|
|
191
|
-
if (!head) {
|
|
192
|
-
if (flags.output === 'json') {
|
|
193
|
-
this.log(JSON.stringify({ ...check, run: null }, null, 2));
|
|
194
|
-
return;
|
|
195
|
-
}
|
|
196
|
-
for (const line of rechecked)
|
|
197
|
-
this.log(line);
|
|
198
|
-
this.log(`Release ${name} has no policy check.${flags.recheck ? '' : ' `--recheck` checks it now from its archive.'}`);
|
|
199
|
-
return;
|
|
200
|
-
}
|
|
201
|
-
await this.reportRun({
|
|
202
|
-
check,
|
|
203
|
-
flags,
|
|
204
|
-
head,
|
|
205
|
-
limit,
|
|
206
|
-
next: flags['release-id'] ? `xano policy runs --release-id ${releaseId}` : `xano policy runs --release ${quoted(name)}`,
|
|
207
|
-
path: `${route}/findings`,
|
|
208
|
-
preface: [...rechecked, ...releaseRunLines(name, releaseId, head)],
|
|
209
|
-
reading: `release ${name}`,
|
|
210
|
-
request,
|
|
211
|
-
});
|
|
212
|
-
}
|
|
213
|
-
/** The release's ID from its name, reading the workspace's releases a page at a time until it is found. */
|
|
214
|
-
async releaseId(request, name) {
|
|
215
|
-
for (let page = 1; page <= RELEASE_PAGES; page++) {
|
|
216
|
-
// eslint-disable-next-line no-await-in-loop -- pages in order, stopping at the release
|
|
217
|
-
const data = await request('/release', 'GET', undefined, { page: String(page), per_page: String(RELEASES_PAGE) });
|
|
218
|
-
const releases = Array.isArray(data) ? data : listItems(data);
|
|
219
|
-
const match = releases.find(release => release.name === name);
|
|
220
|
-
if (match)
|
|
221
|
-
return match.id;
|
|
222
|
-
if (releases.length < RELEASES_PAGE)
|
|
223
|
-
break;
|
|
224
|
-
}
|
|
225
|
-
return this.error(`Release '${name}' not found. \`xano release list\` lists the workspace's releases.`);
|
|
226
|
-
}
|
|
227
|
-
/**
|
|
228
|
-
* One run's summary and a page of its findings (`path`), or under `-o json` the summary with
|
|
229
|
-
* `findings` and `findings_page`, plus what `extra` adds to it.
|
|
230
|
-
*/
|
|
231
|
-
async reportRun(opts) {
|
|
232
|
-
const { flags, head, limit, path, request } = opts;
|
|
233
|
-
const query = findingsQuery(flags, limit);
|
|
234
|
-
const page = flags.all
|
|
235
|
-
? await this.everyFinding(request, path, opts.reading, query)
|
|
236
|
-
: (await request(path, 'GET', undefined, query));
|
|
237
|
-
const items = page.items ?? [];
|
|
238
|
-
const offset = page.offset ?? 0;
|
|
239
|
-
const total = page.total ?? items.length;
|
|
240
|
-
if (flags.output === 'json') {
|
|
241
|
-
const next = offset + items.length < total && items.length > 0 ? offset + items.length : null;
|
|
242
|
-
const findingsPage = { limit: page.limit ?? limit, next_offset: next, offset, total };
|
|
243
|
-
this.log(JSON.stringify({ ...head, ...opts.check, findings: items, findings_page: findingsPage }, null, 2));
|
|
244
|
-
return;
|
|
245
|
-
}
|
|
246
|
-
for (const line of opts.preface ?? [])
|
|
247
|
-
this.log(line);
|
|
248
|
-
const filtered = Object.keys(query).some(name => !['limit', 'offset'].includes(name));
|
|
249
|
-
for (const line of policyRunSummary({ ...head, findings: items }, { filtered, next: opts.next, offset, total }))
|
|
250
|
-
this.log(line);
|
|
251
|
-
if (flags['run-detail'])
|
|
252
|
-
this.logRunDetail(head);
|
|
253
|
-
}
|
|
254
|
-
}
|
|
255
|
-
/** How many findings a page prints: `--limit`, at most 500. */
|
|
256
|
-
function findingsLimit(command, flags) {
|
|
257
|
-
const limit = flags.limit ?? FINDINGS_LIMIT;
|
|
258
|
-
if (limit > FINDINGS_MAX)
|
|
259
|
-
command.error(`--limit prints at most ${FINDINGS_MAX} findings a page; page on with --offset.`);
|
|
260
|
-
return limit;
|
|
261
|
-
}
|
|
262
|
-
/** The findings route's query for one page: `blocking` true or false, and each list filter given. */
|
|
263
|
-
function findingsQuery(flags, limit) {
|
|
264
|
-
const query = { limit: String(limit), offset: String(flags.offset ?? 0) };
|
|
265
|
-
if (flags.blocking || flags.advisory)
|
|
266
|
-
query.blocking = String(flags.blocking);
|
|
267
|
-
for (const name of LIST_FILTERS)
|
|
268
|
-
if (flags[name]?.length)
|
|
269
|
-
query[name] = flags[name];
|
|
270
|
-
if (flags.search?.trim())
|
|
271
|
-
query.q = flags.search;
|
|
272
|
-
return query;
|
|
273
|
-
}
|
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
import PolicyCommand from '../../../policy-command.js';
|
|
2
|
-
export default class PolicyStatus extends PolicyCommand {
|
|
3
|
-
static description: string;
|
|
4
|
-
static examples: string[];
|
|
5
|
-
static flags: {
|
|
6
|
-
'fail-on-findings': import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
7
|
-
'run-detail': import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
8
|
-
branch: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
9
|
-
output: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
10
|
-
workspace: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
11
|
-
config: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
12
|
-
profile: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
13
|
-
verbose: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
14
|
-
};
|
|
15
|
-
run(): Promise<void>;
|
|
16
|
-
/**
|
|
17
|
-
* The run every policy's `latest_run` answer was decided against, or none when the branch has no
|
|
18
|
-
* run: its summary, which counts each policy's findings without listing them.
|
|
19
|
-
*/
|
|
20
|
-
private latestRun;
|
|
21
|
-
}
|
|
@@ -1,54 +0,0 @@
|
|
|
1
|
-
import { Flags } from '@oclif/core';
|
|
2
|
-
import PolicyCommand from '../../../policy-command.js';
|
|
3
|
-
import { policyResultSummary } from '../../../utils/policy/findings.js';
|
|
4
|
-
import { listAllPolicies } from '../../../utils/policy/request.js';
|
|
5
|
-
import { computeStatusRows, enforcementLabel, findingsLabel, statusExitCode, statusExitReason, statusLabel, } from '../../../utils/policy/status.js';
|
|
6
|
-
export default class PolicyStatus extends PolicyCommand {
|
|
7
|
-
static description = 'Show the latest policy check results without evaluating';
|
|
8
|
-
static examples = ['$ xano policy status -o json', '$ xano policy status --fail-on-findings -o json', '$ xano policy status --run-detail'];
|
|
9
|
-
static flags = {
|
|
10
|
-
...PolicyCommand.policyFlags,
|
|
11
|
-
'fail-on-findings': Flags.boolean({
|
|
12
|
-
default: false,
|
|
13
|
-
description: 'Exit 2 for current blocking findings, whatever else is true; otherwise exit 1 for stale, missing or errored evaluation evidence',
|
|
14
|
-
}),
|
|
15
|
-
'run-detail': Flags.boolean({
|
|
16
|
-
default: false,
|
|
17
|
-
description: 'Also print what the latest run recorded: each policy description and the settings each rule ran with',
|
|
18
|
-
}),
|
|
19
|
-
};
|
|
20
|
-
async run() {
|
|
21
|
-
const { flags } = await this.parse(PolicyStatus);
|
|
22
|
-
const { request } = this.policyTarget(flags);
|
|
23
|
-
const policies = await listAllPolicies(request);
|
|
24
|
-
const run = await this.latestRun(request, policies);
|
|
25
|
-
const rows = computeStatusRows(policies, run);
|
|
26
|
-
const failOnFindings = flags['fail-on-findings'] ? { exit: statusExitCode(rows), reason: statusExitReason(rows) } : undefined;
|
|
27
|
-
if (flags.output === 'json') {
|
|
28
|
-
this.log(JSON.stringify({ policies, run: run ?? null, status: rows, ...(failOnFindings ? { fail_on_findings: failOnFindings } : {}) }, null, 2));
|
|
29
|
-
}
|
|
30
|
-
else if (rows.length === 0)
|
|
31
|
-
this.log('No policies found.');
|
|
32
|
-
else {
|
|
33
|
-
for (const row of rows)
|
|
34
|
-
this.log(`${row.key} ${statusLabel(row)} ${enforcementLabel(row.enforcement)} ${findingsLabel(row)} ${row.title ?? ''}`);
|
|
35
|
-
const counted = new Set(rows.filter((row) => row.counted).map((row) => row.key));
|
|
36
|
-
for (const line of policyResultSummary(run?.results?.filter((result) => counted.has(result.policy_key ?? ''))))
|
|
37
|
-
this.log(line);
|
|
38
|
-
if (flags['run-detail'] && run)
|
|
39
|
-
this.logRunDetail(run);
|
|
40
|
-
if (failOnFindings?.reason)
|
|
41
|
-
this.log(failOnFindings.reason);
|
|
42
|
-
}
|
|
43
|
-
if (failOnFindings?.exit)
|
|
44
|
-
process.exitCode = failOnFindings.exit;
|
|
45
|
-
}
|
|
46
|
-
/**
|
|
47
|
-
* The run every policy's `latest_run` answer was decided against, or none when the branch has no
|
|
48
|
-
* run: its summary, which counts each policy's findings without listing them.
|
|
49
|
-
*/
|
|
50
|
-
async latestRun(request, policies) {
|
|
51
|
-
const runId = policies.map((policy) => policy.latest_run?.run_id).find(Boolean);
|
|
52
|
-
return runId ? (await request(`/run/${runId}/summary`)) : undefined;
|
|
53
|
-
}
|
|
54
|
-
}
|
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
import PolicyCommand from '../../../policy-command.js';
|
|
2
|
-
/**
|
|
3
|
-
* The SKILL.md a coding agent reads: the CLI's own frontmatter (`name` and `description`), the
|
|
4
|
-
* instance's content below it. The CLI always writes the frontmatter itself and drops any block the
|
|
5
|
-
* served content opens with: frontmatter configures the agent (`allowed-tools`, `hooks`, `model`),
|
|
6
|
-
* and a workspace knowledge record may replace the platform skill, so its text must never set it.
|
|
7
|
-
* The description is written as a double-quoted YAML scalar, so a `:` or `#` in it stays a
|
|
8
|
-
* description instead of becoming syntax.
|
|
9
|
-
*/
|
|
10
|
-
export declare function buildSkillDocument(description: string, content: string): string;
|
|
11
|
-
export default class SkillsPull extends PolicyCommand {
|
|
12
|
-
static description: string;
|
|
13
|
-
static examples: string[];
|
|
14
|
-
static flags: {
|
|
15
|
-
directory: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
16
|
-
branch: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
17
|
-
output: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
18
|
-
workspace: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
19
|
-
config: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
20
|
-
profile: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
21
|
-
verbose: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
22
|
-
};
|
|
23
|
-
/** Any failure exits 1 with its own message: this is not a `policy *` command to name. */
|
|
24
|
-
protected catch(error: Error & {
|
|
25
|
-
oclif?: {
|
|
26
|
-
exit?: number;
|
|
27
|
-
};
|
|
28
|
-
}): Promise<void>;
|
|
29
|
-
run(): Promise<void>;
|
|
30
|
-
}
|
|
@@ -1,118 +0,0 @@
|
|
|
1
|
-
import { Flags } from '@oclif/core';
|
|
2
|
-
import * as fs from 'node:fs';
|
|
3
|
-
import path from 'node:path';
|
|
4
|
-
import PolicyCommand from '../../../policy-command.js';
|
|
5
|
-
/** The one skill this command installs. The instance generates it from its check catalogue. */
|
|
6
|
-
const SKILL = 'xano-policies';
|
|
7
|
-
/**
|
|
8
|
-
* The served content without a leading YAML frontmatter block, if it opens with one: the lines up
|
|
9
|
-
* to and including the closing `---`, and the blank lines after it.
|
|
10
|
-
*/
|
|
11
|
-
function stripFrontmatter(content) {
|
|
12
|
-
if (!/^---\r?\n/.test(content))
|
|
13
|
-
return content;
|
|
14
|
-
const lines = content.split(/\r?\n/);
|
|
15
|
-
const close = lines.indexOf('---', 1);
|
|
16
|
-
if (close === -1)
|
|
17
|
-
return content;
|
|
18
|
-
let start = close + 1;
|
|
19
|
-
while (start < lines.length && lines[start].trim() === '')
|
|
20
|
-
start++;
|
|
21
|
-
return lines.slice(start).join('\n');
|
|
22
|
-
}
|
|
23
|
-
/**
|
|
24
|
-
* The SKILL.md a coding agent reads: the CLI's own frontmatter (`name` and `description`), the
|
|
25
|
-
* instance's content below it. The CLI always writes the frontmatter itself and drops any block the
|
|
26
|
-
* served content opens with: frontmatter configures the agent (`allowed-tools`, `hooks`, `model`),
|
|
27
|
-
* and a workspace knowledge record may replace the platform skill, so its text must never set it.
|
|
28
|
-
* The description is written as a double-quoted YAML scalar, so a `:` or `#` in it stays a
|
|
29
|
-
* description instead of becoming syntax.
|
|
30
|
-
*/
|
|
31
|
-
export function buildSkillDocument(description, content) {
|
|
32
|
-
// Exactly one trailing newline, whatever the instance sent, so a re-pull is byte-identical.
|
|
33
|
-
const body = `${stripFrontmatter(content).replace(/\n+$/, '')}\n`;
|
|
34
|
-
return `---\nname: ${SKILL}\ndescription: ${JSON.stringify(description)}\n---\n\n${body}`;
|
|
35
|
-
}
|
|
36
|
-
/** True when the path is a symbolic link; false when it is anything else or does not exist. */
|
|
37
|
-
function isSymbolicLink(filePath) {
|
|
38
|
-
try {
|
|
39
|
-
return fs.lstatSync(filePath).isSymbolicLink();
|
|
40
|
-
}
|
|
41
|
-
catch (error) {
|
|
42
|
-
if (error.code === 'ENOENT')
|
|
43
|
-
return false;
|
|
44
|
-
throw error;
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
/**
|
|
48
|
-
* Whether the served skill is the workspace's own knowledge record. A stored record has a positive
|
|
49
|
-
* id; the skill the platform generates has none of its own (the platform serves it under a
|
|
50
|
-
* negative sentinel id), so an item without a positive id is the platform's.
|
|
51
|
-
*/
|
|
52
|
-
function isWorkspaceRecord(skill) {
|
|
53
|
-
return typeof skill.id === 'number' && Number.isInteger(skill.id) && skill.id > 0;
|
|
54
|
-
}
|
|
55
|
-
export default class SkillsPull extends PolicyCommand {
|
|
56
|
-
static description = `Install the ${SKILL} agent skill this instance generates
|
|
57
|
-
|
|
58
|
-
Writes <directory>/.claude/skills/${SKILL}/SKILL.md, the project skill folder Claude Code reads, and replaces that file without a backup. A copy installed anywhere else, such as a global ~/.claude/skills/${SKILL} or another agent's skills folder, is not touched: remove it, so the agent does not load it instead.`;
|
|
59
|
-
static examples = [
|
|
60
|
-
`$ xano skills pull
|
|
61
|
-
Wrote .claude/skills/${SKILL}/SKILL.md (${SKILL} skill for workspace 40, branch live)
|
|
62
|
-
`,
|
|
63
|
-
'$ xano skills pull -d ./my-project',
|
|
64
|
-
'$ xano skills pull -b dev -o json',
|
|
65
|
-
];
|
|
66
|
-
static flags = {
|
|
67
|
-
...PolicyCommand.policyFlags,
|
|
68
|
-
directory: Flags.string({
|
|
69
|
-
char: 'd',
|
|
70
|
-
default: '.',
|
|
71
|
-
description: `Project directory whose .claude/skills/${SKILL}/SKILL.md is written (defaults to current directory)`,
|
|
72
|
-
required: false,
|
|
73
|
-
}),
|
|
74
|
-
};
|
|
75
|
-
/** Any failure exits 1 with its own message: this is not a `policy *` command to name. */
|
|
76
|
-
async catch(error) {
|
|
77
|
-
return this.catchAsOperational(error);
|
|
78
|
-
}
|
|
79
|
-
async run() {
|
|
80
|
-
const { flags } = await this.parse(SkillsPull);
|
|
81
|
-
// The route lives beside /policy under the workspace and is gated by the same
|
|
82
|
-
// `workspace:policy` permission, so it answers failures the way the policy commands do.
|
|
83
|
-
const { branch, request, workspace } = this.policyTarget(flags, { label: 'Agent skills', path: '/agent-skills' });
|
|
84
|
-
// `surface=cli` asks for the terminal wording of the skill, not the Studio one.
|
|
85
|
-
const payload = (await request('', 'GET', undefined, { surface: 'cli' }));
|
|
86
|
-
const items = Array.isArray(payload?.knowledge) ? payload.knowledge : [];
|
|
87
|
-
const skill = items.find((item) => typeof item?.name === 'string' && item.name.trim().toLowerCase() === SKILL);
|
|
88
|
-
if (!skill) {
|
|
89
|
-
this.error(`The instance returned no ${SKILL} skill for workspace ${workspace}, branch ${branch || 'live'}.`);
|
|
90
|
-
}
|
|
91
|
-
const document = buildSkillDocument(skill.description ?? '', skill.content ?? '');
|
|
92
|
-
const filePath = path.join(path.resolve(flags.directory), '.claude', 'skills', SKILL, 'SKILL.md');
|
|
93
|
-
// Writing through a symbolic link would overwrite whatever it points at.
|
|
94
|
-
if (isSymbolicLink(filePath)) {
|
|
95
|
-
this.error(`${filePath} is a symbolic link; remove it and run the command again. Nothing was written.`);
|
|
96
|
-
}
|
|
97
|
-
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
98
|
-
// The file is regenerated on every pull and no backup is kept.
|
|
99
|
-
fs.writeFileSync(filePath, document, 'utf8');
|
|
100
|
-
const own = isWorkspaceRecord(skill);
|
|
101
|
-
if (flags.output === 'json') {
|
|
102
|
-
this.log(JSON.stringify({
|
|
103
|
-
branch,
|
|
104
|
-
bytes: Buffer.byteLength(document, 'utf8'),
|
|
105
|
-
name: SKILL,
|
|
106
|
-
path: filePath,
|
|
107
|
-
source: own ? 'workspace' : 'platform',
|
|
108
|
-
workspace,
|
|
109
|
-
}, null, 2));
|
|
110
|
-
return;
|
|
111
|
-
}
|
|
112
|
-
const relative = path.relative(process.cwd(), filePath);
|
|
113
|
-
const shown = relative && !relative.startsWith('..') ? relative : filePath;
|
|
114
|
-
this.log(`Wrote ${shown} (${SKILL} skill for workspace ${workspace}, branch ${branch || 'live'})`);
|
|
115
|
-
if (own)
|
|
116
|
-
this.log(`This is the workspace's own ${SKILL} knowledge record, which replaces the platform skill.`);
|
|
117
|
-
}
|
|
118
|
-
}
|
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
import BaseCommand from '../../../base-command.js';
|
|
2
|
-
export default class TenantDeployRequestBypass extends BaseCommand {
|
|
3
|
-
static args: {
|
|
4
|
-
id: import("@oclif/core/interfaces").Arg<number, {
|
|
5
|
-
max?: number;
|
|
6
|
-
min?: number;
|
|
7
|
-
}>;
|
|
8
|
-
};
|
|
9
|
-
static description: string;
|
|
10
|
-
static examples: string[];
|
|
11
|
-
static flags: {
|
|
12
|
-
output: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
13
|
-
'override-policy': import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
14
|
-
reason: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
15
|
-
workspace: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
16
|
-
config: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
17
|
-
profile: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
18
|
-
verbose: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
19
|
-
};
|
|
20
|
-
/** Set once the policy gate refuses the bypass deploy, the one failure that exits 2. */
|
|
21
|
-
private refusedByPolicyGate;
|
|
22
|
-
/** Exit 2 is the policy gate's refusal; every other failure, flag errors included, exits 1. */
|
|
23
|
-
protected catch(error: Error & {
|
|
24
|
-
oclif?: {
|
|
25
|
-
exit?: number;
|
|
26
|
-
};
|
|
27
|
-
}): Promise<void>;
|
|
28
|
-
run(): Promise<void>;
|
|
29
|
-
/** The bypass deploy was refused by the policy gate: its verdict, then the refusal, exiting 2. */
|
|
30
|
-
private refuseByPolicyGate;
|
|
31
|
-
}
|
|
@@ -1,109 +0,0 @@
|
|
|
1
|
-
import { Args, Flags } from '@oclif/core';
|
|
2
|
-
import BaseCommand from '../../../base-command.js';
|
|
3
|
-
import { gateOverrideHint, gateRefusal, gateRefusalOutput } from '../../../utils/policy/gate.js';
|
|
4
|
-
export default class TenantDeployRequestBypass extends BaseCommand {
|
|
5
|
-
static args = {
|
|
6
|
-
id: Args.integer({
|
|
7
|
-
description: 'Tenant deploy request ID',
|
|
8
|
-
required: true,
|
|
9
|
-
}),
|
|
10
|
-
};
|
|
11
|
-
static description = "[CRITICAL] STOP and confirm with the user before running this; it deploys the release immediately, past the review workflow. Deploy the request's release past the approval gate under the tenant_center:deploy:bypass_approval permission. Only available if the tenant has enabled bypass (allow_deploy_bypass), and every use is logged with the given reason. The approval request's own status is left untouched. The deploy is still gated on the release's policy check: a refusal exits 2, and --override-policy proceeds past it with the same --reason (needs the workspace:policy update permission).";
|
|
12
|
-
static examples = [
|
|
13
|
-
`$ xano tenant_deploy_request bypass 12 --reason "Prod incident, reviewer unavailable"
|
|
14
|
-
Deployed tenant release, bypassing deploy request #12's approval gate.
|
|
15
|
-
`,
|
|
16
|
-
`$ xano tenant_deploy_request bypass 12 --reason "Prod incident; AUTH-001 finding accepted" --override-policy`,
|
|
17
|
-
];
|
|
18
|
-
static flags = {
|
|
19
|
-
...BaseCommand.baseFlags,
|
|
20
|
-
output: Flags.string({
|
|
21
|
-
char: 'o',
|
|
22
|
-
default: 'summary',
|
|
23
|
-
description: 'Output format',
|
|
24
|
-
options: ['summary', 'json'],
|
|
25
|
-
required: false,
|
|
26
|
-
}),
|
|
27
|
-
'override-policy': Flags.boolean({
|
|
28
|
-
default: false,
|
|
29
|
-
description: 'Also proceed past a blocking policy gate, recording --reason as the override reason (needs the workspace:policy update permission; audited)',
|
|
30
|
-
}),
|
|
31
|
-
reason: Flags.string({
|
|
32
|
-
description: 'Reason for bypassing the approval gate (required, audited)',
|
|
33
|
-
required: true,
|
|
34
|
-
}),
|
|
35
|
-
workspace: Flags.string({
|
|
36
|
-
char: 'w',
|
|
37
|
-
description: 'Workspace ID (uses profile workspace if not provided)',
|
|
38
|
-
required: false,
|
|
39
|
-
}),
|
|
40
|
-
};
|
|
41
|
-
/** Set once the policy gate refuses the bypass deploy, the one failure that exits 2. */
|
|
42
|
-
refusedByPolicyGate = false;
|
|
43
|
-
/** Exit 2 is the policy gate's refusal; every other failure, flag errors included, exits 1. */
|
|
44
|
-
async catch(error) {
|
|
45
|
-
if (this.refusedByPolicyGate)
|
|
46
|
-
return super.catch(error);
|
|
47
|
-
return this.catchAsOperational(error);
|
|
48
|
-
}
|
|
49
|
-
async run() {
|
|
50
|
-
const { args, flags } = await this.parse(TenantDeployRequestBypass);
|
|
51
|
-
const { profile } = this.resolveProfile(flags);
|
|
52
|
-
const workspaceId = flags.workspace || profile.workspace;
|
|
53
|
-
if (!workspaceId) {
|
|
54
|
-
this.error('No workspace ID provided. Use --workspace flag or set one in your profile.');
|
|
55
|
-
}
|
|
56
|
-
const body = {
|
|
57
|
-
reason: flags.reason,
|
|
58
|
-
...(flags['override-policy'] ? { override_policy: true } : {}),
|
|
59
|
-
};
|
|
60
|
-
// A refusal by the policy gate exits 2 with its verdict; the catch that rewords other failures keeps it.
|
|
61
|
-
let refused = null;
|
|
62
|
-
const apiUrl = `${profile.instance_origin}/api:meta/workspace/${workspaceId}/approval_request/${args.id}/override`;
|
|
63
|
-
try {
|
|
64
|
-
const response = await this.verboseFetch(apiUrl, {
|
|
65
|
-
body: JSON.stringify(body),
|
|
66
|
-
headers: {
|
|
67
|
-
accept: 'application/json',
|
|
68
|
-
Authorization: `Bearer ${profile.access_token}`,
|
|
69
|
-
'Content-Type': 'application/json',
|
|
70
|
-
},
|
|
71
|
-
method: 'POST',
|
|
72
|
-
}, flags.verbose, profile.access_token);
|
|
73
|
-
if (!response.ok) {
|
|
74
|
-
refused = await gateRefusal(response);
|
|
75
|
-
if (refused)
|
|
76
|
-
this.refuseByPolicyGate(refused, args.id, flags.output === 'json');
|
|
77
|
-
const message = await this.parseApiError(response, 'Failed to bypass tenant deploy request approval gate');
|
|
78
|
-
this.error(message);
|
|
79
|
-
}
|
|
80
|
-
const deploy = (await response.json());
|
|
81
|
-
if (flags.output === 'json') {
|
|
82
|
-
this.log(JSON.stringify(deploy, null, 2));
|
|
83
|
-
}
|
|
84
|
-
else {
|
|
85
|
-
this.log(`Deployed tenant release, bypassing deploy request #${args.id}'s approval gate.`);
|
|
86
|
-
if (deploy.duration_ms !== undefined)
|
|
87
|
-
this.log(` Time: ${(deploy.duration_ms / 1000).toFixed(1)}s`);
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
catch (error) {
|
|
91
|
-
if (refused)
|
|
92
|
-
throw error;
|
|
93
|
-
if (error instanceof Error) {
|
|
94
|
-
this.error(`Failed to bypass tenant deploy request approval gate: ${error.message}`);
|
|
95
|
-
}
|
|
96
|
-
else {
|
|
97
|
-
this.error(`Failed to bypass tenant deploy request approval gate: ${String(error)}`);
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
/** The bypass deploy was refused by the policy gate: its verdict, then the refusal, exiting 2. */
|
|
102
|
-
refuseByPolicyGate(refused, id, json) {
|
|
103
|
-
for (const line of gateRefusalOutput(refused, json))
|
|
104
|
-
this.log(line);
|
|
105
|
-
const override = `xano tenant_deploy_request bypass ${id} --reason "<why>" --override-policy`;
|
|
106
|
-
this.refusedByPolicyGate = true;
|
|
107
|
-
this.error(`${refused.message}${gateOverrideHint(refused.answer, override)}`, { exit: 2 });
|
|
108
|
-
}
|
|
109
|
-
}
|