@xano/cli 1.0.3 → 1.0.4-beta.10

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.
Files changed (58) hide show
  1. package/README.md +100 -3
  2. package/dist/base-command.d.ts +27 -0
  3. package/dist/base-command.js +124 -1
  4. package/dist/commands/auth/index.d.ts +48 -1
  5. package/dist/commands/auth/index.js +259 -27
  6. package/dist/commands/function/run/index.d.ts +40 -0
  7. package/dist/commands/function/run/index.js +244 -0
  8. package/dist/commands/sandbox/pull/index.js +12 -2
  9. package/dist/commands/sandbox/push/index.d.ts +1 -1
  10. package/dist/commands/sandbox/push/index.js +17 -13
  11. package/dist/commands/static_host/build/create/index.d.ts +9 -1
  12. package/dist/commands/static_host/build/create/index.js +54 -4
  13. package/dist/commands/static_host/build/delete/index.d.ts +19 -0
  14. package/dist/commands/static_host/build/delete/index.js +114 -0
  15. package/dist/commands/static_host/build/get/index.d.ts +1 -1
  16. package/dist/commands/static_host/build/get/index.js +16 -10
  17. package/dist/commands/static_host/build/pull/index.d.ts +52 -0
  18. package/dist/commands/static_host/build/pull/index.js +300 -0
  19. package/dist/commands/static_host/build/push/index.d.ts +23 -0
  20. package/dist/commands/static_host/build/push/index.js +228 -0
  21. package/dist/commands/static_host/create/index.d.ts +17 -0
  22. package/dist/commands/static_host/create/index.js +86 -0
  23. package/dist/commands/static_host/deploy/index.d.ts +18 -0
  24. package/dist/commands/static_host/deploy/index.js +105 -0
  25. package/dist/commands/static_host/edit/index.d.ts +23 -0
  26. package/dist/commands/static_host/edit/index.js +151 -0
  27. package/dist/commands/static_host/get/index.d.ts +18 -0
  28. package/dist/commands/static_host/get/index.js +94 -0
  29. package/dist/commands/static_host/migrate/index.d.ts +44 -0
  30. package/dist/commands/static_host/migrate/index.js +205 -0
  31. package/dist/commands/tenant/snapshot/create/index.d.ts +17 -0
  32. package/dist/commands/tenant/snapshot/create/index.js +78 -0
  33. package/dist/commands/tenant/snapshot/delete/index.d.ts +19 -0
  34. package/dist/commands/tenant/snapshot/delete/index.js +102 -0
  35. package/dist/commands/tenant/snapshot/list/index.d.ts +16 -0
  36. package/dist/commands/tenant/snapshot/list/index.js +96 -0
  37. package/dist/commands/tenant/snapshot/swap/index.d.ts +19 -0
  38. package/dist/commands/tenant/snapshot/swap/index.js +103 -0
  39. package/dist/commands/tenant/unit_test/run/index.d.ts +1 -0
  40. package/dist/commands/tenant/unit_test/run/index.js +7 -0
  41. package/dist/commands/tenant/unit_test/run_all/index.js +7 -3
  42. package/dist/commands/unit_test/run/index.d.ts +1 -0
  43. package/dist/commands/unit_test/run/index.js +8 -0
  44. package/dist/commands/unit_test/run_all/index.js +7 -3
  45. package/dist/commands/workspace/pull/index.js +13 -2
  46. package/dist/commands/workspace/push/index.d.ts +1 -1
  47. package/dist/commands/workspace/push/index.js +15 -5
  48. package/dist/utils/function-input.d.ts +77 -0
  49. package/dist/utils/function-input.js +148 -0
  50. package/dist/utils/knowledge-sync.d.ts +113 -0
  51. package/dist/utils/knowledge-sync.js +404 -0
  52. package/dist/utils/multidoc-push.d.ts +22 -0
  53. package/dist/utils/multidoc-push.js +262 -107
  54. package/dist/utils/reference-checker.js +2 -2
  55. package/dist/utils/static-host-files.d.ts +21 -0
  56. package/dist/utils/static-host-files.js +41 -0
  57. package/oclif.manifest.json +4179 -2636
  58. package/package.json +4 -1
@@ -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: 'Filter by branch name',
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 (flags.branch)
60
- listParams.set('branch', flags.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: 'Filter by branch name',
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 (flags.branch) {
56
- listParams.set('branch', flags.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
- this.log(`Pulled ${writtenCount} documents to ${flags.directory}`);
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>;