@xano/cli 0.0.30 → 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.js +1 -1
- 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/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/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/pull/index.d.ts +1 -0
- package/dist/commands/workspace/pull/index.js +38 -4
- package/dist/commands/workspace/push/index.d.ts +3 -0
- package/dist/commands/workspace/push/index.js +33 -1
- package/oclif.manifest.json +3006 -1049
- package/package.json +10 -1
|
@@ -0,0 +1,120 @@
|
|
|
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 ReleaseList extends BaseCommand {
|
|
8
|
+
static description = 'List all releases in a workspace';
|
|
9
|
+
static examples = [
|
|
10
|
+
`$ xano release list
|
|
11
|
+
Releases in workspace 5:
|
|
12
|
+
- v1.0 (ID: 10) - main
|
|
13
|
+
- v1.1-hotfix (ID: 11) - main [hotfix]
|
|
14
|
+
`,
|
|
15
|
+
`$ xano release list -w 5 --output json`,
|
|
16
|
+
];
|
|
17
|
+
static flags = {
|
|
18
|
+
...BaseCommand.baseFlags,
|
|
19
|
+
output: Flags.string({
|
|
20
|
+
char: 'o',
|
|
21
|
+
default: 'summary',
|
|
22
|
+
description: 'Output format',
|
|
23
|
+
options: ['summary', 'json'],
|
|
24
|
+
required: false,
|
|
25
|
+
}),
|
|
26
|
+
workspace: Flags.string({
|
|
27
|
+
char: 'w',
|
|
28
|
+
description: 'Workspace ID (uses profile workspace if not provided)',
|
|
29
|
+
required: false,
|
|
30
|
+
}),
|
|
31
|
+
};
|
|
32
|
+
async run() {
|
|
33
|
+
const { flags } = await this.parse(ReleaseList);
|
|
34
|
+
const profileName = flags.profile || this.getDefaultProfile();
|
|
35
|
+
const credentials = this.loadCredentials();
|
|
36
|
+
if (!(profileName in credentials.profiles)) {
|
|
37
|
+
this.error(`Profile '${profileName}' not found. Available profiles: ${Object.keys(credentials.profiles).join(', ')}\n` +
|
|
38
|
+
`Create a profile using 'xano profile create'`);
|
|
39
|
+
}
|
|
40
|
+
const profile = credentials.profiles[profileName];
|
|
41
|
+
if (!profile.instance_origin) {
|
|
42
|
+
this.error(`Profile '${profileName}' is missing instance_origin`);
|
|
43
|
+
}
|
|
44
|
+
if (!profile.access_token) {
|
|
45
|
+
this.error(`Profile '${profileName}' is missing access_token`);
|
|
46
|
+
}
|
|
47
|
+
const workspaceId = flags.workspace || profile.workspace;
|
|
48
|
+
if (!workspaceId) {
|
|
49
|
+
this.error('No workspace ID provided. Use --workspace flag or set one in your profile.');
|
|
50
|
+
}
|
|
51
|
+
const apiUrl = `${profile.instance_origin}/api:meta/workspace/${workspaceId}/release`;
|
|
52
|
+
try {
|
|
53
|
+
const response = await this.verboseFetch(apiUrl, {
|
|
54
|
+
headers: {
|
|
55
|
+
'accept': 'application/json',
|
|
56
|
+
'Authorization': `Bearer ${profile.access_token}`,
|
|
57
|
+
},
|
|
58
|
+
method: 'GET',
|
|
59
|
+
}, flags.verbose, profile.access_token);
|
|
60
|
+
if (!response.ok) {
|
|
61
|
+
const errorText = await response.text();
|
|
62
|
+
this.error(`API request failed with status ${response.status}: ${response.statusText}\n${errorText}`);
|
|
63
|
+
}
|
|
64
|
+
const data = await response.json();
|
|
65
|
+
let releases;
|
|
66
|
+
if (Array.isArray(data)) {
|
|
67
|
+
releases = data;
|
|
68
|
+
}
|
|
69
|
+
else if (data && typeof data === 'object' && 'items' in data && Array.isArray(data.items)) {
|
|
70
|
+
releases = data.items;
|
|
71
|
+
}
|
|
72
|
+
else {
|
|
73
|
+
this.error('Unexpected API response format');
|
|
74
|
+
}
|
|
75
|
+
if (flags.output === 'json') {
|
|
76
|
+
this.log(JSON.stringify(releases, null, 2));
|
|
77
|
+
}
|
|
78
|
+
else {
|
|
79
|
+
if (releases.length === 0) {
|
|
80
|
+
this.log('No releases found');
|
|
81
|
+
}
|
|
82
|
+
else {
|
|
83
|
+
this.log(`Releases in workspace ${workspaceId}:`);
|
|
84
|
+
for (const release of releases) {
|
|
85
|
+
const branch = release.branch ? ` - ${release.branch}` : '';
|
|
86
|
+
const hotfix = release.hotfix ? ' [hotfix]' : '';
|
|
87
|
+
this.log(` - ${release.name} (ID: ${release.id})${branch}${hotfix}`);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
catch (error) {
|
|
93
|
+
if (error instanceof Error) {
|
|
94
|
+
this.error(`Failed to list releases: ${error.message}`);
|
|
95
|
+
}
|
|
96
|
+
else {
|
|
97
|
+
this.error(`Failed to list releases: ${String(error)}`);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
loadCredentials() {
|
|
102
|
+
const configDir = path.join(os.homedir(), '.xano');
|
|
103
|
+
const credentialsPath = path.join(configDir, 'credentials.yaml');
|
|
104
|
+
if (!fs.existsSync(credentialsPath)) {
|
|
105
|
+
this.error(`Credentials file not found at ${credentialsPath}\n` +
|
|
106
|
+
`Create a profile using 'xano profile create'`);
|
|
107
|
+
}
|
|
108
|
+
try {
|
|
109
|
+
const fileContent = fs.readFileSync(credentialsPath, 'utf8');
|
|
110
|
+
const parsed = yaml.load(fileContent);
|
|
111
|
+
if (!parsed || typeof parsed !== 'object' || !('profiles' in parsed)) {
|
|
112
|
+
this.error('Credentials file has invalid format.');
|
|
113
|
+
}
|
|
114
|
+
return parsed;
|
|
115
|
+
}
|
|
116
|
+
catch (error) {
|
|
117
|
+
this.error(`Failed to parse credentials file: ${error}`);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import BaseCommand from '../../../../base-command.js';
|
|
2
|
+
export default class TenantBackupCreate 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
|
+
description: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
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 loadCredentials;
|
|
20
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
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 TenantBackupCreate extends BaseCommand {
|
|
8
|
+
static args = {
|
|
9
|
+
tenant_id: Args.integer({
|
|
10
|
+
description: 'Tenant ID to back up',
|
|
11
|
+
required: true,
|
|
12
|
+
}),
|
|
13
|
+
};
|
|
14
|
+
static description = 'Create a backup for a tenant';
|
|
15
|
+
static examples = [
|
|
16
|
+
`$ xano tenant backup create 42 --description "Pre-deploy backup"
|
|
17
|
+
Created backup #15 for tenant 42
|
|
18
|
+
`,
|
|
19
|
+
`$ xano tenant backup create 42 -d "Daily backup" -o json`,
|
|
20
|
+
];
|
|
21
|
+
static flags = {
|
|
22
|
+
...BaseCommand.baseFlags,
|
|
23
|
+
description: Flags.string({
|
|
24
|
+
char: 'd',
|
|
25
|
+
default: '',
|
|
26
|
+
description: 'Backup description',
|
|
27
|
+
required: false,
|
|
28
|
+
}),
|
|
29
|
+
output: Flags.string({
|
|
30
|
+
char: 'o',
|
|
31
|
+
default: 'summary',
|
|
32
|
+
description: 'Output format',
|
|
33
|
+
options: ['summary', 'json'],
|
|
34
|
+
required: false,
|
|
35
|
+
}),
|
|
36
|
+
workspace: Flags.string({
|
|
37
|
+
char: 'w',
|
|
38
|
+
description: 'Workspace ID (uses profile workspace if not provided)',
|
|
39
|
+
required: false,
|
|
40
|
+
}),
|
|
41
|
+
};
|
|
42
|
+
async run() {
|
|
43
|
+
const { args, flags } = await this.parse(TenantBackupCreate);
|
|
44
|
+
const profileName = flags.profile || this.getDefaultProfile();
|
|
45
|
+
const credentials = this.loadCredentials();
|
|
46
|
+
if (!(profileName in credentials.profiles)) {
|
|
47
|
+
this.error(`Profile '${profileName}' not found. Available profiles: ${Object.keys(credentials.profiles).join(', ')}\n` +
|
|
48
|
+
`Create a profile using 'xano profile create'`);
|
|
49
|
+
}
|
|
50
|
+
const profile = credentials.profiles[profileName];
|
|
51
|
+
if (!profile.instance_origin) {
|
|
52
|
+
this.error(`Profile '${profileName}' is missing instance_origin`);
|
|
53
|
+
}
|
|
54
|
+
if (!profile.access_token) {
|
|
55
|
+
this.error(`Profile '${profileName}' is missing access_token`);
|
|
56
|
+
}
|
|
57
|
+
const workspaceId = flags.workspace || profile.workspace;
|
|
58
|
+
if (!workspaceId) {
|
|
59
|
+
this.error('No workspace ID provided. Use --workspace flag or set one in your profile.');
|
|
60
|
+
}
|
|
61
|
+
const tenantId = args.tenant_id;
|
|
62
|
+
const apiUrl = `${profile.instance_origin}/api:meta/workspace/${workspaceId}/tenant/${tenantId}/backup`;
|
|
63
|
+
try {
|
|
64
|
+
const response = await this.verboseFetch(apiUrl, {
|
|
65
|
+
body: JSON.stringify({ description: flags.description }),
|
|
66
|
+
headers: {
|
|
67
|
+
'accept': 'application/json',
|
|
68
|
+
'Authorization': `Bearer ${profile.access_token}`,
|
|
69
|
+
'Content-Type': 'application/json',
|
|
70
|
+
},
|
|
71
|
+
method: 'POST',
|
|
72
|
+
}, flags.verbose, profile.access_token);
|
|
73
|
+
if (!response.ok) {
|
|
74
|
+
const errorText = await response.text();
|
|
75
|
+
this.error(`API request failed with status ${response.status}: ${response.statusText}\n${errorText}`);
|
|
76
|
+
}
|
|
77
|
+
const result = await response.json();
|
|
78
|
+
if (flags.output === 'json') {
|
|
79
|
+
this.log(JSON.stringify(result, null, 2));
|
|
80
|
+
}
|
|
81
|
+
else {
|
|
82
|
+
this.log(`Created backup #${result.id} for tenant ${tenantId}`);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
catch (error) {
|
|
86
|
+
if (error instanceof Error) {
|
|
87
|
+
this.error(`Failed to create backup: ${error.message}`);
|
|
88
|
+
}
|
|
89
|
+
else {
|
|
90
|
+
this.error(`Failed to create backup: ${String(error)}`);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
loadCredentials() {
|
|
95
|
+
const configDir = path.join(os.homedir(), '.xano');
|
|
96
|
+
const credentialsPath = path.join(configDir, 'credentials.yaml');
|
|
97
|
+
if (!fs.existsSync(credentialsPath)) {
|
|
98
|
+
this.error(`Credentials file not found at ${credentialsPath}\n` +
|
|
99
|
+
`Create a profile using 'xano profile create'`);
|
|
100
|
+
}
|
|
101
|
+
try {
|
|
102
|
+
const fileContent = fs.readFileSync(credentialsPath, 'utf8');
|
|
103
|
+
const parsed = yaml.load(fileContent);
|
|
104
|
+
if (!parsed || typeof parsed !== 'object' || !('profiles' in parsed)) {
|
|
105
|
+
this.error('Credentials file has invalid format.');
|
|
106
|
+
}
|
|
107
|
+
return parsed;
|
|
108
|
+
}
|
|
109
|
+
catch (error) {
|
|
110
|
+
this.error(`Failed to parse credentials file: ${error}`);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import BaseCommand from '../../../../base-command.js';
|
|
2
|
+
export default class TenantBackupDelete 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
|
+
'backup-id': import("@oclif/core/interfaces").OptionFlag<number, import("@oclif/core/interfaces").CustomOptions>;
|
|
13
|
+
force: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
14
|
+
output: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
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 confirm;
|
|
21
|
+
private loadCredentials;
|
|
22
|
+
}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { Args, Flags } from '@oclif/core';
|
|
2
|
+
import * as fs from 'node:fs';
|
|
3
|
+
import * as os from 'node:os';
|
|
4
|
+
import * as path from 'node:path';
|
|
5
|
+
import * as readline from 'node:readline';
|
|
6
|
+
import * as yaml from 'js-yaml';
|
|
7
|
+
import BaseCommand from '../../../../base-command.js';
|
|
8
|
+
export default class TenantBackupDelete extends BaseCommand {
|
|
9
|
+
static args = {
|
|
10
|
+
tenant_id: Args.integer({
|
|
11
|
+
description: 'Tenant ID that owns the backup',
|
|
12
|
+
required: true,
|
|
13
|
+
}),
|
|
14
|
+
};
|
|
15
|
+
static description = 'Delete a tenant backup permanently. This action cannot be undone.';
|
|
16
|
+
static examples = [
|
|
17
|
+
`$ xano tenant backup delete 42 --backup-id 10
|
|
18
|
+
Are you sure you want to delete backup #10? This action cannot be undone. (y/N) y
|
|
19
|
+
Deleted backup #10
|
|
20
|
+
`,
|
|
21
|
+
`$ xano tenant backup delete 42 --backup-id 10 --force`,
|
|
22
|
+
`$ xano tenant backup delete 42 --backup-id 10 -o json`,
|
|
23
|
+
];
|
|
24
|
+
static flags = {
|
|
25
|
+
...BaseCommand.baseFlags,
|
|
26
|
+
'backup-id': Flags.integer({
|
|
27
|
+
description: 'Backup ID to delete',
|
|
28
|
+
required: true,
|
|
29
|
+
}),
|
|
30
|
+
force: Flags.boolean({
|
|
31
|
+
char: 'f',
|
|
32
|
+
default: false,
|
|
33
|
+
description: 'Skip confirmation prompt',
|
|
34
|
+
required: false,
|
|
35
|
+
}),
|
|
36
|
+
output: Flags.string({
|
|
37
|
+
char: 'o',
|
|
38
|
+
default: 'summary',
|
|
39
|
+
description: 'Output format',
|
|
40
|
+
options: ['summary', 'json'],
|
|
41
|
+
required: false,
|
|
42
|
+
}),
|
|
43
|
+
workspace: Flags.string({
|
|
44
|
+
char: 'w',
|
|
45
|
+
description: 'Workspace ID (uses profile workspace if not provided)',
|
|
46
|
+
required: false,
|
|
47
|
+
}),
|
|
48
|
+
};
|
|
49
|
+
async run() {
|
|
50
|
+
const { args, flags } = await this.parse(TenantBackupDelete);
|
|
51
|
+
const profileName = flags.profile || this.getDefaultProfile();
|
|
52
|
+
const credentials = this.loadCredentials();
|
|
53
|
+
if (!(profileName in credentials.profiles)) {
|
|
54
|
+
this.error(`Profile '${profileName}' not found. Available profiles: ${Object.keys(credentials.profiles).join(', ')}\n` +
|
|
55
|
+
`Create a profile using 'xano profile create'`);
|
|
56
|
+
}
|
|
57
|
+
const profile = credentials.profiles[profileName];
|
|
58
|
+
if (!profile.instance_origin) {
|
|
59
|
+
this.error(`Profile '${profileName}' is missing instance_origin`);
|
|
60
|
+
}
|
|
61
|
+
if (!profile.access_token) {
|
|
62
|
+
this.error(`Profile '${profileName}' is missing access_token`);
|
|
63
|
+
}
|
|
64
|
+
const workspaceId = flags.workspace || profile.workspace;
|
|
65
|
+
if (!workspaceId) {
|
|
66
|
+
this.error('No workspace ID provided. Use --workspace flag or set one in your profile.');
|
|
67
|
+
}
|
|
68
|
+
const tenantId = args.tenant_id;
|
|
69
|
+
const backupId = flags['backup-id'];
|
|
70
|
+
if (!flags.force) {
|
|
71
|
+
const confirmed = await this.confirm(`Are you sure you want to delete backup #${backupId}? This action cannot be undone.`);
|
|
72
|
+
if (!confirmed) {
|
|
73
|
+
this.log('Deletion cancelled.');
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
const apiUrl = `${profile.instance_origin}/api:meta/workspace/${workspaceId}/tenant/${tenantId}/backup/${backupId}`;
|
|
78
|
+
try {
|
|
79
|
+
const response = await this.verboseFetch(apiUrl, {
|
|
80
|
+
headers: {
|
|
81
|
+
'accept': 'application/json',
|
|
82
|
+
'Authorization': `Bearer ${profile.access_token}`,
|
|
83
|
+
},
|
|
84
|
+
method: 'DELETE',
|
|
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
|
+
if (flags.output === 'json') {
|
|
91
|
+
this.log(JSON.stringify({ backup_id: backupId, deleted: true, tenant_id: tenantId }, null, 2));
|
|
92
|
+
}
|
|
93
|
+
else {
|
|
94
|
+
this.log(`Deleted backup #${backupId}`);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
catch (error) {
|
|
98
|
+
if (error instanceof Error) {
|
|
99
|
+
this.error(`Failed to delete backup: ${error.message}`);
|
|
100
|
+
}
|
|
101
|
+
else {
|
|
102
|
+
this.error(`Failed to delete backup: ${String(error)}`);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
async confirm(message) {
|
|
107
|
+
const rl = readline.createInterface({
|
|
108
|
+
input: process.stdin,
|
|
109
|
+
output: process.stdout,
|
|
110
|
+
});
|
|
111
|
+
return new Promise((resolve) => {
|
|
112
|
+
rl.question(`${message} (y/N) `, (answer) => {
|
|
113
|
+
rl.close();
|
|
114
|
+
resolve(answer.toLowerCase() === 'y' || answer.toLowerCase() === 'yes');
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
loadCredentials() {
|
|
119
|
+
const configDir = path.join(os.homedir(), '.xano');
|
|
120
|
+
const credentialsPath = path.join(configDir, 'credentials.yaml');
|
|
121
|
+
if (!fs.existsSync(credentialsPath)) {
|
|
122
|
+
this.error(`Credentials file not found at ${credentialsPath}\n` +
|
|
123
|
+
`Create a profile using 'xano profile create'`);
|
|
124
|
+
}
|
|
125
|
+
try {
|
|
126
|
+
const fileContent = fs.readFileSync(credentialsPath, 'utf8');
|
|
127
|
+
const parsed = yaml.load(fileContent);
|
|
128
|
+
if (!parsed || typeof parsed !== 'object' || !('profiles' in parsed)) {
|
|
129
|
+
this.error('Credentials file has invalid format.');
|
|
130
|
+
}
|
|
131
|
+
return parsed;
|
|
132
|
+
}
|
|
133
|
+
catch (error) {
|
|
134
|
+
this.error(`Failed to parse credentials file: ${error}`);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import BaseCommand from '../../../../base-command.js';
|
|
2
|
+
export default class TenantBackupExport 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
|
+
'backup-id': import("@oclif/core/interfaces").OptionFlag<number, import("@oclif/core/interfaces").CustomOptions>;
|
|
13
|
+
format: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
14
|
+
output: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
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,147 @@
|
|
|
1
|
+
import { Args, Flags } from '@oclif/core';
|
|
2
|
+
import * as fs from 'node:fs';
|
|
3
|
+
import * as os from 'node:os';
|
|
4
|
+
import * as path from 'node:path';
|
|
5
|
+
import * as yaml from 'js-yaml';
|
|
6
|
+
import BaseCommand from '../../../../base-command.js';
|
|
7
|
+
export default class TenantBackupExport extends BaseCommand {
|
|
8
|
+
static args = {
|
|
9
|
+
tenant_id: Args.integer({
|
|
10
|
+
description: 'Tenant ID to export backup from',
|
|
11
|
+
required: true,
|
|
12
|
+
}),
|
|
13
|
+
};
|
|
14
|
+
static description = 'Export (download) a tenant backup to a local file';
|
|
15
|
+
static examples = [
|
|
16
|
+
`$ xano tenant backup export 42 --backup-id 10
|
|
17
|
+
Downloaded backup #10 to ./tenant-42-backup-10.tar.gz
|
|
18
|
+
`,
|
|
19
|
+
`$ xano tenant backup export 42 --backup-id 10 --output ./backups/my-backup.tar.gz`,
|
|
20
|
+
`$ xano tenant backup export 42 --backup-id 10 -o json`,
|
|
21
|
+
];
|
|
22
|
+
static flags = {
|
|
23
|
+
...BaseCommand.baseFlags,
|
|
24
|
+
'backup-id': Flags.integer({
|
|
25
|
+
description: 'Backup ID to export',
|
|
26
|
+
required: true,
|
|
27
|
+
}),
|
|
28
|
+
format: Flags.string({
|
|
29
|
+
char: 'o',
|
|
30
|
+
default: 'summary',
|
|
31
|
+
description: 'Output format',
|
|
32
|
+
options: ['summary', 'json'],
|
|
33
|
+
required: false,
|
|
34
|
+
}),
|
|
35
|
+
output: Flags.string({
|
|
36
|
+
description: 'Output file path (defaults to ./tenant-{id}-backup-{backup_id}.tar.gz)',
|
|
37
|
+
required: false,
|
|
38
|
+
}),
|
|
39
|
+
workspace: Flags.string({
|
|
40
|
+
char: 'w',
|
|
41
|
+
description: 'Workspace ID (uses profile workspace if not provided)',
|
|
42
|
+
required: false,
|
|
43
|
+
}),
|
|
44
|
+
};
|
|
45
|
+
async run() {
|
|
46
|
+
const { args, flags } = await this.parse(TenantBackupExport);
|
|
47
|
+
const profileName = flags.profile || this.getDefaultProfile();
|
|
48
|
+
const credentials = this.loadCredentials();
|
|
49
|
+
if (!(profileName in credentials.profiles)) {
|
|
50
|
+
this.error(`Profile '${profileName}' not found. Available profiles: ${Object.keys(credentials.profiles).join(', ')}\n` +
|
|
51
|
+
`Create a profile using 'xano profile create'`);
|
|
52
|
+
}
|
|
53
|
+
const profile = credentials.profiles[profileName];
|
|
54
|
+
if (!profile.instance_origin) {
|
|
55
|
+
this.error(`Profile '${profileName}' is missing instance_origin`);
|
|
56
|
+
}
|
|
57
|
+
if (!profile.access_token) {
|
|
58
|
+
this.error(`Profile '${profileName}' is missing access_token`);
|
|
59
|
+
}
|
|
60
|
+
const workspaceId = flags.workspace || profile.workspace;
|
|
61
|
+
if (!workspaceId) {
|
|
62
|
+
this.error('No workspace ID provided. Use --workspace flag or set one in your profile.');
|
|
63
|
+
}
|
|
64
|
+
const tenantId = args.tenant_id;
|
|
65
|
+
const backupId = flags['backup-id'];
|
|
66
|
+
// Step 1: Get signed download URL
|
|
67
|
+
const exportUrl = `${profile.instance_origin}/api:meta/workspace/${workspaceId}/tenant/${tenantId}/backup/${backupId}/export`;
|
|
68
|
+
try {
|
|
69
|
+
const response = await this.verboseFetch(exportUrl, {
|
|
70
|
+
headers: {
|
|
71
|
+
'accept': 'application/json',
|
|
72
|
+
'Authorization': `Bearer ${profile.access_token}`,
|
|
73
|
+
},
|
|
74
|
+
method: 'GET',
|
|
75
|
+
}, flags.verbose, profile.access_token);
|
|
76
|
+
if (!response.ok) {
|
|
77
|
+
const errorText = await response.text();
|
|
78
|
+
this.error(`API request failed with status ${response.status}: ${response.statusText}\n${errorText}`);
|
|
79
|
+
}
|
|
80
|
+
const exportLink = await response.json();
|
|
81
|
+
if (!exportLink.src) {
|
|
82
|
+
this.error('API did not return a download URL');
|
|
83
|
+
}
|
|
84
|
+
// Step 2: Download the file
|
|
85
|
+
const outputPath = flags.output || `tenant-${tenantId}-backup-${backupId}.tar.gz`;
|
|
86
|
+
const resolvedPath = path.resolve(outputPath);
|
|
87
|
+
const downloadResponse = await fetch(exportLink.src);
|
|
88
|
+
if (!downloadResponse.ok) {
|
|
89
|
+
this.error(`Failed to download backup: ${downloadResponse.status} ${downloadResponse.statusText}`);
|
|
90
|
+
}
|
|
91
|
+
if (!downloadResponse.body) {
|
|
92
|
+
this.error('Download response has no body');
|
|
93
|
+
}
|
|
94
|
+
const fileStream = fs.createWriteStream(resolvedPath);
|
|
95
|
+
const reader = downloadResponse.body.getReader();
|
|
96
|
+
let totalBytes = 0;
|
|
97
|
+
// eslint-disable-next-line no-constant-condition
|
|
98
|
+
while (true) {
|
|
99
|
+
// eslint-disable-next-line no-await-in-loop
|
|
100
|
+
const { done, value } = await reader.read();
|
|
101
|
+
if (done)
|
|
102
|
+
break;
|
|
103
|
+
fileStream.write(value);
|
|
104
|
+
totalBytes += value.length;
|
|
105
|
+
}
|
|
106
|
+
fileStream.end();
|
|
107
|
+
await new Promise((resolve, reject) => {
|
|
108
|
+
fileStream.on('finish', resolve);
|
|
109
|
+
fileStream.on('error', reject);
|
|
110
|
+
});
|
|
111
|
+
if (flags.format === 'json') {
|
|
112
|
+
this.log(JSON.stringify({ backup_id: backupId, bytes: totalBytes, file: resolvedPath, tenant_id: tenantId }, null, 2));
|
|
113
|
+
}
|
|
114
|
+
else {
|
|
115
|
+
const sizeMb = (totalBytes / 1024 / 1024).toFixed(2);
|
|
116
|
+
this.log(`Downloaded backup #${backupId} to ${resolvedPath} (${sizeMb} MB)`);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
catch (error) {
|
|
120
|
+
if (error instanceof Error) {
|
|
121
|
+
this.error(`Failed to export backup: ${error.message}`);
|
|
122
|
+
}
|
|
123
|
+
else {
|
|
124
|
+
this.error(`Failed to export backup: ${String(error)}`);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
loadCredentials() {
|
|
129
|
+
const configDir = path.join(os.homedir(), '.xano');
|
|
130
|
+
const credentialsPath = path.join(configDir, 'credentials.yaml');
|
|
131
|
+
if (!fs.existsSync(credentialsPath)) {
|
|
132
|
+
this.error(`Credentials file not found at ${credentialsPath}\n` +
|
|
133
|
+
`Create a profile using 'xano profile create'`);
|
|
134
|
+
}
|
|
135
|
+
try {
|
|
136
|
+
const fileContent = fs.readFileSync(credentialsPath, 'utf8');
|
|
137
|
+
const parsed = yaml.load(fileContent);
|
|
138
|
+
if (!parsed || typeof parsed !== 'object' || !('profiles' in parsed)) {
|
|
139
|
+
this.error('Credentials file has invalid format.');
|
|
140
|
+
}
|
|
141
|
+
return parsed;
|
|
142
|
+
}
|
|
143
|
+
catch (error) {
|
|
144
|
+
this.error(`Failed to parse credentials file: ${error}`);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import BaseCommand from '../../../../base-command.js';
|
|
2
|
+
export default class TenantBackupImport 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
|
+
description: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
13
|
+
file: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
14
|
+
output: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
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
|
+
}
|