@xano/cli 1.3.3 → 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 +137 -3
- 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/delete/index.js +8 -0
- package/dist/commands/sandbox/pull/index.d.ts +7 -0
- package/dist/commands/sandbox/pull/index.js +49 -0
- package/dist/commands/sandbox/push/index.d.ts +4 -0
- package/dist/commands/sandbox/push/index.js +59 -2
- package/dist/commands/sandbox/reset/index.js +8 -0
- 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 +17 -0
- package/dist/commands/workspace/pull/index.js +124 -7
- package/dist/commands/workspace/push/index.d.ts +23 -0
- package/dist/commands/workspace/push/index.js +188 -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/export-compare.d.ts +52 -0
- package/dist/utils/export-compare.js +134 -0
- package/dist/utils/local-config.d.ts +23 -0
- package/dist/utils/local-config.js +62 -2
- package/dist/utils/multidoc-push.d.ts +124 -4
- package/dist/utils/multidoc-push.js +670 -49
- package/dist/utils/object-fetch.d.ts +86 -0
- package/dist/utils/object-fetch.js +265 -0
- 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/dist/utils/reference-checker.d.ts +3 -0
- package/dist/utils/reference-checker.js +3 -2
- package/dist/utils/sandbox-marker.d.ts +9 -0
- package/dist/utils/sandbox-marker.js +119 -0
- package/dist/utils/sync-state.d.ts +71 -0
- package/dist/utils/sync-state.js +353 -0
- package/oclif.manifest.json +6472 -3181
- package/package.json +9 -1
|
@@ -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,6 +1,7 @@
|
|
|
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
6
|
static description = 'List all releases in a workspace';
|
|
6
7
|
static examples = [
|
|
@@ -57,22 +58,19 @@ Releases in workspace 5:
|
|
|
57
58
|
else {
|
|
58
59
|
this.error('Unexpected API response format');
|
|
59
60
|
}
|
|
61
|
+
const checks = await this.policyChecks(profile, String(workspaceId), releases, flags.verbose);
|
|
60
62
|
if (flags.output === 'json') {
|
|
61
|
-
this.log(JSON.stringify(buildPagingJson({ items: releases }, { tier: 'none' }), null, 2));
|
|
63
|
+
this.log(JSON.stringify(buildPagingJson({ items: withPolicyChecks(releases, checks) }, { tier: 'none' }), null, 2));
|
|
62
64
|
}
|
|
63
65
|
else if (releases.length === 0) {
|
|
64
66
|
this.log('No releases found');
|
|
65
67
|
}
|
|
66
68
|
else {
|
|
67
69
|
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
|
-
}
|
|
70
|
+
for (const release of releases)
|
|
71
|
+
this.log(releaseLine(release, checks));
|
|
72
|
+
if (checks)
|
|
73
|
+
this.log('Policy check details: xano policy runs --release <name>');
|
|
76
74
|
}
|
|
77
75
|
}
|
|
78
76
|
catch (error) {
|
|
@@ -84,4 +82,43 @@ Releases in workspace 5:
|
|
|
84
82
|
}
|
|
85
83
|
}
|
|
86
84
|
}
|
|
85
|
+
/**
|
|
86
|
+
* The releases' stored policy checks, read once for the whole list. The list never fails or
|
|
87
|
+
* changes because of this read: it answers `null` when the checks cannot be read (the Policies
|
|
88
|
+
* feature is off, the credential lacks `workspace:policy` read, or the request failed), and the
|
|
89
|
+
* list then prints as it would without policies.
|
|
90
|
+
*/
|
|
91
|
+
async policyChecks(profile, workspace, releases, verbose) {
|
|
92
|
+
if (releases.length === 0)
|
|
93
|
+
return null;
|
|
94
|
+
return readReleasePolicyChecks({ logToStderr: (message) => this.logToStderr(message), verboseFetch: (...args) => this.verboseFetch(...args) }, { branch: '', profile, verbose, workspace }, releases.map(release => release.id));
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
/** A release's summary line, tagged with its stored check when the checks were read. */
|
|
98
|
+
function releaseLine(release, checks) {
|
|
99
|
+
const branch = release.branch ? ` - ${release.branch}` : '';
|
|
100
|
+
const hotfix = release.hotfix ? ' [hotfix]' : '';
|
|
101
|
+
const createdAt = release.created_at
|
|
102
|
+
? ` (${new Date(release.created_at).toLocaleString(undefined, { timeZoneName: 'short' })})`
|
|
103
|
+
: '';
|
|
104
|
+
const policies = checks ? ` ${releasePolicyCheckTag(checks.get(release.id))}` : '';
|
|
105
|
+
return ` - ${release.name} (ID: ${release.id})${branch}${hotfix}${createdAt}${policies}`;
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* The releases for `-o json`: each gains `policy_check` (its stored check as the platform lists
|
|
109
|
+
* it, without the release id it is keyed by, or `null` for a release with no stored check) when
|
|
110
|
+
* the checks were read, and stays as served when they could not be, so a reader can tell an
|
|
111
|
+
* unchecked release (`null`) from checks the CLI could not read (absent).
|
|
112
|
+
*/
|
|
113
|
+
function withPolicyChecks(releases, checks) {
|
|
114
|
+
if (!checks)
|
|
115
|
+
return releases;
|
|
116
|
+
return releases.map(release => {
|
|
117
|
+
const check = checks.get(release.id);
|
|
118
|
+
if (!check)
|
|
119
|
+
return { ...release, policy_check: null };
|
|
120
|
+
const stored = { ...check };
|
|
121
|
+
delete stored.release_id;
|
|
122
|
+
return { ...release, policy_check: stored };
|
|
123
|
+
});
|
|
87
124
|
}
|
|
@@ -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) {
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { Flags } from '@oclif/core';
|
|
2
|
+
import { dirname } from 'node:path';
|
|
2
3
|
import BaseCommand from '../../../base-command.js';
|
|
4
|
+
import { invalidateSandboxMarkerForOrigin } from '../../../utils/sandbox-marker.js';
|
|
3
5
|
export default class SandboxDelete extends BaseCommand {
|
|
4
6
|
static description = '[CRITICAL] NEVER run without explicit user confirmation; this destroys all sandbox data. Deletes your sandbox environment completely (debugging only — it will be re-created on next access).';
|
|
5
7
|
static examples = [
|
|
@@ -43,6 +45,12 @@ Sandbox environment deleted.
|
|
|
43
45
|
this.error(message);
|
|
44
46
|
}
|
|
45
47
|
this.log('Sandbox environment deleted.');
|
|
48
|
+
try {
|
|
49
|
+
invalidateSandboxMarkerForOrigin(dirname(this.getCredentialsPath()), profile.instance_origin);
|
|
50
|
+
}
|
|
51
|
+
catch (error) {
|
|
52
|
+
this.warn(`Skipped invalidating sandbox sync marker: ${error.message}`);
|
|
53
|
+
}
|
|
46
54
|
}
|
|
47
55
|
catch (error) {
|
|
48
56
|
if (error instanceof Error && 'oclif' in error)
|
|
@@ -1,4 +1,10 @@
|
|
|
1
1
|
import BaseCommand from '../../../base-command.js';
|
|
2
|
+
/**
|
|
3
|
+
* Whether pull should write `.xano/sync_state.json`: only when asked to, or to keep an existing
|
|
4
|
+
* record current. A `--draft` pull never writes it, because `--fast` compares against published
|
|
5
|
+
* versions and recording draft bodies as synced would stop those drafts from being pushed.
|
|
6
|
+
*/
|
|
7
|
+
export declare function syncStateAction(flag: boolean, dir: string, draft: boolean): 'draft' | 'skip' | 'write';
|
|
2
8
|
export default class SandboxPull extends BaseCommand {
|
|
3
9
|
static description: string;
|
|
4
10
|
static examples: string[];
|
|
@@ -7,6 +13,7 @@ export default class SandboxPull extends BaseCommand {
|
|
|
7
13
|
draft: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
8
14
|
env: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
9
15
|
records: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
16
|
+
'sync-state': import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
10
17
|
config: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
11
18
|
profile: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
12
19
|
verbose: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
@@ -5,6 +5,18 @@ import path from 'node:path';
|
|
|
5
5
|
import BaseCommand from '../../../base-command.js';
|
|
6
6
|
import { buildApiGroupFolderResolver, buildChannelServerResolver, parseDocument, resolveDocumentPath, } from '../../../utils/document-parser.js';
|
|
7
7
|
import { fetchKnowledge, writeKnowledge } from '../../../utils/knowledge-sync.js';
|
|
8
|
+
import { writeMarkerToken } from '../../../utils/sandbox-marker.js';
|
|
9
|
+
import { instanceHost, recordEntriesForPaths, sandboxTargetIdentity, saveSyncState, syncStateExists, } from '../../../utils/sync-state.js';
|
|
10
|
+
/**
|
|
11
|
+
* Whether pull should write `.xano/sync_state.json`: only when asked to, or to keep an existing
|
|
12
|
+
* record current. A `--draft` pull never writes it, because `--fast` compares against published
|
|
13
|
+
* versions and recording draft bodies as synced would stop those drafts from being pushed.
|
|
14
|
+
*/
|
|
15
|
+
export function syncStateAction(flag, dir, draft) {
|
|
16
|
+
if (!flag && !syncStateExists(dir))
|
|
17
|
+
return 'skip';
|
|
18
|
+
return draft ? 'draft' : 'write';
|
|
19
|
+
}
|
|
8
20
|
export default class SandboxPull extends BaseCommand {
|
|
9
21
|
static description = 'Pull documents from your sandbox environment and split into individual files';
|
|
10
22
|
static examples = [
|
|
@@ -15,6 +27,9 @@ Pulled 42 documents from sandbox environment to current directory
|
|
|
15
27
|
Pulled 42 documents from sandbox environment to ./my-sandbox
|
|
16
28
|
`,
|
|
17
29
|
`$ xano sandbox pull --env --records`,
|
|
30
|
+
`$ xano sandbox pull --sync-state
|
|
31
|
+
Write .xano/sync_state.json after pulling; later pulls keep it current while it exists
|
|
32
|
+
`,
|
|
18
33
|
];
|
|
19
34
|
static flags = {
|
|
20
35
|
...BaseCommand.baseFlags,
|
|
@@ -39,6 +54,11 @@ Pulled 42 documents from sandbox environment to ./my-sandbox
|
|
|
39
54
|
description: 'Include records',
|
|
40
55
|
required: false,
|
|
41
56
|
}),
|
|
57
|
+
'sync-state': Flags.boolean({
|
|
58
|
+
default: false,
|
|
59
|
+
description: 'Write .xano/sync_state.json after a successful pull. Needed only the first time; later pulls keep the record current while it exists.',
|
|
60
|
+
required: false,
|
|
61
|
+
}),
|
|
42
62
|
};
|
|
43
63
|
async run() {
|
|
44
64
|
const { flags } = await this.parse(SandboxPull);
|
|
@@ -97,6 +117,7 @@ Pulled 42 documents from sandbox environment to ./my-sandbox
|
|
|
97
117
|
const getChannelServer = buildChannelServerResolver(documents);
|
|
98
118
|
const filenameCounters = new Map();
|
|
99
119
|
let writtenCount = 0;
|
|
120
|
+
const writtenPaths = [];
|
|
100
121
|
for (const doc of documents) {
|
|
101
122
|
const { baseName, typeDir } = resolveDocumentPath(doc, outputDir, {
|
|
102
123
|
getApiGroupFolder,
|
|
@@ -115,6 +136,7 @@ Pulled 42 documents from sandbox environment to ./my-sandbox
|
|
|
115
136
|
const filename = count === 0 ? `${baseName}.xs` : `${baseName}_${count + 1}.xs`;
|
|
116
137
|
const filePath = path.join(typeDir, filename);
|
|
117
138
|
fs.writeFileSync(filePath, doc.content, 'utf8');
|
|
139
|
+
writtenPaths.push(filePath);
|
|
118
140
|
writtenCount++;
|
|
119
141
|
}
|
|
120
142
|
// ── Pull knowledge ────────────────────────────────────────────────────
|
|
@@ -128,5 +150,32 @@ Pulled 42 documents from sandbox environment to ./my-sandbox
|
|
|
128
150
|
if (knowledgeCount > 0)
|
|
129
151
|
parts.push(`${knowledgeCount} knowledge file${knowledgeCount === 1 ? '' : 's'}`);
|
|
130
152
|
this.log(`Pulled ${parts.join(' + ')} from sandbox environment to ${flags.directory}`);
|
|
153
|
+
const syncState = syncStateAction(flags['sync-state'], outputDir, flags.draft);
|
|
154
|
+
if (syncState === 'skip') {
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
if (syncState === 'draft') {
|
|
158
|
+
this.warn('Skipped writing .xano/sync_state.json: --draft pulls draft versions, which --fast does not compare against');
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
try {
|
|
162
|
+
const tenant = await this.getOrCreateSandbox(profile, flags.verbose);
|
|
163
|
+
if (typeof tenant.created_at !== 'string') {
|
|
164
|
+
this.warn('Could not resolve sandbox fingerprints; skipped writing .xano/sync_state.json');
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
const target = sandboxTargetIdentity(instanceHost(profile.instance_origin), {
|
|
168
|
+
created_at: tenant.created_at,
|
|
169
|
+
id: tenant.id,
|
|
170
|
+
});
|
|
171
|
+
const sandboxToken = writeMarkerToken(path.dirname(this.getCredentialsPath()), target.instance_host, target.branch_created_at);
|
|
172
|
+
saveSyncState(outputDir, target, recordEntriesForPaths(outputDir, writtenPaths), {
|
|
173
|
+
replace: true,
|
|
174
|
+
sandboxToken,
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
catch (error) {
|
|
178
|
+
this.warn(`Skipped writing .xano/sync_state.json: ${error.message}`);
|
|
179
|
+
}
|
|
131
180
|
}
|
|
132
181
|
}
|
|
@@ -7,13 +7,17 @@ export default class SandboxPush extends BaseCommand {
|
|
|
7
7
|
directory: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
8
8
|
'dry-run': import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
9
9
|
env: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
10
|
+
fast: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
10
11
|
force: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
11
12
|
guids: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
12
13
|
records: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
14
|
+
refresh: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
13
15
|
review: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
14
16
|
sync: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
15
17
|
transaction: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
16
18
|
truncate: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
19
|
+
writeback: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
20
|
+
'writeback-diff': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
17
21
|
config: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
18
22
|
profile: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
19
23
|
verbose: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { Flags } from '@oclif/core';
|
|
2
2
|
import * as fs from 'node:fs';
|
|
3
|
-
import { resolve } from 'node:path';
|
|
3
|
+
import { basename, dirname, relative, resolve } from 'node:path';
|
|
4
4
|
import open from 'open';
|
|
5
5
|
import BaseCommand from '../../../base-command.js';
|
|
6
|
+
import { formatWorkspacePushDefaultsBanner, resolvePushDefaults } from '../../../utils/local-config.js';
|
|
6
7
|
import { executePush } from '../../../utils/multidoc-push.js';
|
|
8
|
+
import { policiesSkippedNotice } from '../../../utils/policy/feedback.js';
|
|
7
9
|
export default class SandboxPush extends BaseCommand {
|
|
8
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.';
|
|
9
11
|
static examples = [
|
|
@@ -24,6 +26,18 @@ Preview changes without pushing
|
|
|
24
26
|
`,
|
|
25
27
|
`$ xano sandbox push --force
|
|
26
28
|
Skip preview and push immediately
|
|
29
|
+
`,
|
|
30
|
+
`$ xano sandbox push --fast
|
|
31
|
+
Push only files that changed since the last sync, and write .xano/sync_state.json
|
|
32
|
+
`,
|
|
33
|
+
`$ xano sandbox push --fast --refresh
|
|
34
|
+
Re-compare local files against the sandbox export, then push changes
|
|
35
|
+
`,
|
|
36
|
+
`$ xano sandbox push --no-fast
|
|
37
|
+
Push without --fast even when profile.yaml sets fast: true
|
|
38
|
+
`,
|
|
39
|
+
`$ xano sandbox push --writeback
|
|
40
|
+
Push and write server-formatted XanoScript back to local files
|
|
27
41
|
`,
|
|
28
42
|
`$ xano sandbox push --records --env`,
|
|
29
43
|
`$ xano sandbox push --truncate`,
|
|
@@ -54,6 +68,11 @@ Push and open sandbox review in the browser
|
|
|
54
68
|
description: 'Include environment variables in import',
|
|
55
69
|
required: false,
|
|
56
70
|
}),
|
|
71
|
+
fast: Flags.boolean({
|
|
72
|
+
allowNo: true,
|
|
73
|
+
description: 'Skip files whose contents match .xano/sync_state.json, and write or update that record. Without a record, compares against the sandbox export once. Can be set as `fast: true` in profile.yaml.',
|
|
74
|
+
required: false,
|
|
75
|
+
}),
|
|
57
76
|
force: Flags.boolean({
|
|
58
77
|
default: false,
|
|
59
78
|
description: 'Skip preview and confirmation prompt (for CI/CD pipelines)',
|
|
@@ -70,6 +89,11 @@ Push and open sandbox review in the browser
|
|
|
70
89
|
description: 'Include records in import',
|
|
71
90
|
required: false,
|
|
72
91
|
}),
|
|
92
|
+
refresh: Flags.boolean({
|
|
93
|
+
default: false,
|
|
94
|
+
description: 'With --fast, re-compare local files against the sandbox export even when a sync record exists',
|
|
95
|
+
required: false,
|
|
96
|
+
}),
|
|
73
97
|
review: Flags.boolean({
|
|
74
98
|
default: false,
|
|
75
99
|
description: 'Open sandbox review in the browser after pushing',
|
|
@@ -91,6 +115,15 @@ Push and open sandbox review in the browser
|
|
|
91
115
|
description: '[CRITICAL] STOP and confirm with the user; this truncates live tables before importing.',
|
|
92
116
|
required: false,
|
|
93
117
|
}),
|
|
118
|
+
writeback: Flags.boolean({
|
|
119
|
+
allowNo: true,
|
|
120
|
+
description: 'After a successful push, write server-formatted XanoScript back to the files that were pushed. Can be set as `writeback: true` in profile.yaml.',
|
|
121
|
+
required: false,
|
|
122
|
+
}),
|
|
123
|
+
'writeback-diff': Flags.string({
|
|
124
|
+
description: 'With --writeback, save local vs server copies of rewritten files under this directory',
|
|
125
|
+
required: false,
|
|
126
|
+
}),
|
|
94
127
|
};
|
|
95
128
|
async run() {
|
|
96
129
|
const { flags } = await this.parse(SandboxPush);
|
|
@@ -103,6 +136,17 @@ Push and open sandbox review in the browser
|
|
|
103
136
|
this.error(`Not a directory: ${inputDir}`);
|
|
104
137
|
}
|
|
105
138
|
const baseUrl = `${profile.instance_origin}/api:meta/sandbox`;
|
|
139
|
+
const pushDefaults = resolvePushDefaults({
|
|
140
|
+
flagFast: flags.fast,
|
|
141
|
+
flagWriteback: flags.writeback,
|
|
142
|
+
localFast: this.localProfile?.config.fast,
|
|
143
|
+
localWriteback: this.localProfile?.config.writeback,
|
|
144
|
+
sync: flags.sync,
|
|
145
|
+
});
|
|
146
|
+
if (pushDefaults.fromLocal.length > 0 && this.localProfile) {
|
|
147
|
+
const relativePath = relative(process.cwd(), this.localProfile.path) || basename(this.localProfile.path);
|
|
148
|
+
this.log(formatWorkspacePushDefaultsBanner(pushDefaults.fromLocal, relativePath));
|
|
149
|
+
}
|
|
106
150
|
const target = {
|
|
107
151
|
buildDryRunUrl: (params) => `${baseUrl}/multidoc/dry-run?${params.toString()}`,
|
|
108
152
|
buildPushUrl: (params) => `${baseUrl}/multidoc?${params.toString()}`,
|
|
@@ -118,15 +162,19 @@ Push and open sandbox review in the browser
|
|
|
118
162
|
delete: flags.delete,
|
|
119
163
|
'dry-run': flags['dry-run'],
|
|
120
164
|
env: flags.env,
|
|
165
|
+
fast: pushDefaults.fast,
|
|
121
166
|
force: flags.force,
|
|
122
167
|
guids: flags.guids,
|
|
123
168
|
records: flags.records,
|
|
169
|
+
refresh: flags.refresh,
|
|
124
170
|
sync: flags.sync,
|
|
125
171
|
transaction: flags.transaction,
|
|
126
172
|
truncate: flags.truncate,
|
|
127
173
|
verbose: flags.verbose,
|
|
174
|
+
writeback: pushDefaults.writeback,
|
|
175
|
+
writebackDiff: flags['writeback-diff'],
|
|
128
176
|
};
|
|
129
|
-
await executePush({
|
|
177
|
+
const result = await executePush({
|
|
130
178
|
accessToken: profile.access_token,
|
|
131
179
|
branch: '',
|
|
132
180
|
command: this,
|
|
@@ -135,8 +183,17 @@ Push and open sandbox review in the browser
|
|
|
135
183
|
listUrl: () => `${baseUrl}/knowledge/sync`,
|
|
136
184
|
rootDir: inputDir,
|
|
137
185
|
},
|
|
186
|
+
syncTarget: {
|
|
187
|
+
instanceOrigin: profile.instance_origin,
|
|
188
|
+
kind: 'sandbox',
|
|
189
|
+
markerDir: dirname(this.getCredentialsPath()),
|
|
190
|
+
},
|
|
138
191
|
verboseFetch: this.verboseFetch.bind(this),
|
|
139
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);
|
|
140
197
|
if (flags.review) {
|
|
141
198
|
await this.openReview(profile.instance_origin, profile.access_token, flags.verbose);
|
|
142
199
|
}
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { Flags } from '@oclif/core';
|
|
2
|
+
import { dirname } from 'node:path';
|
|
2
3
|
import BaseCommand from '../../../base-command.js';
|
|
3
4
|
import { pushKnowledge } from '../../../utils/knowledge-sync.js';
|
|
5
|
+
import { invalidateSandboxMarkerForOrigin } from '../../../utils/sandbox-marker.js';
|
|
4
6
|
export default class SandboxReset extends BaseCommand {
|
|
5
7
|
static description = '[CRITICAL] NEVER run without explicit user confirmation; this clears all workspace data and drafts. Resets your sandbox environment.';
|
|
6
8
|
static examples = [
|
|
@@ -55,6 +57,12 @@ Sandbox environment has been reset.
|
|
|
55
57
|
this.warn(`Sandbox environment was reset, but clearing knowledge files failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
56
58
|
}
|
|
57
59
|
this.log('Sandbox environment has been reset.');
|
|
60
|
+
try {
|
|
61
|
+
invalidateSandboxMarkerForOrigin(dirname(this.getCredentialsPath()), profile.instance_origin);
|
|
62
|
+
}
|
|
63
|
+
catch (error) {
|
|
64
|
+
this.warn(`Skipped invalidating sandbox sync marker: ${error.message}`);
|
|
65
|
+
}
|
|
58
66
|
}
|
|
59
67
|
catch (error) {
|
|
60
68
|
if (error instanceof Error && 'oclif' in error)
|
|
@@ -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,
|