@xano/cli 0.0.31 → 0.0.32
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/dist/base-command.d.ts +5 -0
- package/dist/base-command.js +32 -1
- package/dist/commands/branch/create/index.js +3 -3
- package/dist/commands/branch/delete/index.js +2 -2
- package/dist/commands/branch/edit/index.js +3 -3
- package/dist/commands/branch/get/index.js +2 -2
- package/dist/commands/branch/list/index.js +2 -2
- package/dist/commands/branch/set-live/index.js +2 -2
- package/dist/commands/function/create/index.js +2 -2
- package/dist/commands/function/edit/index.js +2 -2
- package/dist/commands/function/get/index.js +2 -2
- package/dist/commands/function/list/index.js +2 -2
- package/dist/commands/platform/get/index.d.ts +18 -0
- package/dist/commands/platform/get/index.js +126 -0
- package/dist/commands/platform/list/index.d.ts +12 -0
- package/dist/commands/platform/list/index.js +113 -0
- package/dist/commands/profile/me/index.js +2 -2
- package/dist/commands/release/create/index.d.ts +18 -0
- package/dist/commands/release/create/index.js +138 -0
- package/dist/commands/release/delete/index.d.ts +21 -0
- package/dist/commands/release/delete/index.js +134 -0
- package/dist/commands/release/edit/index.d.ts +21 -0
- package/dist/commands/release/edit/index.js +137 -0
- package/dist/commands/release/export/index.d.ts +20 -0
- package/dist/commands/release/export/index.js +142 -0
- package/dist/commands/release/get/index.d.ts +19 -0
- package/dist/commands/release/get/index.js +123 -0
- package/dist/commands/release/import/index.d.ts +15 -0
- package/dist/commands/release/import/index.js +114 -0
- package/dist/commands/release/list/index.d.ts +13 -0
- package/dist/commands/release/list/index.js +120 -0
- package/dist/commands/static_host/build/create/index.js +2 -2
- package/dist/commands/static_host/build/get/index.js +2 -2
- package/dist/commands/static_host/build/list/index.js +2 -2
- package/dist/commands/static_host/list/index.js +2 -2
- package/dist/commands/tenant/backup/create/index.d.ts +20 -0
- package/dist/commands/tenant/backup/create/index.js +113 -0
- package/dist/commands/tenant/backup/delete/index.d.ts +22 -0
- package/dist/commands/tenant/backup/delete/index.js +137 -0
- package/dist/commands/tenant/backup/export/index.d.ts +21 -0
- package/dist/commands/tenant/backup/export/index.js +147 -0
- package/dist/commands/tenant/backup/import/index.d.ts +21 -0
- package/dist/commands/tenant/backup/import/index.js +127 -0
- package/dist/commands/tenant/backup/list/index.d.ts +20 -0
- package/dist/commands/tenant/backup/list/index.js +137 -0
- package/dist/commands/tenant/backup/restore/index.d.ts +22 -0
- package/dist/commands/tenant/backup/restore/index.js +141 -0
- package/dist/commands/tenant/create/index.d.ts +21 -0
- package/dist/commands/tenant/create/index.js +155 -0
- package/dist/commands/tenant/delete/index.d.ts +21 -0
- package/dist/commands/tenant/delete/index.js +134 -0
- package/dist/commands/tenant/deploy-platform/index.d.ts +20 -0
- package/dist/commands/tenant/deploy-platform/index.js +116 -0
- package/dist/commands/tenant/deploy-release/index.d.ts +20 -0
- package/dist/commands/tenant/deploy-release/index.js +116 -0
- package/dist/commands/tenant/edit/index.d.ts +26 -0
- package/dist/commands/tenant/edit/index.js +167 -0
- package/dist/commands/tenant/get/index.d.ts +19 -0
- package/dist/commands/tenant/get/index.js +135 -0
- package/dist/commands/tenant/list/index.d.ts +13 -0
- package/dist/commands/tenant/list/index.js +123 -0
- package/dist/commands/workspace/create/index.js +3 -3
- package/dist/commands/workspace/delete/index.js +2 -2
- package/dist/commands/workspace/edit/index.js +3 -3
- package/dist/commands/workspace/get/index.js +2 -2
- package/dist/commands/workspace/list/index.js +2 -2
- package/dist/commands/workspace/pull/index.d.ts +1 -0
- package/dist/commands/workspace/pull/index.js +45 -10
- package/dist/commands/workspace/push/index.d.ts +3 -0
- package/dist/commands/workspace/push/index.js +41 -8
- package/oclif.manifest.json +3213 -1256
- package/package.json +10 -1
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { Args, Flags } from '@oclif/core';
|
|
2
|
+
import * as yaml from 'js-yaml';
|
|
3
|
+
import * as fs from 'node:fs';
|
|
4
|
+
import * as os from 'node:os';
|
|
5
|
+
import * as path from 'node:path';
|
|
6
|
+
import BaseCommand from '../../../../base-command.js';
|
|
7
|
+
export default class TenantRestore extends BaseCommand {
|
|
8
|
+
static args = {
|
|
9
|
+
tenant_id: Args.integer({
|
|
10
|
+
description: 'Tenant ID to restore',
|
|
11
|
+
required: true,
|
|
12
|
+
}),
|
|
13
|
+
};
|
|
14
|
+
static description = 'Restore a tenant from a backup. This replaces the current tenant data.';
|
|
15
|
+
static examples = [
|
|
16
|
+
`$ xano tenant backup restore 42 --backup-id 10
|
|
17
|
+
Are you sure you want to restore tenant 42 from backup 10? This will replace current data. (y/N) y
|
|
18
|
+
Restored tenant 42 from backup #10
|
|
19
|
+
`,
|
|
20
|
+
`$ xano tenant backup restore 42 --backup-id 10 --force -o json`,
|
|
21
|
+
];
|
|
22
|
+
static flags = {
|
|
23
|
+
...BaseCommand.baseFlags,
|
|
24
|
+
'backup-id': Flags.integer({
|
|
25
|
+
description: 'Backup ID to restore from',
|
|
26
|
+
required: true,
|
|
27
|
+
}),
|
|
28
|
+
force: Flags.boolean({
|
|
29
|
+
char: 'f',
|
|
30
|
+
default: false,
|
|
31
|
+
description: 'Skip confirmation prompt',
|
|
32
|
+
required: false,
|
|
33
|
+
}),
|
|
34
|
+
output: Flags.string({
|
|
35
|
+
char: 'o',
|
|
36
|
+
default: 'summary',
|
|
37
|
+
description: 'Output format',
|
|
38
|
+
options: ['summary', 'json'],
|
|
39
|
+
required: false,
|
|
40
|
+
}),
|
|
41
|
+
workspace: Flags.string({
|
|
42
|
+
char: 'w',
|
|
43
|
+
description: 'Workspace ID (uses profile workspace if not provided)',
|
|
44
|
+
required: false,
|
|
45
|
+
}),
|
|
46
|
+
};
|
|
47
|
+
async run() {
|
|
48
|
+
const { args, flags } = await this.parse(TenantRestore);
|
|
49
|
+
const profileName = flags.profile || this.getDefaultProfile();
|
|
50
|
+
const credentials = this.loadCredentials();
|
|
51
|
+
if (!(profileName in credentials.profiles)) {
|
|
52
|
+
this.error(`Profile '${profileName}' not found. Available profiles: ${Object.keys(credentials.profiles).join(', ')}\n` +
|
|
53
|
+
`Create a profile using 'xano profile create'`);
|
|
54
|
+
}
|
|
55
|
+
const profile = credentials.profiles[profileName];
|
|
56
|
+
if (!profile.instance_origin) {
|
|
57
|
+
this.error(`Profile '${profileName}' is missing instance_origin`);
|
|
58
|
+
}
|
|
59
|
+
if (!profile.access_token) {
|
|
60
|
+
this.error(`Profile '${profileName}' is missing access_token`);
|
|
61
|
+
}
|
|
62
|
+
const workspaceId = flags.workspace || profile.workspace;
|
|
63
|
+
if (!workspaceId) {
|
|
64
|
+
this.error('No workspace ID provided. Use --workspace flag or set one in your profile.');
|
|
65
|
+
}
|
|
66
|
+
const tenantId = args.tenant_id;
|
|
67
|
+
const backupId = flags['backup-id'];
|
|
68
|
+
if (!flags.force) {
|
|
69
|
+
const confirmed = await this.confirm(`Are you sure you want to restore tenant ${tenantId} from backup ${backupId}? This will replace current data.`);
|
|
70
|
+
if (!confirmed) {
|
|
71
|
+
this.log('Restore cancelled.');
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
const apiUrl = `${profile.instance_origin}/api:meta/workspace/${workspaceId}/tenant/${tenantId}/restore`;
|
|
76
|
+
try {
|
|
77
|
+
const response = await this.verboseFetch(apiUrl, {
|
|
78
|
+
body: JSON.stringify({ backup_id: backupId }),
|
|
79
|
+
headers: {
|
|
80
|
+
'accept': 'application/json',
|
|
81
|
+
'Authorization': `Bearer ${profile.access_token}`,
|
|
82
|
+
'Content-Type': 'application/json',
|
|
83
|
+
},
|
|
84
|
+
method: 'POST',
|
|
85
|
+
}, flags.verbose, profile.access_token);
|
|
86
|
+
if (!response.ok) {
|
|
87
|
+
const errorText = await response.text();
|
|
88
|
+
this.error(`API request failed with status ${response.status}: ${response.statusText}\n${errorText}`);
|
|
89
|
+
}
|
|
90
|
+
const tenant = await response.json();
|
|
91
|
+
if (flags.output === 'json') {
|
|
92
|
+
this.log(JSON.stringify(tenant, null, 2));
|
|
93
|
+
}
|
|
94
|
+
else {
|
|
95
|
+
this.log(`Restored tenant ${tenantId} from backup #${backupId}`);
|
|
96
|
+
if (tenant.state)
|
|
97
|
+
this.log(` State: ${tenant.state}`);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
catch (error) {
|
|
101
|
+
if (error instanceof Error) {
|
|
102
|
+
this.error(`Failed to restore tenant: ${error.message}`);
|
|
103
|
+
}
|
|
104
|
+
else {
|
|
105
|
+
this.error(`Failed to restore tenant: ${String(error)}`);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
async confirm(message) {
|
|
110
|
+
const readline = await import('node:readline');
|
|
111
|
+
const rl = readline.createInterface({
|
|
112
|
+
input: process.stdin,
|
|
113
|
+
output: process.stdout,
|
|
114
|
+
});
|
|
115
|
+
return new Promise((resolve) => {
|
|
116
|
+
rl.question(`${message} (y/N) `, (answer) => {
|
|
117
|
+
rl.close();
|
|
118
|
+
resolve(answer.toLowerCase() === 'y' || answer.toLowerCase() === 'yes');
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
loadCredentials() {
|
|
123
|
+
const configDir = path.join(os.homedir(), '.xano');
|
|
124
|
+
const credentialsPath = path.join(configDir, 'credentials.yaml');
|
|
125
|
+
if (!fs.existsSync(credentialsPath)) {
|
|
126
|
+
this.error(`Credentials file not found at ${credentialsPath}\n` +
|
|
127
|
+
`Create a profile using 'xano profile create'`);
|
|
128
|
+
}
|
|
129
|
+
try {
|
|
130
|
+
const fileContent = fs.readFileSync(credentialsPath, 'utf8');
|
|
131
|
+
const parsed = yaml.load(fileContent);
|
|
132
|
+
if (!parsed || typeof parsed !== 'object' || !('profiles' in parsed)) {
|
|
133
|
+
this.error('Credentials file has invalid format.');
|
|
134
|
+
}
|
|
135
|
+
return parsed;
|
|
136
|
+
}
|
|
137
|
+
catch (error) {
|
|
138
|
+
this.error(`Failed to parse credentials file: ${error}`);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import BaseCommand from '../../../base-command.js';
|
|
2
|
+
export default class TenantCreate extends BaseCommand {
|
|
3
|
+
static description: string;
|
|
4
|
+
static examples: string[];
|
|
5
|
+
static flags: {
|
|
6
|
+
'cluster-id': import("@oclif/core/interfaces").OptionFlag<number | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
7
|
+
description: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
8
|
+
display: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
9
|
+
domain: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
10
|
+
ingress: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
11
|
+
license: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
12
|
+
output: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
13
|
+
'platform-id': import("@oclif/core/interfaces").OptionFlag<number | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
14
|
+
tasks: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
15
|
+
workspace: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
16
|
+
profile: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
17
|
+
verbose: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
18
|
+
};
|
|
19
|
+
run(): Promise<void>;
|
|
20
|
+
private loadCredentials;
|
|
21
|
+
}
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import { Flags } from '@oclif/core';
|
|
2
|
+
import * as yaml from 'js-yaml';
|
|
3
|
+
import * as fs from 'node:fs';
|
|
4
|
+
import * as os from 'node:os';
|
|
5
|
+
import * as path from 'node:path';
|
|
6
|
+
import BaseCommand from '../../../base-command.js';
|
|
7
|
+
export default class TenantCreate extends BaseCommand {
|
|
8
|
+
static description = 'Create a new tenant in a workspace';
|
|
9
|
+
static examples = [
|
|
10
|
+
`$ xano tenant create --display "Production"
|
|
11
|
+
Created tenant: Production (production) - ID: 42
|
|
12
|
+
`,
|
|
13
|
+
`$ xano tenant create --display "Staging" --description "Staging env" --cluster-id 1 --platform-id 1 --license tier2 -o json`,
|
|
14
|
+
];
|
|
15
|
+
static flags = {
|
|
16
|
+
...BaseCommand.baseFlags,
|
|
17
|
+
'cluster-id': Flags.integer({
|
|
18
|
+
description: 'Cluster ID to deploy to (required for tier2/tier3)',
|
|
19
|
+
required: false,
|
|
20
|
+
}),
|
|
21
|
+
description: Flags.string({
|
|
22
|
+
char: 'd',
|
|
23
|
+
description: 'Tenant description',
|
|
24
|
+
required: false,
|
|
25
|
+
}),
|
|
26
|
+
display: Flags.string({
|
|
27
|
+
description: 'Display name for the tenant',
|
|
28
|
+
required: true,
|
|
29
|
+
}),
|
|
30
|
+
domain: Flags.string({
|
|
31
|
+
description: 'Custom domain for the tenant',
|
|
32
|
+
required: false,
|
|
33
|
+
}),
|
|
34
|
+
ingress: Flags.boolean({
|
|
35
|
+
allowNo: true,
|
|
36
|
+
default: true,
|
|
37
|
+
description: 'Enable ingress',
|
|
38
|
+
}),
|
|
39
|
+
license: Flags.string({
|
|
40
|
+
default: 'tier1',
|
|
41
|
+
description: 'License tier',
|
|
42
|
+
options: ['tier1', 'tier2', 'tier3'],
|
|
43
|
+
required: false,
|
|
44
|
+
}),
|
|
45
|
+
output: Flags.string({
|
|
46
|
+
char: 'o',
|
|
47
|
+
default: 'summary',
|
|
48
|
+
description: 'Output format',
|
|
49
|
+
options: ['summary', 'json'],
|
|
50
|
+
required: false,
|
|
51
|
+
}),
|
|
52
|
+
'platform-id': Flags.integer({
|
|
53
|
+
description: 'Platform ID to use',
|
|
54
|
+
required: false,
|
|
55
|
+
}),
|
|
56
|
+
tasks: Flags.boolean({
|
|
57
|
+
allowNo: true,
|
|
58
|
+
default: true,
|
|
59
|
+
description: 'Enable background tasks',
|
|
60
|
+
}),
|
|
61
|
+
workspace: Flags.string({
|
|
62
|
+
char: 'w',
|
|
63
|
+
description: 'Workspace ID (uses profile workspace if not provided)',
|
|
64
|
+
required: false,
|
|
65
|
+
}),
|
|
66
|
+
};
|
|
67
|
+
async run() {
|
|
68
|
+
const { flags } = await this.parse(TenantCreate);
|
|
69
|
+
const profileName = flags.profile || this.getDefaultProfile();
|
|
70
|
+
const credentials = this.loadCredentials();
|
|
71
|
+
if (!(profileName in credentials.profiles)) {
|
|
72
|
+
this.error(`Profile '${profileName}' not found. Available profiles: ${Object.keys(credentials.profiles).join(', ')}\n` +
|
|
73
|
+
`Create a profile using 'xano profile create'`);
|
|
74
|
+
}
|
|
75
|
+
const profile = credentials.profiles[profileName];
|
|
76
|
+
if (!profile.instance_origin) {
|
|
77
|
+
this.error(`Profile '${profileName}' is missing instance_origin`);
|
|
78
|
+
}
|
|
79
|
+
if (!profile.access_token) {
|
|
80
|
+
this.error(`Profile '${profileName}' is missing access_token`);
|
|
81
|
+
}
|
|
82
|
+
const workspaceId = flags.workspace || profile.workspace;
|
|
83
|
+
if (!workspaceId) {
|
|
84
|
+
this.error('No workspace ID provided. Use --workspace flag or set one in your profile.');
|
|
85
|
+
}
|
|
86
|
+
const body = {
|
|
87
|
+
display: flags.display,
|
|
88
|
+
ingress: flags.ingress,
|
|
89
|
+
license: flags.license,
|
|
90
|
+
tag: [],
|
|
91
|
+
tasks: flags.tasks,
|
|
92
|
+
};
|
|
93
|
+
if (flags.description)
|
|
94
|
+
body.description = flags.description;
|
|
95
|
+
if (flags['cluster-id'])
|
|
96
|
+
body.cluster_id = flags['cluster-id'];
|
|
97
|
+
if (flags['platform-id'])
|
|
98
|
+
body.platform_id = flags['platform-id'];
|
|
99
|
+
if (flags.domain)
|
|
100
|
+
body.domain = flags.domain;
|
|
101
|
+
const apiUrl = `${profile.instance_origin}/api:meta/workspace/${workspaceId}/tenant`;
|
|
102
|
+
try {
|
|
103
|
+
const response = await this.verboseFetch(apiUrl, {
|
|
104
|
+
body: JSON.stringify(body),
|
|
105
|
+
headers: {
|
|
106
|
+
'accept': 'application/json',
|
|
107
|
+
'Authorization': `Bearer ${profile.access_token}`,
|
|
108
|
+
'Content-Type': 'application/json',
|
|
109
|
+
},
|
|
110
|
+
method: 'POST',
|
|
111
|
+
}, flags.verbose, profile.access_token);
|
|
112
|
+
if (!response.ok) {
|
|
113
|
+
const errorText = await response.text();
|
|
114
|
+
this.error(`API request failed with status ${response.status}: ${response.statusText}\n${errorText}`);
|
|
115
|
+
}
|
|
116
|
+
const tenant = await response.json();
|
|
117
|
+
if (flags.output === 'json') {
|
|
118
|
+
this.log(JSON.stringify(tenant, null, 2));
|
|
119
|
+
}
|
|
120
|
+
else {
|
|
121
|
+
this.log(`Created tenant: ${tenant.display || tenant.name} (${tenant.name}) - ID: ${tenant.id}`);
|
|
122
|
+
if (tenant.state) {
|
|
123
|
+
this.log(` State: ${tenant.state}`);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
catch (error) {
|
|
128
|
+
if (error instanceof Error) {
|
|
129
|
+
this.error(`Failed to create tenant: ${error.message}`);
|
|
130
|
+
}
|
|
131
|
+
else {
|
|
132
|
+
this.error(`Failed to create tenant: ${String(error)}`);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
loadCredentials() {
|
|
137
|
+
const configDir = path.join(os.homedir(), '.xano');
|
|
138
|
+
const credentialsPath = path.join(configDir, 'credentials.yaml');
|
|
139
|
+
if (!fs.existsSync(credentialsPath)) {
|
|
140
|
+
this.error(`Credentials file not found at ${credentialsPath}\n` +
|
|
141
|
+
`Create a profile using 'xano profile create'`);
|
|
142
|
+
}
|
|
143
|
+
try {
|
|
144
|
+
const fileContent = fs.readFileSync(credentialsPath, 'utf8');
|
|
145
|
+
const parsed = yaml.load(fileContent);
|
|
146
|
+
if (!parsed || typeof parsed !== 'object' || !('profiles' in parsed)) {
|
|
147
|
+
this.error('Credentials file has invalid format.');
|
|
148
|
+
}
|
|
149
|
+
return parsed;
|
|
150
|
+
}
|
|
151
|
+
catch (error) {
|
|
152
|
+
this.error(`Failed to parse credentials file: ${error}`);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import BaseCommand from '../../../base-command.js';
|
|
2
|
+
export default class TenantDelete extends BaseCommand {
|
|
3
|
+
static args: {
|
|
4
|
+
tenant_id: import("@oclif/core/interfaces").Arg<number, {
|
|
5
|
+
max?: number;
|
|
6
|
+
min?: number;
|
|
7
|
+
}>;
|
|
8
|
+
};
|
|
9
|
+
static description: string;
|
|
10
|
+
static examples: string[];
|
|
11
|
+
static flags: {
|
|
12
|
+
force: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
13
|
+
output: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
14
|
+
workspace: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
15
|
+
profile: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
16
|
+
verbose: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
17
|
+
};
|
|
18
|
+
run(): Promise<void>;
|
|
19
|
+
private confirm;
|
|
20
|
+
private loadCredentials;
|
|
21
|
+
}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { Args, Flags } from '@oclif/core';
|
|
2
|
+
import * as yaml from 'js-yaml';
|
|
3
|
+
import * as fs from 'node:fs';
|
|
4
|
+
import * as os from 'node:os';
|
|
5
|
+
import * as path from 'node:path';
|
|
6
|
+
import BaseCommand from '../../../base-command.js';
|
|
7
|
+
export default class TenantDelete extends BaseCommand {
|
|
8
|
+
static args = {
|
|
9
|
+
tenant_id: Args.integer({
|
|
10
|
+
description: 'Tenant ID to delete',
|
|
11
|
+
required: true,
|
|
12
|
+
}),
|
|
13
|
+
};
|
|
14
|
+
static description = 'Delete a tenant permanently. This destroys all associated infrastructure and cannot be undone.';
|
|
15
|
+
static examples = [
|
|
16
|
+
`$ xano tenant delete 42
|
|
17
|
+
Are you sure you want to delete tenant 42? This action cannot be undone. (y/N) y
|
|
18
|
+
Deleted tenant 42
|
|
19
|
+
`,
|
|
20
|
+
`$ xano tenant delete 42 --force
|
|
21
|
+
Deleted tenant 42
|
|
22
|
+
`,
|
|
23
|
+
`$ xano tenant delete 42 -f -o json`,
|
|
24
|
+
];
|
|
25
|
+
static flags = {
|
|
26
|
+
...BaseCommand.baseFlags,
|
|
27
|
+
force: Flags.boolean({
|
|
28
|
+
char: 'f',
|
|
29
|
+
default: false,
|
|
30
|
+
description: 'Skip confirmation prompt',
|
|
31
|
+
required: false,
|
|
32
|
+
}),
|
|
33
|
+
output: Flags.string({
|
|
34
|
+
char: 'o',
|
|
35
|
+
default: 'summary',
|
|
36
|
+
description: 'Output format',
|
|
37
|
+
options: ['summary', 'json'],
|
|
38
|
+
required: false,
|
|
39
|
+
}),
|
|
40
|
+
workspace: Flags.string({
|
|
41
|
+
char: 'w',
|
|
42
|
+
description: 'Workspace ID (uses profile workspace if not provided)',
|
|
43
|
+
required: false,
|
|
44
|
+
}),
|
|
45
|
+
};
|
|
46
|
+
async run() {
|
|
47
|
+
const { args, flags } = await this.parse(TenantDelete);
|
|
48
|
+
const profileName = flags.profile || this.getDefaultProfile();
|
|
49
|
+
const credentials = this.loadCredentials();
|
|
50
|
+
if (!(profileName in credentials.profiles)) {
|
|
51
|
+
this.error(`Profile '${profileName}' not found. Available profiles: ${Object.keys(credentials.profiles).join(', ')}\n` +
|
|
52
|
+
`Create a profile using 'xano profile create'`);
|
|
53
|
+
}
|
|
54
|
+
const profile = credentials.profiles[profileName];
|
|
55
|
+
if (!profile.instance_origin) {
|
|
56
|
+
this.error(`Profile '${profileName}' is missing instance_origin`);
|
|
57
|
+
}
|
|
58
|
+
if (!profile.access_token) {
|
|
59
|
+
this.error(`Profile '${profileName}' is missing access_token`);
|
|
60
|
+
}
|
|
61
|
+
const workspaceId = flags.workspace || profile.workspace;
|
|
62
|
+
if (!workspaceId) {
|
|
63
|
+
this.error('No workspace ID provided. Use --workspace flag or set one in your profile.');
|
|
64
|
+
}
|
|
65
|
+
const tenantId = args.tenant_id;
|
|
66
|
+
if (!flags.force) {
|
|
67
|
+
const confirmed = await this.confirm(`Are you sure you want to delete tenant ${tenantId}? This action cannot be undone.`);
|
|
68
|
+
if (!confirmed) {
|
|
69
|
+
this.log('Deletion cancelled.');
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
const apiUrl = `${profile.instance_origin}/api:meta/workspace/${workspaceId}/tenant/${tenantId}`;
|
|
74
|
+
try {
|
|
75
|
+
const response = await this.verboseFetch(apiUrl, {
|
|
76
|
+
headers: {
|
|
77
|
+
'accept': 'application/json',
|
|
78
|
+
'Authorization': `Bearer ${profile.access_token}`,
|
|
79
|
+
},
|
|
80
|
+
method: 'DELETE',
|
|
81
|
+
}, flags.verbose, profile.access_token);
|
|
82
|
+
if (!response.ok) {
|
|
83
|
+
const errorText = await response.text();
|
|
84
|
+
this.error(`API request failed with status ${response.status}: ${response.statusText}\n${errorText}`);
|
|
85
|
+
}
|
|
86
|
+
if (flags.output === 'json') {
|
|
87
|
+
this.log(JSON.stringify({ deleted: true, tenant_id: tenantId }, null, 2));
|
|
88
|
+
}
|
|
89
|
+
else {
|
|
90
|
+
this.log(`Deleted tenant ${tenantId}`);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
catch (error) {
|
|
94
|
+
if (error instanceof Error) {
|
|
95
|
+
this.error(`Failed to delete tenant: ${error.message}`);
|
|
96
|
+
}
|
|
97
|
+
else {
|
|
98
|
+
this.error(`Failed to delete tenant: ${String(error)}`);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
async confirm(message) {
|
|
103
|
+
const readline = await import('node:readline');
|
|
104
|
+
const rl = readline.createInterface({
|
|
105
|
+
input: process.stdin,
|
|
106
|
+
output: process.stdout,
|
|
107
|
+
});
|
|
108
|
+
return new Promise((resolve) => {
|
|
109
|
+
rl.question(`${message} (y/N) `, (answer) => {
|
|
110
|
+
rl.close();
|
|
111
|
+
resolve(answer.toLowerCase() === 'y' || answer.toLowerCase() === 'yes');
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
loadCredentials() {
|
|
116
|
+
const configDir = path.join(os.homedir(), '.xano');
|
|
117
|
+
const credentialsPath = path.join(configDir, 'credentials.yaml');
|
|
118
|
+
if (!fs.existsSync(credentialsPath)) {
|
|
119
|
+
this.error(`Credentials file not found at ${credentialsPath}\n` +
|
|
120
|
+
`Create a profile using 'xano profile create'`);
|
|
121
|
+
}
|
|
122
|
+
try {
|
|
123
|
+
const fileContent = fs.readFileSync(credentialsPath, 'utf8');
|
|
124
|
+
const parsed = yaml.load(fileContent);
|
|
125
|
+
if (!parsed || typeof parsed !== 'object' || !('profiles' in parsed)) {
|
|
126
|
+
this.error('Credentials file has invalid format.');
|
|
127
|
+
}
|
|
128
|
+
return parsed;
|
|
129
|
+
}
|
|
130
|
+
catch (error) {
|
|
131
|
+
this.error(`Failed to parse credentials file: ${error}`);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import BaseCommand from '../../../base-command.js';
|
|
2
|
+
export default class TenantDeployPlatform extends BaseCommand {
|
|
3
|
+
static args: {
|
|
4
|
+
tenant_id: import("@oclif/core/interfaces").Arg<number, {
|
|
5
|
+
max?: number;
|
|
6
|
+
min?: number;
|
|
7
|
+
}>;
|
|
8
|
+
};
|
|
9
|
+
static description: string;
|
|
10
|
+
static examples: string[];
|
|
11
|
+
static flags: {
|
|
12
|
+
output: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
13
|
+
'platform-id': import("@oclif/core/interfaces").OptionFlag<number, import("@oclif/core/interfaces").CustomOptions>;
|
|
14
|
+
workspace: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
15
|
+
profile: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
16
|
+
verbose: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
17
|
+
};
|
|
18
|
+
run(): Promise<void>;
|
|
19
|
+
private loadCredentials;
|
|
20
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { Args, Flags } from '@oclif/core';
|
|
2
|
+
import * as yaml from 'js-yaml';
|
|
3
|
+
import * as fs from 'node:fs';
|
|
4
|
+
import * as os from 'node:os';
|
|
5
|
+
import * as path from 'node:path';
|
|
6
|
+
import BaseCommand from '../../../base-command.js';
|
|
7
|
+
export default class TenantDeployPlatform extends BaseCommand {
|
|
8
|
+
static args = {
|
|
9
|
+
tenant_id: Args.integer({
|
|
10
|
+
description: 'Tenant ID to deploy to',
|
|
11
|
+
required: true,
|
|
12
|
+
}),
|
|
13
|
+
};
|
|
14
|
+
static description = 'Deploy a platform version to a tenant';
|
|
15
|
+
static examples = [
|
|
16
|
+
`$ xano tenant deploy-platform 42 --platform-id 5
|
|
17
|
+
Deployed platform 5 to tenant 42
|
|
18
|
+
`,
|
|
19
|
+
`$ xano tenant deploy-platform 42 --platform-id 5 -o json`,
|
|
20
|
+
];
|
|
21
|
+
static flags = {
|
|
22
|
+
...BaseCommand.baseFlags,
|
|
23
|
+
output: Flags.string({
|
|
24
|
+
char: 'o',
|
|
25
|
+
default: 'summary',
|
|
26
|
+
description: 'Output format',
|
|
27
|
+
options: ['summary', 'json'],
|
|
28
|
+
required: false,
|
|
29
|
+
}),
|
|
30
|
+
'platform-id': Flags.integer({
|
|
31
|
+
description: 'Platform ID to deploy',
|
|
32
|
+
required: true,
|
|
33
|
+
}),
|
|
34
|
+
workspace: Flags.string({
|
|
35
|
+
char: 'w',
|
|
36
|
+
description: 'Workspace ID (uses profile workspace if not provided)',
|
|
37
|
+
required: false,
|
|
38
|
+
}),
|
|
39
|
+
};
|
|
40
|
+
async run() {
|
|
41
|
+
const { args, flags } = await this.parse(TenantDeployPlatform);
|
|
42
|
+
const profileName = flags.profile || this.getDefaultProfile();
|
|
43
|
+
const credentials = this.loadCredentials();
|
|
44
|
+
if (!(profileName in credentials.profiles)) {
|
|
45
|
+
this.error(`Profile '${profileName}' not found. Available profiles: ${Object.keys(credentials.profiles).join(', ')}\n` +
|
|
46
|
+
`Create a profile using 'xano profile create'`);
|
|
47
|
+
}
|
|
48
|
+
const profile = credentials.profiles[profileName];
|
|
49
|
+
if (!profile.instance_origin) {
|
|
50
|
+
this.error(`Profile '${profileName}' is missing instance_origin`);
|
|
51
|
+
}
|
|
52
|
+
if (!profile.access_token) {
|
|
53
|
+
this.error(`Profile '${profileName}' is missing access_token`);
|
|
54
|
+
}
|
|
55
|
+
const workspaceId = flags.workspace || profile.workspace;
|
|
56
|
+
if (!workspaceId) {
|
|
57
|
+
this.error('No workspace ID provided. Use --workspace flag or set one in your profile.');
|
|
58
|
+
}
|
|
59
|
+
const tenantId = args.tenant_id;
|
|
60
|
+
const platformId = flags['platform-id'];
|
|
61
|
+
const apiUrl = `${profile.instance_origin}/api:meta/workspace/${workspaceId}/tenant/${tenantId}/platform/deploy`;
|
|
62
|
+
try {
|
|
63
|
+
const response = await this.verboseFetch(apiUrl, {
|
|
64
|
+
body: JSON.stringify({ platform_id: platformId }),
|
|
65
|
+
headers: {
|
|
66
|
+
'accept': 'application/json',
|
|
67
|
+
'Authorization': `Bearer ${profile.access_token}`,
|
|
68
|
+
'Content-Type': 'application/json',
|
|
69
|
+
},
|
|
70
|
+
method: 'POST',
|
|
71
|
+
}, flags.verbose, profile.access_token);
|
|
72
|
+
if (!response.ok) {
|
|
73
|
+
const errorText = await response.text();
|
|
74
|
+
this.error(`API request failed with status ${response.status}: ${response.statusText}\n${errorText}`);
|
|
75
|
+
}
|
|
76
|
+
const tenant = await response.json();
|
|
77
|
+
if (flags.output === 'json') {
|
|
78
|
+
this.log(JSON.stringify(tenant, null, 2));
|
|
79
|
+
}
|
|
80
|
+
else {
|
|
81
|
+
this.log(`Deployed platform ${platformId} to tenant: ${tenant.display || tenant.name} (${tenant.name})`);
|
|
82
|
+
if (tenant.state)
|
|
83
|
+
this.log(` State: ${tenant.state}`);
|
|
84
|
+
if (tenant.platform?.name)
|
|
85
|
+
this.log(` Platform: ${tenant.platform.name}`);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
catch (error) {
|
|
89
|
+
if (error instanceof Error) {
|
|
90
|
+
this.error(`Failed to deploy platform to tenant: ${error.message}`);
|
|
91
|
+
}
|
|
92
|
+
else {
|
|
93
|
+
this.error(`Failed to deploy platform to tenant: ${String(error)}`);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
loadCredentials() {
|
|
98
|
+
const configDir = path.join(os.homedir(), '.xano');
|
|
99
|
+
const credentialsPath = path.join(configDir, 'credentials.yaml');
|
|
100
|
+
if (!fs.existsSync(credentialsPath)) {
|
|
101
|
+
this.error(`Credentials file not found at ${credentialsPath}\n` +
|
|
102
|
+
`Create a profile using 'xano profile create'`);
|
|
103
|
+
}
|
|
104
|
+
try {
|
|
105
|
+
const fileContent = fs.readFileSync(credentialsPath, 'utf8');
|
|
106
|
+
const parsed = yaml.load(fileContent);
|
|
107
|
+
if (!parsed || typeof parsed !== 'object' || !('profiles' in parsed)) {
|
|
108
|
+
this.error('Credentials file has invalid format.');
|
|
109
|
+
}
|
|
110
|
+
return parsed;
|
|
111
|
+
}
|
|
112
|
+
catch (error) {
|
|
113
|
+
this.error(`Failed to parse credentials file: ${error}`);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import BaseCommand from '../../../base-command.js';
|
|
2
|
+
export default class TenantDeployRelease extends BaseCommand {
|
|
3
|
+
static args: {
|
|
4
|
+
tenant_id: import("@oclif/core/interfaces").Arg<number, {
|
|
5
|
+
max?: number;
|
|
6
|
+
min?: number;
|
|
7
|
+
}>;
|
|
8
|
+
};
|
|
9
|
+
static description: string;
|
|
10
|
+
static examples: string[];
|
|
11
|
+
static flags: {
|
|
12
|
+
output: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
13
|
+
'release-id': import("@oclif/core/interfaces").OptionFlag<number, import("@oclif/core/interfaces").CustomOptions>;
|
|
14
|
+
workspace: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
15
|
+
profile: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
16
|
+
verbose: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
17
|
+
};
|
|
18
|
+
run(): Promise<void>;
|
|
19
|
+
private loadCredentials;
|
|
20
|
+
}
|