@xano/cli 1.3.0 → 1.3.1-beta.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 +273 -1
- package/dist/base-command.d.ts +12 -1
- package/dist/base-command.js +36 -22
- package/dist/commands/branch/set_live/index.d.ts +22 -0
- package/dist/commands/branch/set_live/index.js +60 -1
- 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 +40 -0
- package/dist/commands/function/edit/index.d.ts +15 -0
- package/dist/commands/function/edit/index.js +40 -0
- package/dist/commands/function/run/index.d.ts +1 -0
- package/dist/commands/function/run/index.js +10 -2
- package/dist/commands/policy/catalogue/index.d.ts +15 -0
- package/dist/commands/policy/catalogue/index.js +29 -0
- package/dist/commands/policy/create/index.d.ts +18 -0
- package/dist/commands/policy/create/index.js +39 -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 +7 -0
- package/dist/commands/release/deploy/index.d.ts +28 -0
- package/dist/commands/release/deploy/index.js +90 -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 +54 -10
- 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 +186 -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 +116 -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 +124 -5
- package/dist/policy-command.d.ts +90 -0
- package/dist/policy-command.js +94 -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 +55 -2
- package/dist/utils/multidoc-push.js +103 -27
- package/dist/utils/policy/catalogue.d.ts +7 -0
- package/dist/utils/policy/catalogue.js +72 -0
- package/dist/utils/policy/errors.d.ts +12 -0
- package/dist/utils/policy/errors.js +61 -0
- package/dist/utils/policy/feedback.d.ts +68 -0
- package/dist/utils/policy/feedback.js +165 -0
- package/dist/utils/policy/findings.d.ts +23 -0
- package/dist/utils/policy/findings.js +65 -0
- package/dist/utils/policy/gate.d.ts +161 -0
- package/dist/utils/policy/gate.js +201 -0
- package/dist/utils/policy/goal.d.ts +46 -0
- package/dist/utils/policy/goal.js +95 -0
- package/dist/utils/policy/permission.d.ts +23 -0
- package/dist/utils/policy/permission.js +84 -0
- package/dist/utils/policy/push-gate.d.ts +5 -0
- package/dist/utils/policy/push-gate.js +29 -0
- package/dist/utils/policy/release.d.ts +85 -0
- package/dist/utils/policy/release.js +115 -0
- package/dist/utils/policy/request.d.ts +55 -0
- package/dist/utils/policy/request.js +115 -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 +47 -0
- package/dist/utils/policy/status.js +117 -0
- package/dist/utils/policy/types.d.ts +185 -0
- package/dist/utils/policy/types.js +1 -0
- package/oclif.manifest.json +5520 -2511
- package/package.json +7 -1
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Args, Flags } from '@oclif/core';
|
|
2
2
|
import BaseCommand from '../../../base-command.js';
|
|
3
|
+
import { releasePolicyRunLines } from '../../../utils/policy/release.js';
|
|
3
4
|
export default class ReleaseCreate extends BaseCommand {
|
|
4
5
|
static args = {
|
|
5
6
|
name: Args.string({
|
|
@@ -11,6 +12,10 @@ export default class ReleaseCreate extends BaseCommand {
|
|
|
11
12
|
static examples = [
|
|
12
13
|
`$ xano release create "v1.0" --branch main
|
|
13
14
|
Created release: v1.0 - ID: 10
|
|
15
|
+
Branch: main
|
|
16
|
+
Policy check: fail (run 1712), 2 blocking, 1 advisory findings
|
|
17
|
+
Findings: xano policy runs --release v1.0
|
|
18
|
+
Its blocking findings block a deploy to a standard or run tenant: xano tenant deploy_release <tenant> --release v1.0 --check
|
|
14
19
|
`,
|
|
15
20
|
`$ xano release create "v1.1-hotfix" --branch main --hotfix --description "Critical fix" -o json`,
|
|
16
21
|
];
|
|
@@ -90,6 +95,8 @@ Created release: v1.0 - ID: 10
|
|
|
90
95
|
this.log(` Branch: ${release.branch}`);
|
|
91
96
|
if (release.hotfix)
|
|
92
97
|
this.log(` Hotfix: true`);
|
|
98
|
+
for (const line of releasePolicyRunLines(release.policy_run, release.name ?? args.name))
|
|
99
|
+
this.log(line);
|
|
93
100
|
}
|
|
94
101
|
}
|
|
95
102
|
catch (error) {
|
|
@@ -9,12 +9,40 @@ export default class ReleaseDeploy extends BaseCommand {
|
|
|
9
9
|
branch: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
10
10
|
force: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
11
11
|
output: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
12
|
+
'policy-override': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
12
13
|
set_live: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
13
14
|
workspace: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
14
15
|
config: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
15
16
|
profile: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
16
17
|
verbose: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
17
18
|
};
|
|
19
|
+
/** Set once the platform refuses set live for a weakened mandatory policy: already explained, it exits 1 as it is. */
|
|
20
|
+
private refusedByPermission;
|
|
21
|
+
/** Set once the set-live policy gate refuses, the one failure that exits 2. */
|
|
22
|
+
private refusedByPolicyGate;
|
|
23
|
+
/** Exit 2 is the set-live gate's refusal; every other failure, flag errors included, exits 1. */
|
|
24
|
+
protected catch(error: Error & {
|
|
25
|
+
oclif?: {
|
|
26
|
+
exit?: number;
|
|
27
|
+
};
|
|
28
|
+
}): Promise<void>;
|
|
18
29
|
run(): Promise<void>;
|
|
19
30
|
private confirm;
|
|
31
|
+
/** The set-live verdict a successful deploy answered with --set_live (`pass`, `overridden`, ...). */
|
|
32
|
+
private logPolicyOutcome;
|
|
33
|
+
/** Stops the command when set live was refused after the branch was created: by the gate (exit 2) or for a weakened mandatory policy (exit 1). */
|
|
34
|
+
private refuseIfSetLiveRefused;
|
|
35
|
+
/**
|
|
36
|
+
* The set-live gate refused after the branch was created: the verdict, the refusal, and how to
|
|
37
|
+
* proceed, exiting 2. Under `-o json` the refusal is `{branch_created, set_live, branch, message,
|
|
38
|
+
* policy_gate}`.
|
|
39
|
+
*/
|
|
40
|
+
private refuseSetLive;
|
|
41
|
+
/**
|
|
42
|
+
* Set live was refused after the branch was created because the release weakens (or lacks) a
|
|
43
|
+
* mandatory policy of the live branch, and the credential lacks `workspace:policy` update. That is a
|
|
44
|
+
* permission refusal, not a blocking finding: it exits 1 (under `-o json` as `{error}`) with the
|
|
45
|
+
* platform's sentence, which names the branch, and how to go on.
|
|
46
|
+
*/
|
|
47
|
+
private refuseWeakening;
|
|
20
48
|
}
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { Args, Flags } from '@oclif/core';
|
|
2
2
|
import BaseCommand from '../../../base-command.js';
|
|
3
|
+
import { BLANK_POLICY_OVERRIDE, blankPolicyOverride, gateOverrideHint, gateRefusal, liveGateLines, quoted, weakeningRefusal, } from '../../../utils/policy/gate.js';
|
|
4
|
+
import { policyWeakeningGuidance } from '../../../utils/policy/permission.js';
|
|
3
5
|
export default class ReleaseDeploy extends BaseCommand {
|
|
4
6
|
static args = {
|
|
5
7
|
release_name: Args.string({
|
|
@@ -7,7 +9,7 @@ export default class ReleaseDeploy extends BaseCommand {
|
|
|
7
9
|
required: true,
|
|
8
10
|
}),
|
|
9
11
|
};
|
|
10
|
-
static description =
|
|
12
|
+
static description = "[IMPORTANT] ALWAYS confirm with the user before deploying a release. Deploys a release to its workspace as a new branch. With --set_live the branch is then set live through the set-live policy gate: when blocking findings refuse it, the branch stays, set live is refused, and the command exits 2 unless --policy-override gives a reason. A release that weakens a mandatory policy of the live branch needs workspace:policy update to be set live; without it the branch stays and the command exits 1.";
|
|
11
13
|
static examples = [
|
|
12
14
|
`$ xano release deploy "v1.0"
|
|
13
15
|
Are you sure you want to deploy release "v1.0"? (y/N) y
|
|
@@ -18,6 +20,7 @@ Deployed release "v1.0" to workspace 40 (branch: v1.0)
|
|
|
18
20
|
`,
|
|
19
21
|
`$ xano release deploy "v1.0" --branch "restore-v1" --no-set_live`,
|
|
20
22
|
`$ xano release deploy "v1.0" -w 40 -o json --force`,
|
|
23
|
+
`$ xano release deploy "v1.0" --branch "rollback-v1" --set_live --policy-override "Rollback approved; finding tracked in JIRA-12"`,
|
|
21
24
|
];
|
|
22
25
|
static flags = {
|
|
23
26
|
...BaseCommand.baseFlags,
|
|
@@ -39,6 +42,7 @@ Deployed release "v1.0" to workspace 40 (branch: v1.0)
|
|
|
39
42
|
options: ['summary', 'json'],
|
|
40
43
|
required: false,
|
|
41
44
|
}),
|
|
45
|
+
'policy-override': Flags.string({ description: 'With --set_live, set the branch live past a blocking set-live policy gate with an audited reason (requires workspace:policy update)' }),
|
|
42
46
|
set_live: Flags.boolean({
|
|
43
47
|
default: false,
|
|
44
48
|
description: '[CRITICAL] STOP and confirm with the user before setting the deployed branch as live.',
|
|
@@ -50,8 +54,20 @@ Deployed release "v1.0" to workspace 40 (branch: v1.0)
|
|
|
50
54
|
required: false,
|
|
51
55
|
}),
|
|
52
56
|
};
|
|
57
|
+
/** Set once the platform refuses set live for a weakened mandatory policy: already explained, it exits 1 as it is. */
|
|
58
|
+
refusedByPermission = false;
|
|
59
|
+
/** Set once the set-live policy gate refuses, the one failure that exits 2. */
|
|
60
|
+
refusedByPolicyGate = false;
|
|
61
|
+
/** Exit 2 is the set-live gate's refusal; every other failure, flag errors included, exits 1. */
|
|
62
|
+
async catch(error) {
|
|
63
|
+
if (this.refusedByPolicyGate)
|
|
64
|
+
return super.catch(error);
|
|
65
|
+
return this.catchAsOperational(error);
|
|
66
|
+
}
|
|
53
67
|
async run() {
|
|
54
68
|
const { args, flags } = await this.parse(ReleaseDeploy);
|
|
69
|
+
if (blankPolicyOverride(flags['policy-override']))
|
|
70
|
+
this.error(BLANK_POLICY_OVERRIDE);
|
|
55
71
|
const { profile } = this.resolveProfile(flags);
|
|
56
72
|
const workspaceId = flags.workspace || profile.workspace;
|
|
57
73
|
if (!workspaceId) {
|
|
@@ -59,11 +75,10 @@ Deployed release "v1.0" to workspace 40 (branch: v1.0)
|
|
|
59
75
|
}
|
|
60
76
|
const releaseName = encodeURIComponent(args.release_name);
|
|
61
77
|
const apiUrl = `${profile.instance_origin}/api:meta/workspace/${workspaceId}/release/${releaseName}/deploy`;
|
|
62
|
-
const body =
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
body.branch = flags.branch;
|
|
78
|
+
const body = deployBody(flags);
|
|
79
|
+
if (body.override_reason && !flags.set_live) {
|
|
80
|
+
this.error('--policy-override only applies with --set_live: a branch that is not set live is not gated.');
|
|
81
|
+
}
|
|
67
82
|
if (!flags.force) {
|
|
68
83
|
const confirmed = await this.confirm(`Are you sure you want to deploy release "${args.release_name}" to workspace ${workspaceId}?`);
|
|
69
84
|
if (!confirmed) {
|
|
@@ -84,6 +99,7 @@ Deployed release "v1.0" to workspace 40 (branch: v1.0)
|
|
|
84
99
|
method: 'POST',
|
|
85
100
|
}, flags.verbose, profile.access_token);
|
|
86
101
|
if (!response.ok) {
|
|
102
|
+
await this.refuseIfSetLiveRefused(response, flags, args.release_name);
|
|
87
103
|
const errorText = await response.text();
|
|
88
104
|
this.error(`API request failed with status ${response.status}: ${response.statusText}\n${errorText}`);
|
|
89
105
|
}
|
|
@@ -98,10 +114,17 @@ Deployed release "v1.0" to workspace 40 (branch: v1.0)
|
|
|
98
114
|
this.log(`Deployed release "${release.name}" to workspace ${workspaceId} (branch: ${branchLabel}${liveStatus})`);
|
|
99
115
|
if (release.description)
|
|
100
116
|
this.log(` Description: ${release.description}`);
|
|
117
|
+
this.logPolicyOutcome(release);
|
|
101
118
|
this.log(` Time: ${elapsed}s`);
|
|
102
119
|
}
|
|
120
|
+
// A policy of the release the new branch could not take is never dropped silently.
|
|
121
|
+
const skipped = release.policies_skipped?.message;
|
|
122
|
+
if (skipped)
|
|
123
|
+
this.warn(skipped);
|
|
103
124
|
}
|
|
104
125
|
catch (error) {
|
|
126
|
+
if (this.refusedByPolicyGate || this.refusedByPermission)
|
|
127
|
+
throw error;
|
|
105
128
|
if (error instanceof Error) {
|
|
106
129
|
this.error(`Failed to deploy release: ${error.message}`);
|
|
107
130
|
}
|
|
@@ -123,4 +146,65 @@ Deployed release "v1.0" to workspace 40 (branch: v1.0)
|
|
|
123
146
|
});
|
|
124
147
|
});
|
|
125
148
|
}
|
|
149
|
+
/** The set-live verdict a successful deploy answered with --set_live (`pass`, `overridden`, ...). */
|
|
150
|
+
logPolicyOutcome(release) {
|
|
151
|
+
const status = release.policy_gate?.status;
|
|
152
|
+
if (status)
|
|
153
|
+
this.log(` Policy gate: ${status}`);
|
|
154
|
+
}
|
|
155
|
+
/** Stops the command when set live was refused after the branch was created: by the gate (exit 2) or for a weakened mandatory policy (exit 1). */
|
|
156
|
+
async refuseIfSetLiveRefused(response, flags, releaseName) {
|
|
157
|
+
const refused = await gateRefusal(response);
|
|
158
|
+
if (refused)
|
|
159
|
+
this.refuseSetLive(refused, flags, releaseName);
|
|
160
|
+
const weakening = await weakeningRefusal(response);
|
|
161
|
+
if (weakening)
|
|
162
|
+
this.refuseWeakening(weakening, flags);
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* The set-live gate refused after the branch was created: the verdict, the refusal, and how to
|
|
166
|
+
* proceed, exiting 2. Under `-o json` the refusal is `{branch_created, set_live, branch, message,
|
|
167
|
+
* policy_gate}`.
|
|
168
|
+
*/
|
|
169
|
+
refuseSetLive(refused, flags, releaseName) {
|
|
170
|
+
const answer = refused.answer;
|
|
171
|
+
const label = answer.branch?.label || flags.branch || 'the release branch';
|
|
172
|
+
if (flags.output === 'json') {
|
|
173
|
+
this.log(JSON.stringify({ branch: answer.branch ?? null, branch_created: true, message: refused.message, policy_gate: answer, set_live: false }, null, 2));
|
|
174
|
+
}
|
|
175
|
+
else {
|
|
176
|
+
for (const line of liveGateLines(answer, 'release'))
|
|
177
|
+
this.log(line);
|
|
178
|
+
}
|
|
179
|
+
const workspace = flags.workspace ? ` -w ${quoted(flags.workspace)}` : '';
|
|
180
|
+
const rerun = `xano branch delete ${quoted(label)}${workspace} && xano release deploy ${quoted(releaseName)}${workspace}${flags.branch ? ` --branch ${quoted(flags.branch)}` : ''} --set_live --policy-override "<why>"`;
|
|
181
|
+
const hint = gateOverrideHint(answer, rerun);
|
|
182
|
+
this.refusedByPolicyGate = true;
|
|
183
|
+
this.error(`The branch was created; set live was refused. ${refused.message}${hint}${answer.can_override && !answer.override_denied ? `\nOr set "${label}" live from Studio's Branches panel, which asks for the reason.` : ''}`, { exit: 2 });
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* Set live was refused after the branch was created because the release weakens (or lacks) a
|
|
187
|
+
* mandatory policy of the live branch, and the credential lacks `workspace:policy` update. That is a
|
|
188
|
+
* permission refusal, not a blocking finding: it exits 1 (under `-o json` as `{error}`) with the
|
|
189
|
+
* platform's sentence, which names the branch, and how to go on.
|
|
190
|
+
*/
|
|
191
|
+
refuseWeakening(refused, flags) {
|
|
192
|
+
const label = refused.payload.branch?.label || flags.branch;
|
|
193
|
+
const workspace = flags.workspace ? ` -w ${quoted(flags.workspace)}` : '';
|
|
194
|
+
const ways = label
|
|
195
|
+
? [`The branch stays. To remove it: xano branch delete ${quoted(label)}${workspace}`, `Someone with that permission can set it live: xano branch set_live ${quoted(label)}${workspace}`]
|
|
196
|
+
: [];
|
|
197
|
+
this.refusedByPermission = true;
|
|
198
|
+
this.error(`${refused.message}${policyWeakeningGuidance(refused.payload, ways, true)}`);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
/** The deploy request's body: the new branch's label, set_live, and the override reason when one is given. */
|
|
202
|
+
function deployBody(flags) {
|
|
203
|
+
const body = { set_live: flags.set_live };
|
|
204
|
+
if (flags.branch)
|
|
205
|
+
body.branch = flags.branch;
|
|
206
|
+
const overrideReason = flags['policy-override']?.trim();
|
|
207
|
+
if (overrideReason)
|
|
208
|
+
body.override_reason = overrideReason;
|
|
209
|
+
return body;
|
|
126
210
|
}
|
|
@@ -2,6 +2,7 @@ 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 { releaseImportRefusal, releasePolicyRunLines } from '../../../utils/policy/release.js';
|
|
5
6
|
export default class ReleaseImport extends BaseCommand {
|
|
6
7
|
static args = {};
|
|
7
8
|
static description = '[IMPORTANT] ALWAYS confirm with the user before importing a release. Imports a release file into a workspace.';
|
|
@@ -58,7 +59,8 @@ Imported release as #15
|
|
|
58
59
|
}, flags.verbose, profile.access_token);
|
|
59
60
|
if (!response.ok) {
|
|
60
61
|
const errorText = await response.text();
|
|
61
|
-
this.error(
|
|
62
|
+
this.error(releaseImportRefusal(errorText, response.status, apiUrl) ??
|
|
63
|
+
`API request failed with status ${response.status}: ${response.statusText}\n${errorText}`);
|
|
62
64
|
}
|
|
63
65
|
const result = await response.json();
|
|
64
66
|
if (flags.output === 'json') {
|
|
@@ -67,6 +69,8 @@ Imported release as #15
|
|
|
67
69
|
else {
|
|
68
70
|
const sizeMb = (fileBuffer.length / 1024 / 1024).toFixed(2);
|
|
69
71
|
this.log(`Imported release as #${result.id} (${sizeMb} MB)`);
|
|
72
|
+
for (const line of releasePolicyRunLines(result.policy_run, result.name, result.id))
|
|
73
|
+
this.log(line);
|
|
70
74
|
}
|
|
71
75
|
}
|
|
72
76
|
catch (error) {
|
|
@@ -10,4 +10,11 @@ export default class ReleaseList extends BaseCommand {
|
|
|
10
10
|
verbose: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
11
11
|
};
|
|
12
12
|
run(): Promise<void>;
|
|
13
|
+
/**
|
|
14
|
+
* The releases' stored policy checks, read once for the whole list. The list never fails or
|
|
15
|
+
* changes because of this read: it answers `null` when the checks cannot be read (the Policies
|
|
16
|
+
* feature is off, the credential lacks `workspace:policy` read, or the request failed), and the
|
|
17
|
+
* list then prints as it would without policies.
|
|
18
|
+
*/
|
|
19
|
+
private policyChecks;
|
|
13
20
|
}
|
|
@@ -1,13 +1,21 @@
|
|
|
1
1
|
import { Flags } from '@oclif/core';
|
|
2
2
|
import BaseCommand from '../../../base-command.js';
|
|
3
3
|
import { buildPagingJson } from '../../../utils/paging.js';
|
|
4
|
+
import { readReleasePolicyChecks, releasePolicyCheckTag } from '../../../utils/policy/release.js';
|
|
4
5
|
export default class ReleaseList extends BaseCommand {
|
|
5
|
-
static description =
|
|
6
|
+
static description = "List all releases in a workspace, each with its stored policy check when the credential can read policies (the release's own check, as Studio's Policies column shows it)";
|
|
6
7
|
static examples = [
|
|
7
8
|
`$ xano release list
|
|
8
9
|
Releases in workspace 5:
|
|
9
10
|
- v1.0 (ID: 10) - main
|
|
10
11
|
- v1.1-hotfix (ID: 11) - main [hotfix]
|
|
12
|
+
`,
|
|
13
|
+
`$ xano release list
|
|
14
|
+
Releases in workspace 5:
|
|
15
|
+
- v1.2 (ID: 12) - main (9/28/2026, 4:12:03 PM PDT) [policies: 2 blocking, 1 advisory]
|
|
16
|
+
- v1.1-hotfix (ID: 11) - main [hotfix] (9/27/2026, 9:40:15 AM PDT) [policies: passed]
|
|
17
|
+
- v1.0 (ID: 10) - main (9/20/2026, 1:05:44 PM PDT) [policies: not checked]
|
|
18
|
+
Policy check details: xano policy runs --release <name>
|
|
11
19
|
`,
|
|
12
20
|
`$ xano release list -w 5 --output json`,
|
|
13
21
|
];
|
|
@@ -57,22 +65,19 @@ Releases in workspace 5:
|
|
|
57
65
|
else {
|
|
58
66
|
this.error('Unexpected API response format');
|
|
59
67
|
}
|
|
68
|
+
const checks = await this.policyChecks(profile, String(workspaceId), releases, flags.verbose);
|
|
60
69
|
if (flags.output === 'json') {
|
|
61
|
-
this.log(JSON.stringify(buildPagingJson({ items: releases }, { tier: 'none' }), null, 2));
|
|
70
|
+
this.log(JSON.stringify(buildPagingJson({ items: withPolicyChecks(releases, checks) }, { tier: 'none' }), null, 2));
|
|
62
71
|
}
|
|
63
72
|
else if (releases.length === 0) {
|
|
64
73
|
this.log('No releases found');
|
|
65
74
|
}
|
|
66
75
|
else {
|
|
67
76
|
this.log(`Releases in workspace ${workspaceId}:`);
|
|
68
|
-
for (const release of releases)
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
? ` (${new Date(release.created_at).toLocaleString(undefined, { timeZoneName: 'short' })})`
|
|
73
|
-
: '';
|
|
74
|
-
this.log(` - ${release.name} (ID: ${release.id})${branch}${hotfix}${createdAt}`);
|
|
75
|
-
}
|
|
77
|
+
for (const release of releases)
|
|
78
|
+
this.log(releaseLine(release, checks));
|
|
79
|
+
if (checks)
|
|
80
|
+
this.log('Policy check details: xano policy runs --release <name>');
|
|
76
81
|
}
|
|
77
82
|
}
|
|
78
83
|
catch (error) {
|
|
@@ -84,4 +89,43 @@ Releases in workspace 5:
|
|
|
84
89
|
}
|
|
85
90
|
}
|
|
86
91
|
}
|
|
92
|
+
/**
|
|
93
|
+
* The releases' stored policy checks, read once for the whole list. The list never fails or
|
|
94
|
+
* changes because of this read: it answers `null` when the checks cannot be read (the Policies
|
|
95
|
+
* feature is off, the credential lacks `workspace:policy` read, or the request failed), and the
|
|
96
|
+
* list then prints as it would without policies.
|
|
97
|
+
*/
|
|
98
|
+
async policyChecks(profile, workspace, releases, verbose) {
|
|
99
|
+
if (releases.length === 0)
|
|
100
|
+
return null;
|
|
101
|
+
return readReleasePolicyChecks({ logToStderr: (message) => this.logToStderr(message), verboseFetch: (...args) => this.verboseFetch(...args) }, { branch: '', profile, verbose, workspace }, releases.map(release => release.id));
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
/** A release's summary line, tagged with its stored check when the checks were read. */
|
|
105
|
+
function releaseLine(release, checks) {
|
|
106
|
+
const branch = release.branch ? ` - ${release.branch}` : '';
|
|
107
|
+
const hotfix = release.hotfix ? ' [hotfix]' : '';
|
|
108
|
+
const createdAt = release.created_at
|
|
109
|
+
? ` (${new Date(release.created_at).toLocaleString(undefined, { timeZoneName: 'short' })})`
|
|
110
|
+
: '';
|
|
111
|
+
const policies = checks ? ` ${releasePolicyCheckTag(checks.get(release.id))}` : '';
|
|
112
|
+
return ` - ${release.name} (ID: ${release.id})${branch}${hotfix}${createdAt}${policies}`;
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* The releases for `-o json`: each gains `policy_check` (its stored check as the platform lists
|
|
116
|
+
* it, without the release id it is keyed by, or `null` for a release with no stored check) when
|
|
117
|
+
* the checks were read, and stays as served when they could not be, so a reader can tell an
|
|
118
|
+
* unchecked release (`null`) from checks the CLI could not read (absent).
|
|
119
|
+
*/
|
|
120
|
+
function withPolicyChecks(releases, checks) {
|
|
121
|
+
if (!checks)
|
|
122
|
+
return releases;
|
|
123
|
+
return releases.map(release => {
|
|
124
|
+
const check = checks.get(release.id);
|
|
125
|
+
if (!check)
|
|
126
|
+
return { ...release, policy_check: null };
|
|
127
|
+
const stored = { ...check };
|
|
128
|
+
delete stored.release_id;
|
|
129
|
+
return { ...release, policy_check: stored };
|
|
130
|
+
});
|
|
87
131
|
}
|
|
@@ -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) {
|
|
@@ -4,6 +4,7 @@ import { resolve } from 'node:path';
|
|
|
4
4
|
import open from 'open';
|
|
5
5
|
import BaseCommand from '../../../base-command.js';
|
|
6
6
|
import { executePush } from '../../../utils/multidoc-push.js';
|
|
7
|
+
import { policiesSkippedNotice } from '../../../utils/policy/feedback.js';
|
|
7
8
|
export default class SandboxPush extends BaseCommand {
|
|
8
9
|
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.';
|
|
9
10
|
static examples = [
|
|
@@ -126,7 +127,7 @@ Push and open sandbox review in the browser
|
|
|
126
127
|
truncate: flags.truncate,
|
|
127
128
|
verbose: flags.verbose,
|
|
128
129
|
};
|
|
129
|
-
await executePush({
|
|
130
|
+
const result = await executePush({
|
|
130
131
|
accessToken: profile.access_token,
|
|
131
132
|
branch: '',
|
|
132
133
|
command: this,
|
|
@@ -137,6 +138,10 @@ Push and open sandbox review in the browser
|
|
|
137
138
|
},
|
|
138
139
|
verboseFetch: this.verboseFetch.bind(this),
|
|
139
140
|
}, target, pushFlags);
|
|
141
|
+
// When the platform leaves policy files out, it says so once.
|
|
142
|
+
const skipped = policiesSkippedNotice(result.response ?? result.preview);
|
|
143
|
+
if (skipped)
|
|
144
|
+
this.warn(skipped);
|
|
140
145
|
if (flags.review) {
|
|
141
146
|
await this.openReview(profile.instance_origin, profile.access_token, flags.verbose);
|
|
142
147
|
}
|
|
@@ -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>;
|