@xano/cli 1.0.4 → 1.0.6-beta.0
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 +139 -4
- package/dist/base-command.d.ts +27 -0
- package/dist/base-command.js +124 -1
- package/dist/commands/auth/index.d.ts +45 -1
- package/dist/commands/auth/index.js +197 -29
- package/dist/commands/ephemeral/create/index.d.ts +18 -0
- package/dist/commands/ephemeral/create/index.js +103 -0
- package/dist/commands/ephemeral/delete/index.d.ts +18 -0
- package/dist/commands/ephemeral/delete/index.js +99 -0
- package/dist/commands/ephemeral/edit/index.d.ts +18 -0
- package/dist/commands/ephemeral/edit/index.js +109 -0
- package/dist/commands/ephemeral/get/index.d.ts +21 -0
- package/dist/commands/ephemeral/get/index.js +125 -0
- package/dist/commands/ephemeral/impersonate/index.d.ts +19 -0
- package/dist/commands/ephemeral/impersonate/index.js +112 -0
- package/dist/commands/ephemeral/list/index.d.ts +19 -0
- package/dist/commands/ephemeral/list/index.js +122 -0
- package/dist/commands/ephemeral/pull/index.d.ts +23 -0
- package/dist/commands/ephemeral/pull/index.js +248 -0
- package/dist/commands/{knowledge → ephemeral}/push/index.d.ts +8 -5
- package/dist/commands/ephemeral/push/index.js +143 -0
- package/dist/commands/function/run/index.d.ts +40 -0
- package/dist/commands/function/run/index.js +244 -0
- package/dist/commands/knowledge/{pull → get}/index.d.ts +6 -2
- package/dist/commands/knowledge/get/index.js +126 -0
- package/dist/commands/knowledge/list/index.d.ts +18 -0
- package/dist/commands/knowledge/list/index.js +135 -0
- package/dist/commands/sandbox/pull/index.js +12 -2
- package/dist/commands/sandbox/push/index.d.ts +1 -1
- package/dist/commands/sandbox/push/index.js +19 -14
- package/dist/commands/sandbox/reset/index.js +12 -0
- package/dist/commands/static_host/build/create/index.d.ts +9 -1
- package/dist/commands/static_host/build/create/index.js +54 -4
- package/dist/commands/static_host/build/delete/index.d.ts +19 -0
- package/dist/commands/static_host/build/delete/index.js +114 -0
- package/dist/commands/static_host/build/get/index.d.ts +1 -1
- package/dist/commands/static_host/build/get/index.js +16 -10
- package/dist/commands/static_host/build/pull/index.d.ts +52 -0
- package/dist/commands/static_host/build/pull/index.js +300 -0
- package/dist/commands/static_host/build/push/index.d.ts +23 -0
- package/dist/commands/static_host/build/push/index.js +228 -0
- package/dist/commands/static_host/create/index.d.ts +17 -0
- package/dist/commands/static_host/create/index.js +86 -0
- package/dist/commands/static_host/deploy/index.d.ts +18 -0
- package/dist/commands/static_host/deploy/index.js +105 -0
- package/dist/commands/static_host/edit/index.d.ts +23 -0
- package/dist/commands/static_host/edit/index.js +151 -0
- package/dist/commands/static_host/get/index.d.ts +18 -0
- package/dist/commands/static_host/get/index.js +94 -0
- package/dist/commands/static_host/migrate/index.d.ts +44 -0
- package/dist/commands/static_host/migrate/index.js +205 -0
- package/dist/commands/tenant/snapshot/create/index.d.ts +17 -0
- package/dist/commands/tenant/snapshot/create/index.js +78 -0
- package/dist/commands/tenant/snapshot/delete/index.d.ts +19 -0
- package/dist/commands/tenant/snapshot/delete/index.js +102 -0
- package/dist/commands/tenant/snapshot/list/index.d.ts +16 -0
- package/dist/commands/tenant/snapshot/list/index.js +96 -0
- package/dist/commands/tenant/snapshot/swap/index.d.ts +19 -0
- package/dist/commands/tenant/snapshot/swap/index.js +103 -0
- package/dist/commands/tenant/unit_test/run/index.d.ts +1 -0
- package/dist/commands/tenant/unit_test/run/index.js +7 -0
- package/dist/commands/tenant/unit_test/run_all/index.js +7 -3
- package/dist/commands/unit_test/run/index.d.ts +1 -0
- package/dist/commands/unit_test/run/index.js +8 -0
- package/dist/commands/unit_test/run_all/index.js +7 -3
- package/dist/commands/workspace/pull/index.js +13 -2
- package/dist/commands/workspace/push/index.d.ts +1 -1
- package/dist/commands/workspace/push/index.js +15 -5
- package/dist/utils/function-input.d.ts +77 -0
- package/dist/utils/function-input.js +148 -0
- package/dist/utils/knowledge-sync.d.ts +84 -79
- package/dist/utils/knowledge-sync.js +376 -323
- package/dist/utils/multidoc-push.d.ts +30 -18
- package/dist/utils/multidoc-push.js +284 -148
- package/dist/utils/reference-checker.js +2 -2
- package/dist/utils/static-host-files.d.ts +21 -0
- package/dist/utils/static-host-files.js +41 -0
- package/oclif.manifest.json +5046 -2691
- package/package.json +10 -1
- package/dist/commands/knowledge/pull/index.js +0 -86
- package/dist/commands/knowledge/push/index.js +0 -126
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { Args, Flags } from '@oclif/core';
|
|
2
|
+
import BaseCommand from '../../../../base-command.js';
|
|
3
|
+
export default class TenantSnapshotCreate extends BaseCommand {
|
|
4
|
+
static args = {
|
|
5
|
+
tenant_name: Args.string({
|
|
6
|
+
description: 'Tenant name to snapshot',
|
|
7
|
+
required: true,
|
|
8
|
+
}),
|
|
9
|
+
};
|
|
10
|
+
static description = "Create a database snapshot for a tenant (an instant clone of the tenant's database)";
|
|
11
|
+
static examples = [
|
|
12
|
+
`$ xano tenant snapshot create t1234-abcd-xyz1 --label before-v2
|
|
13
|
+
Created snapshot t1234-abcd-xyz1_bk_20260603_203614 for tenant t1234-abcd-xyz1
|
|
14
|
+
`,
|
|
15
|
+
`$ xano tenant snapshot create t1234-abcd-xyz1 -l before-v2 -o json`,
|
|
16
|
+
];
|
|
17
|
+
static flags = {
|
|
18
|
+
...BaseCommand.baseFlags,
|
|
19
|
+
label: Flags.string({
|
|
20
|
+
char: 'l',
|
|
21
|
+
default: '',
|
|
22
|
+
description: 'Optional label appended to the snapshot description (alphanumeric)',
|
|
23
|
+
required: false,
|
|
24
|
+
}),
|
|
25
|
+
output: Flags.string({
|
|
26
|
+
char: 'o',
|
|
27
|
+
default: 'summary',
|
|
28
|
+
description: 'Output format',
|
|
29
|
+
options: ['summary', 'json'],
|
|
30
|
+
required: false,
|
|
31
|
+
}),
|
|
32
|
+
workspace: Flags.string({
|
|
33
|
+
char: 'w',
|
|
34
|
+
description: 'Workspace ID (uses profile workspace if not provided)',
|
|
35
|
+
required: false,
|
|
36
|
+
}),
|
|
37
|
+
};
|
|
38
|
+
async run() {
|
|
39
|
+
const { args, flags } = await this.parse(TenantSnapshotCreate);
|
|
40
|
+
const { profile } = this.resolveProfile(flags);
|
|
41
|
+
const workspaceId = flags.workspace || profile.workspace;
|
|
42
|
+
if (!workspaceId) {
|
|
43
|
+
this.error('No workspace ID provided. Use --workspace flag or set one in your profile.');
|
|
44
|
+
}
|
|
45
|
+
const tenantName = args.tenant_name;
|
|
46
|
+
const apiUrl = `${profile.instance_origin}/api:meta/workspace/${workspaceId}/tenant/${tenantName}/snapshot`;
|
|
47
|
+
try {
|
|
48
|
+
const response = await this.verboseFetch(apiUrl, {
|
|
49
|
+
body: JSON.stringify({ label: flags.label }),
|
|
50
|
+
headers: {
|
|
51
|
+
'accept': 'application/json',
|
|
52
|
+
'Authorization': `Bearer ${profile.access_token}`,
|
|
53
|
+
'Content-Type': 'application/json',
|
|
54
|
+
},
|
|
55
|
+
method: 'POST',
|
|
56
|
+
}, flags.verbose, profile.access_token);
|
|
57
|
+
if (!response.ok) {
|
|
58
|
+
const errorText = await response.text();
|
|
59
|
+
this.error(`API request failed with status ${response.status}: ${response.statusText}\n${errorText}`);
|
|
60
|
+
}
|
|
61
|
+
const result = (await response.json());
|
|
62
|
+
if (flags.output === 'json') {
|
|
63
|
+
this.log(JSON.stringify(result, null, 2));
|
|
64
|
+
}
|
|
65
|
+
else {
|
|
66
|
+
this.log(`Created snapshot ${result.backup} for tenant ${tenantName}`);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
catch (error) {
|
|
70
|
+
if (error instanceof Error) {
|
|
71
|
+
this.error(`Failed to create snapshot: ${error.message}`);
|
|
72
|
+
}
|
|
73
|
+
else {
|
|
74
|
+
this.error(`Failed to create snapshot: ${String(error)}`);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import BaseCommand from '../../../../base-command.js';
|
|
2
|
+
export default class TenantSnapshotDelete 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
|
+
force: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
10
|
+
output: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
11
|
+
snapshot: 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
|
+
config: 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 confirm;
|
|
19
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { Args, Flags } from '@oclif/core';
|
|
2
|
+
import * as readline from 'node:readline';
|
|
3
|
+
import BaseCommand from '../../../../base-command.js';
|
|
4
|
+
export default class TenantSnapshotDelete extends BaseCommand {
|
|
5
|
+
static args = {
|
|
6
|
+
tenant_name: Args.string({
|
|
7
|
+
description: 'Tenant name that owns the snapshot',
|
|
8
|
+
required: true,
|
|
9
|
+
}),
|
|
10
|
+
};
|
|
11
|
+
static description = '[CRITICAL] NEVER delete a snapshot without explicit user confirmation; this permanently drops the snapshot database. The live and original databases cannot be deleted.';
|
|
12
|
+
static examples = [
|
|
13
|
+
`$ xano tenant snapshot delete t1234-abcd-xyz1 --snapshot t1234-abcd-xyz1_bk_20260603_203614
|
|
14
|
+
Are you sure you want to delete snapshot "t1234-abcd-xyz1_bk_20260603_203614"? This action cannot be undone. (y/N) y
|
|
15
|
+
Deleted snapshot t1234-abcd-xyz1_bk_20260603_203614
|
|
16
|
+
`,
|
|
17
|
+
`$ xano tenant snapshot delete t1234-abcd-xyz1 --snapshot t1234-abcd-xyz1_bk_20260603_203614 --force`,
|
|
18
|
+
];
|
|
19
|
+
static flags = {
|
|
20
|
+
...BaseCommand.baseFlags,
|
|
21
|
+
force: Flags.boolean({
|
|
22
|
+
char: 'f',
|
|
23
|
+
default: false,
|
|
24
|
+
description: '[CRITICAL] Skips the confirmation prompt.',
|
|
25
|
+
required: false,
|
|
26
|
+
}),
|
|
27
|
+
output: Flags.string({
|
|
28
|
+
char: 'o',
|
|
29
|
+
default: 'summary',
|
|
30
|
+
description: 'Output format',
|
|
31
|
+
options: ['summary', 'json'],
|
|
32
|
+
required: false,
|
|
33
|
+
}),
|
|
34
|
+
snapshot: Flags.string({
|
|
35
|
+
description: 'Snapshot database name to delete',
|
|
36
|
+
required: true,
|
|
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(TenantSnapshotDelete);
|
|
46
|
+
const { profile } = this.resolveProfile(flags);
|
|
47
|
+
const workspaceId = flags.workspace || profile.workspace;
|
|
48
|
+
if (!workspaceId) {
|
|
49
|
+
this.error('No workspace ID provided. Use --workspace flag or set one in your profile.');
|
|
50
|
+
}
|
|
51
|
+
const tenantName = args.tenant_name;
|
|
52
|
+
const { snapshot } = flags;
|
|
53
|
+
if (!flags.force) {
|
|
54
|
+
const confirmed = await this.confirm(`Are you sure you want to delete snapshot "${snapshot}"? This action cannot be undone.`);
|
|
55
|
+
if (!confirmed) {
|
|
56
|
+
this.log('Deletion cancelled.');
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
const queryParams = new URLSearchParams({ snapshot });
|
|
61
|
+
const apiUrl = `${profile.instance_origin}/api:meta/workspace/${workspaceId}/tenant/${tenantName}/snapshot?${queryParams.toString()}`;
|
|
62
|
+
try {
|
|
63
|
+
const response = await this.verboseFetch(apiUrl, {
|
|
64
|
+
headers: {
|
|
65
|
+
accept: 'application/json',
|
|
66
|
+
Authorization: `Bearer ${profile.access_token}`,
|
|
67
|
+
},
|
|
68
|
+
method: 'DELETE',
|
|
69
|
+
}, flags.verbose, profile.access_token);
|
|
70
|
+
if (!response.ok) {
|
|
71
|
+
const errorText = await response.text();
|
|
72
|
+
this.error(`API request failed with status ${response.status}: ${response.statusText}\n${errorText}`);
|
|
73
|
+
}
|
|
74
|
+
if (flags.output === 'json') {
|
|
75
|
+
this.log(JSON.stringify({ deleted: true, snapshot, tenant_name: tenantName }, null, 2));
|
|
76
|
+
}
|
|
77
|
+
else {
|
|
78
|
+
this.log(`Deleted snapshot ${snapshot}`);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
catch (error) {
|
|
82
|
+
if (error instanceof Error) {
|
|
83
|
+
this.error(`Failed to delete snapshot: ${error.message}`);
|
|
84
|
+
}
|
|
85
|
+
else {
|
|
86
|
+
this.error(`Failed to delete snapshot: ${String(error)}`);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
async confirm(message) {
|
|
91
|
+
const rl = readline.createInterface({
|
|
92
|
+
input: process.stdin,
|
|
93
|
+
output: process.stdout,
|
|
94
|
+
});
|
|
95
|
+
return new Promise((resolve) => {
|
|
96
|
+
rl.question(`${message} (y/N) `, (answer) => {
|
|
97
|
+
rl.close();
|
|
98
|
+
resolve(answer.toLowerCase() === 'y' || answer.toLowerCase() === 'yes');
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import BaseCommand from '../../../../base-command.js';
|
|
2
|
+
export default class TenantSnapshotList 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
|
+
workspace: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
11
|
+
config: 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
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { Args, Flags } from '@oclif/core';
|
|
2
|
+
import BaseCommand from '../../../../base-command.js';
|
|
3
|
+
export default class TenantSnapshotList extends BaseCommand {
|
|
4
|
+
static args = {
|
|
5
|
+
tenant_name: Args.string({
|
|
6
|
+
description: 'Tenant name to list snapshots for',
|
|
7
|
+
required: true,
|
|
8
|
+
}),
|
|
9
|
+
};
|
|
10
|
+
static description = 'List database snapshots for a tenant';
|
|
11
|
+
static examples = [
|
|
12
|
+
`$ xano tenant snapshot list t1234-abcd-xyz1
|
|
13
|
+
Snapshots for tenant t1234-abcd-xyz1:
|
|
14
|
+
- t1234-abcd-xyz1 (25 MB) [ORIGINAL]
|
|
15
|
+
- t1234-abcd-xyz1_bk_20260603_203614 (25 MB, 2026-06-03 20:36:14) [LIVE]
|
|
16
|
+
`,
|
|
17
|
+
`$ xano tenant snapshot list t1234-abcd-xyz1 -o json`,
|
|
18
|
+
];
|
|
19
|
+
static flags = {
|
|
20
|
+
...BaseCommand.baseFlags,
|
|
21
|
+
output: Flags.string({
|
|
22
|
+
char: 'o',
|
|
23
|
+
default: 'summary',
|
|
24
|
+
description: 'Output format',
|
|
25
|
+
options: ['summary', 'json'],
|
|
26
|
+
required: false,
|
|
27
|
+
}),
|
|
28
|
+
workspace: Flags.string({
|
|
29
|
+
char: 'w',
|
|
30
|
+
description: 'Workspace ID (uses profile workspace if not provided)',
|
|
31
|
+
required: false,
|
|
32
|
+
}),
|
|
33
|
+
};
|
|
34
|
+
async run() {
|
|
35
|
+
const { args, flags } = await this.parse(TenantSnapshotList);
|
|
36
|
+
const { profile } = this.resolveProfile(flags);
|
|
37
|
+
const workspaceId = flags.workspace || profile.workspace;
|
|
38
|
+
if (!workspaceId) {
|
|
39
|
+
this.error('No workspace ID provided. Use --workspace flag or set one in your profile.');
|
|
40
|
+
}
|
|
41
|
+
const tenantName = args.tenant_name;
|
|
42
|
+
const apiUrl = `${profile.instance_origin}/api:meta/workspace/${workspaceId}/tenant/${tenantName}/snapshot`;
|
|
43
|
+
try {
|
|
44
|
+
const response = await this.verboseFetch(apiUrl, {
|
|
45
|
+
headers: {
|
|
46
|
+
'accept': 'application/json',
|
|
47
|
+
'Authorization': `Bearer ${profile.access_token}`,
|
|
48
|
+
},
|
|
49
|
+
method: 'GET',
|
|
50
|
+
}, flags.verbose, profile.access_token);
|
|
51
|
+
if (!response.ok) {
|
|
52
|
+
const errorText = await response.text();
|
|
53
|
+
this.error(`API request failed with status ${response.status}: ${response.statusText}\n${errorText}`);
|
|
54
|
+
}
|
|
55
|
+
const data = (await response.json());
|
|
56
|
+
let snapshots;
|
|
57
|
+
if (Array.isArray(data)) {
|
|
58
|
+
snapshots = data;
|
|
59
|
+
}
|
|
60
|
+
else if (data && typeof data === 'object' && 'items' in data && Array.isArray(data.items)) {
|
|
61
|
+
snapshots = data.items;
|
|
62
|
+
}
|
|
63
|
+
else {
|
|
64
|
+
this.error('Unexpected API response format');
|
|
65
|
+
}
|
|
66
|
+
if (flags.output === 'json') {
|
|
67
|
+
this.log(JSON.stringify(snapshots, null, 2));
|
|
68
|
+
}
|
|
69
|
+
else if (snapshots.length === 0) {
|
|
70
|
+
this.log(`No snapshots found for tenant ${tenantName}`);
|
|
71
|
+
}
|
|
72
|
+
else {
|
|
73
|
+
this.log(`Snapshots for tenant ${tenantName}:`);
|
|
74
|
+
for (const snapshot of snapshots) {
|
|
75
|
+
const size = snapshot.size_pretty || (snapshot.size_bytes ? `${snapshot.size_bytes} bytes` : 'unknown');
|
|
76
|
+
const meta = snapshot.created ? `${size}, ${snapshot.created}` : size;
|
|
77
|
+
const tags = [];
|
|
78
|
+
if (snapshot.is_original)
|
|
79
|
+
tags.push('ORIGINAL');
|
|
80
|
+
if (snapshot.is_live)
|
|
81
|
+
tags.push('LIVE');
|
|
82
|
+
const tagStr = tags.length > 0 ? ` [${tags.join(', ')}]` : '';
|
|
83
|
+
this.log(` - ${snapshot.name} (${meta})${tagStr}`);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
catch (error) {
|
|
88
|
+
if (error instanceof Error) {
|
|
89
|
+
this.error(`Failed to list snapshots: ${error.message}`);
|
|
90
|
+
}
|
|
91
|
+
else {
|
|
92
|
+
this.error(`Failed to list snapshots: ${String(error)}`);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import BaseCommand from '../../../../base-command.js';
|
|
2
|
+
export default class TenantSnapshotSwap 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
|
+
force: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
10
|
+
output: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
11
|
+
snapshot: 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
|
+
config: 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 confirm;
|
|
19
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { Args, Flags } from '@oclif/core';
|
|
2
|
+
import * as readline from 'node:readline';
|
|
3
|
+
import BaseCommand from '../../../../base-command.js';
|
|
4
|
+
export default class TenantSnapshotSwap extends BaseCommand {
|
|
5
|
+
static args = {
|
|
6
|
+
tenant_name: Args.string({
|
|
7
|
+
description: 'Tenant name to swap',
|
|
8
|
+
required: true,
|
|
9
|
+
}),
|
|
10
|
+
};
|
|
11
|
+
static description = "Swap a tenant's live database to a snapshot. This repoints the tenant; the current live database is left untouched, so you can swap back. To roll back, swap to the original database name.";
|
|
12
|
+
static examples = [
|
|
13
|
+
`$ xano tenant snapshot swap t1234-abcd-xyz1 --snapshot t1234-abcd-xyz1_bk_20260603_203614
|
|
14
|
+
Swapped tenant t1234-abcd-xyz1 from t1234-abcd-xyz1 to t1234-abcd-xyz1_bk_20260603_203614
|
|
15
|
+
`,
|
|
16
|
+
`$ xano tenant snapshot swap t1234-abcd-xyz1 --snapshot t1234-abcd-xyz1 --force`,
|
|
17
|
+
];
|
|
18
|
+
static flags = {
|
|
19
|
+
...BaseCommand.baseFlags,
|
|
20
|
+
force: Flags.boolean({
|
|
21
|
+
char: 'f',
|
|
22
|
+
default: false,
|
|
23
|
+
description: 'Skips the confirmation prompt.',
|
|
24
|
+
required: false,
|
|
25
|
+
}),
|
|
26
|
+
output: Flags.string({
|
|
27
|
+
char: 'o',
|
|
28
|
+
default: 'summary',
|
|
29
|
+
description: 'Output format',
|
|
30
|
+
options: ['summary', 'json'],
|
|
31
|
+
required: false,
|
|
32
|
+
}),
|
|
33
|
+
snapshot: Flags.string({
|
|
34
|
+
description: 'Snapshot database name to swap to (use the original tenant name to roll back)',
|
|
35
|
+
required: true,
|
|
36
|
+
}),
|
|
37
|
+
workspace: Flags.string({
|
|
38
|
+
char: 'w',
|
|
39
|
+
description: 'Workspace ID (uses profile workspace if not provided)',
|
|
40
|
+
required: false,
|
|
41
|
+
}),
|
|
42
|
+
};
|
|
43
|
+
async run() {
|
|
44
|
+
const { args, flags } = await this.parse(TenantSnapshotSwap);
|
|
45
|
+
const { profile } = this.resolveProfile(flags);
|
|
46
|
+
const workspaceId = flags.workspace || profile.workspace;
|
|
47
|
+
if (!workspaceId) {
|
|
48
|
+
this.error('No workspace ID provided. Use --workspace flag or set one in your profile.');
|
|
49
|
+
}
|
|
50
|
+
const tenantName = args.tenant_name;
|
|
51
|
+
const { snapshot } = flags;
|
|
52
|
+
if (!flags.force) {
|
|
53
|
+
const confirmed = await this.confirm(`Swap tenant ${tenantName} to "${snapshot}"? This repoints the tenant's live database.`);
|
|
54
|
+
if (!confirmed) {
|
|
55
|
+
this.log('Swap cancelled.');
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
const apiUrl = `${profile.instance_origin}/api:meta/workspace/${workspaceId}/tenant/${tenantName}/snapshot/swap`;
|
|
60
|
+
try {
|
|
61
|
+
const response = await this.verboseFetch(apiUrl, {
|
|
62
|
+
body: JSON.stringify({ snapshot }),
|
|
63
|
+
headers: {
|
|
64
|
+
'accept': 'application/json',
|
|
65
|
+
'Authorization': `Bearer ${profile.access_token}`,
|
|
66
|
+
'Content-Type': 'application/json',
|
|
67
|
+
},
|
|
68
|
+
method: 'POST',
|
|
69
|
+
}, flags.verbose, profile.access_token);
|
|
70
|
+
if (!response.ok) {
|
|
71
|
+
const errorText = await response.text();
|
|
72
|
+
this.error(`API request failed with status ${response.status}: ${response.statusText}\n${errorText}`);
|
|
73
|
+
}
|
|
74
|
+
const result = (await response.json());
|
|
75
|
+
if (flags.output === 'json') {
|
|
76
|
+
this.log(JSON.stringify(result, null, 2));
|
|
77
|
+
}
|
|
78
|
+
else {
|
|
79
|
+
this.log(`Swapped tenant ${tenantName} from ${result.from ?? 'unknown'} to ${result.to ?? snapshot}`);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
catch (error) {
|
|
83
|
+
if (error instanceof Error) {
|
|
84
|
+
this.error(`Failed to swap snapshot: ${error.message}`);
|
|
85
|
+
}
|
|
86
|
+
else {
|
|
87
|
+
this.error(`Failed to swap snapshot: ${String(error)}`);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
async confirm(message) {
|
|
92
|
+
const rl = readline.createInterface({
|
|
93
|
+
input: process.stdin,
|
|
94
|
+
output: process.stdout,
|
|
95
|
+
});
|
|
96
|
+
return new Promise((resolve) => {
|
|
97
|
+
rl.question(`${message} (y/N) `, (answer) => {
|
|
98
|
+
rl.close();
|
|
99
|
+
resolve(answer.toLowerCase() === 'y' || answer.toLowerCase() === 'yes');
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
}
|
|
@@ -6,6 +6,7 @@ export default class TenantUnitTestRun extends BaseCommand {
|
|
|
6
6
|
static description: string;
|
|
7
7
|
static examples: string[];
|
|
8
8
|
static flags: {
|
|
9
|
+
branch: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
9
10
|
output: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
10
11
|
tenant: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
11
12
|
workspace: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
@@ -17,6 +17,11 @@ Result: PASS
|
|
|
17
17
|
];
|
|
18
18
|
static flags = {
|
|
19
19
|
...BaseCommand.baseFlags,
|
|
20
|
+
branch: Flags.string({
|
|
21
|
+
char: 'b',
|
|
22
|
+
description: 'Branch the unit test belongs to (uses profile branch if not provided, then the live branch)',
|
|
23
|
+
required: false,
|
|
24
|
+
}),
|
|
20
25
|
output: Flags.string({
|
|
21
26
|
char: 'o',
|
|
22
27
|
default: 'summary',
|
|
@@ -43,12 +48,14 @@ Result: PASS
|
|
|
43
48
|
this.error('No workspace ID provided. Use --workspace flag or set one in your profile.');
|
|
44
49
|
}
|
|
45
50
|
const tenantName = encodeURIComponent(flags.tenant);
|
|
51
|
+
const branch = flags.branch ?? profile.branch;
|
|
46
52
|
const apiUrl = `${profile.instance_origin}/api:meta/workspace/${workspaceId}/tenant/${tenantName}/unit_test/${encodeURIComponent(args.unit_test_id)}/run`;
|
|
47
53
|
try {
|
|
48
54
|
if (flags.output === 'summary') {
|
|
49
55
|
this.log(`Running unit test ${args.unit_test_id}...`);
|
|
50
56
|
}
|
|
51
57
|
const response = await this.verboseFetch(apiUrl, {
|
|
58
|
+
body: JSON.stringify(branch ? { branch } : {}),
|
|
52
59
|
headers: {
|
|
53
60
|
accept: 'application/json',
|
|
54
61
|
Authorization: `Bearer ${profile.access_token}`,
|
|
@@ -18,7 +18,7 @@ Results: 4 passed, 1 failed
|
|
|
18
18
|
...BaseCommand.baseFlags,
|
|
19
19
|
branch: Flags.string({
|
|
20
20
|
char: 'b',
|
|
21
|
-
description: '
|
|
21
|
+
description: 'Branch to run tests from (uses profile branch if not provided, then the live branch)',
|
|
22
22
|
required: false,
|
|
23
23
|
}),
|
|
24
24
|
'obj-type': Flags.string({
|
|
@@ -53,11 +53,14 @@ Results: 4 passed, 1 failed
|
|
|
53
53
|
}
|
|
54
54
|
const tenantName = encodeURIComponent(flags.tenant);
|
|
55
55
|
const baseUrl = `${profile.instance_origin}/api:meta/workspace/${workspaceId}/tenant/${tenantName}/unit_test`;
|
|
56
|
+
// Resolve the branch once so the listed tests and the tests we run come
|
|
57
|
+
// from the same branch.
|
|
58
|
+
const branch = flags.branch ?? profile.branch;
|
|
56
59
|
try {
|
|
57
60
|
const listParams = new URLSearchParams();
|
|
58
61
|
listParams.set('per_page', '10000');
|
|
59
|
-
if (
|
|
60
|
-
listParams.set('branch',
|
|
62
|
+
if (branch)
|
|
63
|
+
listParams.set('branch', branch);
|
|
61
64
|
if (flags['obj-type'])
|
|
62
65
|
listParams.set('obj_type', flags['obj-type']);
|
|
63
66
|
const listResponse = await this.verboseFetch(`${baseUrl}?${listParams}`, {
|
|
@@ -94,6 +97,7 @@ Results: 4 passed, 1 failed
|
|
|
94
97
|
const runUrl = `${baseUrl}/${test.id}/run`;
|
|
95
98
|
try {
|
|
96
99
|
const runResponse = await this.verboseFetch(runUrl, {
|
|
100
|
+
body: JSON.stringify(branch ? { branch } : {}),
|
|
97
101
|
headers: {
|
|
98
102
|
accept: 'application/json',
|
|
99
103
|
Authorization: `Bearer ${profile.access_token}`,
|
|
@@ -6,6 +6,7 @@ export default class UnitTestRun extends BaseCommand {
|
|
|
6
6
|
static description: string;
|
|
7
7
|
static examples: string[];
|
|
8
8
|
static flags: {
|
|
9
|
+
branch: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
9
10
|
output: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
10
11
|
workspace: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
11
12
|
config: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
@@ -14,9 +14,15 @@ Running unit test abc-123...
|
|
|
14
14
|
Result: PASS
|
|
15
15
|
`,
|
|
16
16
|
`$ xano unit-test run abc-123 -o json`,
|
|
17
|
+
`$ xano unit-test run abc-123 --branch dev`,
|
|
17
18
|
];
|
|
18
19
|
static flags = {
|
|
19
20
|
...BaseCommand.baseFlags,
|
|
21
|
+
branch: Flags.string({
|
|
22
|
+
char: 'b',
|
|
23
|
+
description: 'Branch the unit test belongs to (uses profile branch if not provided, then the live branch)',
|
|
24
|
+
required: false,
|
|
25
|
+
}),
|
|
20
26
|
output: Flags.string({
|
|
21
27
|
char: 'o',
|
|
22
28
|
default: 'summary',
|
|
@@ -37,12 +43,14 @@ Result: PASS
|
|
|
37
43
|
if (!workspaceId) {
|
|
38
44
|
this.error('No workspace ID provided. Use --workspace flag or set one in your profile.');
|
|
39
45
|
}
|
|
46
|
+
const branch = flags.branch ?? profile.branch;
|
|
40
47
|
const apiUrl = `${profile.instance_origin}/api:meta/workspace/${workspaceId}/unit_test/${args.unit_test_id}/run`;
|
|
41
48
|
try {
|
|
42
49
|
if (flags.output === 'summary') {
|
|
43
50
|
this.log(`Running unit test ${args.unit_test_id}...`);
|
|
44
51
|
}
|
|
45
52
|
const response = await this.verboseFetch(apiUrl, {
|
|
53
|
+
body: JSON.stringify(branch ? { branch } : {}),
|
|
46
54
|
headers: {
|
|
47
55
|
'accept': 'application/json',
|
|
48
56
|
'Authorization': `Bearer ${profile.access_token}`,
|
|
@@ -19,7 +19,7 @@ Results: 2 passed, 1 failed
|
|
|
19
19
|
...BaseCommand.baseFlags,
|
|
20
20
|
branch: Flags.string({
|
|
21
21
|
char: 'b',
|
|
22
|
-
description: '
|
|
22
|
+
description: 'Branch to run tests from (uses profile branch if not provided, then the live branch)',
|
|
23
23
|
required: false,
|
|
24
24
|
}),
|
|
25
25
|
'obj-type': Flags.string({
|
|
@@ -48,12 +48,15 @@ Results: 2 passed, 1 failed
|
|
|
48
48
|
this.error('No workspace ID provided. Use --workspace flag or set one in your profile.');
|
|
49
49
|
}
|
|
50
50
|
const baseUrl = `${profile.instance_origin}/api:meta/workspace/${workspaceId}/unit_test`;
|
|
51
|
+
// Resolve the branch once so the listed tests and the tests we run come
|
|
52
|
+
// from the same branch.
|
|
53
|
+
const branch = flags.branch ?? profile.branch;
|
|
51
54
|
try {
|
|
52
55
|
// Step 1: List all unit tests
|
|
53
56
|
const listParams = new URLSearchParams();
|
|
54
57
|
listParams.set('per_page', '10000');
|
|
55
|
-
if (
|
|
56
|
-
listParams.set('branch',
|
|
58
|
+
if (branch) {
|
|
59
|
+
listParams.set('branch', branch);
|
|
57
60
|
}
|
|
58
61
|
if (flags['obj-type']) {
|
|
59
62
|
listParams.set('obj_type', flags['obj-type']);
|
|
@@ -93,6 +96,7 @@ Results: 2 passed, 1 failed
|
|
|
93
96
|
const runUrl = `${baseUrl}/${test.id}/run`;
|
|
94
97
|
try {
|
|
95
98
|
const runResponse = await this.verboseFetch(runUrl, {
|
|
99
|
+
body: JSON.stringify(branch ? { branch } : {}),
|
|
96
100
|
headers: {
|
|
97
101
|
accept: 'application/json',
|
|
98
102
|
Authorization: `Bearer ${profile.access_token}`,
|
|
@@ -4,11 +4,12 @@ import * as path from 'node:path';
|
|
|
4
4
|
import snakeCase from 'lodash.snakecase';
|
|
5
5
|
import BaseCommand from '../../../base-command.js';
|
|
6
6
|
import { buildApiGroupFolderResolver, parseDocument } from '../../../utils/document-parser.js';
|
|
7
|
+
import { fetchKnowledge, writeKnowledge } from '../../../utils/knowledge-sync.js';
|
|
7
8
|
export default class Pull extends BaseCommand {
|
|
8
9
|
static description = 'Pull a workspace multidoc from the Xano Metadata API and split into individual files';
|
|
9
10
|
static examples = [
|
|
10
11
|
`$ xano workspace pull
|
|
11
|
-
Pulled 42 documents to current directory
|
|
12
|
+
Pulled 42 documents + 5 knowledge files to current directory
|
|
12
13
|
`,
|
|
13
14
|
`$ xano workspace pull -d ./my-workspace
|
|
14
15
|
Pulled 42 documents to ./my-workspace
|
|
@@ -239,7 +240,17 @@ Pulled 58 documents
|
|
|
239
240
|
fs.writeFileSync(filePath, doc.content, 'utf8');
|
|
240
241
|
writtenCount++;
|
|
241
242
|
}
|
|
242
|
-
|
|
243
|
+
// ── Pull knowledge ────────────────────────────────────────────────────
|
|
244
|
+
const knowledgeUrl = `${profile.instance_origin}/api:meta/workspace/${workspaceId}/knowledge/sync`;
|
|
245
|
+
const knowledgeObjects = await fetchKnowledge(knowledgeUrl, branch, profile.access_token, this.verboseFetch.bind(this), flags.verbose);
|
|
246
|
+
let knowledgeCount = 0;
|
|
247
|
+
if (knowledgeObjects.length > 0) {
|
|
248
|
+
knowledgeCount = writeKnowledge(knowledgeObjects, outputDir);
|
|
249
|
+
}
|
|
250
|
+
const parts = [`${writtenCount} documents`];
|
|
251
|
+
if (knowledgeCount > 0)
|
|
252
|
+
parts.push(`${knowledgeCount} knowledge file${knowledgeCount === 1 ? '' : 's'}`);
|
|
253
|
+
this.log(`Pulled ${parts.join(' + ')} to ${flags.directory}`);
|
|
243
254
|
}
|
|
244
255
|
/**
|
|
245
256
|
* Sanitize a document name for use as a filename.
|
|
@@ -4,8 +4,8 @@ export default class Push extends BaseCommand {
|
|
|
4
4
|
static examples: string[];
|
|
5
5
|
static flags: {
|
|
6
6
|
branch: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
7
|
-
directory: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
8
7
|
delete: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
8
|
+
directory: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
9
9
|
'dry-run': import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
10
10
|
env: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
11
11
|
exclude: import("@oclif/core/interfaces").OptionFlag<string[] | undefined, import("@oclif/core/interfaces").CustomOptions>;
|