@xano/cli 0.0.37 → 0.0.40
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 +325 -102
- package/dist/commands/auth/index.d.ts +0 -2
- package/dist/commands/auth/index.js +2 -55
- package/dist/commands/profile/create/index.d.ts +0 -2
- package/dist/commands/profile/create/index.js +0 -15
- package/dist/commands/profile/edit/index.d.ts +0 -4
- package/dist/commands/profile/edit/index.js +7 -38
- package/dist/commands/profile/wizard/index.d.ts +0 -2
- package/dist/commands/profile/wizard/index.js +0 -106
- package/dist/commands/profile/{project → workspace}/index.d.ts +1 -1
- package/dist/commands/profile/{project → workspace}/index.js +10 -10
- package/dist/commands/release/delete/index.d.ts +2 -4
- package/dist/commands/release/delete/index.js +39 -12
- package/dist/commands/release/edit/index.d.ts +2 -4
- package/dist/commands/release/edit/index.js +31 -5
- package/dist/commands/release/export/index.d.ts +2 -4
- package/dist/commands/release/export/index.js +39 -11
- package/dist/commands/release/get/index.d.ts +2 -4
- package/dist/commands/release/get/index.js +31 -5
- package/dist/commands/release/pull/index.d.ts +31 -0
- package/dist/commands/release/pull/index.js +345 -0
- package/dist/commands/release/push/index.d.ts +26 -0
- package/dist/commands/release/push/index.js +230 -0
- package/dist/commands/tenant/backup/delete/index.d.ts +1 -1
- package/dist/commands/tenant/backup/delete/index.js +8 -9
- package/dist/commands/tenant/backup/export/index.d.ts +1 -1
- package/dist/commands/tenant/backup/export/index.js +9 -10
- package/dist/commands/tenant/backup/restore/index.d.ts +1 -1
- package/dist/commands/tenant/backup/restore/index.js +8 -9
- package/dist/commands/tenant/cluster/create/index.d.ts +18 -0
- package/dist/commands/tenant/cluster/create/index.js +149 -0
- package/dist/commands/{run/sessions/start → tenant/cluster/delete}/index.d.ts +9 -3
- package/dist/commands/tenant/cluster/delete/index.js +125 -0
- package/dist/commands/tenant/cluster/edit/index.d.ts +22 -0
- package/dist/commands/tenant/cluster/edit/index.js +128 -0
- package/dist/commands/{run/sessions → tenant/cluster}/get/index.d.ts +7 -3
- package/dist/commands/tenant/cluster/get/index.js +114 -0
- package/dist/commands/{run/info → tenant/cluster/license/get}/index.d.ts +10 -7
- package/dist/commands/tenant/cluster/license/get/index.js +118 -0
- package/dist/commands/tenant/cluster/license/set/index.d.ts +21 -0
- package/dist/commands/tenant/cluster/license/set/index.js +132 -0
- package/dist/commands/{run/env → tenant/cluster}/list/index.d.ts +3 -3
- package/dist/commands/tenant/cluster/list/index.js +109 -0
- package/dist/commands/tenant/create/index.d.ts +6 -3
- package/dist/commands/tenant/create/index.js +28 -20
- package/dist/commands/tenant/deploy_platform/index.d.ts +1 -1
- package/dist/commands/tenant/deploy_platform/index.js +8 -9
- package/dist/commands/tenant/deploy_release/index.d.ts +1 -1
- package/dist/commands/tenant/deploy_release/index.js +13 -13
- package/dist/commands/tenant/env/delete/index.d.ts +19 -0
- package/dist/commands/tenant/env/delete/index.js +139 -0
- package/dist/commands/{run/projects/create → tenant/env/get}/index.d.ts +7 -4
- package/dist/commands/tenant/env/get/index.js +113 -0
- package/dist/commands/{run/projects/update → tenant/env/get_all}/index.d.ts +7 -5
- package/dist/commands/tenant/env/get_all/index.js +123 -0
- package/dist/commands/{run/secrets/get → tenant/env/list}/index.d.ts +5 -3
- package/dist/commands/tenant/env/list/index.js +116 -0
- package/dist/commands/tenant/env/set/index.d.ts +18 -0
- package/dist/commands/tenant/env/set/index.js +122 -0
- package/dist/commands/tenant/env/set_all/index.d.ts +18 -0
- package/dist/commands/tenant/env/set_all/index.js +131 -0
- package/dist/commands/tenant/get/index.js +6 -5
- package/dist/commands/tenant/impersonate/index.d.ts +19 -0
- package/dist/commands/tenant/impersonate/index.js +146 -0
- package/dist/commands/tenant/license/get/index.d.ts +18 -0
- package/dist/commands/tenant/license/get/index.js +127 -0
- package/dist/commands/tenant/license/set/index.d.ts +19 -0
- package/dist/commands/tenant/license/set/index.js +141 -0
- package/dist/commands/tenant/list/index.js +6 -6
- package/dist/commands/tenant/pull/index.d.ts +31 -0
- package/dist/commands/tenant/pull/index.js +327 -0
- package/dist/commands/tenant/push/index.d.ts +24 -0
- package/dist/commands/tenant/push/index.js +245 -0
- package/oclif.manifest.json +2076 -1670
- package/package.json +1 -19
- package/dist/commands/run/env/delete/index.d.ts +0 -14
- package/dist/commands/run/env/delete/index.js +0 -65
- package/dist/commands/run/env/get/index.d.ts +0 -14
- package/dist/commands/run/env/get/index.js +0 -52
- package/dist/commands/run/env/list/index.js +0 -56
- package/dist/commands/run/env/set/index.d.ts +0 -14
- package/dist/commands/run/env/set/index.js +0 -51
- package/dist/commands/run/exec/index.d.ts +0 -31
- package/dist/commands/run/exec/index.js +0 -431
- package/dist/commands/run/info/index.js +0 -160
- package/dist/commands/run/projects/create/index.js +0 -75
- package/dist/commands/run/projects/delete/index.d.ts +0 -14
- package/dist/commands/run/projects/delete/index.js +0 -65
- package/dist/commands/run/projects/list/index.d.ts +0 -13
- package/dist/commands/run/projects/list/index.js +0 -66
- package/dist/commands/run/projects/update/index.js +0 -86
- package/dist/commands/run/secrets/delete/index.d.ts +0 -14
- package/dist/commands/run/secrets/delete/index.js +0 -65
- package/dist/commands/run/secrets/get/index.js +0 -52
- package/dist/commands/run/secrets/list/index.d.ts +0 -12
- package/dist/commands/run/secrets/list/index.js +0 -60
- package/dist/commands/run/secrets/set/index.d.ts +0 -16
- package/dist/commands/run/secrets/set/index.js +0 -74
- package/dist/commands/run/sessions/delete/index.d.ts +0 -14
- package/dist/commands/run/sessions/delete/index.js +0 -65
- package/dist/commands/run/sessions/get/index.js +0 -72
- package/dist/commands/run/sessions/list/index.d.ts +0 -13
- package/dist/commands/run/sessions/list/index.js +0 -64
- package/dist/commands/run/sessions/start/index.js +0 -56
- package/dist/commands/run/sessions/stop/index.d.ts +0 -14
- package/dist/commands/run/sessions/stop/index.js +0 -56
- package/dist/commands/run/sink/get/index.d.ts +0 -14
- package/dist/commands/run/sink/get/index.js +0 -63
- package/dist/lib/base-run-command.d.ts +0 -41
- package/dist/lib/base-run-command.js +0 -75
- package/dist/lib/run-http-client.d.ts +0 -64
- package/dist/lib/run-http-client.js +0 -171
- package/dist/lib/run-types.d.ts +0 -226
- package/dist/lib/run-types.js +0 -5
|
@@ -0,0 +1,230 @@
|
|
|
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 ReleasePush extends BaseCommand {
|
|
8
|
+
static args = {
|
|
9
|
+
directory: Args.string({
|
|
10
|
+
description: 'Directory containing .xs documents to create the release from',
|
|
11
|
+
required: true,
|
|
12
|
+
}),
|
|
13
|
+
};
|
|
14
|
+
static description = 'Create a new release from local XanoScript files via the multidoc endpoint';
|
|
15
|
+
static examples = [
|
|
16
|
+
`$ xano release push ./my-release -n "v1.0"
|
|
17
|
+
Created release: v1.0 - ID: 10
|
|
18
|
+
`,
|
|
19
|
+
`$ xano release push ./output -n "v2.0" -w 40 -d "Major update"
|
|
20
|
+
Created release: v2.0 - ID: 15
|
|
21
|
+
`,
|
|
22
|
+
`$ xano release push ./backup -n "v1.1-hotfix" --hotfix --profile production
|
|
23
|
+
Created release: v1.1-hotfix - ID: 20
|
|
24
|
+
`,
|
|
25
|
+
`$ xano release push ./my-release -n "v1.0" --no-records --no-env
|
|
26
|
+
Create release from schema only, skip records and environment variables
|
|
27
|
+
`,
|
|
28
|
+
`$ xano release push ./my-release -n "v1.0" -o json
|
|
29
|
+
Output release details as JSON
|
|
30
|
+
`,
|
|
31
|
+
];
|
|
32
|
+
static flags = {
|
|
33
|
+
...BaseCommand.baseFlags,
|
|
34
|
+
description: Flags.string({
|
|
35
|
+
char: 'd',
|
|
36
|
+
default: '',
|
|
37
|
+
description: 'Release description',
|
|
38
|
+
required: false,
|
|
39
|
+
}),
|
|
40
|
+
env: Flags.boolean({
|
|
41
|
+
allowNo: true,
|
|
42
|
+
default: true,
|
|
43
|
+
description: 'Include environment variables (default: true, use --no-env to exclude)',
|
|
44
|
+
required: false,
|
|
45
|
+
}),
|
|
46
|
+
hotfix: Flags.boolean({
|
|
47
|
+
default: false,
|
|
48
|
+
description: 'Mark as a hotfix release',
|
|
49
|
+
required: false,
|
|
50
|
+
}),
|
|
51
|
+
name: Flags.string({
|
|
52
|
+
char: 'n',
|
|
53
|
+
description: 'Name for the release',
|
|
54
|
+
required: true,
|
|
55
|
+
}),
|
|
56
|
+
output: Flags.string({
|
|
57
|
+
char: 'o',
|
|
58
|
+
default: 'summary',
|
|
59
|
+
description: 'Output format',
|
|
60
|
+
options: ['summary', 'json'],
|
|
61
|
+
required: false,
|
|
62
|
+
}),
|
|
63
|
+
records: Flags.boolean({
|
|
64
|
+
allowNo: true,
|
|
65
|
+
default: true,
|
|
66
|
+
description: 'Include records (default: true, use --no-records to exclude)',
|
|
67
|
+
required: false,
|
|
68
|
+
}),
|
|
69
|
+
workspace: Flags.string({
|
|
70
|
+
char: 'w',
|
|
71
|
+
description: 'Workspace ID (optional if set in profile)',
|
|
72
|
+
required: false,
|
|
73
|
+
}),
|
|
74
|
+
};
|
|
75
|
+
async run() {
|
|
76
|
+
const { args, flags } = await this.parse(ReleasePush);
|
|
77
|
+
// Get profile name (default or from flag/env)
|
|
78
|
+
const profileName = flags.profile || this.getDefaultProfile();
|
|
79
|
+
// Load credentials
|
|
80
|
+
const credentials = this.loadCredentials();
|
|
81
|
+
// Get the profile configuration
|
|
82
|
+
if (!(profileName in credentials.profiles)) {
|
|
83
|
+
this.error(`Profile '${profileName}' not found. Available profiles: ${Object.keys(credentials.profiles).join(', ')}\n` +
|
|
84
|
+
`Create a profile using 'xano profile:create'`);
|
|
85
|
+
}
|
|
86
|
+
const profile = credentials.profiles[profileName];
|
|
87
|
+
// Validate required fields
|
|
88
|
+
if (!profile.instance_origin) {
|
|
89
|
+
this.error(`Profile '${profileName}' is missing instance_origin`);
|
|
90
|
+
}
|
|
91
|
+
if (!profile.access_token) {
|
|
92
|
+
this.error(`Profile '${profileName}' is missing access_token`);
|
|
93
|
+
}
|
|
94
|
+
// Determine workspace_id from flag or profile
|
|
95
|
+
let workspaceId;
|
|
96
|
+
if (flags.workspace) {
|
|
97
|
+
workspaceId = flags.workspace;
|
|
98
|
+
}
|
|
99
|
+
else if (profile.workspace) {
|
|
100
|
+
workspaceId = profile.workspace;
|
|
101
|
+
}
|
|
102
|
+
else {
|
|
103
|
+
this.error(`Workspace ID is required. Either:\n` +
|
|
104
|
+
` 1. Provide it as a flag: xano release push <directory> -n <name> -w <workspace_id>\n` +
|
|
105
|
+
` 2. Set it in your profile using: xano profile:edit ${profileName} -w <workspace_id>`);
|
|
106
|
+
}
|
|
107
|
+
// Resolve the input directory
|
|
108
|
+
const inputDir = path.resolve(args.directory);
|
|
109
|
+
if (!fs.existsSync(inputDir)) {
|
|
110
|
+
this.error(`Directory not found: ${inputDir}`);
|
|
111
|
+
}
|
|
112
|
+
if (!fs.statSync(inputDir).isDirectory()) {
|
|
113
|
+
this.error(`Not a directory: ${inputDir}`);
|
|
114
|
+
}
|
|
115
|
+
// Collect all .xs files from the directory tree
|
|
116
|
+
const files = this.collectFiles(inputDir);
|
|
117
|
+
if (files.length === 0) {
|
|
118
|
+
this.error(`No .xs files found in ${args.directory}`);
|
|
119
|
+
}
|
|
120
|
+
// Read each file and join with --- separator
|
|
121
|
+
const documents = [];
|
|
122
|
+
for (const filePath of files) {
|
|
123
|
+
const content = fs.readFileSync(filePath, 'utf8').trim();
|
|
124
|
+
if (content) {
|
|
125
|
+
documents.push(content);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
if (documents.length === 0) {
|
|
129
|
+
this.error(`All .xs files in ${args.directory} are empty`);
|
|
130
|
+
}
|
|
131
|
+
const multidoc = documents.join('\n---\n');
|
|
132
|
+
// Construct the API URL with query params
|
|
133
|
+
const queryParams = new URLSearchParams({
|
|
134
|
+
description: flags.description,
|
|
135
|
+
env: flags.env.toString(),
|
|
136
|
+
hotfix: flags.hotfix.toString(),
|
|
137
|
+
name: flags.name,
|
|
138
|
+
records: flags.records.toString(),
|
|
139
|
+
});
|
|
140
|
+
const apiUrl = `${profile.instance_origin}/api:meta/workspace/${workspaceId}/release/multidoc?${queryParams.toString()}`;
|
|
141
|
+
// POST the multidoc to the API
|
|
142
|
+
const requestHeaders = {
|
|
143
|
+
accept: 'application/json',
|
|
144
|
+
Authorization: `Bearer ${profile.access_token}`,
|
|
145
|
+
'Content-Type': 'text/x-xanoscript',
|
|
146
|
+
};
|
|
147
|
+
try {
|
|
148
|
+
const response = await this.verboseFetch(apiUrl, {
|
|
149
|
+
body: multidoc,
|
|
150
|
+
headers: requestHeaders,
|
|
151
|
+
method: 'POST',
|
|
152
|
+
}, flags.verbose, profile.access_token);
|
|
153
|
+
if (!response.ok) {
|
|
154
|
+
const errorText = await response.text();
|
|
155
|
+
let errorMessage = `Release creation failed (${response.status})`;
|
|
156
|
+
try {
|
|
157
|
+
const errorJson = JSON.parse(errorText);
|
|
158
|
+
errorMessage += `: ${errorJson.message}`;
|
|
159
|
+
if (errorJson.payload?.param) {
|
|
160
|
+
errorMessage += `\n Parameter: ${errorJson.payload.param}`;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
catch {
|
|
164
|
+
errorMessage += `\n${errorText}`;
|
|
165
|
+
}
|
|
166
|
+
this.error(errorMessage);
|
|
167
|
+
}
|
|
168
|
+
const release = await response.json();
|
|
169
|
+
if (flags.output === 'json') {
|
|
170
|
+
this.log(JSON.stringify(release, null, 2));
|
|
171
|
+
}
|
|
172
|
+
else {
|
|
173
|
+
this.log(`Created release: ${release.name} - ID: ${release.id}`);
|
|
174
|
+
if (release.branch)
|
|
175
|
+
this.log(` Branch: ${release.branch}`);
|
|
176
|
+
if (release.hotfix)
|
|
177
|
+
this.log(` Hotfix: true`);
|
|
178
|
+
if (release.description)
|
|
179
|
+
this.log(` Description: ${release.description}`);
|
|
180
|
+
this.log(` Documents: ${documents.length}`);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
catch (error) {
|
|
184
|
+
if (error instanceof Error) {
|
|
185
|
+
this.error(`Failed to create release: ${error.message}`);
|
|
186
|
+
}
|
|
187
|
+
else {
|
|
188
|
+
this.error(`Failed to create release: ${String(error)}`);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* Recursively collect all .xs files from a directory, sorted by
|
|
194
|
+
* type subdirectory name then filename for deterministic ordering.
|
|
195
|
+
*/
|
|
196
|
+
collectFiles(dir) {
|
|
197
|
+
const files = [];
|
|
198
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
199
|
+
for (const entry of entries) {
|
|
200
|
+
const fullPath = path.join(dir, entry.name);
|
|
201
|
+
if (entry.isDirectory()) {
|
|
202
|
+
files.push(...this.collectFiles(fullPath));
|
|
203
|
+
}
|
|
204
|
+
else if (entry.isFile() && entry.name.endsWith('.xs')) {
|
|
205
|
+
files.push(fullPath);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
return files.sort();
|
|
209
|
+
}
|
|
210
|
+
loadCredentials() {
|
|
211
|
+
const configDir = path.join(os.homedir(), '.xano');
|
|
212
|
+
const credentialsPath = path.join(configDir, 'credentials.yaml');
|
|
213
|
+
// Check if credentials file exists
|
|
214
|
+
if (!fs.existsSync(credentialsPath)) {
|
|
215
|
+
this.error(`Credentials file not found at ${credentialsPath}\n` + `Create a profile using 'xano profile:create'`);
|
|
216
|
+
}
|
|
217
|
+
// Read credentials file
|
|
218
|
+
try {
|
|
219
|
+
const fileContent = fs.readFileSync(credentialsPath, 'utf8');
|
|
220
|
+
const parsed = yaml.load(fileContent);
|
|
221
|
+
if (!parsed || typeof parsed !== 'object' || !('profiles' in parsed)) {
|
|
222
|
+
this.error('Credentials file has invalid format.');
|
|
223
|
+
}
|
|
224
|
+
return parsed;
|
|
225
|
+
}
|
|
226
|
+
catch (error) {
|
|
227
|
+
this.error(`Failed to parse credentials file: ${error}`);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
}
|
|
@@ -6,7 +6,7 @@ export default class TenantBackupDelete extends BaseCommand {
|
|
|
6
6
|
static description: string;
|
|
7
7
|
static examples: string[];
|
|
8
8
|
static flags: {
|
|
9
|
-
|
|
9
|
+
backup_id: import("@oclif/core/interfaces").OptionFlag<number, import("@oclif/core/interfaces").CustomOptions>;
|
|
10
10
|
force: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
11
11
|
output: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
12
12
|
workspace: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
@@ -14,16 +14,16 @@ export default class TenantBackupDelete extends BaseCommand {
|
|
|
14
14
|
};
|
|
15
15
|
static description = 'Delete a tenant backup permanently. This action cannot be undone.';
|
|
16
16
|
static examples = [
|
|
17
|
-
`$ xano tenant backup delete t1234-abcd-xyz1 --
|
|
17
|
+
`$ xano tenant backup delete t1234-abcd-xyz1 --backup_id 10
|
|
18
18
|
Are you sure you want to delete backup #10? This action cannot be undone. (y/N) y
|
|
19
19
|
Deleted backup #10
|
|
20
20
|
`,
|
|
21
|
-
`$ xano tenant backup delete t1234-abcd-xyz1 --
|
|
22
|
-
`$ xano tenant backup delete t1234-abcd-xyz1 --
|
|
21
|
+
`$ xano tenant backup delete t1234-abcd-xyz1 --backup_id 10 --force`,
|
|
22
|
+
`$ xano tenant backup delete t1234-abcd-xyz1 --backup_id 10 -o json`,
|
|
23
23
|
];
|
|
24
24
|
static flags = {
|
|
25
25
|
...BaseCommand.baseFlags,
|
|
26
|
-
|
|
26
|
+
backup_id: Flags.integer({
|
|
27
27
|
description: 'Backup ID to delete',
|
|
28
28
|
required: true,
|
|
29
29
|
}),
|
|
@@ -66,7 +66,7 @@ Deleted backup #10
|
|
|
66
66
|
this.error('No workspace ID provided. Use --workspace flag or set one in your profile.');
|
|
67
67
|
}
|
|
68
68
|
const tenantName = args.tenant_name;
|
|
69
|
-
const backupId = flags
|
|
69
|
+
const backupId = flags.backup_id;
|
|
70
70
|
if (!flags.force) {
|
|
71
71
|
const confirmed = await this.confirm(`Are you sure you want to delete backup #${backupId}? This action cannot be undone.`);
|
|
72
72
|
if (!confirmed) {
|
|
@@ -78,8 +78,8 @@ Deleted backup #10
|
|
|
78
78
|
try {
|
|
79
79
|
const response = await this.verboseFetch(apiUrl, {
|
|
80
80
|
headers: {
|
|
81
|
-
|
|
82
|
-
|
|
81
|
+
accept: 'application/json',
|
|
82
|
+
Authorization: `Bearer ${profile.access_token}`,
|
|
83
83
|
},
|
|
84
84
|
method: 'DELETE',
|
|
85
85
|
}, flags.verbose, profile.access_token);
|
|
@@ -119,8 +119,7 @@ Deleted backup #10
|
|
|
119
119
|
const configDir = path.join(os.homedir(), '.xano');
|
|
120
120
|
const credentialsPath = path.join(configDir, 'credentials.yaml');
|
|
121
121
|
if (!fs.existsSync(credentialsPath)) {
|
|
122
|
-
this.error(`Credentials file not found at ${credentialsPath}\n` +
|
|
123
|
-
`Create a profile using 'xano profile create'`);
|
|
122
|
+
this.error(`Credentials file not found at ${credentialsPath}\n` + `Create a profile using 'xano profile create'`);
|
|
124
123
|
}
|
|
125
124
|
try {
|
|
126
125
|
const fileContent = fs.readFileSync(credentialsPath, 'utf8');
|
|
@@ -6,7 +6,7 @@ export default class TenantBackupExport extends BaseCommand {
|
|
|
6
6
|
static description: string;
|
|
7
7
|
static examples: string[];
|
|
8
8
|
static flags: {
|
|
9
|
-
|
|
9
|
+
backup_id: import("@oclif/core/interfaces").OptionFlag<number, import("@oclif/core/interfaces").CustomOptions>;
|
|
10
10
|
format: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
11
11
|
output: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
12
12
|
workspace: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
@@ -13,15 +13,15 @@ export default class TenantBackupExport extends BaseCommand {
|
|
|
13
13
|
};
|
|
14
14
|
static description = 'Export (download) a tenant backup to a local file';
|
|
15
15
|
static examples = [
|
|
16
|
-
`$ xano tenant backup export t1234-abcd-xyz1 --
|
|
16
|
+
`$ xano tenant backup export t1234-abcd-xyz1 --backup_id 10
|
|
17
17
|
Downloaded backup #10 to ./tenant-t1234-abcd-xyz1-backup-10.tar.gz
|
|
18
18
|
`,
|
|
19
|
-
`$ xano tenant backup export t1234-abcd-xyz1 --
|
|
20
|
-
`$ xano tenant backup export t1234-abcd-xyz1 --
|
|
19
|
+
`$ xano tenant backup export t1234-abcd-xyz1 --backup_id 10 --output ./backups/my-backup.tar.gz`,
|
|
20
|
+
`$ xano tenant backup export t1234-abcd-xyz1 --backup_id 10 -o json`,
|
|
21
21
|
];
|
|
22
22
|
static flags = {
|
|
23
23
|
...BaseCommand.baseFlags,
|
|
24
|
-
|
|
24
|
+
backup_id: Flags.integer({
|
|
25
25
|
description: 'Backup ID to export',
|
|
26
26
|
required: true,
|
|
27
27
|
}),
|
|
@@ -62,14 +62,14 @@ Downloaded backup #10 to ./tenant-t1234-abcd-xyz1-backup-10.tar.gz
|
|
|
62
62
|
this.error('No workspace ID provided. Use --workspace flag or set one in your profile.');
|
|
63
63
|
}
|
|
64
64
|
const tenantName = args.tenant_name;
|
|
65
|
-
const backupId = flags
|
|
65
|
+
const backupId = flags.backup_id;
|
|
66
66
|
// Step 1: Get signed download URL
|
|
67
67
|
const exportUrl = `${profile.instance_origin}/api:meta/workspace/${workspaceId}/tenant/${tenantName}/backup/${backupId}/export`;
|
|
68
68
|
try {
|
|
69
69
|
const response = await this.verboseFetch(exportUrl, {
|
|
70
70
|
headers: {
|
|
71
|
-
|
|
72
|
-
|
|
71
|
+
accept: 'application/json',
|
|
72
|
+
Authorization: `Bearer ${profile.access_token}`,
|
|
73
73
|
},
|
|
74
74
|
method: 'GET',
|
|
75
75
|
}, flags.verbose, profile.access_token);
|
|
@@ -77,7 +77,7 @@ Downloaded backup #10 to ./tenant-t1234-abcd-xyz1-backup-10.tar.gz
|
|
|
77
77
|
const errorText = await response.text();
|
|
78
78
|
this.error(`API request failed with status ${response.status}: ${response.statusText}\n${errorText}`);
|
|
79
79
|
}
|
|
80
|
-
const exportLink = await response.json();
|
|
80
|
+
const exportLink = (await response.json());
|
|
81
81
|
if (!exportLink.src) {
|
|
82
82
|
this.error('API did not return a download URL');
|
|
83
83
|
}
|
|
@@ -129,8 +129,7 @@ Downloaded backup #10 to ./tenant-t1234-abcd-xyz1-backup-10.tar.gz
|
|
|
129
129
|
const configDir = path.join(os.homedir(), '.xano');
|
|
130
130
|
const credentialsPath = path.join(configDir, 'credentials.yaml');
|
|
131
131
|
if (!fs.existsSync(credentialsPath)) {
|
|
132
|
-
this.error(`Credentials file not found at ${credentialsPath}\n` +
|
|
133
|
-
`Create a profile using 'xano profile create'`);
|
|
132
|
+
this.error(`Credentials file not found at ${credentialsPath}\n` + `Create a profile using 'xano profile create'`);
|
|
134
133
|
}
|
|
135
134
|
try {
|
|
136
135
|
const fileContent = fs.readFileSync(credentialsPath, 'utf8');
|
|
@@ -6,7 +6,7 @@ export default class TenantRestore extends BaseCommand {
|
|
|
6
6
|
static description: string;
|
|
7
7
|
static examples: string[];
|
|
8
8
|
static flags: {
|
|
9
|
-
|
|
9
|
+
backup_id: import("@oclif/core/interfaces").OptionFlag<number, import("@oclif/core/interfaces").CustomOptions>;
|
|
10
10
|
force: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
11
11
|
output: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
12
12
|
workspace: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
@@ -13,15 +13,15 @@ export default class TenantRestore extends BaseCommand {
|
|
|
13
13
|
};
|
|
14
14
|
static description = 'Restore a tenant from a backup. This replaces the current tenant data.';
|
|
15
15
|
static examples = [
|
|
16
|
-
`$ xano tenant backup restore t1234-abcd-xyz1 --
|
|
16
|
+
`$ xano tenant backup restore t1234-abcd-xyz1 --backup_id 10
|
|
17
17
|
Are you sure you want to restore tenant t1234-abcd-xyz1 from backup 10? This will replace current data. (y/N) y
|
|
18
18
|
Restored tenant t1234-abcd-xyz1 from backup #10
|
|
19
19
|
`,
|
|
20
|
-
`$ xano tenant backup restore t1234-abcd-xyz1 --
|
|
20
|
+
`$ xano tenant backup restore t1234-abcd-xyz1 --backup_id 10 --force -o json`,
|
|
21
21
|
];
|
|
22
22
|
static flags = {
|
|
23
23
|
...BaseCommand.baseFlags,
|
|
24
|
-
|
|
24
|
+
backup_id: Flags.integer({
|
|
25
25
|
description: 'Backup ID to restore from',
|
|
26
26
|
required: true,
|
|
27
27
|
}),
|
|
@@ -64,7 +64,7 @@ Restored tenant t1234-abcd-xyz1 from backup #10
|
|
|
64
64
|
this.error('No workspace ID provided. Use --workspace flag or set one in your profile.');
|
|
65
65
|
}
|
|
66
66
|
const tenantName = args.tenant_name;
|
|
67
|
-
const backupId = flags
|
|
67
|
+
const backupId = flags.backup_id;
|
|
68
68
|
if (!flags.force) {
|
|
69
69
|
const confirmed = await this.confirm(`Are you sure you want to restore tenant ${tenantName} from backup ${backupId}? This will replace current data.`);
|
|
70
70
|
if (!confirmed) {
|
|
@@ -77,8 +77,8 @@ Restored tenant t1234-abcd-xyz1 from backup #10
|
|
|
77
77
|
const response = await this.verboseFetch(apiUrl, {
|
|
78
78
|
body: JSON.stringify({ backup_id: backupId }),
|
|
79
79
|
headers: {
|
|
80
|
-
|
|
81
|
-
|
|
80
|
+
accept: 'application/json',
|
|
81
|
+
Authorization: `Bearer ${profile.access_token}`,
|
|
82
82
|
'Content-Type': 'application/json',
|
|
83
83
|
},
|
|
84
84
|
method: 'POST',
|
|
@@ -87,7 +87,7 @@ Restored tenant t1234-abcd-xyz1 from backup #10
|
|
|
87
87
|
const errorText = await response.text();
|
|
88
88
|
this.error(`API request failed with status ${response.status}: ${response.statusText}\n${errorText}`);
|
|
89
89
|
}
|
|
90
|
-
const tenant = await response.json();
|
|
90
|
+
const tenant = (await response.json());
|
|
91
91
|
if (flags.output === 'json') {
|
|
92
92
|
this.log(JSON.stringify(tenant, null, 2));
|
|
93
93
|
}
|
|
@@ -123,8 +123,7 @@ Restored tenant t1234-abcd-xyz1 from backup #10
|
|
|
123
123
|
const configDir = path.join(os.homedir(), '.xano');
|
|
124
124
|
const credentialsPath = path.join(configDir, 'credentials.yaml');
|
|
125
125
|
if (!fs.existsSync(credentialsPath)) {
|
|
126
|
-
this.error(`Credentials file not found at ${credentialsPath}\n` +
|
|
127
|
-
`Create a profile using 'xano profile create'`);
|
|
126
|
+
this.error(`Credentials file not found at ${credentialsPath}\n` + `Create a profile using 'xano profile create'`);
|
|
128
127
|
}
|
|
129
128
|
try {
|
|
130
129
|
const fileContent = fs.readFileSync(credentialsPath, 'utf8');
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import BaseCommand from '../../../../base-command.js';
|
|
2
|
+
export default class TenantClusterCreate extends BaseCommand {
|
|
3
|
+
static description: string;
|
|
4
|
+
static examples: string[];
|
|
5
|
+
static flags: {
|
|
6
|
+
credentials: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
7
|
+
credentials_file: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
8
|
+
description: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
9
|
+
domain: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
10
|
+
name: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
11
|
+
output: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
12
|
+
type: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
13
|
+
profile: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
14
|
+
verbose: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
15
|
+
};
|
|
16
|
+
run(): Promise<void>;
|
|
17
|
+
private loadCredentials;
|
|
18
|
+
}
|
|
@@ -0,0 +1,149 @@
|
|
|
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 TenantClusterCreate extends BaseCommand {
|
|
8
|
+
static description = 'Create a new tenant cluster';
|
|
9
|
+
static examples = [
|
|
10
|
+
`$ xano tenant cluster create --name "us-east-1" --credentials_file ./kubeconfig.yaml
|
|
11
|
+
Created tenant cluster: us-east-1 (standard) - ID: 3
|
|
12
|
+
`,
|
|
13
|
+
`$ xano tenant cluster create --name "eu-west-1" --credentials "..." --type run --description "EU run cluster" -o json`,
|
|
14
|
+
];
|
|
15
|
+
static flags = {
|
|
16
|
+
...BaseCommand.baseFlags,
|
|
17
|
+
credentials: Flags.string({
|
|
18
|
+
description: 'Kubeconfig credentials (raw text)',
|
|
19
|
+
exclusive: ['credentials_file'],
|
|
20
|
+
required: false,
|
|
21
|
+
}),
|
|
22
|
+
credentials_file: Flags.string({
|
|
23
|
+
description: 'Path to kubeconfig credentials file',
|
|
24
|
+
exclusive: ['credentials'],
|
|
25
|
+
required: false,
|
|
26
|
+
}),
|
|
27
|
+
description: Flags.string({
|
|
28
|
+
char: 'd',
|
|
29
|
+
description: 'Cluster description',
|
|
30
|
+
required: false,
|
|
31
|
+
}),
|
|
32
|
+
domain: Flags.string({
|
|
33
|
+
description: 'Custom domain for the cluster',
|
|
34
|
+
required: false,
|
|
35
|
+
}),
|
|
36
|
+
name: Flags.string({
|
|
37
|
+
char: 'n',
|
|
38
|
+
description: 'Cluster name',
|
|
39
|
+
required: true,
|
|
40
|
+
}),
|
|
41
|
+
output: Flags.string({
|
|
42
|
+
char: 'o',
|
|
43
|
+
default: 'summary',
|
|
44
|
+
description: 'Output format',
|
|
45
|
+
options: ['summary', 'json'],
|
|
46
|
+
required: false,
|
|
47
|
+
}),
|
|
48
|
+
type: Flags.string({
|
|
49
|
+
default: 'standard',
|
|
50
|
+
description: 'Cluster type',
|
|
51
|
+
options: ['standard', 'run'],
|
|
52
|
+
required: false,
|
|
53
|
+
}),
|
|
54
|
+
};
|
|
55
|
+
async run() {
|
|
56
|
+
const { flags } = await this.parse(TenantClusterCreate);
|
|
57
|
+
const profileName = flags.profile || this.getDefaultProfile();
|
|
58
|
+
const credentials = this.loadCredentials();
|
|
59
|
+
if (!(profileName in credentials.profiles)) {
|
|
60
|
+
this.error(`Profile '${profileName}' not found. Available profiles: ${Object.keys(credentials.profiles).join(', ')}\n` +
|
|
61
|
+
`Create a profile using 'xano profile create'`);
|
|
62
|
+
}
|
|
63
|
+
const profile = credentials.profiles[profileName];
|
|
64
|
+
if (!profile.instance_origin) {
|
|
65
|
+
this.error(`Profile '${profileName}' is missing instance_origin`);
|
|
66
|
+
}
|
|
67
|
+
if (!profile.access_token) {
|
|
68
|
+
this.error(`Profile '${profileName}' is missing access_token`);
|
|
69
|
+
}
|
|
70
|
+
// Resolve credentials from flag or file
|
|
71
|
+
let credentialsValue;
|
|
72
|
+
if (flags.credentials) {
|
|
73
|
+
credentialsValue = flags.credentials;
|
|
74
|
+
}
|
|
75
|
+
else if (flags.credentials_file) {
|
|
76
|
+
const filePath = path.resolve(flags.credentials_file);
|
|
77
|
+
if (!fs.existsSync(filePath)) {
|
|
78
|
+
this.error(`Credentials file not found: ${filePath}`);
|
|
79
|
+
}
|
|
80
|
+
credentialsValue = fs.readFileSync(filePath, 'utf8');
|
|
81
|
+
}
|
|
82
|
+
else {
|
|
83
|
+
this.error('Either --credentials or --credentials_file must be provided');
|
|
84
|
+
}
|
|
85
|
+
const body = {
|
|
86
|
+
credentials: credentialsValue,
|
|
87
|
+
name: flags.name,
|
|
88
|
+
type: flags.type,
|
|
89
|
+
};
|
|
90
|
+
if (flags.description)
|
|
91
|
+
body.description = flags.description;
|
|
92
|
+
if (flags.domain)
|
|
93
|
+
body.domain = flags.domain;
|
|
94
|
+
const apiUrl = `${profile.instance_origin}/api:meta/tenant/cluster`;
|
|
95
|
+
try {
|
|
96
|
+
const response = await this.verboseFetch(apiUrl, {
|
|
97
|
+
body: JSON.stringify(body),
|
|
98
|
+
headers: {
|
|
99
|
+
accept: 'application/json',
|
|
100
|
+
Authorization: `Bearer ${profile.access_token}`,
|
|
101
|
+
'Content-Type': 'application/json',
|
|
102
|
+
},
|
|
103
|
+
method: 'POST',
|
|
104
|
+
}, flags.verbose, profile.access_token);
|
|
105
|
+
if (!response.ok) {
|
|
106
|
+
const errorText = await response.text();
|
|
107
|
+
this.error(`API request failed with status ${response.status}: ${response.statusText}\n${errorText}`);
|
|
108
|
+
}
|
|
109
|
+
const cluster = (await response.json());
|
|
110
|
+
if (flags.output === 'json') {
|
|
111
|
+
this.log(JSON.stringify(cluster, null, 2));
|
|
112
|
+
}
|
|
113
|
+
else {
|
|
114
|
+
const type = cluster.type ? ` (${cluster.type})` : '';
|
|
115
|
+
this.log(`Created tenant cluster: ${cluster.name}${type} - ID: ${cluster.id}`);
|
|
116
|
+
if (cluster.description)
|
|
117
|
+
this.log(` Description: ${cluster.description}`);
|
|
118
|
+
if (cluster.domain)
|
|
119
|
+
this.log(` Domain: ${cluster.domain}`);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
catch (error) {
|
|
123
|
+
if (error instanceof Error) {
|
|
124
|
+
this.error(`Failed to create tenant cluster: ${error.message}`);
|
|
125
|
+
}
|
|
126
|
+
else {
|
|
127
|
+
this.error(`Failed to create tenant cluster: ${String(error)}`);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
loadCredentials() {
|
|
132
|
+
const configDir = path.join(os.homedir(), '.xano');
|
|
133
|
+
const credentialsPath = path.join(configDir, 'credentials.yaml');
|
|
134
|
+
if (!fs.existsSync(credentialsPath)) {
|
|
135
|
+
this.error(`Credentials file not found at ${credentialsPath}\n` + `Create a profile using 'xano profile create'`);
|
|
136
|
+
}
|
|
137
|
+
try {
|
|
138
|
+
const fileContent = fs.readFileSync(credentialsPath, 'utf8');
|
|
139
|
+
const parsed = yaml.load(fileContent);
|
|
140
|
+
if (!parsed || typeof parsed !== 'object' || !('profiles' in parsed)) {
|
|
141
|
+
this.error('Credentials file has invalid format.');
|
|
142
|
+
}
|
|
143
|
+
return parsed;
|
|
144
|
+
}
|
|
145
|
+
catch (error) {
|
|
146
|
+
this.error(`Failed to parse credentials file: ${error}`);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
@@ -1,14 +1,20 @@
|
|
|
1
|
-
import
|
|
2
|
-
export default class
|
|
1
|
+
import BaseCommand from '../../../../base-command.js';
|
|
2
|
+
export default class TenantClusterDelete extends BaseCommand {
|
|
3
3
|
static args: {
|
|
4
|
-
|
|
4
|
+
cluster_id: import("@oclif/core/interfaces").Arg<number, {
|
|
5
|
+
max?: number;
|
|
6
|
+
min?: number;
|
|
7
|
+
}>;
|
|
5
8
|
};
|
|
6
9
|
static description: string;
|
|
7
10
|
static examples: string[];
|
|
8
11
|
static flags: {
|
|
12
|
+
force: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
9
13
|
output: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
10
14
|
profile: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
11
15
|
verbose: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
12
16
|
};
|
|
13
17
|
run(): Promise<void>;
|
|
18
|
+
private confirm;
|
|
19
|
+
private loadCredentials;
|
|
14
20
|
}
|