@xano/cli 1.0.6-beta.2 → 1.0.6-beta.20
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 +217 -6
- package/dist/base-command.d.ts +5 -0
- package/dist/base-command.js +61 -4
- package/dist/commands/branch/list/index.js +5 -1
- package/dist/commands/ephemeral/create/index.js +3 -3
- package/dist/commands/ephemeral/list/index.js +2 -1
- package/dist/commands/ephemeral/pull/index.d.ts +0 -6
- package/dist/commands/ephemeral/pull/index.js +10 -95
- package/dist/commands/ephemeral/push/index.d.ts +10 -0
- package/dist/commands/ephemeral/push/index.js +77 -0
- package/dist/commands/ephemeral/static_host/build/list/index.d.ts +2 -3
- package/dist/commands/ephemeral/static_host/build/list/index.js +19 -40
- package/dist/commands/ephemeral/static_host/list/index.d.ts +2 -3
- package/dist/commands/ephemeral/static_host/list/index.js +20 -40
- package/dist/commands/flatten/index.d.ts +34 -0
- package/dist/commands/flatten/index.js +140 -0
- package/dist/commands/function/list/index.d.ts +4 -4
- package/dist/commands/function/list/index.js +30 -38
- package/dist/commands/function/run/index.d.ts +1 -0
- package/dist/commands/function/run/index.js +19 -5
- package/dist/commands/knowledge/list/index.js +2 -1
- package/dist/commands/platform/list/index.js +13 -14
- package/dist/commands/release/list/index.js +13 -14
- package/dist/commands/release/pull/index.d.ts +0 -6
- package/dist/commands/release/pull/index.js +10 -95
- package/dist/commands/sandbox/env/list/index.js +4 -0
- package/dist/commands/sandbox/pull/index.d.ts +0 -1
- package/dist/commands/sandbox/pull/index.js +10 -77
- package/dist/commands/sandbox/unit_test/list/index.d.ts +2 -0
- package/dist/commands/sandbox/unit_test/list/index.js +9 -14
- package/dist/commands/sandbox/unit_test/run/index.js +17 -11
- package/dist/commands/sandbox/unit_test/run_all/index.d.ts +1 -0
- package/dist/commands/sandbox/unit_test/run_all/index.js +76 -38
- package/dist/commands/sandbox/workflow_test/list/index.d.ts +2 -0
- package/dist/commands/sandbox/workflow_test/list/index.js +9 -14
- package/dist/commands/sandbox/workflow_test/run/index.js +12 -9
- package/dist/commands/sandbox/workflow_test/run_all/index.d.ts +1 -0
- package/dist/commands/sandbox/workflow_test/run_all/index.js +74 -36
- package/dist/commands/static_host/build/list/index.d.ts +2 -3
- package/dist/commands/static_host/build/list/index.js +19 -40
- package/dist/commands/static_host/build/pull/index.js +12 -9
- package/dist/commands/static_host/list/index.d.ts +2 -3
- package/dist/commands/static_host/list/index.js +20 -40
- package/dist/commands/static_host/migrate/index.js +14 -10
- package/dist/commands/tenant/backup/list/index.js +13 -14
- package/dist/commands/tenant/cluster/list/index.js +9 -10
- 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 +21 -0
- package/dist/commands/tenant/deploy_release/index.js +94 -2
- package/dist/commands/tenant/edit/index.d.ts +2 -0
- package/dist/commands/tenant/edit/index.js +39 -4
- package/dist/commands/tenant/env/list/index.js +4 -0
- package/dist/commands/tenant/list/index.js +19 -20
- package/dist/commands/tenant/pull/index.d.ts +0 -6
- package/dist/commands/tenant/pull/index.js +10 -106
- package/dist/commands/tenant/snapshot/list/index.js +5 -1
- package/dist/commands/tenant/unit_test/list/index.d.ts +4 -2
- package/dist/commands/tenant/unit_test/list/index.js +9 -14
- package/dist/commands/tenant/unit_test/run/index.js +19 -11
- package/dist/commands/tenant/unit_test/run_all/index.d.ts +1 -0
- package/dist/commands/tenant/unit_test/run_all/index.js +80 -38
- package/dist/commands/tenant/workflow_test/list/index.d.ts +4 -2
- package/dist/commands/tenant/workflow_test/list/index.js +9 -14
- package/dist/commands/tenant/workflow_test/run/index.js +14 -9
- package/dist/commands/tenant/workflow_test/run_all/index.d.ts +1 -0
- package/dist/commands/tenant/workflow_test/run_all/index.js +78 -36
- package/dist/commands/tenant_deploy_request/bypass/index.d.ts +20 -0
- package/dist/commands/tenant_deploy_request/bypass/index.js +79 -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 +19 -0
- package/dist/commands/tenant_deploy_request/get/index.js +75 -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 +21 -0
- package/dist/commands/tenant_deploy_request/set_status/index.js +112 -0
- package/dist/commands/unit_test/list/index.d.ts +3 -1
- package/dist/commands/unit_test/list/index.js +9 -14
- package/dist/commands/unit_test/run/index.js +19 -11
- package/dist/commands/unit_test/run_all/index.d.ts +1 -0
- package/dist/commands/unit_test/run_all/index.js +81 -41
- package/dist/commands/workflow_test/list/index.d.ts +3 -1
- package/dist/commands/workflow_test/list/index.js +10 -13
- package/dist/commands/workflow_test/run/index.js +15 -10
- package/dist/commands/workflow_test/run_all/index.d.ts +1 -0
- package/dist/commands/workflow_test/run_all/index.js +82 -39
- package/dist/commands/workspace/git/pull/index.js +48 -7
- package/dist/commands/workspace/list/index.js +7 -13
- package/dist/commands/workspace/pull/index.d.ts +0 -6
- package/dist/commands/workspace/pull/index.js +10 -115
- package/dist/utils/concurrency.d.ts +29 -0
- package/dist/utils/concurrency.js +54 -0
- package/dist/utils/document-parser.d.ts +90 -0
- package/dist/utils/document-parser.js +273 -1
- package/dist/utils/flatten.d.ts +36 -0
- package/dist/utils/flatten.js +68 -0
- package/dist/utils/knowledge-sync.d.ts +1 -0
- package/dist/utils/knowledge-sync.js +11 -5
- package/dist/utils/microservice-wait.d.ts +53 -0
- package/dist/utils/microservice-wait.js +83 -0
- package/dist/utils/multidoc-push.d.ts +88 -0
- package/dist/utils/multidoc-push.js +377 -8
- package/dist/utils/paging.d.ts +145 -0
- package/dist/utils/paging.js +199 -0
- package/dist/utils/reference-checker.js +13 -0
- package/oclif.manifest.json +4999 -3973
- package/package.json +1 -1
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Flags } from '@oclif/core';
|
|
2
2
|
import BaseCommand from '../../../base-command.js';
|
|
3
|
+
import { buildPagingJson, buildPagingParams, formatPagingFooter, normalizeListResponse, pagingFlags } from '../../../utils/paging.js';
|
|
3
4
|
export default class StaticHostList extends BaseCommand {
|
|
4
5
|
static args = {};
|
|
5
6
|
static description = 'List all static hosts in a workspace from the Xano Metadata API';
|
|
@@ -15,13 +16,17 @@ Available static hosts:
|
|
|
15
16
|
- another-host (ID: 2)
|
|
16
17
|
`,
|
|
17
18
|
`$ xano static_host:list -w 40 --output json
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
19
|
+
{
|
|
20
|
+
"count": 1,
|
|
21
|
+
"page": 1,
|
|
22
|
+
"total": 3,
|
|
23
|
+
"items": [
|
|
24
|
+
{
|
|
25
|
+
"id": 1,
|
|
26
|
+
"name": "my-static-host"
|
|
27
|
+
}
|
|
28
|
+
]
|
|
29
|
+
}
|
|
25
30
|
`,
|
|
26
31
|
`$ xano static_host:list -p staging -o json --page 2
|
|
27
32
|
[
|
|
@@ -41,16 +46,7 @@ Available static hosts:
|
|
|
41
46
|
options: ['summary', 'json'],
|
|
42
47
|
required: false,
|
|
43
48
|
}),
|
|
44
|
-
page:
|
|
45
|
-
default: 1,
|
|
46
|
-
description: 'Page number for pagination',
|
|
47
|
-
required: false,
|
|
48
|
-
}),
|
|
49
|
-
per_page: Flags.integer({
|
|
50
|
-
default: 50,
|
|
51
|
-
description: 'Number of results per page',
|
|
52
|
-
required: false,
|
|
53
|
-
}),
|
|
49
|
+
...pagingFlags('page-only-envelope', { fixedPerPage: 100 }),
|
|
54
50
|
workspace: Flags.string({
|
|
55
51
|
char: 'w',
|
|
56
52
|
description: 'Workspace ID (optional if set in profile)',
|
|
@@ -74,13 +70,7 @@ Available static hosts:
|
|
|
74
70
|
` 2. Set it in your profile using: xano profile:edit ${profileName} -w <workspace_id>`);
|
|
75
71
|
}
|
|
76
72
|
// Build query parameters
|
|
77
|
-
const queryParams = new URLSearchParams(
|
|
78
|
-
page: flags.page.toString(),
|
|
79
|
-
});
|
|
80
|
-
// Only add per_page if it's not the default value
|
|
81
|
-
if (flags.per_page !== 50) {
|
|
82
|
-
queryParams.append('per_page', flags.per_page.toString());
|
|
83
|
-
}
|
|
73
|
+
const queryParams = new URLSearchParams(buildPagingParams(flags, 'page-only-envelope'));
|
|
84
74
|
// Construct the API URL
|
|
85
75
|
const apiUrl = `${profile.instance_origin}/api:meta/workspace/${workspaceId}/static_host?${queryParams.toString()}`;
|
|
86
76
|
// Fetch static hosts from the API
|
|
@@ -96,24 +86,11 @@ Available static hosts:
|
|
|
96
86
|
const errorText = await response.text();
|
|
97
87
|
this.error(`API request failed with status ${response.status}: ${response.statusText}\n${errorText}`);
|
|
98
88
|
}
|
|
99
|
-
const
|
|
100
|
-
|
|
101
|
-
let staticHosts;
|
|
102
|
-
if (Array.isArray(data)) {
|
|
103
|
-
staticHosts = data;
|
|
104
|
-
}
|
|
105
|
-
else if (data && typeof data === 'object' && 'static_hosts' in data && Array.isArray(data.static_hosts)) {
|
|
106
|
-
staticHosts = data.static_hosts;
|
|
107
|
-
}
|
|
108
|
-
else if (data && typeof data === 'object' && 'items' in data && Array.isArray(data.items)) {
|
|
109
|
-
staticHosts = data.items;
|
|
110
|
-
}
|
|
111
|
-
else {
|
|
112
|
-
this.error('Unexpected API response format');
|
|
113
|
-
}
|
|
89
|
+
const list = normalizeListResponse(await response.json(), ['static_hosts']);
|
|
90
|
+
const staticHosts = list.items;
|
|
114
91
|
// Output results
|
|
115
92
|
if (flags.output === 'json') {
|
|
116
|
-
this.log(JSON.stringify(
|
|
93
|
+
this.log(JSON.stringify(buildPagingJson(list, { page: flags.page, tier: 'page-only-envelope' }), null, 2));
|
|
117
94
|
}
|
|
118
95
|
else {
|
|
119
96
|
// summary format
|
|
@@ -132,6 +109,9 @@ Available static hosts:
|
|
|
132
109
|
}
|
|
133
110
|
}
|
|
134
111
|
}
|
|
112
|
+
const footer = formatPagingFooter(list, { noun: 'static host', page: flags.page, tier: 'page-only-envelope' });
|
|
113
|
+
if (footer)
|
|
114
|
+
this.log(footer);
|
|
135
115
|
}
|
|
136
116
|
}
|
|
137
117
|
catch (error) {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Args, Flags } from '@oclif/core';
|
|
2
2
|
import BaseCommand from '../../../base-command.js';
|
|
3
|
+
import { normalizeListResponse } from '../../../utils/paging.js';
|
|
3
4
|
export default class StaticHostMigrate extends BaseCommand {
|
|
4
5
|
static args = {
|
|
5
6
|
static_host: Args.string({
|
|
@@ -85,9 +86,12 @@ Would migrate 3 v1 host(s): newsite, marketing, legacy
|
|
|
85
86
|
/** List all static hosts in the workspace (paginates until exhausted). */
|
|
86
87
|
async listHosts(profile, workspaceId, verbose) {
|
|
87
88
|
const hosts = [];
|
|
88
|
-
const perPage = 100;
|
|
89
89
|
for (let page = 1;; page++) {
|
|
90
|
-
|
|
90
|
+
// per_page is not an input on this endpoint — the server fixes the page
|
|
91
|
+
// size — so it is deliberately not sent. Termination uses the response's
|
|
92
|
+
// own itemsTotal rather than "the page came back full", which would skip
|
|
93
|
+
// hosts if the server page size ever differed from a hardcoded guess.
|
|
94
|
+
const listUrl = `${profile.instance_origin}/api:meta/workspace/${workspaceId}/static_host?page=${page}`;
|
|
91
95
|
// eslint-disable-next-line no-await-in-loop
|
|
92
96
|
const response = await this.verboseFetch(listUrl, {
|
|
93
97
|
headers: {
|
|
@@ -100,14 +104,14 @@ Would migrate 3 v1 host(s): newsite, marketing, legacy
|
|
|
100
104
|
const errorText = await response.text(); // eslint-disable-line no-await-in-loop
|
|
101
105
|
this.error(`Failed to list static hosts: ${response.status} ${response.statusText}\n${errorText}`);
|
|
102
106
|
}
|
|
103
|
-
const
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
if (
|
|
107
|
+
const list = normalizeListResponse(await response.json(), ['static_hosts']); // eslint-disable-line no-await-in-loop
|
|
108
|
+
hosts.push(...list.items);
|
|
109
|
+
// Stop on the server's own total when it gives us one. Fall back to an
|
|
110
|
+
// empty page otherwise — one extra request, but it cannot under-collect
|
|
111
|
+
// and silently migrate a subset.
|
|
112
|
+
if (list.items.length === 0)
|
|
113
|
+
break;
|
|
114
|
+
if (list.itemsTotal !== undefined && hosts.length >= list.itemsTotal)
|
|
111
115
|
break;
|
|
112
116
|
}
|
|
113
117
|
return hosts;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Args, Flags } from '@oclif/core';
|
|
2
2
|
import BaseCommand from '../../../../base-command.js';
|
|
3
|
+
import { buildPagingJson } from '../../../../utils/paging.js';
|
|
3
4
|
export default class TenantBackupList extends BaseCommand {
|
|
4
5
|
static args = {
|
|
5
6
|
tenant_name: Args.string({
|
|
@@ -70,23 +71,21 @@ Backups for tenant t1234-abcd-xyz1:
|
|
|
70
71
|
this.error('Unexpected API response format');
|
|
71
72
|
}
|
|
72
73
|
if (flags.output === 'json') {
|
|
73
|
-
this.log(JSON.stringify(backups, null, 2));
|
|
74
|
+
this.log(JSON.stringify(buildPagingJson({ items: backups }, { tier: 'none' }), null, 2));
|
|
75
|
+
}
|
|
76
|
+
else if (backups.length === 0) {
|
|
77
|
+
this.log(`No backups found for tenant ${tenantName}`);
|
|
74
78
|
}
|
|
75
79
|
else {
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
let date = 'unknown';
|
|
83
|
-
if (backup.created_at) {
|
|
84
|
-
const d = new Date(backup.created_at);
|
|
85
|
-
date = Number.isNaN(d.getTime()) ? backup.created_at : d.toISOString().split('T')[0];
|
|
86
|
-
}
|
|
87
|
-
const desc = backup.description || 'No description';
|
|
88
|
-
this.log(` - #${backup.id} - ${desc} (${date})`);
|
|
80
|
+
this.log(`Backups for tenant ${tenantName}:`);
|
|
81
|
+
for (const backup of backups) {
|
|
82
|
+
let date = 'unknown';
|
|
83
|
+
if (backup.created_at) {
|
|
84
|
+
const d = new Date(backup.created_at);
|
|
85
|
+
date = Number.isNaN(d.getTime()) ? backup.created_at : d.toISOString().split('T')[0];
|
|
89
86
|
}
|
|
87
|
+
const desc = backup.description || 'No description';
|
|
88
|
+
this.log(` - #${backup.id} - ${desc} (${date})`);
|
|
90
89
|
}
|
|
91
90
|
}
|
|
92
91
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Flags } from '@oclif/core';
|
|
2
2
|
import BaseCommand from '../../../../base-command.js';
|
|
3
|
+
import { buildPagingJson } from '../../../../utils/paging.js';
|
|
3
4
|
export default class TenantClusterList extends BaseCommand {
|
|
4
5
|
static description = 'List all tenant clusters';
|
|
5
6
|
static examples = [
|
|
@@ -48,18 +49,16 @@ Tenant clusters:
|
|
|
48
49
|
this.error('Unexpected API response format');
|
|
49
50
|
}
|
|
50
51
|
if (flags.output === 'json') {
|
|
51
|
-
this.log(JSON.stringify(clusters, null, 2));
|
|
52
|
+
this.log(JSON.stringify(buildPagingJson({ items: clusters }, { tier: 'none' }), null, 2));
|
|
53
|
+
}
|
|
54
|
+
else if (clusters.length === 0) {
|
|
55
|
+
this.log('No tenant clusters found');
|
|
52
56
|
}
|
|
53
57
|
else {
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
this.log('Tenant clusters:');
|
|
59
|
-
for (const cluster of clusters) {
|
|
60
|
-
const type = cluster.type ? ` (${cluster.type})` : '';
|
|
61
|
-
this.log(` - ${cluster.name}${type} [ID: ${cluster.id}]`);
|
|
62
|
-
}
|
|
58
|
+
this.log('Tenant clusters:');
|
|
59
|
+
for (const cluster of clusters) {
|
|
60
|
+
const type = cluster.type ? ` (${cluster.type})` : '';
|
|
61
|
+
this.log(` - ${cluster.name}${type} [ID: ${cluster.id}]`);
|
|
63
62
|
}
|
|
64
63
|
}
|
|
65
64
|
}
|
|
@@ -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
|
type: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
17
20
|
tasks: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
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
|
type: Flags.string({
|
|
58
74
|
default: 'tier1',
|
|
59
75
|
description: 'Tenant type',
|
|
@@ -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,
|
|
@@ -14,4 +14,25 @@ export default class TenantDeployRelease extends BaseCommand {
|
|
|
14
14
|
verbose: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
15
15
|
};
|
|
16
16
|
run(): Promise<void>;
|
|
17
|
+
/**
|
|
18
|
+
* Does an approved tenant_deploy_request already exist for this (tenant, release)
|
|
19
|
+
* pair? The list endpoint has no release filter, so this fetches approved
|
|
20
|
+
* requests for the tenant and filters client-side by the resolved release name.
|
|
21
|
+
*/
|
|
22
|
+
private hasApprovedDeployRequest;
|
|
23
|
+
/**
|
|
24
|
+
* Does this tenant have the deploy approval gate enabled?
|
|
25
|
+
*
|
|
26
|
+
* Returns false if the tenant has `allow_quick_deploy` set — that flag lets a
|
|
27
|
+
* tenant skip the approval gate entirely for quick deploys, regardless of
|
|
28
|
+
* `required_reviewers`.
|
|
29
|
+
*
|
|
30
|
+
* Deliberately uses `tenant list` rather than `tenant get`: the single-tenant
|
|
31
|
+
* GET route's output whitelist (tenant-detail.yaml) omits `deploy_settings`
|
|
32
|
+
* (which holds `required_reviewers`, `allow_deploy_bypass`, and
|
|
33
|
+
* `allow_quick_deploy`) even though the list route's output includes it
|
|
34
|
+
* — confirmed live against a running instance. Filed as a backend bug; this
|
|
35
|
+
* is the workaround until it's fixed. See TENANT_DEPLOY_REQUEST_TEST.md.
|
|
36
|
+
*/
|
|
37
|
+
private tenantRequiresApproval;
|
|
17
38
|
}
|
|
@@ -43,6 +43,27 @@ Deployed release "v1.0" to tenant: My Tenant (my-tenant)
|
|
|
43
43
|
}
|
|
44
44
|
const releaseName = flags.release;
|
|
45
45
|
const tenantName = args.tenant_name;
|
|
46
|
+
// Pre-flight: the "approval required" error must always be shown before any
|
|
47
|
+
// permission/RBAC error. The backend's own deploy route checks RBAC first
|
|
48
|
+
// and the approval gate second (inside mvp:tenant_deploy_release), so a
|
|
49
|
+
// caller who lacks deploy permission would otherwise never learn a request
|
|
50
|
+
// is what's actually needed. Checking here first, with an endpoint that
|
|
51
|
+
// only needs read scope, guarantees the right error is shown regardless of
|
|
52
|
+
// whether the caller can deploy at all.
|
|
53
|
+
const requiresApproval = await this.tenantRequiresApproval(profile, workspaceId, tenantName, flags.verbose);
|
|
54
|
+
if (requiresApproval) {
|
|
55
|
+
const hasApprovedRequest = await this.hasApprovedDeployRequest({
|
|
56
|
+
profile,
|
|
57
|
+
releaseName,
|
|
58
|
+
tenantName,
|
|
59
|
+
verbose: flags.verbose,
|
|
60
|
+
workspaceId,
|
|
61
|
+
});
|
|
62
|
+
if (!hasApprovedRequest) {
|
|
63
|
+
this.error(`Tenant "${tenantName}" requires an approved deploy request before "${releaseName}" can be deployed.\n` +
|
|
64
|
+
`Run: xano tenant_deploy_request create "Deploy ${releaseName} to ${tenantName}" --tenant ${tenantName} --release ${releaseName} --reviewers <id,id,...>`);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
46
67
|
const apiUrl = `${profile.instance_origin}/api:meta/workspace/${workspaceId}/tenant/${tenantName}/deploy`;
|
|
47
68
|
this.warn('This may take a few minutes. Please be patient.');
|
|
48
69
|
const startTime = Date.now();
|
|
@@ -57,8 +78,17 @@ Deployed release "v1.0" to tenant: My Tenant (my-tenant)
|
|
|
57
78
|
method: 'POST',
|
|
58
79
|
}, flags.verbose, profile.access_token);
|
|
59
80
|
if (!response.ok) {
|
|
60
|
-
|
|
61
|
-
|
|
81
|
+
let message = await this.parseApiError(response, 'API request failed');
|
|
82
|
+
// Only reached once the pre-flight above has already confirmed the
|
|
83
|
+
// approval gate is satisfied or not applicable — so a permission error
|
|
84
|
+
// here is genuinely about RBAC, not about a missing approval. If the
|
|
85
|
+
// tenant supports deploy requests, mention that path as an alternative
|
|
86
|
+
// to direct deploy, since opening a request doesn't require deploy
|
|
87
|
+
// permission.
|
|
88
|
+
if ((response.status === 401 || response.status === 403) && requiresApproval) {
|
|
89
|
+
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,...>`;
|
|
90
|
+
}
|
|
91
|
+
this.error(message);
|
|
62
92
|
}
|
|
63
93
|
const tenant = (await response.json());
|
|
64
94
|
if (flags.output === 'json') {
|
|
@@ -83,4 +113,66 @@ Deployed release "v1.0" to tenant: My Tenant (my-tenant)
|
|
|
83
113
|
}
|
|
84
114
|
}
|
|
85
115
|
}
|
|
116
|
+
/**
|
|
117
|
+
* Does an approved tenant_deploy_request already exist for this (tenant, release)
|
|
118
|
+
* pair? The list endpoint has no release filter, so this fetches approved
|
|
119
|
+
* requests for the tenant and filters client-side by the resolved release name.
|
|
120
|
+
*/
|
|
121
|
+
async hasApprovedDeployRequest(opts) {
|
|
122
|
+
const { profile, releaseName, tenantName, verbose, workspaceId } = opts;
|
|
123
|
+
const params = new URLSearchParams({ per_page: '100', status: 'approved', tenant_name: tenantName });
|
|
124
|
+
const apiUrl = `${profile.instance_origin}/api:meta/workspace/${workspaceId}/approval_request?${params}`;
|
|
125
|
+
const response = await this.verboseFetch(apiUrl, {
|
|
126
|
+
headers: {
|
|
127
|
+
accept: 'application/json',
|
|
128
|
+
Authorization: `Bearer ${profile.access_token}`,
|
|
129
|
+
},
|
|
130
|
+
method: 'GET',
|
|
131
|
+
}, verbose, profile.access_token);
|
|
132
|
+
if (!response.ok) {
|
|
133
|
+
// A read-scope failure here means something is more broadly wrong (e.g.
|
|
134
|
+
// no workspace access at all) — surface it rather than silently treating
|
|
135
|
+
// it as "no approval found", which would misreport as approval-required.
|
|
136
|
+
const message = await this.parseApiError(response, 'Failed to check for an approved deploy request');
|
|
137
|
+
this.error(message);
|
|
138
|
+
}
|
|
139
|
+
const data = (await response.json());
|
|
140
|
+
const items = Array.isArray(data) ? data : (data.items ?? []);
|
|
141
|
+
return items.some((item) => item.status === 'approved' && item._release?.name === releaseName);
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Does this tenant have the deploy approval gate enabled?
|
|
145
|
+
*
|
|
146
|
+
* Returns false if the tenant has `allow_quick_deploy` set — that flag lets a
|
|
147
|
+
* tenant skip the approval gate entirely for quick deploys, regardless of
|
|
148
|
+
* `required_reviewers`.
|
|
149
|
+
*
|
|
150
|
+
* Deliberately uses `tenant list` rather than `tenant get`: the single-tenant
|
|
151
|
+
* GET route's output whitelist (tenant-detail.yaml) omits `deploy_settings`
|
|
152
|
+
* (which holds `required_reviewers`, `allow_deploy_bypass`, and
|
|
153
|
+
* `allow_quick_deploy`) even though the list route's output includes it
|
|
154
|
+
* — confirmed live against a running instance. Filed as a backend bug; this
|
|
155
|
+
* is the workaround until it's fixed. See TENANT_DEPLOY_REQUEST_TEST.md.
|
|
156
|
+
*/
|
|
157
|
+
async tenantRequiresApproval(profile, workspaceId, tenantName, verbose) {
|
|
158
|
+
const params = new URLSearchParams({ per_page: '100' });
|
|
159
|
+
const apiUrl = `${profile.instance_origin}/api:meta/workspace/${workspaceId}/tenant?${params}`;
|
|
160
|
+
const response = await this.verboseFetch(apiUrl, {
|
|
161
|
+
headers: {
|
|
162
|
+
accept: 'application/json',
|
|
163
|
+
Authorization: `Bearer ${profile.access_token}`,
|
|
164
|
+
},
|
|
165
|
+
method: 'GET',
|
|
166
|
+
}, verbose, profile.access_token);
|
|
167
|
+
if (!response.ok) {
|
|
168
|
+
const message = await this.parseApiError(response, 'Failed to look up tenant');
|
|
169
|
+
this.error(message);
|
|
170
|
+
}
|
|
171
|
+
const data = (await response.json());
|
|
172
|
+
const items = Array.isArray(data) ? data : (data.items ?? []);
|
|
173
|
+
const tenant = items.find((item) => item.name === tenantName);
|
|
174
|
+
if (tenant?.deploy_settings?.allow_quick_deploy)
|
|
175
|
+
return false;
|
|
176
|
+
return (tenant?.deploy_settings?.required_reviewers ?? 0) > 0;
|
|
177
|
+
}
|
|
86
178
|
}
|
|
@@ -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>;
|
|
@@ -13,9 +13,15 @@ export default class TenantEdit extends BaseCommand {
|
|
|
13
13
|
Updated tenant: New Name (my-tenant) - ID: 42
|
|
14
14
|
`,
|
|
15
15
|
`$ xano tenant edit t1234-abcd-xyz1 --no-tasks --no-ingress -o json`,
|
|
16
|
+
`$ xano tenant edit t1234-abcd-xyz1 --required_reviewers 1`,
|
|
16
17
|
];
|
|
17
18
|
static flags = {
|
|
18
19
|
...BaseCommand.baseFlags,
|
|
20
|
+
allow_quick_deploy: Flags.boolean({
|
|
21
|
+
allowNo: true,
|
|
22
|
+
description: 'Allow this tenant to skip the deploy approval gate entirely for quick deploys, regardless of --required_reviewers.',
|
|
23
|
+
required: false,
|
|
24
|
+
}),
|
|
19
25
|
description: Flags.string({
|
|
20
26
|
char: 'd',
|
|
21
27
|
description: 'New description',
|
|
@@ -50,6 +56,10 @@ Updated tenant: New Name (my-tenant) - ID: 42
|
|
|
50
56
|
description: '[CRITICAL] NEVER disable RBAC without explicit user confirmation; this removes role-based access controls on the tenant. Enables/disables RBAC.',
|
|
51
57
|
required: false,
|
|
52
58
|
}),
|
|
59
|
+
required_reviewers: Flags.integer({
|
|
60
|
+
description: 'When greater than 0, that many distinct reviewers must approve a tenant_deploy_request before a release may be deployed to this tenant. 0 means not gated.',
|
|
61
|
+
required: false,
|
|
62
|
+
}),
|
|
53
63
|
tasks: Flags.boolean({
|
|
54
64
|
allowNo: true,
|
|
55
65
|
description: '[IMPORTANT] ALWAYS confirm with the user before disabling background tasks; this stops scheduled jobs on the tenant. Enables/disables background tasks.',
|
|
@@ -76,8 +86,17 @@ Updated tenant: New Name (my-tenant) - ID: 42
|
|
|
76
86
|
'Content-Type': 'application/json',
|
|
77
87
|
};
|
|
78
88
|
try {
|
|
79
|
-
// Fetch current tenant state (PUT requires all fields)
|
|
80
|
-
|
|
89
|
+
// Fetch current tenant state (PUT requires all fields). Deliberately uses
|
|
90
|
+
// `tenant list` rather than `tenant get`: the single-tenant GET route's
|
|
91
|
+
// output whitelist (tenant-detail.yaml) omits `deploy_settings` (which
|
|
92
|
+
// holds `required_reviewers`, `allow_deploy_bypass`, and
|
|
93
|
+
// `allow_quick_deploy`) even though the list route's output includes it
|
|
94
|
+
// — confirmed live against a running instance. Using GET here would
|
|
95
|
+
// silently reset those fields to their defaults on every edit that doesn't
|
|
96
|
+
// explicitly pass them. Filed as a backend bug; this is the workaround
|
|
97
|
+
// until it's fixed. See TENANT_DEPLOY_REQUEST_TEST.md.
|
|
98
|
+
const listParams = new URLSearchParams({ per_page: '100' });
|
|
99
|
+
const getResponse = await this.verboseFetch(`${profile.instance_origin}/api:meta/workspace/${workspaceId}/tenant?${listParams}`, {
|
|
81
100
|
headers: {
|
|
82
101
|
'accept': 'application/json',
|
|
83
102
|
'Authorization': `Bearer ${profile.access_token}`,
|
|
@@ -88,9 +107,25 @@ Updated tenant: New Name (my-tenant) - ID: 42
|
|
|
88
107
|
const errorText = await getResponse.text();
|
|
89
108
|
this.error(`Failed to fetch tenant: ${getResponse.status} ${getResponse.statusText}\n${errorText}`);
|
|
90
109
|
}
|
|
91
|
-
const
|
|
92
|
-
|
|
110
|
+
const listData = (await getResponse.json());
|
|
111
|
+
const listItems = Array.isArray(listData) ? listData : (listData.items ?? []);
|
|
112
|
+
const current = listItems.find((item) => item.name === tenantName);
|
|
113
|
+
if (!current) {
|
|
114
|
+
this.error(`Tenant "${tenantName}" not found`);
|
|
115
|
+
}
|
|
116
|
+
// Merge in user-provided values. allow_deploy_bypass has no edit flag (it's
|
|
117
|
+
// fixed at `tenant create` time), but the PUT still requires the full body,
|
|
118
|
+
// so its current value is preserved here rather than reset.
|
|
93
119
|
const body = {
|
|
120
|
+
deploy_settings: {
|
|
121
|
+
allow_deploy_bypass: current.deploy_settings?.allow_deploy_bypass ?? false,
|
|
122
|
+
allow_quick_deploy: flags.allow_quick_deploy !== undefined
|
|
123
|
+
? flags.allow_quick_deploy
|
|
124
|
+
: (current.deploy_settings?.allow_quick_deploy ?? false),
|
|
125
|
+
required_reviewers: flags.required_reviewers !== undefined
|
|
126
|
+
? flags.required_reviewers
|
|
127
|
+
: (current.deploy_settings?.required_reviewers ?? 0),
|
|
128
|
+
},
|
|
94
129
|
description: flags.description !== undefined ? flags.description : (current.description ?? ''),
|
|
95
130
|
display: flags.display !== undefined ? flags.display : (current.display ?? current.name),
|
|
96
131
|
domain: flags.domain !== undefined ? flags.domain : (current.domain ?? ''),
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Args, Flags } from '@oclif/core';
|
|
2
2
|
import BaseCommand from '../../../../base-command.js';
|
|
3
|
+
import { formatPagingFooter } from '../../../../utils/paging.js';
|
|
3
4
|
export default class TenantEnvList extends BaseCommand {
|
|
4
5
|
static args = {
|
|
5
6
|
tenant_name: Args.string({
|
|
@@ -68,6 +69,9 @@ Environment variables for tenant my-tenant:
|
|
|
68
69
|
this.log(` - ${envVar.name}`);
|
|
69
70
|
}
|
|
70
71
|
}
|
|
72
|
+
const footer = formatPagingFooter({ items: envVars }, { noun: 'environment variable', tier: 'none' });
|
|
73
|
+
if (footer)
|
|
74
|
+
this.log(footer);
|
|
71
75
|
}
|
|
72
76
|
}
|
|
73
77
|
catch (error) {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Flags } from '@oclif/core';
|
|
2
2
|
import BaseCommand from '../../../base-command.js';
|
|
3
|
+
import { buildPagingJson } from '../../../utils/paging.js';
|
|
3
4
|
export default class TenantList extends BaseCommand {
|
|
4
5
|
static description = 'List all tenants in a workspace';
|
|
5
6
|
static examples = [
|
|
@@ -65,28 +66,26 @@ Tenants in workspace 5:
|
|
|
65
66
|
this.error('Unexpected API response format');
|
|
66
67
|
}
|
|
67
68
|
if (flags.output === 'json') {
|
|
68
|
-
this.log(JSON.stringify(tenants, null, 2));
|
|
69
|
+
this.log(JSON.stringify(buildPagingJson({ items: tenants }, { tier: 'none' }), null, 2));
|
|
70
|
+
}
|
|
71
|
+
else if (tenants.length === 0) {
|
|
72
|
+
this.log('No tenants found');
|
|
69
73
|
}
|
|
70
74
|
else {
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
if (releaseName)
|
|
86
|
-
this.log(` Release: ${releaseName} (ID: ${releaseId})`);
|
|
87
|
-
if (tenant.platform?.name)
|
|
88
|
-
this.log(` Platform: ${tenant.platform.name}`);
|
|
89
|
-
}
|
|
75
|
+
this.log(`Tenants in workspace ${workspaceId}:`);
|
|
76
|
+
for (const tenant of tenants) {
|
|
77
|
+
const state = tenant.state ? ` [${tenant.state}]` : '';
|
|
78
|
+
const license = tenant.license ? ` - ${tenant.license}` : '';
|
|
79
|
+
const typeLabel = tenant.type && tenant.type !== 'standard' ? ` [${tenant.type}]` : '';
|
|
80
|
+
this.log(` - ${tenant.display || tenant.name} (${tenant.name})${state}${license}${typeLabel}`);
|
|
81
|
+
if (tenant.cluster?.name)
|
|
82
|
+
this.log(` Cluster: ${tenant.cluster.name}`);
|
|
83
|
+
const releaseName = typeof tenant.release === 'string' ? tenant.release : tenant.release?.name;
|
|
84
|
+
const releaseId = typeof tenant.release === 'object' ? tenant.release?.id : undefined;
|
|
85
|
+
if (releaseName)
|
|
86
|
+
this.log(` Release: ${releaseName} (ID: ${releaseId})`);
|
|
87
|
+
if (tenant.platform?.name)
|
|
88
|
+
this.log(` Platform: ${tenant.platform.name}`);
|
|
90
89
|
}
|
|
91
90
|
}
|
|
92
91
|
}
|
|
@@ -14,10 +14,4 @@ export default class Pull extends BaseCommand {
|
|
|
14
14
|
verbose: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
15
15
|
};
|
|
16
16
|
run(): Promise<void>;
|
|
17
|
-
/**
|
|
18
|
-
* Sanitize a document name for use as a filename.
|
|
19
|
-
* Strips quotes, replaces spaces with underscores, and removes
|
|
20
|
-
* characters that are unsafe in filenames.
|
|
21
|
-
*/
|
|
22
|
-
private sanitizeFilename;
|
|
23
17
|
}
|