@xano/cli 1.3.4 → 1.3.5-beta.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +69 -2
- package/dist/base-command.d.ts +12 -1
- package/dist/base-command.js +36 -22
- package/dist/commands/branch/set_live/index.d.ts +14 -0
- package/dist/commands/branch/set_live/index.js +38 -0
- package/dist/commands/ephemeral/push/index.js +6 -1
- package/dist/commands/function/create/index.d.ts +15 -0
- package/dist/commands/function/create/index.js +39 -0
- package/dist/commands/function/edit/index.d.ts +15 -0
- package/dist/commands/function/edit/index.js +39 -0
- package/dist/commands/function/run/index.js +5 -11
- package/dist/commands/policy/catalogue/index.d.ts +16 -0
- package/dist/commands/policy/catalogue/index.js +60 -0
- package/dist/commands/policy/coverage/index.d.ts +17 -0
- package/dist/commands/policy/coverage/index.js +33 -0
- package/dist/commands/policy/create/index.d.ts +18 -0
- package/dist/commands/policy/create/index.js +44 -0
- package/dist/commands/policy/delete/index.d.ts +19 -0
- package/dist/commands/policy/delete/index.js +61 -0
- package/dist/commands/policy/evaluate/index.d.ts +32 -0
- package/dist/commands/policy/evaluate/index.js +71 -0
- package/dist/commands/policy/list/index.d.ts +14 -0
- package/dist/commands/policy/list/index.js +21 -0
- package/dist/commands/policy/parse/index.d.ts +19 -0
- package/dist/commands/policy/parse/index.js +18 -0
- package/dist/commands/policy/publish/index.d.ts +20 -0
- package/dist/commands/policy/publish/index.js +38 -0
- package/dist/commands/policy/runs/index.d.ts +54 -0
- package/dist/commands/policy/runs/index.js +273 -0
- package/dist/commands/policy/status/index.d.ts +21 -0
- package/dist/commands/policy/status/index.js +54 -0
- package/dist/commands/release/create/index.js +4 -0
- package/dist/commands/release/deploy/index.d.ts +19 -0
- package/dist/commands/release/deploy/index.js +68 -6
- package/dist/commands/release/import/index.js +5 -1
- package/dist/commands/release/list/index.d.ts +7 -0
- package/dist/commands/release/list/index.js +46 -9
- package/dist/commands/release/push/index.js +19 -1
- package/dist/commands/sandbox/push/index.js +6 -1
- package/dist/commands/skills/pull/index.d.ts +30 -0
- package/dist/commands/skills/pull/index.js +118 -0
- package/dist/commands/tenant/create/index.d.ts +3 -0
- package/dist/commands/tenant/create/index.js +21 -0
- package/dist/commands/tenant/deploy_release/index.d.ts +40 -0
- package/dist/commands/tenant/deploy_release/index.js +182 -4
- package/dist/commands/tenant/edit/index.d.ts +2 -0
- package/dist/commands/tenant/edit/index.js +39 -4
- package/dist/commands/tenant_deploy_request/bypass/index.d.ts +31 -0
- package/dist/commands/tenant_deploy_request/bypass/index.js +109 -0
- package/dist/commands/tenant_deploy_request/create/index.d.ts +21 -0
- package/dist/commands/tenant_deploy_request/create/index.js +108 -0
- package/dist/commands/tenant_deploy_request/edit/index.d.ts +22 -0
- package/dist/commands/tenant_deploy_request/edit/index.js +103 -0
- package/dist/commands/tenant_deploy_request/get/index.d.ts +25 -0
- package/dist/commands/tenant_deploy_request/get/index.js +113 -0
- package/dist/commands/tenant_deploy_request/list/index.d.ts +20 -0
- package/dist/commands/tenant_deploy_request/list/index.js +122 -0
- package/dist/commands/tenant_deploy_request/revision/index.d.ts +21 -0
- package/dist/commands/tenant_deploy_request/revision/index.js +83 -0
- package/dist/commands/tenant_deploy_request/set_status/index.d.ts +38 -0
- package/dist/commands/tenant_deploy_request/set_status/index.js +159 -0
- package/dist/commands/workspace/pull/index.d.ts +10 -0
- package/dist/commands/workspace/pull/index.js +70 -7
- package/dist/commands/workspace/push/index.d.ts +19 -0
- package/dist/commands/workspace/push/index.js +135 -6
- package/dist/policy-command.d.ts +91 -0
- package/dist/policy-command.js +95 -0
- package/dist/utils/document-parser.d.ts +10 -0
- package/dist/utils/document-parser.js +21 -0
- package/dist/utils/multidoc-push.d.ts +57 -2
- package/dist/utils/multidoc-push.js +102 -33
- package/dist/utils/policy/catalogue.d.ts +85 -0
- package/dist/utils/policy/catalogue.js +196 -0
- package/dist/utils/policy/coverage.d.ts +19 -0
- package/dist/utils/policy/coverage.js +93 -0
- package/dist/utils/policy/errors.d.ts +20 -0
- package/dist/utils/policy/errors.js +93 -0
- package/dist/utils/policy/feedback.d.ts +90 -0
- package/dist/utils/policy/feedback.js +210 -0
- package/dist/utils/policy/findings.d.ts +29 -0
- package/dist/utils/policy/findings.js +82 -0
- package/dist/utils/policy/gate.d.ts +138 -0
- package/dist/utils/policy/gate.js +193 -0
- package/dist/utils/policy/permission.d.ts +7 -0
- package/dist/utils/policy/permission.js +55 -0
- package/dist/utils/policy/push-gate.d.ts +5 -0
- package/dist/utils/policy/push-gate.js +43 -0
- package/dist/utils/policy/release.d.ts +85 -0
- package/dist/utils/policy/release.js +117 -0
- package/dist/utils/policy/request.d.ts +55 -0
- package/dist/utils/policy/request.js +119 -0
- package/dist/utils/policy/runs.d.ts +30 -0
- package/dist/utils/policy/runs.js +101 -0
- package/dist/utils/policy/status.d.ts +55 -0
- package/dist/utils/policy/status.js +127 -0
- package/dist/utils/policy/template.d.ts +66 -0
- package/dist/utils/policy/template.js +152 -0
- package/dist/utils/policy/types.d.ts +295 -0
- package/dist/utils/policy/types.js +1 -0
- package/oclif.manifest.json +5782 -2573
- package/package.json +9 -1
|
@@ -2,7 +2,9 @@ import { Flags } from '@oclif/core';
|
|
|
2
2
|
import * as fs from 'node:fs';
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import BaseCommand from '../../../base-command.js';
|
|
5
|
-
import { findFilesWithGuid } from '../../../utils/document-parser.js';
|
|
5
|
+
import { findFilesWithGuid, parseDocument } from '../../../utils/document-parser.js';
|
|
6
|
+
import { policiesSkippedNotice } from '../../../utils/policy/feedback.js';
|
|
7
|
+
import { releasePolicyRunLines } from '../../../utils/policy/release.js';
|
|
6
8
|
export default class ReleasePush extends BaseCommand {
|
|
7
9
|
static description = '[IMPORTANT] ALWAYS confirm with the user before creating a release from local files. Creates a new release from local XanoScript files via the multidoc endpoint.';
|
|
8
10
|
static examples = [
|
|
@@ -110,6 +112,12 @@ Output release details as JSON
|
|
|
110
112
|
if (documentEntries.length === 0) {
|
|
111
113
|
this.error(`All .xs files in ${flags.directory} are empty`);
|
|
112
114
|
}
|
|
115
|
+
// A release carries its policy files like its code.
|
|
116
|
+
const policyKeys = documentEntries
|
|
117
|
+
.map((entry) => parseDocument(entry.content))
|
|
118
|
+
.filter((document) => document?.type === 'policy')
|
|
119
|
+
.map((document) => document.name)
|
|
120
|
+
.sort();
|
|
113
121
|
const multidoc = documentEntries.map((d) => d.content).join('\n---\n');
|
|
114
122
|
// Construct the API URL with query params
|
|
115
123
|
const queryParams = new URLSearchParams({
|
|
@@ -158,6 +166,10 @@ Output release details as JSON
|
|
|
158
166
|
this.error(errorMessage);
|
|
159
167
|
}
|
|
160
168
|
const release = (await response.json());
|
|
169
|
+
// The platform says which policy files it left out: all of them while the Policies feature is
|
|
170
|
+
// off, and those of a caller who may not author policies, whose release carries the live
|
|
171
|
+
// branch's policies instead.
|
|
172
|
+
const skipped = policiesSkippedNotice(release);
|
|
161
173
|
if (flags.output === 'json') {
|
|
162
174
|
this.log(JSON.stringify(release, null, 2));
|
|
163
175
|
}
|
|
@@ -171,8 +183,14 @@ Output release details as JSON
|
|
|
171
183
|
this.log(` Description: ${release.description}`);
|
|
172
184
|
const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
|
|
173
185
|
this.log(` Documents: ${documentEntries.length}`);
|
|
186
|
+
if (policyKeys.length > 0 && !skipped)
|
|
187
|
+
this.log(` Policy documents: ${policyKeys.length} (${policyKeys.join(', ')})`);
|
|
188
|
+
for (const line of releasePolicyRunLines(release.policy_run, release.name ?? flags.name))
|
|
189
|
+
this.log(line);
|
|
174
190
|
this.log(` Time: ${elapsed}s`);
|
|
175
191
|
}
|
|
192
|
+
if (skipped)
|
|
193
|
+
this.warn(skipped);
|
|
176
194
|
}
|
|
177
195
|
catch (error) {
|
|
178
196
|
if (error instanceof Error) {
|
|
@@ -5,6 +5,7 @@ import open from 'open';
|
|
|
5
5
|
import BaseCommand from '../../../base-command.js';
|
|
6
6
|
import { formatWorkspacePushDefaultsBanner, resolvePushDefaults } from '../../../utils/local-config.js';
|
|
7
7
|
import { executePush } from '../../../utils/multidoc-push.js';
|
|
8
|
+
import { policiesSkippedNotice } from '../../../utils/policy/feedback.js';
|
|
8
9
|
export default class SandboxPush extends BaseCommand {
|
|
9
10
|
static description = '[IMPORTANT] ALWAYS run --dry-run first and show the user the output before pushing. Push local documents to your sandbox environment via multidoc import. By default, only changed files are pushed (partial mode). Use --sync to push all files. Shows a preview of changes before pushing unless --force is specified. Use --dry-run to preview only. Include/exclude glob filters are intentionally not supported on sandbox push — partial pushes can hide deletions during review and lead to data loss when promoted to the workspace. Pushing into a sandbox that currently holds a different workspace will prompt for confirmation; run `xano sandbox reset` first to start clean.';
|
|
10
11
|
static examples = [
|
|
@@ -173,7 +174,7 @@ Push and open sandbox review in the browser
|
|
|
173
174
|
writeback: pushDefaults.writeback,
|
|
174
175
|
writebackDiff: flags['writeback-diff'],
|
|
175
176
|
};
|
|
176
|
-
await executePush({
|
|
177
|
+
const result = await executePush({
|
|
177
178
|
accessToken: profile.access_token,
|
|
178
179
|
branch: '',
|
|
179
180
|
command: this,
|
|
@@ -189,6 +190,10 @@ Push and open sandbox review in the browser
|
|
|
189
190
|
},
|
|
190
191
|
verboseFetch: this.verboseFetch.bind(this),
|
|
191
192
|
}, target, pushFlags);
|
|
193
|
+
// When the platform leaves policy files out, it says so once.
|
|
194
|
+
const skipped = policiesSkippedNotice(result.response ?? result.preview);
|
|
195
|
+
if (skipped)
|
|
196
|
+
this.warn(skipped);
|
|
192
197
|
if (flags.review) {
|
|
193
198
|
await this.openReview(profile.instance_origin, profile.access_token, flags.verbose);
|
|
194
199
|
}
|
|
@@ -0,0 +1,30 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
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
|
+
}
|
|
@@ -6,6 +6,8 @@ export default class TenantCreate extends BaseCommand {
|
|
|
6
6
|
static description: string;
|
|
7
7
|
static examples: string[];
|
|
8
8
|
static flags: {
|
|
9
|
+
allow_deploy_bypass: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
10
|
+
allow_quick_deploy: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
9
11
|
cluster_id: import("@oclif/core/interfaces").OptionFlag<number | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
10
12
|
description: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
11
13
|
domain: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
@@ -13,6 +15,7 @@ export default class TenantCreate extends BaseCommand {
|
|
|
13
15
|
license: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
14
16
|
output: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
15
17
|
platform_id: import("@oclif/core/interfaces").OptionFlag<number | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
18
|
+
required_reviewers: import("@oclif/core/interfaces").OptionFlag<number, import("@oclif/core/interfaces").CustomOptions>;
|
|
16
19
|
tasks: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
17
20
|
type: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
18
21
|
workspace: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
@@ -17,9 +17,20 @@ Created tenant: Production (production) - ID: 42
|
|
|
17
17
|
`,
|
|
18
18
|
`$ xano tenant create "Staging" --description "Staging env" --cluster_id 1 --platform_id 1 --type tier2 -o json`,
|
|
19
19
|
`$ xano tenant create "Staging" --type tier2 --cluster_id 1 --license ./license.yaml`,
|
|
20
|
+
`$ xano tenant create "Production" --required_reviewers 1 --allow_deploy_bypass`,
|
|
20
21
|
];
|
|
21
22
|
static flags = {
|
|
22
23
|
...BaseCommand.baseFlags,
|
|
24
|
+
allow_deploy_bypass: Flags.boolean({
|
|
25
|
+
default: false,
|
|
26
|
+
description: 'Allow authorized users to bypass the deploy approval gate on this tenant (via tenant_deploy_request bypass). Has no effect unless --required_reviewers is also set above 0.',
|
|
27
|
+
required: false,
|
|
28
|
+
}),
|
|
29
|
+
allow_quick_deploy: Flags.boolean({
|
|
30
|
+
default: false,
|
|
31
|
+
description: 'Allow this tenant to skip the deploy approval gate entirely for quick deploys, regardless of --required_reviewers.',
|
|
32
|
+
required: false,
|
|
33
|
+
}),
|
|
23
34
|
cluster_id: Flags.integer({
|
|
24
35
|
description: 'Cluster ID to deploy to (required for tier2/tier3)',
|
|
25
36
|
required: false,
|
|
@@ -54,6 +65,11 @@ Created tenant: Production (production) - ID: 42
|
|
|
54
65
|
description: 'Platform ID to use',
|
|
55
66
|
required: false,
|
|
56
67
|
}),
|
|
68
|
+
required_reviewers: Flags.integer({
|
|
69
|
+
default: 0,
|
|
70
|
+
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 (default) means not gated.',
|
|
71
|
+
required: false,
|
|
72
|
+
}),
|
|
57
73
|
tasks: Flags.boolean({
|
|
58
74
|
allowNo: true,
|
|
59
75
|
default: true,
|
|
@@ -79,6 +95,11 @@ Created tenant: Production (production) - ID: 42
|
|
|
79
95
|
this.error('No workspace ID provided. Use --workspace flag or set one in your profile.');
|
|
80
96
|
}
|
|
81
97
|
const body = {
|
|
98
|
+
deploy_settings: {
|
|
99
|
+
allow_deploy_bypass: flags.allow_deploy_bypass,
|
|
100
|
+
allow_quick_deploy: flags.allow_quick_deploy,
|
|
101
|
+
required_reviewers: flags.required_reviewers,
|
|
102
|
+
},
|
|
82
103
|
display: args.display,
|
|
83
104
|
ingress: flags.ingress,
|
|
84
105
|
license: flags.type,
|
|
@@ -6,12 +6,52 @@ export default class TenantDeployRelease extends BaseCommand {
|
|
|
6
6
|
static description: string;
|
|
7
7
|
static examples: string[];
|
|
8
8
|
static flags: {
|
|
9
|
+
check: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
9
10
|
output: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
11
|
+
'override-reason': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
10
12
|
release: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
11
13
|
workspace: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
12
14
|
config: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
13
15
|
profile: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
14
16
|
verbose: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
15
17
|
};
|
|
18
|
+
/** Set once the policy gate refuses the deploy, the one failure that exits 2. */
|
|
19
|
+
private refusedByPolicyGate;
|
|
20
|
+
/** Exit 2 is the policy gate's refusal; every other failure, flag errors included, exits 1. */
|
|
21
|
+
protected catch(error: Error & {
|
|
22
|
+
oclif?: {
|
|
23
|
+
exit?: number;
|
|
24
|
+
};
|
|
25
|
+
}): Promise<void>;
|
|
16
26
|
run(): Promise<void>;
|
|
27
|
+
/**
|
|
28
|
+
* `--check`: what the policy gate would decide for this release on this tenant, without deploying.
|
|
29
|
+
* Exits 2 when the deploy would be refused; a preview the platform cannot answer exits 1.
|
|
30
|
+
*/
|
|
31
|
+
private checkPolicyGate;
|
|
32
|
+
/**
|
|
33
|
+
* Does an approved tenant_deploy_request already exist for this (tenant, release)
|
|
34
|
+
* pair? The list endpoint has no release filter, so this fetches approved
|
|
35
|
+
* requests for the tenant and filters client-side by the resolved release name.
|
|
36
|
+
*/
|
|
37
|
+
private hasApprovedDeployRequest;
|
|
38
|
+
/** A deploy the policy gate refused: its verdict, then the refusal and how to proceed, exiting 2. */
|
|
39
|
+
private refuseByPolicyGate;
|
|
40
|
+
/**
|
|
41
|
+
* Does this tenant have the deploy approval gate enabled?
|
|
42
|
+
*
|
|
43
|
+
* Returns false if the tenant has `allow_quick_deploy` set — that flag lets a
|
|
44
|
+
* tenant skip the approval gate entirely for quick deploys, regardless of
|
|
45
|
+
* `required_reviewers`.
|
|
46
|
+
*
|
|
47
|
+
* Deliberately uses `tenant list` rather than `tenant get`: the single-tenant
|
|
48
|
+
* GET route's output whitelist (tenant-detail.yaml) omits `deploy_settings`
|
|
49
|
+
* (which holds `required_reviewers`, `allow_deploy_bypass`, and
|
|
50
|
+
* `allow_quick_deploy`) even though the list route's output includes it
|
|
51
|
+
* — confirmed live against a running instance. Filed as a backend bug; this
|
|
52
|
+
* is the workaround until it's fixed. See TENANT_DEPLOY_REQUEST_TEST.md.
|
|
53
|
+
*/
|
|
54
|
+
private tenantRequiresApproval;
|
|
55
|
+
/** A release policy the tenant's import could not land (its key is held by another policy) is never dropped silently. */
|
|
56
|
+
private warnPoliciesSkipped;
|
|
17
57
|
}
|
|
@@ -1,5 +1,7 @@
|
|
|
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';
|
|
3
5
|
export default class TenantDeployRelease extends BaseCommand {
|
|
4
6
|
static args = {
|
|
5
7
|
tenant_name: Args.string({
|
|
@@ -7,7 +9,7 @@ export default class TenantDeployRelease extends BaseCommand {
|
|
|
7
9
|
required: true,
|
|
8
10
|
}),
|
|
9
11
|
};
|
|
10
|
-
static description =
|
|
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.";
|
|
11
13
|
static examples = [
|
|
12
14
|
`$ xano tenant deploy_release t1234-abcd-xyz1 --release v1.0
|
|
13
15
|
Deployed release "v1.0" to tenant: My Tenant (my-tenant)
|
|
@@ -16,6 +18,12 @@ Deployed release "v1.0" to tenant: My Tenant (my-tenant)
|
|
|
16
18
|
];
|
|
17
19
|
static flags = {
|
|
18
20
|
...BaseCommand.baseFlags,
|
|
21
|
+
check: Flags.boolean({
|
|
22
|
+
default: false,
|
|
23
|
+
description: "Preview the release's policy gate on this tenant without deploying; exits 2 when the deploy would be refused",
|
|
24
|
+
exclusive: ['override-reason'],
|
|
25
|
+
hidden: true,
|
|
26
|
+
}),
|
|
19
27
|
output: Flags.string({
|
|
20
28
|
char: 'o',
|
|
21
29
|
default: 'summary',
|
|
@@ -23,6 +31,11 @@ Deployed release "v1.0" to tenant: My Tenant (my-tenant)
|
|
|
23
31
|
options: ['summary', 'json'],
|
|
24
32
|
required: false,
|
|
25
33
|
}),
|
|
34
|
+
'override-reason': Flags.string({
|
|
35
|
+
description: 'Deploy past a blocking policy gate, recording this reason (needs the workspace:policy update permission; audited)',
|
|
36
|
+
exclusive: ['check'],
|
|
37
|
+
hidden: true,
|
|
38
|
+
}),
|
|
26
39
|
release: Flags.string({
|
|
27
40
|
char: 'r',
|
|
28
41
|
description: 'Release name to deploy',
|
|
@@ -34,6 +47,14 @@ Deployed release "v1.0" to tenant: My Tenant (my-tenant)
|
|
|
34
47
|
required: false,
|
|
35
48
|
}),
|
|
36
49
|
};
|
|
50
|
+
/** Set once the policy gate refuses the deploy, the one failure that exits 2. */
|
|
51
|
+
refusedByPolicyGate = false;
|
|
52
|
+
/** Exit 2 is the policy gate's refusal; every other failure, flag errors included, exits 1. */
|
|
53
|
+
async catch(error) {
|
|
54
|
+
if (this.refusedByPolicyGate)
|
|
55
|
+
return super.catch(error);
|
|
56
|
+
return this.catchAsOperational(error);
|
|
57
|
+
}
|
|
37
58
|
async run() {
|
|
38
59
|
const { args, flags } = await this.parse(TenantDeployRelease);
|
|
39
60
|
const { profile } = this.resolveProfile(flags);
|
|
@@ -43,12 +64,46 @@ Deployed release "v1.0" to tenant: My Tenant (my-tenant)
|
|
|
43
64
|
}
|
|
44
65
|
const releaseName = flags.release;
|
|
45
66
|
const tenantName = args.tenant_name;
|
|
67
|
+
if (flags.check) {
|
|
68
|
+
await this.checkPolicyGate({ flags, profile, releaseName, tenantName, workspaceId });
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
const overrideReason = flags['override-reason']?.trim();
|
|
72
|
+
if (flags['override-reason'] !== undefined && !overrideReason) {
|
|
73
|
+
this.error('--override-reason needs a reason: say why this deploy may proceed past the policy gate.');
|
|
74
|
+
}
|
|
75
|
+
// Pre-flight: the "approval required" error must always be shown before any
|
|
76
|
+
// permission/RBAC error. The backend's own deploy route checks RBAC first
|
|
77
|
+
// and the approval gate second (inside mvp:tenant_deploy_release), so a
|
|
78
|
+
// caller who lacks deploy permission would otherwise never learn a request
|
|
79
|
+
// is what's actually needed. Checking here first, with an endpoint that
|
|
80
|
+
// only needs read scope, guarantees the right error is shown regardless of
|
|
81
|
+
// whether the caller can deploy at all.
|
|
82
|
+
const requiresApproval = await this.tenantRequiresApproval(profile, workspaceId, tenantName, flags.verbose);
|
|
83
|
+
if (requiresApproval) {
|
|
84
|
+
const hasApprovedRequest = await this.hasApprovedDeployRequest({
|
|
85
|
+
profile,
|
|
86
|
+
releaseName,
|
|
87
|
+
tenantName,
|
|
88
|
+
verbose: flags.verbose,
|
|
89
|
+
workspaceId,
|
|
90
|
+
});
|
|
91
|
+
if (!hasApprovedRequest) {
|
|
92
|
+
this.error(`Tenant "${tenantName}" requires an approved deploy request before "${releaseName}" can be deployed.\n` +
|
|
93
|
+
`Run: xano tenant_deploy_request create "Deploy ${releaseName} to ${tenantName}" --tenant ${tenantName} --release ${releaseName} --reviewers <id,id,...>`);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
46
96
|
const apiUrl = `${profile.instance_origin}/api:meta/workspace/${workspaceId}/tenant/${tenantName}/deploy`;
|
|
47
97
|
this.warn('This may take a few minutes. Please be patient.');
|
|
48
98
|
const startTime = Date.now();
|
|
99
|
+
// A refusal by the policy gate exits 2 with its verdict; the catch that rewords other failures keeps it.
|
|
100
|
+
let refused = null;
|
|
49
101
|
try {
|
|
50
102
|
const response = await this.verboseFetch(apiUrl, {
|
|
51
|
-
body: JSON.stringify({
|
|
103
|
+
body: JSON.stringify({
|
|
104
|
+
release_name: releaseName,
|
|
105
|
+
...(overrideReason ? { override_policy: true, override_reason: overrideReason } : {}),
|
|
106
|
+
}),
|
|
52
107
|
headers: {
|
|
53
108
|
accept: 'application/json',
|
|
54
109
|
Authorization: `Bearer ${profile.access_token}`,
|
|
@@ -57,8 +112,22 @@ Deployed release "v1.0" to tenant: My Tenant (my-tenant)
|
|
|
57
112
|
method: 'POST',
|
|
58
113
|
}, flags.verbose, profile.access_token);
|
|
59
114
|
if (!response.ok) {
|
|
60
|
-
|
|
61
|
-
|
|
115
|
+
refused = await gateRefusal(response);
|
|
116
|
+
if (refused) {
|
|
117
|
+
const override = `xano tenant deploy_release ${quoted(tenantName)} --release ${quoted(releaseName)} --override-reason "<why>"`;
|
|
118
|
+
this.refuseByPolicyGate(refused, flags, override);
|
|
119
|
+
}
|
|
120
|
+
let message = await this.parseApiError(response, 'API request failed');
|
|
121
|
+
// Only reached once the pre-flight above has already confirmed the
|
|
122
|
+
// approval gate is satisfied or not applicable — so a permission error
|
|
123
|
+
// here is genuinely about RBAC, not about a missing approval. If the
|
|
124
|
+
// tenant supports deploy requests, mention that path as an alternative
|
|
125
|
+
// to direct deploy, since opening a request doesn't require deploy
|
|
126
|
+
// permission.
|
|
127
|
+
if ((response.status === 401 || response.status === 403) && requiresApproval) {
|
|
128
|
+
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,...>`;
|
|
129
|
+
}
|
|
130
|
+
this.error(message);
|
|
62
131
|
}
|
|
63
132
|
const tenant = (await response.json());
|
|
64
133
|
if (flags.output === 'json') {
|
|
@@ -71,10 +140,15 @@ Deployed release "v1.0" to tenant: My Tenant (my-tenant)
|
|
|
71
140
|
this.log(` State: ${tenant.state}`);
|
|
72
141
|
if (tenant.release?.name)
|
|
73
142
|
this.log(` Release: ${tenant.release.name}`);
|
|
143
|
+
if (tenant.policy_gate?.status && tenant.policy_gate.status !== 'disabled')
|
|
144
|
+
this.log(` ${deployedGateLine(tenant.policy_gate)}`);
|
|
74
145
|
this.log(` Time: ${elapsed}s`);
|
|
75
146
|
}
|
|
147
|
+
this.warnPoliciesSkipped(tenant);
|
|
76
148
|
}
|
|
77
149
|
catch (error) {
|
|
150
|
+
if (refused)
|
|
151
|
+
throw error;
|
|
78
152
|
if (error instanceof Error) {
|
|
79
153
|
this.error(`Failed to deploy to tenant: ${error.message}`);
|
|
80
154
|
}
|
|
@@ -83,4 +157,108 @@ Deployed release "v1.0" to tenant: My Tenant (my-tenant)
|
|
|
83
157
|
}
|
|
84
158
|
}
|
|
85
159
|
}
|
|
160
|
+
/**
|
|
161
|
+
* `--check`: what the policy gate would decide for this release on this tenant, without deploying.
|
|
162
|
+
* Exits 2 when the deploy would be refused; a preview the platform cannot answer exits 1.
|
|
163
|
+
*/
|
|
164
|
+
async checkPolicyGate(opts) {
|
|
165
|
+
const { flags, profile, releaseName, tenantName, workspaceId } = opts;
|
|
166
|
+
const result = await fetchPolicyGate({ verboseFetch: (...args) => this.verboseFetch(...args) }, { profile, release: releaseName, tenant: tenantName, verbose: flags.verbose, workspace: workspaceId });
|
|
167
|
+
if ('unavailable' in result) {
|
|
168
|
+
const { message, payload, status } = result.unavailable;
|
|
169
|
+
this.error(`Policy gate preview failed (${status}): ${message}${policyPermissionGuidance(status, payload)}`);
|
|
170
|
+
}
|
|
171
|
+
const { answer } = result;
|
|
172
|
+
const exit = gateExitCode(answer);
|
|
173
|
+
if (flags.output === 'json') {
|
|
174
|
+
this.log(JSON.stringify(answer, null, 2));
|
|
175
|
+
}
|
|
176
|
+
else {
|
|
177
|
+
this.log(`Checked release "${releaseName}" for tenant "${tenantName}"; nothing was deployed.`);
|
|
178
|
+
for (const line of gateLines(answer))
|
|
179
|
+
this.log(line);
|
|
180
|
+
if (exit) {
|
|
181
|
+
this.log(answer.can_override
|
|
182
|
+
? `To deploy past the gate: xano tenant deploy_release ${quoted(tenantName)} --release ${quoted(releaseName)} --override-reason "<why>"`
|
|
183
|
+
: 'A deploy is refused unless someone with the workspace:policy update permission overrides the gate.');
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
if (exit)
|
|
187
|
+
process.exitCode = exit;
|
|
188
|
+
}
|
|
189
|
+
/**
|
|
190
|
+
* Does an approved tenant_deploy_request already exist for this (tenant, release)
|
|
191
|
+
* pair? The list endpoint has no release filter, so this fetches approved
|
|
192
|
+
* requests for the tenant and filters client-side by the resolved release name.
|
|
193
|
+
*/
|
|
194
|
+
async hasApprovedDeployRequest(opts) {
|
|
195
|
+
const { profile, releaseName, tenantName, verbose, workspaceId } = opts;
|
|
196
|
+
const params = new URLSearchParams({ per_page: '100', status: 'approved', tenant_name: tenantName });
|
|
197
|
+
const apiUrl = `${profile.instance_origin}/api:meta/workspace/${workspaceId}/approval_request?${params}`;
|
|
198
|
+
const response = await this.verboseFetch(apiUrl, {
|
|
199
|
+
headers: {
|
|
200
|
+
accept: 'application/json',
|
|
201
|
+
Authorization: `Bearer ${profile.access_token}`,
|
|
202
|
+
},
|
|
203
|
+
method: 'GET',
|
|
204
|
+
}, verbose, profile.access_token);
|
|
205
|
+
if (!response.ok) {
|
|
206
|
+
// A read-scope failure here means something is more broadly wrong (e.g.
|
|
207
|
+
// no workspace access at all) — surface it rather than silently treating
|
|
208
|
+
// it as "no approval found", which would misreport as approval-required.
|
|
209
|
+
const message = await this.parseApiError(response, 'Failed to check for an approved deploy request');
|
|
210
|
+
this.error(message);
|
|
211
|
+
}
|
|
212
|
+
const data = (await response.json());
|
|
213
|
+
const items = Array.isArray(data) ? data : (data.items ?? []);
|
|
214
|
+
return items.some((item) => item.status === 'approved' && item._release?.name === releaseName);
|
|
215
|
+
}
|
|
216
|
+
/** A deploy the policy gate refused: its verdict, then the refusal and how to proceed, exiting 2. */
|
|
217
|
+
refuseByPolicyGate(refused, flags, override) {
|
|
218
|
+
for (const line of gateRefusalOutput(refused, flags.output === 'json'))
|
|
219
|
+
this.log(line);
|
|
220
|
+
this.refusedByPolicyGate = true;
|
|
221
|
+
this.error(`${refused.message}${gateOverrideHint(refused.answer, override)}`, { exit: 2 });
|
|
222
|
+
}
|
|
223
|
+
/**
|
|
224
|
+
* Does this tenant have the deploy approval gate enabled?
|
|
225
|
+
*
|
|
226
|
+
* Returns false if the tenant has `allow_quick_deploy` set — that flag lets a
|
|
227
|
+
* tenant skip the approval gate entirely for quick deploys, regardless of
|
|
228
|
+
* `required_reviewers`.
|
|
229
|
+
*
|
|
230
|
+
* Deliberately uses `tenant list` rather than `tenant get`: the single-tenant
|
|
231
|
+
* GET route's output whitelist (tenant-detail.yaml) omits `deploy_settings`
|
|
232
|
+
* (which holds `required_reviewers`, `allow_deploy_bypass`, and
|
|
233
|
+
* `allow_quick_deploy`) even though the list route's output includes it
|
|
234
|
+
* — confirmed live against a running instance. Filed as a backend bug; this
|
|
235
|
+
* is the workaround until it's fixed. See TENANT_DEPLOY_REQUEST_TEST.md.
|
|
236
|
+
*/
|
|
237
|
+
async tenantRequiresApproval(profile, workspaceId, tenantName, verbose) {
|
|
238
|
+
const params = new URLSearchParams({ per_page: '100' });
|
|
239
|
+
const apiUrl = `${profile.instance_origin}/api:meta/workspace/${workspaceId}/tenant?${params}`;
|
|
240
|
+
const response = await this.verboseFetch(apiUrl, {
|
|
241
|
+
headers: {
|
|
242
|
+
accept: 'application/json',
|
|
243
|
+
Authorization: `Bearer ${profile.access_token}`,
|
|
244
|
+
},
|
|
245
|
+
method: 'GET',
|
|
246
|
+
}, verbose, profile.access_token);
|
|
247
|
+
if (!response.ok) {
|
|
248
|
+
const message = await this.parseApiError(response, 'Failed to look up tenant');
|
|
249
|
+
this.error(message);
|
|
250
|
+
}
|
|
251
|
+
const data = (await response.json());
|
|
252
|
+
const items = Array.isArray(data) ? data : (data.items ?? []);
|
|
253
|
+
const tenant = items.find((item) => item.name === tenantName);
|
|
254
|
+
if (tenant?.deploy_settings?.allow_quick_deploy)
|
|
255
|
+
return false;
|
|
256
|
+
return (tenant?.deploy_settings?.required_reviewers ?? 0) > 0;
|
|
257
|
+
}
|
|
258
|
+
/** A release policy the tenant's import could not land (its key is held by another policy) is never dropped silently. */
|
|
259
|
+
warnPoliciesSkipped(tenant) {
|
|
260
|
+
const message = tenant.policies_skipped?.message;
|
|
261
|
+
if (message)
|
|
262
|
+
this.warn(message);
|
|
263
|
+
}
|
|
86
264
|
}
|
|
@@ -6,6 +6,7 @@ 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>;
|
|
9
10
|
description: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
10
11
|
display: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
11
12
|
domain: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
@@ -13,6 +14,7 @@ export default class TenantEdit extends BaseCommand {
|
|
|
13
14
|
output: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
14
15
|
proxy: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
15
16
|
rbac: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
17
|
+
required_reviewers: import("@oclif/core/interfaces").OptionFlag<number | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
16
18
|
tasks: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
17
19
|
workspace: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
18
20
|
config: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|