@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,137 @@
|
|
|
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 ReleaseEdit extends BaseCommand {
|
|
8
|
+
static args = {
|
|
9
|
+
release_id: Args.integer({
|
|
10
|
+
description: 'Release ID to edit',
|
|
11
|
+
required: true,
|
|
12
|
+
}),
|
|
13
|
+
};
|
|
14
|
+
static description = 'Edit an existing release';
|
|
15
|
+
static examples = [
|
|
16
|
+
`$ xano release edit 10 --name "v1.0-final" --description "Updated description"
|
|
17
|
+
Updated release: v1.0-final - ID: 10
|
|
18
|
+
`,
|
|
19
|
+
`$ xano release edit 10 --description "New description" -o json`,
|
|
20
|
+
];
|
|
21
|
+
static flags = {
|
|
22
|
+
...BaseCommand.baseFlags,
|
|
23
|
+
description: Flags.string({
|
|
24
|
+
char: 'd',
|
|
25
|
+
description: 'New description',
|
|
26
|
+
required: false,
|
|
27
|
+
}),
|
|
28
|
+
name: Flags.string({
|
|
29
|
+
char: 'n',
|
|
30
|
+
description: 'New name for the release',
|
|
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(ReleaseEdit);
|
|
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 releaseId = args.release_id;
|
|
66
|
+
const baseUrl = `${profile.instance_origin}/api:meta/workspace/${workspaceId}/release/${releaseId}`;
|
|
67
|
+
const headers = {
|
|
68
|
+
'accept': 'application/json',
|
|
69
|
+
'Authorization': `Bearer ${profile.access_token}`,
|
|
70
|
+
'Content-Type': 'application/json',
|
|
71
|
+
};
|
|
72
|
+
try {
|
|
73
|
+
// Fetch current release state (PUT requires all fields)
|
|
74
|
+
const getResponse = await this.verboseFetch(baseUrl, {
|
|
75
|
+
headers: {
|
|
76
|
+
'accept': 'application/json',
|
|
77
|
+
'Authorization': `Bearer ${profile.access_token}`,
|
|
78
|
+
},
|
|
79
|
+
method: 'GET',
|
|
80
|
+
}, flags.verbose, profile.access_token);
|
|
81
|
+
if (!getResponse.ok) {
|
|
82
|
+
const errorText = await getResponse.text();
|
|
83
|
+
this.error(`Failed to fetch release: ${getResponse.status} ${getResponse.statusText}\n${errorText}`);
|
|
84
|
+
}
|
|
85
|
+
const current = await getResponse.json();
|
|
86
|
+
// Merge in user-provided values
|
|
87
|
+
const body = {
|
|
88
|
+
description: flags.description !== undefined ? flags.description : (current.description ?? ''),
|
|
89
|
+
name: flags.name !== undefined ? flags.name : current.name,
|
|
90
|
+
};
|
|
91
|
+
// Update release
|
|
92
|
+
const putResponse = await this.verboseFetch(baseUrl, {
|
|
93
|
+
body: JSON.stringify(body),
|
|
94
|
+
headers,
|
|
95
|
+
method: 'PUT',
|
|
96
|
+
}, flags.verbose, profile.access_token);
|
|
97
|
+
if (!putResponse.ok) {
|
|
98
|
+
const errorText = await putResponse.text();
|
|
99
|
+
this.error(`API request failed with status ${putResponse.status}: ${putResponse.statusText}\n${errorText}`);
|
|
100
|
+
}
|
|
101
|
+
const release = await putResponse.json();
|
|
102
|
+
if (flags.output === 'json') {
|
|
103
|
+
this.log(JSON.stringify(release, null, 2));
|
|
104
|
+
}
|
|
105
|
+
else {
|
|
106
|
+
this.log(`Updated release: ${release.name} - ID: ${release.id}`);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
catch (error) {
|
|
110
|
+
if (error instanceof Error) {
|
|
111
|
+
this.error(`Failed to edit release: ${error.message}`);
|
|
112
|
+
}
|
|
113
|
+
else {
|
|
114
|
+
this.error(`Failed to edit release: ${String(error)}`);
|
|
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,20 @@
|
|
|
1
|
+
import BaseCommand from '../../../base-command.js';
|
|
2
|
+
export default class ReleaseExport extends BaseCommand {
|
|
3
|
+
static args: {
|
|
4
|
+
release_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
|
+
format: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
13
|
+
output: import("@oclif/core/interfaces").OptionFlag<string | undefined, 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,142 @@
|
|
|
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 ReleaseExport extends BaseCommand {
|
|
8
|
+
static args = {
|
|
9
|
+
release_id: Args.integer({
|
|
10
|
+
description: 'Release ID to export',
|
|
11
|
+
required: true,
|
|
12
|
+
}),
|
|
13
|
+
};
|
|
14
|
+
static description = 'Export (download) a release to a local file';
|
|
15
|
+
static examples = [
|
|
16
|
+
`$ xano release export 10
|
|
17
|
+
Downloaded release #10 to ./release-10.tar.gz
|
|
18
|
+
`,
|
|
19
|
+
`$ xano release export 10 --output ./backups/my-release.tar.gz`,
|
|
20
|
+
`$ xano release export 10 -o json`,
|
|
21
|
+
];
|
|
22
|
+
static flags = {
|
|
23
|
+
...BaseCommand.baseFlags,
|
|
24
|
+
format: Flags.string({
|
|
25
|
+
char: 'o',
|
|
26
|
+
default: 'summary',
|
|
27
|
+
description: 'Output format',
|
|
28
|
+
options: ['summary', 'json'],
|
|
29
|
+
required: false,
|
|
30
|
+
}),
|
|
31
|
+
output: Flags.string({
|
|
32
|
+
description: 'Output file path (defaults to ./release-{id}.tar.gz)',
|
|
33
|
+
required: false,
|
|
34
|
+
}),
|
|
35
|
+
workspace: Flags.string({
|
|
36
|
+
char: 'w',
|
|
37
|
+
description: 'Workspace ID (uses profile workspace if not provided)',
|
|
38
|
+
required: false,
|
|
39
|
+
}),
|
|
40
|
+
};
|
|
41
|
+
async run() {
|
|
42
|
+
const { args, flags } = await this.parse(ReleaseExport);
|
|
43
|
+
const profileName = flags.profile || this.getDefaultProfile();
|
|
44
|
+
const credentials = this.loadCredentials();
|
|
45
|
+
if (!(profileName in credentials.profiles)) {
|
|
46
|
+
this.error(`Profile '${profileName}' not found. Available profiles: ${Object.keys(credentials.profiles).join(', ')}\n` +
|
|
47
|
+
`Create a profile using 'xano profile create'`);
|
|
48
|
+
}
|
|
49
|
+
const profile = credentials.profiles[profileName];
|
|
50
|
+
if (!profile.instance_origin) {
|
|
51
|
+
this.error(`Profile '${profileName}' is missing instance_origin`);
|
|
52
|
+
}
|
|
53
|
+
if (!profile.access_token) {
|
|
54
|
+
this.error(`Profile '${profileName}' is missing access_token`);
|
|
55
|
+
}
|
|
56
|
+
const workspaceId = flags.workspace || profile.workspace;
|
|
57
|
+
if (!workspaceId) {
|
|
58
|
+
this.error('No workspace ID provided. Use --workspace flag or set one in your profile.');
|
|
59
|
+
}
|
|
60
|
+
const releaseId = args.release_id;
|
|
61
|
+
// Step 1: Get signed download URL
|
|
62
|
+
const exportUrl = `${profile.instance_origin}/api:meta/workspace/${workspaceId}/release/${releaseId}/export`;
|
|
63
|
+
try {
|
|
64
|
+
const response = await this.verboseFetch(exportUrl, {
|
|
65
|
+
headers: {
|
|
66
|
+
'accept': 'application/json',
|
|
67
|
+
'Authorization': `Bearer ${profile.access_token}`,
|
|
68
|
+
},
|
|
69
|
+
method: 'GET',
|
|
70
|
+
}, flags.verbose, profile.access_token);
|
|
71
|
+
if (!response.ok) {
|
|
72
|
+
const errorText = await response.text();
|
|
73
|
+
this.error(`API request failed with status ${response.status}: ${response.statusText}\n${errorText}`);
|
|
74
|
+
}
|
|
75
|
+
const exportLink = await response.json();
|
|
76
|
+
if (!exportLink.src) {
|
|
77
|
+
this.error('API did not return a download URL');
|
|
78
|
+
}
|
|
79
|
+
// Step 2: Download the file
|
|
80
|
+
const outputPath = flags.output || `release-${releaseId}.tar.gz`;
|
|
81
|
+
const resolvedPath = path.resolve(outputPath);
|
|
82
|
+
const downloadResponse = await fetch(exportLink.src);
|
|
83
|
+
if (!downloadResponse.ok) {
|
|
84
|
+
this.error(`Failed to download release: ${downloadResponse.status} ${downloadResponse.statusText}`);
|
|
85
|
+
}
|
|
86
|
+
if (!downloadResponse.body) {
|
|
87
|
+
this.error('Download response has no body');
|
|
88
|
+
}
|
|
89
|
+
const fileStream = fs.createWriteStream(resolvedPath);
|
|
90
|
+
const reader = downloadResponse.body.getReader();
|
|
91
|
+
let totalBytes = 0;
|
|
92
|
+
// eslint-disable-next-line no-constant-condition
|
|
93
|
+
while (true) {
|
|
94
|
+
// eslint-disable-next-line no-await-in-loop
|
|
95
|
+
const { done, value } = await reader.read();
|
|
96
|
+
if (done)
|
|
97
|
+
break;
|
|
98
|
+
fileStream.write(value);
|
|
99
|
+
totalBytes += value.length;
|
|
100
|
+
}
|
|
101
|
+
fileStream.end();
|
|
102
|
+
await new Promise((resolve, reject) => {
|
|
103
|
+
fileStream.on('finish', resolve);
|
|
104
|
+
fileStream.on('error', reject);
|
|
105
|
+
});
|
|
106
|
+
if (flags.format === 'json') {
|
|
107
|
+
this.log(JSON.stringify({ bytes: totalBytes, file: resolvedPath, release_id: releaseId }, null, 2));
|
|
108
|
+
}
|
|
109
|
+
else {
|
|
110
|
+
const sizeMb = (totalBytes / 1024 / 1024).toFixed(2);
|
|
111
|
+
this.log(`Downloaded release #${releaseId} to ${resolvedPath} (${sizeMb} MB)`);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
catch (error) {
|
|
115
|
+
if (error instanceof Error) {
|
|
116
|
+
this.error(`Failed to export release: ${error.message}`);
|
|
117
|
+
}
|
|
118
|
+
else {
|
|
119
|
+
this.error(`Failed to export release: ${String(error)}`);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
loadCredentials() {
|
|
124
|
+
const configDir = path.join(os.homedir(), '.xano');
|
|
125
|
+
const credentialsPath = path.join(configDir, 'credentials.yaml');
|
|
126
|
+
if (!fs.existsSync(credentialsPath)) {
|
|
127
|
+
this.error(`Credentials file not found at ${credentialsPath}\n` +
|
|
128
|
+
`Create a profile using 'xano profile create'`);
|
|
129
|
+
}
|
|
130
|
+
try {
|
|
131
|
+
const fileContent = fs.readFileSync(credentialsPath, 'utf8');
|
|
132
|
+
const parsed = yaml.load(fileContent);
|
|
133
|
+
if (!parsed || typeof parsed !== 'object' || !('profiles' in parsed)) {
|
|
134
|
+
this.error('Credentials file has invalid format.');
|
|
135
|
+
}
|
|
136
|
+
return parsed;
|
|
137
|
+
}
|
|
138
|
+
catch (error) {
|
|
139
|
+
this.error(`Failed to parse credentials file: ${error}`);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import BaseCommand from '../../../base-command.js';
|
|
2
|
+
export default class ReleaseGet extends BaseCommand {
|
|
3
|
+
static args: {
|
|
4
|
+
release_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
|
+
workspace: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
14
|
+
profile: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
15
|
+
verbose: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
16
|
+
};
|
|
17
|
+
run(): Promise<void>;
|
|
18
|
+
private loadCredentials;
|
|
19
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
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 ReleaseGet extends BaseCommand {
|
|
8
|
+
static args = {
|
|
9
|
+
release_id: Args.integer({
|
|
10
|
+
description: 'Release ID to retrieve',
|
|
11
|
+
required: true,
|
|
12
|
+
}),
|
|
13
|
+
};
|
|
14
|
+
static description = 'Get details of a specific release';
|
|
15
|
+
static examples = [
|
|
16
|
+
`$ xano release get 10
|
|
17
|
+
Release: v1.0 - ID: 10
|
|
18
|
+
Branch: main
|
|
19
|
+
Description: Initial release
|
|
20
|
+
Hotfix: false
|
|
21
|
+
`,
|
|
22
|
+
`$ xano release get 10 -w 5 -o json`,
|
|
23
|
+
];
|
|
24
|
+
static flags = {
|
|
25
|
+
...BaseCommand.baseFlags,
|
|
26
|
+
output: Flags.string({
|
|
27
|
+
char: 'o',
|
|
28
|
+
default: 'summary',
|
|
29
|
+
description: 'Output format',
|
|
30
|
+
options: ['summary', 'json'],
|
|
31
|
+
required: false,
|
|
32
|
+
}),
|
|
33
|
+
workspace: Flags.string({
|
|
34
|
+
char: 'w',
|
|
35
|
+
description: 'Workspace ID (uses profile workspace if not provided)',
|
|
36
|
+
required: false,
|
|
37
|
+
}),
|
|
38
|
+
};
|
|
39
|
+
async run() {
|
|
40
|
+
const { args, flags } = await this.parse(ReleaseGet);
|
|
41
|
+
const profileName = flags.profile || this.getDefaultProfile();
|
|
42
|
+
const credentials = this.loadCredentials();
|
|
43
|
+
if (!(profileName in credentials.profiles)) {
|
|
44
|
+
this.error(`Profile '${profileName}' not found. Available profiles: ${Object.keys(credentials.profiles).join(', ')}\n` +
|
|
45
|
+
`Create a profile using 'xano profile create'`);
|
|
46
|
+
}
|
|
47
|
+
const profile = credentials.profiles[profileName];
|
|
48
|
+
if (!profile.instance_origin) {
|
|
49
|
+
this.error(`Profile '${profileName}' is missing instance_origin`);
|
|
50
|
+
}
|
|
51
|
+
if (!profile.access_token) {
|
|
52
|
+
this.error(`Profile '${profileName}' is missing access_token`);
|
|
53
|
+
}
|
|
54
|
+
const workspaceId = flags.workspace || profile.workspace;
|
|
55
|
+
if (!workspaceId) {
|
|
56
|
+
this.error('No workspace ID provided. Use --workspace flag or set one in your profile.');
|
|
57
|
+
}
|
|
58
|
+
const releaseId = args.release_id;
|
|
59
|
+
const apiUrl = `${profile.instance_origin}/api:meta/workspace/${workspaceId}/release/${releaseId}`;
|
|
60
|
+
try {
|
|
61
|
+
const response = await this.verboseFetch(apiUrl, {
|
|
62
|
+
headers: {
|
|
63
|
+
'accept': 'application/json',
|
|
64
|
+
'Authorization': `Bearer ${profile.access_token}`,
|
|
65
|
+
},
|
|
66
|
+
method: 'GET',
|
|
67
|
+
}, flags.verbose, profile.access_token);
|
|
68
|
+
if (!response.ok) {
|
|
69
|
+
const errorText = await response.text();
|
|
70
|
+
this.error(`API request failed with status ${response.status}: ${response.statusText}\n${errorText}`);
|
|
71
|
+
}
|
|
72
|
+
const release = await response.json();
|
|
73
|
+
if (flags.output === 'json') {
|
|
74
|
+
this.log(JSON.stringify(release, null, 2));
|
|
75
|
+
}
|
|
76
|
+
else {
|
|
77
|
+
this.log(`Release: ${release.name} - ID: ${release.id}`);
|
|
78
|
+
if (release.branch)
|
|
79
|
+
this.log(` Branch: ${release.branch}`);
|
|
80
|
+
if (release.description)
|
|
81
|
+
this.log(` Description: ${release.description}`);
|
|
82
|
+
if (release.hotfix !== undefined)
|
|
83
|
+
this.log(` Hotfix: ${release.hotfix}`);
|
|
84
|
+
if (release.resource_size !== undefined)
|
|
85
|
+
this.log(` Resource Size: ${release.resource_size}`);
|
|
86
|
+
if (release.tables && release.tables.length > 0) {
|
|
87
|
+
this.log(` Tables: ${release.tables.map(t => t.name || t.id).join(', ')}`);
|
|
88
|
+
}
|
|
89
|
+
if (release.created_at) {
|
|
90
|
+
const createdDate = new Date(release.created_at).toISOString().split('T')[0];
|
|
91
|
+
this.log(` Created: ${createdDate}`);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
catch (error) {
|
|
96
|
+
if (error instanceof Error) {
|
|
97
|
+
this.error(`Failed to get release: ${error.message}`);
|
|
98
|
+
}
|
|
99
|
+
else {
|
|
100
|
+
this.error(`Failed to get release: ${String(error)}`);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
loadCredentials() {
|
|
105
|
+
const configDir = path.join(os.homedir(), '.xano');
|
|
106
|
+
const credentialsPath = path.join(configDir, 'credentials.yaml');
|
|
107
|
+
if (!fs.existsSync(credentialsPath)) {
|
|
108
|
+
this.error(`Credentials file not found at ${credentialsPath}\n` +
|
|
109
|
+
`Create a profile using 'xano profile create'`);
|
|
110
|
+
}
|
|
111
|
+
try {
|
|
112
|
+
const fileContent = fs.readFileSync(credentialsPath, 'utf8');
|
|
113
|
+
const parsed = yaml.load(fileContent);
|
|
114
|
+
if (!parsed || typeof parsed !== 'object' || !('profiles' in parsed)) {
|
|
115
|
+
this.error('Credentials file has invalid format.');
|
|
116
|
+
}
|
|
117
|
+
return parsed;
|
|
118
|
+
}
|
|
119
|
+
catch (error) {
|
|
120
|
+
this.error(`Failed to parse credentials file: ${error}`);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import BaseCommand from '../../../base-command.js';
|
|
2
|
+
export default class ReleaseImport extends BaseCommand {
|
|
3
|
+
static args: {};
|
|
4
|
+
static description: string;
|
|
5
|
+
static examples: string[];
|
|
6
|
+
static flags: {
|
|
7
|
+
file: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
8
|
+
output: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
9
|
+
workspace: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
10
|
+
profile: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
11
|
+
verbose: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
12
|
+
};
|
|
13
|
+
run(): Promise<void>;
|
|
14
|
+
private loadCredentials;
|
|
15
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { 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 ReleaseImport extends BaseCommand {
|
|
8
|
+
static args = {};
|
|
9
|
+
static description = 'Import a release file into a workspace';
|
|
10
|
+
static examples = [
|
|
11
|
+
`$ xano release import --file ./my-release.tar.gz
|
|
12
|
+
Imported release as #15
|
|
13
|
+
`,
|
|
14
|
+
`$ xano release import --file ./my-release.tar.gz -o json`,
|
|
15
|
+
];
|
|
16
|
+
static flags = {
|
|
17
|
+
...BaseCommand.baseFlags,
|
|
18
|
+
file: Flags.string({
|
|
19
|
+
char: 'f',
|
|
20
|
+
description: 'Path to the release file (.tar.gz)',
|
|
21
|
+
required: true,
|
|
22
|
+
}),
|
|
23
|
+
output: Flags.string({
|
|
24
|
+
char: 'o',
|
|
25
|
+
default: 'summary',
|
|
26
|
+
description: 'Output format',
|
|
27
|
+
options: ['summary', 'json'],
|
|
28
|
+
required: false,
|
|
29
|
+
}),
|
|
30
|
+
workspace: Flags.string({
|
|
31
|
+
char: 'w',
|
|
32
|
+
description: 'Workspace ID (uses profile workspace if not provided)',
|
|
33
|
+
required: false,
|
|
34
|
+
}),
|
|
35
|
+
};
|
|
36
|
+
async run() {
|
|
37
|
+
const { flags } = await this.parse(ReleaseImport);
|
|
38
|
+
const profileName = flags.profile || this.getDefaultProfile();
|
|
39
|
+
const credentials = this.loadCredentials();
|
|
40
|
+
if (!(profileName in credentials.profiles)) {
|
|
41
|
+
this.error(`Profile '${profileName}' not found. Available profiles: ${Object.keys(credentials.profiles).join(', ')}\n` +
|
|
42
|
+
`Create a profile using 'xano profile create'`);
|
|
43
|
+
}
|
|
44
|
+
const profile = credentials.profiles[profileName];
|
|
45
|
+
if (!profile.instance_origin) {
|
|
46
|
+
this.error(`Profile '${profileName}' is missing instance_origin`);
|
|
47
|
+
}
|
|
48
|
+
if (!profile.access_token) {
|
|
49
|
+
this.error(`Profile '${profileName}' is missing access_token`);
|
|
50
|
+
}
|
|
51
|
+
const workspaceId = flags.workspace || profile.workspace;
|
|
52
|
+
if (!workspaceId) {
|
|
53
|
+
this.error('No workspace ID provided. Use --workspace flag or set one in your profile.');
|
|
54
|
+
}
|
|
55
|
+
const filePath = path.resolve(flags.file);
|
|
56
|
+
if (!fs.existsSync(filePath)) {
|
|
57
|
+
this.error(`File not found: ${filePath}`);
|
|
58
|
+
}
|
|
59
|
+
const apiUrl = `${profile.instance_origin}/api:meta/workspace/${workspaceId}/release/import`;
|
|
60
|
+
try {
|
|
61
|
+
const fileBuffer = fs.readFileSync(filePath);
|
|
62
|
+
const blob = new Blob([fileBuffer], { type: 'application/gzip' });
|
|
63
|
+
const formData = new FormData();
|
|
64
|
+
formData.append('file', blob, path.basename(filePath));
|
|
65
|
+
const response = await this.verboseFetch(apiUrl, {
|
|
66
|
+
body: formData,
|
|
67
|
+
headers: {
|
|
68
|
+
'accept': 'application/json',
|
|
69
|
+
'Authorization': `Bearer ${profile.access_token}`,
|
|
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
|
+
const sizeMb = (fileBuffer.length / 1024 / 1024).toFixed(2);
|
|
83
|
+
this.log(`Imported release as #${result.id} (${sizeMb} MB)`);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
catch (error) {
|
|
87
|
+
if (error instanceof Error) {
|
|
88
|
+
this.error(`Failed to import release: ${error.message}`);
|
|
89
|
+
}
|
|
90
|
+
else {
|
|
91
|
+
this.error(`Failed to import release: ${String(error)}`);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
loadCredentials() {
|
|
96
|
+
const configDir = path.join(os.homedir(), '.xano');
|
|
97
|
+
const credentialsPath = path.join(configDir, 'credentials.yaml');
|
|
98
|
+
if (!fs.existsSync(credentialsPath)) {
|
|
99
|
+
this.error(`Credentials file not found at ${credentialsPath}\n` +
|
|
100
|
+
`Create a profile using 'xano profile create'`);
|
|
101
|
+
}
|
|
102
|
+
try {
|
|
103
|
+
const fileContent = fs.readFileSync(credentialsPath, 'utf8');
|
|
104
|
+
const parsed = yaml.load(fileContent);
|
|
105
|
+
if (!parsed || typeof parsed !== 'object' || !('profiles' in parsed)) {
|
|
106
|
+
this.error('Credentials file has invalid format.');
|
|
107
|
+
}
|
|
108
|
+
return parsed;
|
|
109
|
+
}
|
|
110
|
+
catch (error) {
|
|
111
|
+
this.error(`Failed to parse credentials file: ${error}`);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import BaseCommand from '../../../base-command.js';
|
|
2
|
+
export default class ReleaseList extends BaseCommand {
|
|
3
|
+
static description: string;
|
|
4
|
+
static examples: string[];
|
|
5
|
+
static flags: {
|
|
6
|
+
output: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
7
|
+
workspace: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
8
|
+
profile: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
9
|
+
verbose: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
10
|
+
};
|
|
11
|
+
run(): Promise<void>;
|
|
12
|
+
private loadCredentials;
|
|
13
|
+
}
|