@xano/cli 0.0.37 → 0.0.40

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