@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,141 @@
|
|
|
1
|
+
import { Args, Flags } from '@oclif/core';
|
|
2
|
+
import * as yaml from 'js-yaml';
|
|
3
|
+
import * as fs from 'node:fs';
|
|
4
|
+
import * as os from 'node:os';
|
|
5
|
+
import * as path from 'node:path';
|
|
6
|
+
import BaseCommand from '../../../../base-command.js';
|
|
7
|
+
export default class TenantLicenseSet extends BaseCommand {
|
|
8
|
+
static args = {
|
|
9
|
+
tenant_name: Args.string({
|
|
10
|
+
description: 'Tenant name',
|
|
11
|
+
required: true,
|
|
12
|
+
}),
|
|
13
|
+
};
|
|
14
|
+
static description = 'Set/update the license for a tenant';
|
|
15
|
+
static examples = [
|
|
16
|
+
`$ xano tenant license set my-tenant
|
|
17
|
+
Reads from license_my-tenant.yaml
|
|
18
|
+
`,
|
|
19
|
+
`$ xano tenant license set my-tenant --file ./license.yaml`,
|
|
20
|
+
`$ xano tenant license set my-tenant --value 'key: value'`,
|
|
21
|
+
`$ xano tenant license set my-tenant -o json`,
|
|
22
|
+
];
|
|
23
|
+
static flags = {
|
|
24
|
+
...BaseCommand.baseFlags,
|
|
25
|
+
clean: Flags.boolean({
|
|
26
|
+
default: false,
|
|
27
|
+
description: 'Remove the source file after successful upload',
|
|
28
|
+
exclusive: ['value'],
|
|
29
|
+
required: false,
|
|
30
|
+
}),
|
|
31
|
+
file: Flags.string({
|
|
32
|
+
char: 'f',
|
|
33
|
+
description: 'Path to license file (default: license_<tenant_name>.yaml)',
|
|
34
|
+
exclusive: ['value'],
|
|
35
|
+
required: false,
|
|
36
|
+
}),
|
|
37
|
+
output: Flags.string({
|
|
38
|
+
char: 'o',
|
|
39
|
+
default: 'summary',
|
|
40
|
+
description: 'Output format',
|
|
41
|
+
options: ['summary', 'json'],
|
|
42
|
+
required: false,
|
|
43
|
+
}),
|
|
44
|
+
value: Flags.string({
|
|
45
|
+
description: 'Inline license value',
|
|
46
|
+
exclusive: ['file', 'clean'],
|
|
47
|
+
required: false,
|
|
48
|
+
}),
|
|
49
|
+
workspace: Flags.string({
|
|
50
|
+
char: 'w',
|
|
51
|
+
description: 'Workspace ID (uses profile workspace if not provided)',
|
|
52
|
+
required: false,
|
|
53
|
+
}),
|
|
54
|
+
};
|
|
55
|
+
async run() {
|
|
56
|
+
const { args, flags } = await this.parse(TenantLicenseSet);
|
|
57
|
+
const tenantName = args.tenant_name;
|
|
58
|
+
let licenseValue;
|
|
59
|
+
let sourceFilePath;
|
|
60
|
+
if (flags.value) {
|
|
61
|
+
licenseValue = flags.value;
|
|
62
|
+
}
|
|
63
|
+
else {
|
|
64
|
+
sourceFilePath = path.resolve(flags.file || `license_${tenantName}.yaml`);
|
|
65
|
+
if (!fs.existsSync(sourceFilePath)) {
|
|
66
|
+
this.error(`File not found: ${sourceFilePath}`);
|
|
67
|
+
}
|
|
68
|
+
licenseValue = fs.readFileSync(sourceFilePath, 'utf8');
|
|
69
|
+
}
|
|
70
|
+
const profileName = flags.profile || this.getDefaultProfile();
|
|
71
|
+
const credentials = this.loadCredentials();
|
|
72
|
+
if (!(profileName in credentials.profiles)) {
|
|
73
|
+
this.error(`Profile '${profileName}' not found. Available profiles: ${Object.keys(credentials.profiles).join(', ')}\n` +
|
|
74
|
+
`Create a profile using 'xano profile create'`);
|
|
75
|
+
}
|
|
76
|
+
const profile = credentials.profiles[profileName];
|
|
77
|
+
if (!profile.instance_origin) {
|
|
78
|
+
this.error(`Profile '${profileName}' is missing instance_origin`);
|
|
79
|
+
}
|
|
80
|
+
if (!profile.access_token) {
|
|
81
|
+
this.error(`Profile '${profileName}' is missing access_token`);
|
|
82
|
+
}
|
|
83
|
+
const workspaceId = flags.workspace || profile.workspace;
|
|
84
|
+
if (!workspaceId) {
|
|
85
|
+
this.error('No workspace ID provided. Use --workspace flag or set one in your profile.');
|
|
86
|
+
}
|
|
87
|
+
const apiUrl = `${profile.instance_origin}/api:meta/workspace/${workspaceId}/tenant/${tenantName}/license`;
|
|
88
|
+
try {
|
|
89
|
+
const response = await this.verboseFetch(apiUrl, {
|
|
90
|
+
body: JSON.stringify({ value: licenseValue }),
|
|
91
|
+
headers: {
|
|
92
|
+
accept: 'application/json',
|
|
93
|
+
Authorization: `Bearer ${profile.access_token}`,
|
|
94
|
+
'Content-Type': 'application/json',
|
|
95
|
+
},
|
|
96
|
+
method: 'POST',
|
|
97
|
+
}, flags.verbose, profile.access_token);
|
|
98
|
+
if (!response.ok) {
|
|
99
|
+
const errorText = await response.text();
|
|
100
|
+
this.error(`API request failed with status ${response.status}: ${response.statusText}\n${errorText}`);
|
|
101
|
+
}
|
|
102
|
+
const result = await response.json();
|
|
103
|
+
if (flags.output === 'json') {
|
|
104
|
+
this.log(JSON.stringify(result, null, 2));
|
|
105
|
+
}
|
|
106
|
+
else {
|
|
107
|
+
this.log(`Tenant license updated successfully for ${tenantName}`);
|
|
108
|
+
}
|
|
109
|
+
if (flags.clean && sourceFilePath && fs.existsSync(sourceFilePath)) {
|
|
110
|
+
fs.unlinkSync(sourceFilePath);
|
|
111
|
+
this.log(`Removed ${sourceFilePath}`);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
catch (error) {
|
|
115
|
+
if (error instanceof Error) {
|
|
116
|
+
this.error(`Failed to set tenant license: ${error.message}`);
|
|
117
|
+
}
|
|
118
|
+
else {
|
|
119
|
+
this.error(`Failed to set tenant license: ${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` + `Create a profile using 'xano profile create'`);
|
|
128
|
+
}
|
|
129
|
+
try {
|
|
130
|
+
const fileContent = fs.readFileSync(credentialsPath, 'utf8');
|
|
131
|
+
const parsed = yaml.load(fileContent);
|
|
132
|
+
if (!parsed || typeof parsed !== 'object' || !('profiles' in parsed)) {
|
|
133
|
+
this.error('Credentials file has invalid format.');
|
|
134
|
+
}
|
|
135
|
+
return parsed;
|
|
136
|
+
}
|
|
137
|
+
catch (error) {
|
|
138
|
+
this.error(`Failed to parse credentials file: ${error}`);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
@@ -52,8 +52,8 @@ Tenants in workspace 5:
|
|
|
52
52
|
try {
|
|
53
53
|
const response = await this.verboseFetch(apiUrl, {
|
|
54
54
|
headers: {
|
|
55
|
-
|
|
56
|
-
|
|
55
|
+
accept: 'application/json',
|
|
56
|
+
Authorization: `Bearer ${profile.access_token}`,
|
|
57
57
|
},
|
|
58
58
|
method: 'GET',
|
|
59
59
|
}, flags.verbose, profile.access_token);
|
|
@@ -61,7 +61,7 @@ Tenants in workspace 5:
|
|
|
61
61
|
const errorText = await response.text();
|
|
62
62
|
this.error(`API request failed with status ${response.status}: ${response.statusText}\n${errorText}`);
|
|
63
63
|
}
|
|
64
|
-
const data = await response.json();
|
|
64
|
+
const data = (await response.json());
|
|
65
65
|
let tenants;
|
|
66
66
|
if (Array.isArray(data)) {
|
|
67
67
|
tenants = data;
|
|
@@ -87,7 +87,8 @@ Tenants in workspace 5:
|
|
|
87
87
|
for (const tenant of tenants) {
|
|
88
88
|
const state = tenant.state ? ` [${tenant.state}]` : '';
|
|
89
89
|
const license = tenant.license ? ` - ${tenant.license}` : '';
|
|
90
|
-
|
|
90
|
+
const ephemeral = tenant.ephemeral ? ' [ephemeral]' : '';
|
|
91
|
+
this.log(` - ${tenant.display || tenant.name} (${tenant.name})${state}${license}${ephemeral}`);
|
|
91
92
|
}
|
|
92
93
|
}
|
|
93
94
|
}
|
|
@@ -105,8 +106,7 @@ Tenants in workspace 5:
|
|
|
105
106
|
const configDir = path.join(os.homedir(), '.xano');
|
|
106
107
|
const credentialsPath = path.join(configDir, 'credentials.yaml');
|
|
107
108
|
if (!fs.existsSync(credentialsPath)) {
|
|
108
|
-
this.error(`Credentials file not found at ${credentialsPath}\n` +
|
|
109
|
-
`Create a profile using 'xano profile create'`);
|
|
109
|
+
this.error(`Credentials file not found at ${credentialsPath}\n` + `Create a profile using 'xano profile create'`);
|
|
110
110
|
}
|
|
111
111
|
try {
|
|
112
112
|
const fileContent = fs.readFileSync(credentialsPath, 'utf8');
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import BaseCommand from '../../../base-command.js';
|
|
2
|
+
export default class Pull extends BaseCommand {
|
|
3
|
+
static args: {
|
|
4
|
+
directory: import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
|
|
5
|
+
};
|
|
6
|
+
static description: string;
|
|
7
|
+
static examples: string[];
|
|
8
|
+
static flags: {
|
|
9
|
+
draft: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
10
|
+
env: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
11
|
+
records: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
12
|
+
tenant: 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
|
+
/**
|
|
20
|
+
* Parse a single document to extract its type, name, and optional verb.
|
|
21
|
+
* Skips leading comment lines (starting with //) to find the first
|
|
22
|
+
* meaningful line containing the type keyword and name.
|
|
23
|
+
*/
|
|
24
|
+
private parseDocument;
|
|
25
|
+
/**
|
|
26
|
+
* Sanitize a document name for use as a filename.
|
|
27
|
+
* Strips quotes, replaces spaces with underscores, and removes
|
|
28
|
+
* characters that are unsafe in filenames.
|
|
29
|
+
*/
|
|
30
|
+
private sanitizeFilename;
|
|
31
|
+
}
|
|
@@ -0,0 +1,327 @@
|
|
|
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 snakeCase from 'lodash.snakecase';
|
|
7
|
+
import BaseCommand from '../../../base-command.js';
|
|
8
|
+
export default class Pull extends BaseCommand {
|
|
9
|
+
static args = {
|
|
10
|
+
directory: Args.string({
|
|
11
|
+
description: 'Output directory for pulled documents',
|
|
12
|
+
required: true,
|
|
13
|
+
}),
|
|
14
|
+
};
|
|
15
|
+
static description = 'Pull a tenant multidoc from the Xano Metadata API and split into individual files';
|
|
16
|
+
static examples = [
|
|
17
|
+
`$ xano tenant pull ./my-tenant -t my-tenant
|
|
18
|
+
Pulled 42 documents from tenant my-tenant to ./my-tenant
|
|
19
|
+
`,
|
|
20
|
+
`$ xano tenant pull ./output -t my-tenant -w 40
|
|
21
|
+
Pulled 15 documents from tenant my-tenant to ./output
|
|
22
|
+
`,
|
|
23
|
+
`$ xano tenant pull ./backup -t my-tenant --profile production --env --records
|
|
24
|
+
Pulled 58 documents from tenant my-tenant to ./backup
|
|
25
|
+
`,
|
|
26
|
+
`$ xano tenant pull ./my-tenant -t my-tenant --draft
|
|
27
|
+
Pulled 42 documents from tenant my-tenant to ./my-tenant
|
|
28
|
+
`,
|
|
29
|
+
];
|
|
30
|
+
static flags = {
|
|
31
|
+
...BaseCommand.baseFlags,
|
|
32
|
+
draft: Flags.boolean({
|
|
33
|
+
default: false,
|
|
34
|
+
description: 'Include draft versions',
|
|
35
|
+
required: false,
|
|
36
|
+
}),
|
|
37
|
+
env: Flags.boolean({
|
|
38
|
+
default: false,
|
|
39
|
+
description: 'Include environment variables',
|
|
40
|
+
required: false,
|
|
41
|
+
}),
|
|
42
|
+
records: Flags.boolean({
|
|
43
|
+
default: false,
|
|
44
|
+
description: 'Include records',
|
|
45
|
+
required: false,
|
|
46
|
+
}),
|
|
47
|
+
tenant: Flags.string({
|
|
48
|
+
char: 't',
|
|
49
|
+
description: 'Tenant name to pull from',
|
|
50
|
+
required: true,
|
|
51
|
+
}),
|
|
52
|
+
workspace: Flags.string({
|
|
53
|
+
char: 'w',
|
|
54
|
+
description: 'Workspace ID (optional if set in profile)',
|
|
55
|
+
required: false,
|
|
56
|
+
}),
|
|
57
|
+
};
|
|
58
|
+
async run() {
|
|
59
|
+
const { args, flags } = await this.parse(Pull);
|
|
60
|
+
// Get profile name (default or from flag/env)
|
|
61
|
+
const profileName = flags.profile || this.getDefaultProfile();
|
|
62
|
+
// Load credentials
|
|
63
|
+
const credentials = this.loadCredentials();
|
|
64
|
+
// Get the profile configuration
|
|
65
|
+
if (!(profileName in credentials.profiles)) {
|
|
66
|
+
this.error(`Profile '${profileName}' not found. Available profiles: ${Object.keys(credentials.profiles).join(', ')}\n` +
|
|
67
|
+
`Create a profile using 'xano profile:create'`);
|
|
68
|
+
}
|
|
69
|
+
const profile = credentials.profiles[profileName];
|
|
70
|
+
// Validate required fields
|
|
71
|
+
if (!profile.instance_origin) {
|
|
72
|
+
this.error(`Profile '${profileName}' is missing instance_origin`);
|
|
73
|
+
}
|
|
74
|
+
if (!profile.access_token) {
|
|
75
|
+
this.error(`Profile '${profileName}' is missing access_token`);
|
|
76
|
+
}
|
|
77
|
+
// Determine workspace_id from flag or profile
|
|
78
|
+
let workspaceId;
|
|
79
|
+
if (flags.workspace) {
|
|
80
|
+
workspaceId = flags.workspace;
|
|
81
|
+
}
|
|
82
|
+
else if (profile.workspace) {
|
|
83
|
+
workspaceId = profile.workspace;
|
|
84
|
+
}
|
|
85
|
+
else {
|
|
86
|
+
this.error(`Workspace ID is required. Either:\n` +
|
|
87
|
+
` 1. Provide it as a flag: xano tenant pull <directory> -t <tenant_name> -w <workspace_id>\n` +
|
|
88
|
+
` 2. Set it in your profile using: xano profile:edit ${profileName} -w <workspace_id>`);
|
|
89
|
+
}
|
|
90
|
+
const tenantName = flags.tenant;
|
|
91
|
+
// Build query parameters
|
|
92
|
+
const queryParams = new URLSearchParams({
|
|
93
|
+
env: flags.env.toString(),
|
|
94
|
+
include_draft: flags.draft.toString(),
|
|
95
|
+
records: flags.records.toString(),
|
|
96
|
+
});
|
|
97
|
+
// Construct the API URL
|
|
98
|
+
const apiUrl = `${profile.instance_origin}/api:meta/workspace/${workspaceId}/tenant/${tenantName}/multidoc?${queryParams.toString()}`;
|
|
99
|
+
// Fetch multidoc from the API
|
|
100
|
+
let responseText;
|
|
101
|
+
const requestHeaders = {
|
|
102
|
+
accept: 'application/json',
|
|
103
|
+
Authorization: `Bearer ${profile.access_token}`,
|
|
104
|
+
};
|
|
105
|
+
try {
|
|
106
|
+
const response = await this.verboseFetch(apiUrl, {
|
|
107
|
+
headers: requestHeaders,
|
|
108
|
+
method: 'GET',
|
|
109
|
+
}, flags.verbose, profile.access_token);
|
|
110
|
+
if (!response.ok) {
|
|
111
|
+
const errorText = await response.text();
|
|
112
|
+
this.error(`API request failed with status ${response.status}: ${response.statusText}\n${errorText}`);
|
|
113
|
+
}
|
|
114
|
+
responseText = await response.text();
|
|
115
|
+
}
|
|
116
|
+
catch (error) {
|
|
117
|
+
if (error instanceof Error) {
|
|
118
|
+
this.error(`Failed to fetch multidoc: ${error.message}`);
|
|
119
|
+
}
|
|
120
|
+
else {
|
|
121
|
+
this.error(`Failed to fetch multidoc: ${String(error)}`);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
// Split the response into individual documents
|
|
125
|
+
const rawDocuments = responseText.split('\n---\n');
|
|
126
|
+
// Parse each document
|
|
127
|
+
const documents = [];
|
|
128
|
+
for (const raw of rawDocuments) {
|
|
129
|
+
const trimmed = raw.trim();
|
|
130
|
+
if (!trimmed) {
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
const parsed = this.parseDocument(trimmed);
|
|
134
|
+
if (parsed) {
|
|
135
|
+
documents.push(parsed);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
if (documents.length === 0) {
|
|
139
|
+
this.log('No documents found in response');
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
// Resolve the output directory
|
|
143
|
+
const outputDir = path.resolve(args.directory);
|
|
144
|
+
// Create the output directory if it doesn't exist
|
|
145
|
+
fs.mkdirSync(outputDir, { recursive: true });
|
|
146
|
+
// Track filenames per type to handle duplicates
|
|
147
|
+
const filenameCounters = new Map();
|
|
148
|
+
let writtenCount = 0;
|
|
149
|
+
for (const doc of documents) {
|
|
150
|
+
let typeDir;
|
|
151
|
+
let baseName;
|
|
152
|
+
if (doc.type === 'workspace') {
|
|
153
|
+
// workspace → workspace/{name}.xs
|
|
154
|
+
typeDir = path.join(outputDir, 'workspace');
|
|
155
|
+
baseName = this.sanitizeFilename(doc.name);
|
|
156
|
+
}
|
|
157
|
+
else if (doc.type === 'workspace_trigger') {
|
|
158
|
+
// workspace_trigger → workspace/trigger/{name}.xs
|
|
159
|
+
typeDir = path.join(outputDir, 'workspace', 'trigger');
|
|
160
|
+
baseName = this.sanitizeFilename(doc.name);
|
|
161
|
+
}
|
|
162
|
+
else if (doc.type === 'agent') {
|
|
163
|
+
// agent → ai/agent/{name}.xs
|
|
164
|
+
typeDir = path.join(outputDir, 'ai', 'agent');
|
|
165
|
+
baseName = this.sanitizeFilename(doc.name);
|
|
166
|
+
}
|
|
167
|
+
else if (doc.type === 'mcp_server') {
|
|
168
|
+
// mcp_server → ai/mcp_server/{name}.xs
|
|
169
|
+
typeDir = path.join(outputDir, 'ai', 'mcp_server');
|
|
170
|
+
baseName = this.sanitizeFilename(doc.name);
|
|
171
|
+
}
|
|
172
|
+
else if (doc.type === 'tool') {
|
|
173
|
+
// tool → ai/tool/{name}.xs
|
|
174
|
+
typeDir = path.join(outputDir, 'ai', 'tool');
|
|
175
|
+
baseName = this.sanitizeFilename(doc.name);
|
|
176
|
+
}
|
|
177
|
+
else if (doc.type === 'agent_trigger') {
|
|
178
|
+
// agent_trigger → ai/agent/trigger/{name}.xs
|
|
179
|
+
typeDir = path.join(outputDir, 'ai', 'agent', 'trigger');
|
|
180
|
+
baseName = this.sanitizeFilename(doc.name);
|
|
181
|
+
}
|
|
182
|
+
else if (doc.type === 'mcp_server_trigger') {
|
|
183
|
+
// mcp_server_trigger → ai/mcp_server/trigger/{name}.xs
|
|
184
|
+
typeDir = path.join(outputDir, 'ai', 'mcp_server', 'trigger');
|
|
185
|
+
baseName = this.sanitizeFilename(doc.name);
|
|
186
|
+
}
|
|
187
|
+
else if (doc.type === 'table_trigger') {
|
|
188
|
+
// table_trigger → table/trigger/{name}.xs
|
|
189
|
+
typeDir = path.join(outputDir, 'table', 'trigger');
|
|
190
|
+
baseName = this.sanitizeFilename(doc.name);
|
|
191
|
+
}
|
|
192
|
+
else if (doc.type === 'realtime_channel') {
|
|
193
|
+
// realtime_channel → realtime/channel/{name}.xs
|
|
194
|
+
typeDir = path.join(outputDir, 'realtime', 'channel');
|
|
195
|
+
baseName = this.sanitizeFilename(doc.name);
|
|
196
|
+
}
|
|
197
|
+
else if (doc.type === 'realtime_trigger') {
|
|
198
|
+
// realtime_trigger → realtime/trigger/{name}.xs
|
|
199
|
+
typeDir = path.join(outputDir, 'realtime', 'trigger');
|
|
200
|
+
baseName = this.sanitizeFilename(doc.name);
|
|
201
|
+
}
|
|
202
|
+
else if (doc.type === 'api_group') {
|
|
203
|
+
// api_group "test" → api/test/api_group.xs
|
|
204
|
+
const groupFolder = snakeCase(doc.name);
|
|
205
|
+
typeDir = path.join(outputDir, 'api', groupFolder);
|
|
206
|
+
baseName = 'api_group';
|
|
207
|
+
}
|
|
208
|
+
else if (doc.type === 'query' && doc.apiGroup) {
|
|
209
|
+
// query in group "test" → api/test/{query_name}.xs
|
|
210
|
+
const groupFolder = snakeCase(doc.apiGroup);
|
|
211
|
+
const nameParts = doc.name.split('/');
|
|
212
|
+
const leafName = nameParts.pop();
|
|
213
|
+
const folderParts = nameParts.map((part) => snakeCase(part));
|
|
214
|
+
typeDir = path.join(outputDir, 'api', groupFolder, ...folderParts);
|
|
215
|
+
baseName = this.sanitizeFilename(leafName);
|
|
216
|
+
if (doc.verb) {
|
|
217
|
+
baseName = `${baseName}_${doc.verb}`;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
else {
|
|
221
|
+
// Default: split folder path from name
|
|
222
|
+
const nameParts = doc.name.split('/');
|
|
223
|
+
const leafName = nameParts.pop();
|
|
224
|
+
const folderParts = nameParts.map((part) => snakeCase(part));
|
|
225
|
+
typeDir = path.join(outputDir, doc.type, ...folderParts);
|
|
226
|
+
baseName = this.sanitizeFilename(leafName);
|
|
227
|
+
if (doc.verb) {
|
|
228
|
+
baseName = `${baseName}_${doc.verb}`;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
fs.mkdirSync(typeDir, { recursive: true });
|
|
232
|
+
// Track duplicates per directory
|
|
233
|
+
const dirKey = path.relative(outputDir, typeDir);
|
|
234
|
+
if (!filenameCounters.has(dirKey)) {
|
|
235
|
+
filenameCounters.set(dirKey, new Map());
|
|
236
|
+
}
|
|
237
|
+
const typeCounters = filenameCounters.get(dirKey);
|
|
238
|
+
const count = typeCounters.get(baseName) || 0;
|
|
239
|
+
typeCounters.set(baseName, count + 1);
|
|
240
|
+
// Append numeric suffix for duplicates
|
|
241
|
+
let filename;
|
|
242
|
+
filename = count === 0 ? `${baseName}.xs` : `${baseName}_${count + 1}.xs`;
|
|
243
|
+
const filePath = path.join(typeDir, filename);
|
|
244
|
+
fs.writeFileSync(filePath, doc.content, 'utf8');
|
|
245
|
+
writtenCount++;
|
|
246
|
+
}
|
|
247
|
+
this.log(`Pulled ${writtenCount} documents from tenant ${tenantName} to ${args.directory}`);
|
|
248
|
+
}
|
|
249
|
+
loadCredentials() {
|
|
250
|
+
const configDir = path.join(os.homedir(), '.xano');
|
|
251
|
+
const credentialsPath = path.join(configDir, 'credentials.yaml');
|
|
252
|
+
// Check if credentials file exists
|
|
253
|
+
if (!fs.existsSync(credentialsPath)) {
|
|
254
|
+
this.error(`Credentials file not found at ${credentialsPath}\n` + `Create a profile using 'xano profile:create'`);
|
|
255
|
+
}
|
|
256
|
+
// Read credentials file
|
|
257
|
+
try {
|
|
258
|
+
const fileContent = fs.readFileSync(credentialsPath, 'utf8');
|
|
259
|
+
const parsed = yaml.load(fileContent);
|
|
260
|
+
if (!parsed || typeof parsed !== 'object' || !('profiles' in parsed)) {
|
|
261
|
+
this.error('Credentials file has invalid format.');
|
|
262
|
+
}
|
|
263
|
+
return parsed;
|
|
264
|
+
}
|
|
265
|
+
catch (error) {
|
|
266
|
+
this.error(`Failed to parse credentials file: ${error}`);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
/**
|
|
270
|
+
* Parse a single document to extract its type, name, and optional verb.
|
|
271
|
+
* Skips leading comment lines (starting with //) to find the first
|
|
272
|
+
* meaningful line containing the type keyword and name.
|
|
273
|
+
*/
|
|
274
|
+
parseDocument(content) {
|
|
275
|
+
const lines = content.split('\n');
|
|
276
|
+
// Find the first non-comment line
|
|
277
|
+
let firstLine = null;
|
|
278
|
+
for (const line of lines) {
|
|
279
|
+
const trimmedLine = line.trim();
|
|
280
|
+
if (trimmedLine && !trimmedLine.startsWith('//')) {
|
|
281
|
+
firstLine = trimmedLine;
|
|
282
|
+
break;
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
if (!firstLine) {
|
|
286
|
+
return null;
|
|
287
|
+
}
|
|
288
|
+
// Parse the type keyword and name from the first meaningful line
|
|
289
|
+
// Expected formats:
|
|
290
|
+
// type name {
|
|
291
|
+
// type name verb=GET {
|
|
292
|
+
// type "name with spaces" {
|
|
293
|
+
// type "name with spaces" verb=PATCH {
|
|
294
|
+
const match = firstLine.match(/^(\w+)\s+("(?:[^"\\]|\\.)*"|\S+)(?:\s+(.*))?/);
|
|
295
|
+
if (!match) {
|
|
296
|
+
return null;
|
|
297
|
+
}
|
|
298
|
+
const type = match[1];
|
|
299
|
+
let name = match[2];
|
|
300
|
+
const rest = match[3] || '';
|
|
301
|
+
// Strip surrounding quotes from the name
|
|
302
|
+
if (name.startsWith('"') && name.endsWith('"')) {
|
|
303
|
+
name = name.slice(1, -1);
|
|
304
|
+
}
|
|
305
|
+
// Extract verb if present (e.g., verb=GET)
|
|
306
|
+
let verb;
|
|
307
|
+
const verbMatch = rest.match(/verb=(\S+)/);
|
|
308
|
+
if (verbMatch) {
|
|
309
|
+
verb = verbMatch[1];
|
|
310
|
+
}
|
|
311
|
+
// Extract api_group if present (e.g., api_group = "test")
|
|
312
|
+
let apiGroup;
|
|
313
|
+
const apiGroupMatch = content.match(/api_group\s*=\s*"([^"]*)"/);
|
|
314
|
+
if (apiGroupMatch) {
|
|
315
|
+
apiGroup = apiGroupMatch[1];
|
|
316
|
+
}
|
|
317
|
+
return { apiGroup, content, name, type, verb };
|
|
318
|
+
}
|
|
319
|
+
/**
|
|
320
|
+
* Sanitize a document name for use as a filename.
|
|
321
|
+
* Strips quotes, replaces spaces with underscores, and removes
|
|
322
|
+
* characters that are unsafe in filenames.
|
|
323
|
+
*/
|
|
324
|
+
sanitizeFilename(name) {
|
|
325
|
+
return snakeCase(name.replaceAll('"', ''));
|
|
326
|
+
}
|
|
327
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import BaseCommand from '../../../base-command.js';
|
|
2
|
+
export default class Push extends BaseCommand {
|
|
3
|
+
static args: {
|
|
4
|
+
directory: import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
|
|
5
|
+
};
|
|
6
|
+
static description: string;
|
|
7
|
+
static examples: string[];
|
|
8
|
+
static flags: {
|
|
9
|
+
env: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
10
|
+
records: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
11
|
+
tenant: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
12
|
+
truncate: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
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
|
+
/**
|
|
19
|
+
* Recursively collect all .xs files from a directory, sorted by
|
|
20
|
+
* type subdirectory name then filename for deterministic ordering.
|
|
21
|
+
*/
|
|
22
|
+
private collectFiles;
|
|
23
|
+
private loadCredentials;
|
|
24
|
+
}
|