@xano/cli 0.0.37 → 0.0.39

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