@xano/cli 0.0.37 → 0.0.39
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 +8 -9
- 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 +2218 -1813
- 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,122 @@
|
|
|
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 TenantEnvSet extends BaseCommand {
|
|
8
|
+
static args = {
|
|
9
|
+
tenant_name: Args.string({
|
|
10
|
+
description: 'Tenant name',
|
|
11
|
+
required: true,
|
|
12
|
+
}),
|
|
13
|
+
};
|
|
14
|
+
static description = 'Set (create or update) an environment variable for a tenant';
|
|
15
|
+
static examples = [
|
|
16
|
+
`$ xano tenant env set my-tenant --name DATABASE_URL --value postgres://localhost:5432/mydb
|
|
17
|
+
Environment variable 'DATABASE_URL' set for tenant my-tenant
|
|
18
|
+
`,
|
|
19
|
+
`$ xano tenant env set my-tenant --name DATABASE_URL --value postgres://localhost:5432/mydb -w 5 -o json`,
|
|
20
|
+
];
|
|
21
|
+
static flags = {
|
|
22
|
+
...BaseCommand.baseFlags,
|
|
23
|
+
name: Flags.string({
|
|
24
|
+
char: 'n',
|
|
25
|
+
description: 'Environment variable name',
|
|
26
|
+
required: true,
|
|
27
|
+
}),
|
|
28
|
+
output: Flags.string({
|
|
29
|
+
char: 'o',
|
|
30
|
+
default: 'summary',
|
|
31
|
+
description: 'Output format',
|
|
32
|
+
options: ['summary', 'json'],
|
|
33
|
+
required: false,
|
|
34
|
+
}),
|
|
35
|
+
value: Flags.string({
|
|
36
|
+
description: 'Environment variable value',
|
|
37
|
+
required: true,
|
|
38
|
+
}),
|
|
39
|
+
workspace: Flags.string({
|
|
40
|
+
char: 'w',
|
|
41
|
+
description: 'Workspace ID (uses profile workspace if not provided)',
|
|
42
|
+
required: false,
|
|
43
|
+
}),
|
|
44
|
+
};
|
|
45
|
+
async run() {
|
|
46
|
+
const { args, flags } = await this.parse(TenantEnvSet);
|
|
47
|
+
const profileName = flags.profile || this.getDefaultProfile();
|
|
48
|
+
const credentials = this.loadCredentials();
|
|
49
|
+
if (!(profileName in credentials.profiles)) {
|
|
50
|
+
this.error(`Profile '${profileName}' not found. Available profiles: ${Object.keys(credentials.profiles).join(', ')}\n` +
|
|
51
|
+
`Create a profile using 'xano profile create'`);
|
|
52
|
+
}
|
|
53
|
+
const profile = credentials.profiles[profileName];
|
|
54
|
+
if (!profile.instance_origin) {
|
|
55
|
+
this.error(`Profile '${profileName}' is missing instance_origin`);
|
|
56
|
+
}
|
|
57
|
+
if (!profile.access_token) {
|
|
58
|
+
this.error(`Profile '${profileName}' is missing access_token`);
|
|
59
|
+
}
|
|
60
|
+
const workspaceId = flags.workspace || profile.workspace;
|
|
61
|
+
if (!workspaceId) {
|
|
62
|
+
this.error('No workspace ID provided. Use --workspace flag or set one in your profile.');
|
|
63
|
+
}
|
|
64
|
+
const tenantName = args.tenant_name;
|
|
65
|
+
const envName = flags.name;
|
|
66
|
+
const apiUrl = `${profile.instance_origin}/api:meta/workspace/${workspaceId}/tenant/${tenantName}/env/${envName}`;
|
|
67
|
+
const body = {
|
|
68
|
+
env: {
|
|
69
|
+
name: envName,
|
|
70
|
+
value: flags.value,
|
|
71
|
+
},
|
|
72
|
+
};
|
|
73
|
+
try {
|
|
74
|
+
const response = await this.verboseFetch(apiUrl, {
|
|
75
|
+
body: JSON.stringify(body),
|
|
76
|
+
headers: {
|
|
77
|
+
accept: 'application/json',
|
|
78
|
+
Authorization: `Bearer ${profile.access_token}`,
|
|
79
|
+
'Content-Type': 'application/json',
|
|
80
|
+
},
|
|
81
|
+
method: 'PATCH',
|
|
82
|
+
}, flags.verbose, profile.access_token);
|
|
83
|
+
if (!response.ok) {
|
|
84
|
+
const errorText = await response.text();
|
|
85
|
+
this.error(`API request failed with status ${response.status}: ${response.statusText}\n${errorText}`);
|
|
86
|
+
}
|
|
87
|
+
if (flags.output === 'json') {
|
|
88
|
+
const result = await response.json();
|
|
89
|
+
this.log(JSON.stringify(result, null, 2));
|
|
90
|
+
}
|
|
91
|
+
else {
|
|
92
|
+
this.log(`Environment variable '${envName}' set for tenant ${tenantName}`);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
catch (error) {
|
|
96
|
+
if (error instanceof Error) {
|
|
97
|
+
this.error(`Failed to set tenant environment variable: ${error.message}`);
|
|
98
|
+
}
|
|
99
|
+
else {
|
|
100
|
+
this.error(`Failed to set tenant environment variable: ${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` + `Create a profile using 'xano profile create'`);
|
|
109
|
+
}
|
|
110
|
+
try {
|
|
111
|
+
const fileContent = fs.readFileSync(credentialsPath, 'utf8');
|
|
112
|
+
const parsed = yaml.load(fileContent);
|
|
113
|
+
if (!parsed || typeof parsed !== 'object' || !('profiles' in parsed)) {
|
|
114
|
+
this.error('Credentials file has invalid format.');
|
|
115
|
+
}
|
|
116
|
+
return parsed;
|
|
117
|
+
}
|
|
118
|
+
catch (error) {
|
|
119
|
+
this.error(`Failed to parse credentials file: ${error}`);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import BaseCommand from '../../../../base-command.js';
|
|
2
|
+
export default class TenantEnvSetAll extends BaseCommand {
|
|
3
|
+
static args: {
|
|
4
|
+
tenant_name: import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
|
|
5
|
+
};
|
|
6
|
+
static description: string;
|
|
7
|
+
static examples: string[];
|
|
8
|
+
static flags: {
|
|
9
|
+
clean: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
10
|
+
file: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
11
|
+
output: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
12
|
+
workspace: import("@oclif/core/interfaces").OptionFlag<string | undefined, 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,131 @@
|
|
|
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 TenantEnvSetAll extends BaseCommand {
|
|
8
|
+
static args = {
|
|
9
|
+
tenant_name: Args.string({
|
|
10
|
+
description: 'Tenant name',
|
|
11
|
+
required: true,
|
|
12
|
+
}),
|
|
13
|
+
};
|
|
14
|
+
static description = 'Set all environment variables for a tenant from a YAML file (replaces all existing)';
|
|
15
|
+
static examples = [
|
|
16
|
+
`$ xano tenant env set_all my-tenant
|
|
17
|
+
Reads from env_my-tenant.yaml
|
|
18
|
+
`,
|
|
19
|
+
`$ xano tenant env set_all my-tenant --file ./my-env.yaml`,
|
|
20
|
+
`$ xano tenant env set_all my-tenant -o json`,
|
|
21
|
+
];
|
|
22
|
+
static flags = {
|
|
23
|
+
...BaseCommand.baseFlags,
|
|
24
|
+
clean: Flags.boolean({
|
|
25
|
+
default: false,
|
|
26
|
+
description: 'Remove the source file after successful upload',
|
|
27
|
+
required: false,
|
|
28
|
+
}),
|
|
29
|
+
file: Flags.string({
|
|
30
|
+
char: 'f',
|
|
31
|
+
description: 'Path to env file (default: env_<tenant_name>.yaml)',
|
|
32
|
+
required: false,
|
|
33
|
+
}),
|
|
34
|
+
output: Flags.string({
|
|
35
|
+
char: 'o',
|
|
36
|
+
default: 'summary',
|
|
37
|
+
description: 'Output format',
|
|
38
|
+
options: ['summary', 'json'],
|
|
39
|
+
required: false,
|
|
40
|
+
}),
|
|
41
|
+
workspace: Flags.string({
|
|
42
|
+
char: 'w',
|
|
43
|
+
description: 'Workspace ID (uses profile workspace if not provided)',
|
|
44
|
+
required: false,
|
|
45
|
+
}),
|
|
46
|
+
};
|
|
47
|
+
async run() {
|
|
48
|
+
const { args, flags } = await this.parse(TenantEnvSetAll);
|
|
49
|
+
const tenantName = args.tenant_name;
|
|
50
|
+
const sourceFilePath = path.resolve(flags.file || `env_${tenantName}.yaml`);
|
|
51
|
+
if (!fs.existsSync(sourceFilePath)) {
|
|
52
|
+
this.error(`File not found: ${sourceFilePath}`);
|
|
53
|
+
}
|
|
54
|
+
const fileContent = fs.readFileSync(sourceFilePath, 'utf8');
|
|
55
|
+
const envMap = yaml.load(fileContent);
|
|
56
|
+
if (!envMap || typeof envMap !== 'object') {
|
|
57
|
+
this.error('Invalid env file format. Expected a YAML map of key: value pairs.');
|
|
58
|
+
}
|
|
59
|
+
const envs = Object.entries(envMap).map(([name, value]) => ({ name, value: String(value) }));
|
|
60
|
+
const profileName = flags.profile || this.getDefaultProfile();
|
|
61
|
+
const credentials = this.loadCredentials();
|
|
62
|
+
if (!(profileName in credentials.profiles)) {
|
|
63
|
+
this.error(`Profile '${profileName}' not found. Available profiles: ${Object.keys(credentials.profiles).join(', ')}\n` +
|
|
64
|
+
`Create a profile using 'xano profile create'`);
|
|
65
|
+
}
|
|
66
|
+
const profile = credentials.profiles[profileName];
|
|
67
|
+
if (!profile.instance_origin) {
|
|
68
|
+
this.error(`Profile '${profileName}' is missing instance_origin`);
|
|
69
|
+
}
|
|
70
|
+
if (!profile.access_token) {
|
|
71
|
+
this.error(`Profile '${profileName}' is missing access_token`);
|
|
72
|
+
}
|
|
73
|
+
const workspaceId = flags.workspace || profile.workspace;
|
|
74
|
+
if (!workspaceId) {
|
|
75
|
+
this.error('No workspace ID provided. Use --workspace flag or set one in your profile.');
|
|
76
|
+
}
|
|
77
|
+
const apiUrl = `${profile.instance_origin}/api:meta/workspace/${workspaceId}/tenant/${tenantName}/env_all`;
|
|
78
|
+
try {
|
|
79
|
+
const response = await this.verboseFetch(apiUrl, {
|
|
80
|
+
body: JSON.stringify({ envs }),
|
|
81
|
+
headers: {
|
|
82
|
+
accept: 'application/json',
|
|
83
|
+
Authorization: `Bearer ${profile.access_token}`,
|
|
84
|
+
'Content-Type': 'application/json',
|
|
85
|
+
},
|
|
86
|
+
method: 'PUT',
|
|
87
|
+
}, flags.verbose, profile.access_token);
|
|
88
|
+
if (!response.ok) {
|
|
89
|
+
const errorText = await response.text();
|
|
90
|
+
this.error(`API request failed with status ${response.status}: ${response.statusText}\n${errorText}`);
|
|
91
|
+
}
|
|
92
|
+
if (flags.output === 'json') {
|
|
93
|
+
const result = await response.json();
|
|
94
|
+
this.log(JSON.stringify(result, null, 2));
|
|
95
|
+
}
|
|
96
|
+
else {
|
|
97
|
+
this.log(`All environment variables updated for tenant ${tenantName} (${envs.length} variables)`);
|
|
98
|
+
}
|
|
99
|
+
if (flags.clean && fs.existsSync(sourceFilePath)) {
|
|
100
|
+
fs.unlinkSync(sourceFilePath);
|
|
101
|
+
this.log(`Removed ${sourceFilePath}`);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
catch (error) {
|
|
105
|
+
if (error instanceof Error) {
|
|
106
|
+
this.error(`Failed to set tenant environment variables: ${error.message}`);
|
|
107
|
+
}
|
|
108
|
+
else {
|
|
109
|
+
this.error(`Failed to set tenant environment variables: ${String(error)}`);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
loadCredentials() {
|
|
114
|
+
const configDir = path.join(os.homedir(), '.xano');
|
|
115
|
+
const credentialsPath = path.join(configDir, 'credentials.yaml');
|
|
116
|
+
if (!fs.existsSync(credentialsPath)) {
|
|
117
|
+
this.error(`Credentials file not found at ${credentialsPath}\n` + `Create a profile using 'xano profile create'`);
|
|
118
|
+
}
|
|
119
|
+
try {
|
|
120
|
+
const fileContent = fs.readFileSync(credentialsPath, 'utf8');
|
|
121
|
+
const parsed = yaml.load(fileContent);
|
|
122
|
+
if (!parsed || typeof parsed !== 'object' || !('profiles' in parsed)) {
|
|
123
|
+
this.error('Credentials file has invalid format.');
|
|
124
|
+
}
|
|
125
|
+
return parsed;
|
|
126
|
+
}
|
|
127
|
+
catch (error) {
|
|
128
|
+
this.error(`Failed to parse credentials file: ${error}`);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
@@ -62,8 +62,8 @@ Tenant: My Tenant (my-tenant)
|
|
|
62
62
|
try {
|
|
63
63
|
const response = await this.verboseFetch(apiUrl, {
|
|
64
64
|
headers: {
|
|
65
|
-
|
|
66
|
-
|
|
65
|
+
accept: 'application/json',
|
|
66
|
+
Authorization: `Bearer ${profile.access_token}`,
|
|
67
67
|
},
|
|
68
68
|
method: 'GET',
|
|
69
69
|
}, flags.verbose, profile.access_token);
|
|
@@ -71,7 +71,7 @@ Tenant: My Tenant (my-tenant)
|
|
|
71
71
|
const errorText = await response.text();
|
|
72
72
|
this.error(`API request failed with status ${response.status}: ${response.statusText}\n${errorText}`);
|
|
73
73
|
}
|
|
74
|
-
const tenant = await response.json();
|
|
74
|
+
const tenant = (await response.json());
|
|
75
75
|
if (flags.output === 'json') {
|
|
76
76
|
this.log(JSON.stringify(tenant, null, 2));
|
|
77
77
|
}
|
|
@@ -97,6 +97,8 @@ Tenant: My Tenant (my-tenant)
|
|
|
97
97
|
this.log(` Tasks: ${tenant.tasks}`);
|
|
98
98
|
if (tenant.ingress !== undefined)
|
|
99
99
|
this.log(` Ingress: ${tenant.ingress}`);
|
|
100
|
+
if (tenant.ephemeral)
|
|
101
|
+
this.log(` Ephemeral: ${tenant.ephemeral}`);
|
|
100
102
|
if (tenant.deployed_at) {
|
|
101
103
|
const d = new Date(tenant.deployed_at);
|
|
102
104
|
const deployedDate = Number.isNaN(d.getTime()) ? tenant.deployed_at : d.toISOString().split('T')[0];
|
|
@@ -117,8 +119,7 @@ Tenant: My Tenant (my-tenant)
|
|
|
117
119
|
const configDir = path.join(os.homedir(), '.xano');
|
|
118
120
|
const credentialsPath = path.join(configDir, 'credentials.yaml');
|
|
119
121
|
if (!fs.existsSync(credentialsPath)) {
|
|
120
|
-
this.error(`Credentials file not found at ${credentialsPath}\n` +
|
|
121
|
-
`Create a profile using 'xano profile create'`);
|
|
122
|
+
this.error(`Credentials file not found at ${credentialsPath}\n` + `Create a profile using 'xano profile create'`);
|
|
122
123
|
}
|
|
123
124
|
try {
|
|
124
125
|
const fileContent = fs.readFileSync(credentialsPath, 'utf8');
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import BaseCommand from '../../../base-command.js';
|
|
2
|
+
export default class TenantImpersonate extends BaseCommand {
|
|
3
|
+
static args: {
|
|
4
|
+
tenant_name: import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
|
|
5
|
+
};
|
|
6
|
+
static description: string;
|
|
7
|
+
static examples: string[];
|
|
8
|
+
static flags: {
|
|
9
|
+
output: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
10
|
+
'url-only': import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
11
|
+
workspace: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
12
|
+
profile: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
13
|
+
verbose: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
14
|
+
};
|
|
15
|
+
run(): Promise<void>;
|
|
16
|
+
private getImpersonateResponse;
|
|
17
|
+
private getFrontendUrl;
|
|
18
|
+
private loadCredentials;
|
|
19
|
+
}
|
|
@@ -0,0 +1,146 @@
|
|
|
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 open from 'open';
|
|
7
|
+
import BaseCommand from '../../../base-command.js';
|
|
8
|
+
export default class TenantImpersonate extends BaseCommand {
|
|
9
|
+
static args = {
|
|
10
|
+
tenant_name: Args.string({
|
|
11
|
+
description: 'Tenant name to impersonate',
|
|
12
|
+
required: true,
|
|
13
|
+
}),
|
|
14
|
+
};
|
|
15
|
+
static description = 'Impersonate a tenant and open it in the browser';
|
|
16
|
+
static examples = [
|
|
17
|
+
`$ xano tenant impersonate my-tenant
|
|
18
|
+
Opening browser...
|
|
19
|
+
Impersonation successful!
|
|
20
|
+
`,
|
|
21
|
+
`$ xano tenant impersonate my-tenant -o json`,
|
|
22
|
+
];
|
|
23
|
+
static flags = {
|
|
24
|
+
...BaseCommand.baseFlags,
|
|
25
|
+
output: Flags.string({
|
|
26
|
+
char: 'o',
|
|
27
|
+
default: 'summary',
|
|
28
|
+
description: 'Output format',
|
|
29
|
+
options: ['summary', 'json'],
|
|
30
|
+
required: false,
|
|
31
|
+
}),
|
|
32
|
+
'url-only': Flags.boolean({
|
|
33
|
+
char: 'u',
|
|
34
|
+
default: false,
|
|
35
|
+
description: 'Print the URL without opening the browser',
|
|
36
|
+
required: false,
|
|
37
|
+
}),
|
|
38
|
+
workspace: Flags.string({
|
|
39
|
+
char: 'w',
|
|
40
|
+
description: 'Workspace ID (uses profile workspace if not provided)',
|
|
41
|
+
required: false,
|
|
42
|
+
}),
|
|
43
|
+
};
|
|
44
|
+
async run() {
|
|
45
|
+
const { args, flags } = await this.parse(TenantImpersonate);
|
|
46
|
+
const profileName = flags.profile || this.getDefaultProfile();
|
|
47
|
+
const credentials = this.loadCredentials();
|
|
48
|
+
if (!(profileName in credentials.profiles)) {
|
|
49
|
+
this.error(`Profile '${profileName}' not found. Available profiles: ${Object.keys(credentials.profiles).join(', ')}\n` +
|
|
50
|
+
`Create a profile using 'xano auth'`);
|
|
51
|
+
}
|
|
52
|
+
const profile = credentials.profiles[profileName];
|
|
53
|
+
if (!profile.instance_origin) {
|
|
54
|
+
this.error(`Profile '${profileName}' is missing instance_origin`);
|
|
55
|
+
}
|
|
56
|
+
if (!profile.access_token) {
|
|
57
|
+
this.error(`Profile '${profileName}' is missing access_token`);
|
|
58
|
+
}
|
|
59
|
+
const workspaceId = flags.workspace || profile.workspace;
|
|
60
|
+
if (!workspaceId) {
|
|
61
|
+
this.error('No workspace ID provided. Use --workspace flag or set one in your profile.');
|
|
62
|
+
}
|
|
63
|
+
const tenantName = args.tenant_name;
|
|
64
|
+
try {
|
|
65
|
+
const response = await this.getImpersonateResponse(profile, workspaceId, tenantName);
|
|
66
|
+
if (flags.output === 'json') {
|
|
67
|
+
this.log(JSON.stringify(response, null, 2));
|
|
68
|
+
}
|
|
69
|
+
else {
|
|
70
|
+
const frontendUrl = this.getFrontendUrl(profile.instance_origin);
|
|
71
|
+
const params = new URLSearchParams({
|
|
72
|
+
_ti: response._ti,
|
|
73
|
+
});
|
|
74
|
+
const impersonateUrl = `${frontendUrl}/tenant-impersonate?${params.toString()}`;
|
|
75
|
+
if (flags['url-only']) {
|
|
76
|
+
this.log(impersonateUrl);
|
|
77
|
+
}
|
|
78
|
+
else {
|
|
79
|
+
this.log('Opening browser...');
|
|
80
|
+
await open(impersonateUrl);
|
|
81
|
+
this.log('Impersonation successful!');
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
process.exit(0);
|
|
85
|
+
}
|
|
86
|
+
catch (error) {
|
|
87
|
+
if (error instanceof Error) {
|
|
88
|
+
this.error(`Failed to impersonate tenant: ${error.message}`);
|
|
89
|
+
}
|
|
90
|
+
else {
|
|
91
|
+
this.error(`Failed to impersonate tenant: ${String(error)}`);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
async getImpersonateResponse(profile, workspaceId, tenantName) {
|
|
96
|
+
const apiUrl = `${profile.instance_origin}/api:meta/workspace/${workspaceId}/tenant/${encodeURIComponent(tenantName)}/impersonate`;
|
|
97
|
+
const { verbose } = await this.parse(TenantImpersonate).then((r) => r.flags);
|
|
98
|
+
const response = await this.verboseFetch(apiUrl, {
|
|
99
|
+
headers: {
|
|
100
|
+
Authorization: `Bearer ${profile.access_token}`,
|
|
101
|
+
accept: 'application/json',
|
|
102
|
+
},
|
|
103
|
+
method: 'GET',
|
|
104
|
+
}, verbose, profile.access_token);
|
|
105
|
+
if (!response.ok) {
|
|
106
|
+
const errorText = await response.text();
|
|
107
|
+
throw new Error(`API request failed with status ${response.status}: ${response.statusText}\n${errorText}`);
|
|
108
|
+
}
|
|
109
|
+
const result = (await response.json());
|
|
110
|
+
if (!result._ti) {
|
|
111
|
+
throw new Error('No one-time token returned from impersonate API');
|
|
112
|
+
}
|
|
113
|
+
return result;
|
|
114
|
+
}
|
|
115
|
+
getFrontendUrl(instanceOrigin) {
|
|
116
|
+
try {
|
|
117
|
+
const url = new URL(instanceOrigin);
|
|
118
|
+
if (url.hostname === 'localhost' || url.hostname === '127.0.0.1') {
|
|
119
|
+
url.port = '4200';
|
|
120
|
+
return url.origin;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
catch {
|
|
124
|
+
// fall through
|
|
125
|
+
}
|
|
126
|
+
return instanceOrigin;
|
|
127
|
+
}
|
|
128
|
+
loadCredentials() {
|
|
129
|
+
const configDir = path.join(os.homedir(), '.xano');
|
|
130
|
+
const credentialsPath = path.join(configDir, 'credentials.yaml');
|
|
131
|
+
if (!fs.existsSync(credentialsPath)) {
|
|
132
|
+
this.error(`Credentials file not found at ${credentialsPath}\n` + `Create a profile using 'xano auth'`);
|
|
133
|
+
}
|
|
134
|
+
try {
|
|
135
|
+
const fileContent = fs.readFileSync(credentialsPath, 'utf8');
|
|
136
|
+
const parsed = yaml.load(fileContent);
|
|
137
|
+
if (!parsed || typeof parsed !== 'object' || !('profiles' in parsed)) {
|
|
138
|
+
this.error('Credentials file has invalid format.');
|
|
139
|
+
}
|
|
140
|
+
return parsed;
|
|
141
|
+
}
|
|
142
|
+
catch (error) {
|
|
143
|
+
this.error(`Failed to parse credentials file: ${error}`);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import BaseCommand from '../../../../base-command.js';
|
|
2
|
+
export default class TenantLicenseGet extends BaseCommand {
|
|
3
|
+
static args: {
|
|
4
|
+
tenant_name: import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
|
|
5
|
+
};
|
|
6
|
+
static description: string;
|
|
7
|
+
static examples: string[];
|
|
8
|
+
static flags: {
|
|
9
|
+
file: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
10
|
+
output: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
11
|
+
view: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
12
|
+
workspace: import("@oclif/core/interfaces").OptionFlag<string | undefined, 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,127 @@
|
|
|
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 TenantLicenseGet extends BaseCommand {
|
|
8
|
+
static args = {
|
|
9
|
+
tenant_name: Args.string({
|
|
10
|
+
description: 'Tenant name',
|
|
11
|
+
required: true,
|
|
12
|
+
}),
|
|
13
|
+
};
|
|
14
|
+
static description = 'Get the license for a tenant';
|
|
15
|
+
static examples = [
|
|
16
|
+
`$ xano tenant license get my-tenant
|
|
17
|
+
License saved to license_my-tenant.yaml
|
|
18
|
+
`,
|
|
19
|
+
`$ xano tenant license get my-tenant --file ./my-license.yaml
|
|
20
|
+
License saved to my-license.yaml
|
|
21
|
+
`,
|
|
22
|
+
`$ xano tenant license get my-tenant --view`,
|
|
23
|
+
`$ xano tenant license get my-tenant -o json`,
|
|
24
|
+
];
|
|
25
|
+
static flags = {
|
|
26
|
+
...BaseCommand.baseFlags,
|
|
27
|
+
file: Flags.string({
|
|
28
|
+
char: 'f',
|
|
29
|
+
description: 'Output file path (default: license_<tenant_name>.yaml)',
|
|
30
|
+
required: false,
|
|
31
|
+
}),
|
|
32
|
+
output: Flags.string({
|
|
33
|
+
char: 'o',
|
|
34
|
+
default: 'summary',
|
|
35
|
+
description: 'Output format',
|
|
36
|
+
options: ['summary', 'json'],
|
|
37
|
+
required: false,
|
|
38
|
+
}),
|
|
39
|
+
view: Flags.boolean({
|
|
40
|
+
default: false,
|
|
41
|
+
description: 'Print license to stdout instead of saving to file',
|
|
42
|
+
required: false,
|
|
43
|
+
}),
|
|
44
|
+
workspace: Flags.string({
|
|
45
|
+
char: 'w',
|
|
46
|
+
description: 'Workspace ID (uses profile workspace if not provided)',
|
|
47
|
+
required: false,
|
|
48
|
+
}),
|
|
49
|
+
};
|
|
50
|
+
async run() {
|
|
51
|
+
const { args, flags } = await this.parse(TenantLicenseGet);
|
|
52
|
+
const profileName = flags.profile || this.getDefaultProfile();
|
|
53
|
+
const credentials = this.loadCredentials();
|
|
54
|
+
if (!(profileName in credentials.profiles)) {
|
|
55
|
+
this.error(`Profile '${profileName}' not found. Available profiles: ${Object.keys(credentials.profiles).join(', ')}\n` +
|
|
56
|
+
`Create a profile using 'xano profile create'`);
|
|
57
|
+
}
|
|
58
|
+
const profile = credentials.profiles[profileName];
|
|
59
|
+
if (!profile.instance_origin) {
|
|
60
|
+
this.error(`Profile '${profileName}' is missing instance_origin`);
|
|
61
|
+
}
|
|
62
|
+
if (!profile.access_token) {
|
|
63
|
+
this.error(`Profile '${profileName}' is missing access_token`);
|
|
64
|
+
}
|
|
65
|
+
const workspaceId = flags.workspace || profile.workspace;
|
|
66
|
+
if (!workspaceId) {
|
|
67
|
+
this.error('No workspace ID provided. Use --workspace flag or set one in your profile.');
|
|
68
|
+
}
|
|
69
|
+
const tenantName = args.tenant_name;
|
|
70
|
+
const apiUrl = `${profile.instance_origin}/api:meta/workspace/${workspaceId}/tenant/${tenantName}/license`;
|
|
71
|
+
try {
|
|
72
|
+
const response = await this.verboseFetch(apiUrl, {
|
|
73
|
+
headers: {
|
|
74
|
+
accept: 'application/json',
|
|
75
|
+
Authorization: `Bearer ${profile.access_token}`,
|
|
76
|
+
},
|
|
77
|
+
method: 'GET',
|
|
78
|
+
}, flags.verbose, profile.access_token);
|
|
79
|
+
if (!response.ok) {
|
|
80
|
+
const errorText = await response.text();
|
|
81
|
+
this.error(`API request failed with status ${response.status}: ${response.statusText}\n${errorText}`);
|
|
82
|
+
}
|
|
83
|
+
const license = await response.json();
|
|
84
|
+
// The license is a raw YAML string — write it directly, not yaml.dump'd
|
|
85
|
+
const licenseContent = typeof license === 'string' ? license : JSON.stringify(license, null, 2);
|
|
86
|
+
if (flags.view || flags.output === 'json') {
|
|
87
|
+
if (flags.output === 'json') {
|
|
88
|
+
this.log(JSON.stringify(license, null, 2));
|
|
89
|
+
}
|
|
90
|
+
else {
|
|
91
|
+
this.log(licenseContent);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
else {
|
|
95
|
+
const filePath = path.resolve(flags.file || `license_${tenantName}.yaml`);
|
|
96
|
+
fs.writeFileSync(filePath, licenseContent, 'utf8');
|
|
97
|
+
this.log(`License saved to ${filePath}`);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
catch (error) {
|
|
101
|
+
if (error instanceof Error) {
|
|
102
|
+
this.error(`Failed to get tenant license: ${error.message}`);
|
|
103
|
+
}
|
|
104
|
+
else {
|
|
105
|
+
this.error(`Failed to get tenant license: ${String(error)}`);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
loadCredentials() {
|
|
110
|
+
const configDir = path.join(os.homedir(), '.xano');
|
|
111
|
+
const credentialsPath = path.join(configDir, 'credentials.yaml');
|
|
112
|
+
if (!fs.existsSync(credentialsPath)) {
|
|
113
|
+
this.error(`Credentials file not found at ${credentialsPath}\n` + `Create a profile using 'xano profile create'`);
|
|
114
|
+
}
|
|
115
|
+
try {
|
|
116
|
+
const fileContent = fs.readFileSync(credentialsPath, 'utf8');
|
|
117
|
+
const parsed = yaml.load(fileContent);
|
|
118
|
+
if (!parsed || typeof parsed !== 'object' || !('profiles' in parsed)) {
|
|
119
|
+
this.error('Credentials file has invalid format.');
|
|
120
|
+
}
|
|
121
|
+
return parsed;
|
|
122
|
+
}
|
|
123
|
+
catch (error) {
|
|
124
|
+
this.error(`Failed to parse credentials file: ${error}`);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import BaseCommand from '../../../../base-command.js';
|
|
2
|
+
export default class TenantLicenseSet extends BaseCommand {
|
|
3
|
+
static args: {
|
|
4
|
+
tenant_name: import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
|
|
5
|
+
};
|
|
6
|
+
static description: string;
|
|
7
|
+
static examples: string[];
|
|
8
|
+
static flags: {
|
|
9
|
+
clean: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
10
|
+
file: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
11
|
+
output: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
12
|
+
value: import("@oclif/core/interfaces").OptionFlag<string | undefined, 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
|
+
}
|