@xano/cli 0.0.39 → 0.0.41
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 +2 -2
- package/dist/commands/profile/me/index.js +33 -14
- package/dist/commands/tenant/deploy_release/index.d.ts +1 -1
- package/dist/commands/tenant/deploy_release/index.js +9 -8
- package/dist/commands/workspace/pull/index.d.ts +0 -6
- package/dist/commands/workspace/pull/index.js +5 -55
- package/dist/commands/workspace/push/index.d.ts +1 -0
- package/dist/commands/workspace/push/index.js +112 -8
- package/dist/utils/document-parser.d.ts +17 -0
- package/dist/utils/document-parser.js +61 -0
- package/oclif.manifest.json +1615 -1607
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -312,8 +312,8 @@ xano tenant push ./my-tenant -t <tenant_name> --truncate
|
|
|
312
312
|
# Deploy a platform version
|
|
313
313
|
xano tenant deploy_platform <tenant_name> --platform_id 5
|
|
314
314
|
|
|
315
|
-
# Deploy a release
|
|
316
|
-
xano tenant deploy_release <tenant_name> --
|
|
315
|
+
# Deploy a release by name
|
|
316
|
+
xano tenant deploy_release <tenant_name> --release v1.0
|
|
317
317
|
```
|
|
318
318
|
|
|
319
319
|
#### Tenant License
|
|
@@ -62,8 +62,8 @@ User Information:
|
|
|
62
62
|
try {
|
|
63
63
|
const response = await this.verboseFetch(apiUrl, {
|
|
64
64
|
headers: {
|
|
65
|
-
|
|
66
|
-
|
|
65
|
+
accept: 'application/json',
|
|
66
|
+
Authorization: `Bearer ${profile.access_token}`,
|
|
67
67
|
},
|
|
68
68
|
method: 'GET',
|
|
69
69
|
}, flags.verbose, profile.access_token);
|
|
@@ -71,7 +71,7 @@ User Information:
|
|
|
71
71
|
const errorText = await response.text();
|
|
72
72
|
this.error(`API request failed with status ${response.status}: ${response.statusText}\n${errorText}`);
|
|
73
73
|
}
|
|
74
|
-
const data = await response.json();
|
|
74
|
+
const data = (await response.json());
|
|
75
75
|
// Output results
|
|
76
76
|
if (flags.output === 'json') {
|
|
77
77
|
this.log(JSON.stringify(data, null, 2));
|
|
@@ -92,17 +92,37 @@ User Information:
|
|
|
92
92
|
const date = new Date(data.created_at * 1000);
|
|
93
93
|
this.log(` Created: ${date.toISOString()}`);
|
|
94
94
|
}
|
|
95
|
-
// Display
|
|
95
|
+
// Display extra fields from the API response
|
|
96
96
|
const knownFields = new Set(['created_at', 'email', 'id', 'name']);
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
97
|
+
// In default mode, show condensed instance info from extras
|
|
98
|
+
// In verbose mode, show all extra fields with full detail
|
|
99
|
+
const extras = data.extras;
|
|
100
|
+
if (extras?.instance && typeof extras.instance === 'object') {
|
|
101
|
+
const inst = extras.instance;
|
|
102
|
+
this.log('');
|
|
103
|
+
this.log('Instance Information:');
|
|
104
|
+
if (inst.id)
|
|
105
|
+
this.log(` ID: ${inst.id}`);
|
|
106
|
+
if (inst.name)
|
|
107
|
+
this.log(` Name: ${inst.name}`);
|
|
108
|
+
if (inst.display)
|
|
109
|
+
this.log(` Display: ${inst.display}`);
|
|
110
|
+
}
|
|
111
|
+
if (flags.verbose) {
|
|
112
|
+
knownFields.add('extras');
|
|
113
|
+
for (const [key, value] of Object.entries(data)) {
|
|
114
|
+
if (!knownFields.has(key) && value !== null && value !== undefined) {
|
|
115
|
+
if (typeof value === 'object') {
|
|
116
|
+
this.log(` ${this.formatKey(key)}: ${JSON.stringify(value, null, 2)}`);
|
|
117
|
+
}
|
|
118
|
+
else {
|
|
119
|
+
this.log(` ${this.formatKey(key)}: ${value}`);
|
|
120
|
+
}
|
|
104
121
|
}
|
|
105
122
|
}
|
|
123
|
+
if (extras) {
|
|
124
|
+
this.log(` Extras: ${JSON.stringify(extras, null, 2)}`);
|
|
125
|
+
}
|
|
106
126
|
}
|
|
107
127
|
}
|
|
108
128
|
}
|
|
@@ -119,7 +139,7 @@ User Information:
|
|
|
119
139
|
// Convert snake_case to Title Case
|
|
120
140
|
return key
|
|
121
141
|
.split('_')
|
|
122
|
-
.map(word => word.charAt(0).toUpperCase() + word.slice(1))
|
|
142
|
+
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
|
|
123
143
|
.join(' ');
|
|
124
144
|
}
|
|
125
145
|
loadCredentials() {
|
|
@@ -127,8 +147,7 @@ User Information:
|
|
|
127
147
|
const credentialsPath = path.join(configDir, 'credentials.yaml');
|
|
128
148
|
// Check if credentials file exists
|
|
129
149
|
if (!fs.existsSync(credentialsPath)) {
|
|
130
|
-
this.error(`Credentials file not found at ${credentialsPath}\n` +
|
|
131
|
-
`Create a profile using 'xano profile:create'`);
|
|
150
|
+
this.error(`Credentials file not found at ${credentialsPath}\n` + `Create a profile using 'xano profile:create'`);
|
|
132
151
|
}
|
|
133
152
|
// Read credentials file
|
|
134
153
|
try {
|
|
@@ -7,7 +7,7 @@ export default class TenantDeployRelease extends BaseCommand {
|
|
|
7
7
|
static examples: string[];
|
|
8
8
|
static flags: {
|
|
9
9
|
output: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
10
|
-
|
|
10
|
+
release: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
11
11
|
workspace: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
12
12
|
profile: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
13
13
|
verbose: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
@@ -13,10 +13,10 @@ export default class TenantDeployRelease extends BaseCommand {
|
|
|
13
13
|
};
|
|
14
14
|
static description = 'Deploy a release to a tenant';
|
|
15
15
|
static examples = [
|
|
16
|
-
`$ xano tenant deploy_release t1234-abcd-xyz1 --
|
|
17
|
-
Deployed release
|
|
16
|
+
`$ xano tenant deploy_release t1234-abcd-xyz1 --release v1.0
|
|
17
|
+
Deployed release "v1.0" to tenant: My Tenant (my-tenant)
|
|
18
18
|
`,
|
|
19
|
-
`$ xano tenant deploy_release t1234-abcd-xyz1 --
|
|
19
|
+
`$ xano tenant deploy_release t1234-abcd-xyz1 --release v1.0 -o json`,
|
|
20
20
|
];
|
|
21
21
|
static flags = {
|
|
22
22
|
...BaseCommand.baseFlags,
|
|
@@ -27,8 +27,9 @@ Deployed release 10 to tenant: My Tenant (my-tenant)
|
|
|
27
27
|
options: ['summary', 'json'],
|
|
28
28
|
required: false,
|
|
29
29
|
}),
|
|
30
|
-
|
|
31
|
-
|
|
30
|
+
release: Flags.string({
|
|
31
|
+
char: 'r',
|
|
32
|
+
description: 'Release name to deploy',
|
|
32
33
|
required: true,
|
|
33
34
|
}),
|
|
34
35
|
workspace: Flags.string({
|
|
@@ -56,12 +57,12 @@ Deployed release 10 to tenant: My Tenant (my-tenant)
|
|
|
56
57
|
if (!workspaceId) {
|
|
57
58
|
this.error('No workspace ID provided. Use --workspace flag or set one in your profile.');
|
|
58
59
|
}
|
|
60
|
+
const releaseName = flags.release;
|
|
59
61
|
const tenantName = args.tenant_name;
|
|
60
|
-
const releaseId = flags.release_id;
|
|
61
62
|
const apiUrl = `${profile.instance_origin}/api:meta/workspace/${workspaceId}/tenant/${tenantName}/deploy`;
|
|
62
63
|
try {
|
|
63
64
|
const response = await this.verboseFetch(apiUrl, {
|
|
64
|
-
body: JSON.stringify({
|
|
65
|
+
body: JSON.stringify({ release_name: releaseName }),
|
|
65
66
|
headers: {
|
|
66
67
|
accept: 'application/json',
|
|
67
68
|
Authorization: `Bearer ${profile.access_token}`,
|
|
@@ -78,7 +79,7 @@ Deployed release 10 to tenant: My Tenant (my-tenant)
|
|
|
78
79
|
this.log(JSON.stringify(tenant, null, 2));
|
|
79
80
|
}
|
|
80
81
|
else {
|
|
81
|
-
this.log(`Deployed release ${
|
|
82
|
+
this.log(`Deployed release "${releaseName}" to tenant: ${tenant.display || tenant.name} (${tenant.name})`);
|
|
82
83
|
if (tenant.state)
|
|
83
84
|
this.log(` State: ${tenant.state}`);
|
|
84
85
|
if (tenant.release?.name)
|
|
@@ -16,12 +16,6 @@ export default class Pull extends BaseCommand {
|
|
|
16
16
|
};
|
|
17
17
|
run(): Promise<void>;
|
|
18
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
19
|
/**
|
|
26
20
|
* Sanitize a document name for use as a filename.
|
|
27
21
|
* Strips quotes, replaces spaces with underscores, and removes
|
|
@@ -5,6 +5,7 @@ import * as os from 'node:os';
|
|
|
5
5
|
import * as path from 'node:path';
|
|
6
6
|
import snakeCase from 'lodash.snakecase';
|
|
7
7
|
import BaseCommand from '../../../base-command.js';
|
|
8
|
+
import { parseDocument } from '../../../utils/document-parser.js';
|
|
8
9
|
export default class Pull extends BaseCommand {
|
|
9
10
|
static args = {
|
|
10
11
|
directory: Args.string({
|
|
@@ -104,8 +105,8 @@ Pulled 42 documents to ./my-workspace
|
|
|
104
105
|
// Fetch multidoc from the API
|
|
105
106
|
let responseText;
|
|
106
107
|
const requestHeaders = {
|
|
107
|
-
|
|
108
|
-
|
|
108
|
+
accept: 'application/json',
|
|
109
|
+
Authorization: `Bearer ${profile.access_token}`,
|
|
109
110
|
};
|
|
110
111
|
try {
|
|
111
112
|
const response = await this.verboseFetch(apiUrl, {
|
|
@@ -135,7 +136,7 @@ Pulled 42 documents to ./my-workspace
|
|
|
135
136
|
if (!trimmed) {
|
|
136
137
|
continue;
|
|
137
138
|
}
|
|
138
|
-
const parsed =
|
|
139
|
+
const parsed = parseDocument(trimmed);
|
|
139
140
|
if (parsed) {
|
|
140
141
|
documents.push(parsed);
|
|
141
142
|
}
|
|
@@ -256,8 +257,7 @@ Pulled 42 documents to ./my-workspace
|
|
|
256
257
|
const credentialsPath = path.join(configDir, 'credentials.yaml');
|
|
257
258
|
// Check if credentials file exists
|
|
258
259
|
if (!fs.existsSync(credentialsPath)) {
|
|
259
|
-
this.error(`Credentials file not found at ${credentialsPath}\n` +
|
|
260
|
-
`Create a profile using 'xano profile:create'`);
|
|
260
|
+
this.error(`Credentials file not found at ${credentialsPath}\n` + `Create a profile using 'xano profile:create'`);
|
|
261
261
|
}
|
|
262
262
|
// Read credentials file
|
|
263
263
|
try {
|
|
@@ -272,56 +272,6 @@ Pulled 42 documents to ./my-workspace
|
|
|
272
272
|
this.error(`Failed to parse credentials file: ${error}`);
|
|
273
273
|
}
|
|
274
274
|
}
|
|
275
|
-
/**
|
|
276
|
-
* Parse a single document to extract its type, name, and optional verb.
|
|
277
|
-
* Skips leading comment lines (starting with //) to find the first
|
|
278
|
-
* meaningful line containing the type keyword and name.
|
|
279
|
-
*/
|
|
280
|
-
parseDocument(content) {
|
|
281
|
-
const lines = content.split('\n');
|
|
282
|
-
// Find the first non-comment line
|
|
283
|
-
let firstLine = null;
|
|
284
|
-
for (const line of lines) {
|
|
285
|
-
const trimmedLine = line.trim();
|
|
286
|
-
if (trimmedLine && !trimmedLine.startsWith('//')) {
|
|
287
|
-
firstLine = trimmedLine;
|
|
288
|
-
break;
|
|
289
|
-
}
|
|
290
|
-
}
|
|
291
|
-
if (!firstLine) {
|
|
292
|
-
return null;
|
|
293
|
-
}
|
|
294
|
-
// Parse the type keyword and name from the first meaningful line
|
|
295
|
-
// Expected formats:
|
|
296
|
-
// type name {
|
|
297
|
-
// type name verb=GET {
|
|
298
|
-
// type "name with spaces" {
|
|
299
|
-
// type "name with spaces" verb=PATCH {
|
|
300
|
-
const match = firstLine.match(/^(\w+)\s+("(?:[^"\\]|\\.)*"|\S+)(?:\s+(.*))?/);
|
|
301
|
-
if (!match) {
|
|
302
|
-
return null;
|
|
303
|
-
}
|
|
304
|
-
const type = match[1];
|
|
305
|
-
let name = match[2];
|
|
306
|
-
const rest = match[3] || '';
|
|
307
|
-
// Strip surrounding quotes from the name
|
|
308
|
-
if (name.startsWith('"') && name.endsWith('"')) {
|
|
309
|
-
name = name.slice(1, -1);
|
|
310
|
-
}
|
|
311
|
-
// Extract verb if present (e.g., verb=GET)
|
|
312
|
-
let verb;
|
|
313
|
-
const verbMatch = rest.match(/verb=(\S+)/);
|
|
314
|
-
if (verbMatch) {
|
|
315
|
-
verb = verbMatch[1];
|
|
316
|
-
}
|
|
317
|
-
// Extract api_group if present (e.g., api_group = "test")
|
|
318
|
-
let apiGroup;
|
|
319
|
-
const apiGroupMatch = content.match(/api_group\s*=\s*"([^"]*)"/);
|
|
320
|
-
if (apiGroupMatch) {
|
|
321
|
-
apiGroup = apiGroupMatch[1];
|
|
322
|
-
}
|
|
323
|
-
return { apiGroup, content, name, type, verb };
|
|
324
|
-
}
|
|
325
275
|
/**
|
|
326
276
|
* Sanitize a document name for use as a filename.
|
|
327
277
|
* Strips quotes, replaces spaces with underscores, and removes
|
|
@@ -9,6 +9,7 @@ export default class Push extends BaseCommand {
|
|
|
9
9
|
branch: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
10
10
|
env: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
11
11
|
records: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
12
|
+
'sync-guids': import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
12
13
|
truncate: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
13
14
|
workspace: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
14
15
|
profile: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
@@ -4,6 +4,7 @@ import * as fs from 'node:fs';
|
|
|
4
4
|
import * as os from 'node:os';
|
|
5
5
|
import * as path from 'node:path';
|
|
6
6
|
import BaseCommand from '../../../base-command.js';
|
|
7
|
+
import { buildDocumentKey, parseDocument } from '../../../utils/document-parser.js';
|
|
7
8
|
export default class Push extends BaseCommand {
|
|
8
9
|
static args = {
|
|
9
10
|
directory: Args.string({
|
|
@@ -60,6 +61,12 @@ Push schema only, skip records and environment variables
|
|
|
60
61
|
description: 'Include records in import (default: true, use --no-records to exclude)',
|
|
61
62
|
required: false,
|
|
62
63
|
}),
|
|
64
|
+
'sync-guids': Flags.boolean({
|
|
65
|
+
allowNo: true,
|
|
66
|
+
default: true,
|
|
67
|
+
description: 'Write server-assigned GUIDs back to local files (use --no-sync-guids to skip)',
|
|
68
|
+
required: false,
|
|
69
|
+
}),
|
|
63
70
|
truncate: Flags.boolean({
|
|
64
71
|
default: false,
|
|
65
72
|
description: 'Truncate all table records before importing',
|
|
@@ -116,18 +123,27 @@ Push schema only, skip records and environment variables
|
|
|
116
123
|
if (files.length === 0) {
|
|
117
124
|
this.error(`No .xs files found in ${args.directory}`);
|
|
118
125
|
}
|
|
119
|
-
// Read each file and
|
|
120
|
-
const
|
|
126
|
+
// Read each file and track file path alongside content
|
|
127
|
+
const documentEntries = [];
|
|
121
128
|
for (const filePath of files) {
|
|
122
129
|
const content = fs.readFileSync(filePath, 'utf8').trim();
|
|
123
130
|
if (content) {
|
|
124
|
-
|
|
131
|
+
documentEntries.push({ content, filePath });
|
|
125
132
|
}
|
|
126
133
|
}
|
|
127
|
-
if (
|
|
134
|
+
if (documentEntries.length === 0) {
|
|
128
135
|
this.error(`All .xs files in ${args.directory} are empty`);
|
|
129
136
|
}
|
|
130
|
-
const multidoc =
|
|
137
|
+
const multidoc = documentEntries.map((d) => d.content).join('\n---\n');
|
|
138
|
+
// Build lookup map from document key to file path (for GUID writeback)
|
|
139
|
+
const documentFileMap = new Map();
|
|
140
|
+
for (const entry of documentEntries) {
|
|
141
|
+
const parsed = parseDocument(entry.content);
|
|
142
|
+
if (parsed) {
|
|
143
|
+
const key = buildDocumentKey(parsed.type, parsed.name, parsed.verb, parsed.apiGroup);
|
|
144
|
+
documentFileMap.set(key, entry.filePath);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
131
147
|
// Determine branch from flag or profile
|
|
132
148
|
const branch = flags.branch || profile.branch || '';
|
|
133
149
|
// Construct the API URL
|
|
@@ -165,10 +181,49 @@ Push schema only, skip records and environment variables
|
|
|
165
181
|
}
|
|
166
182
|
this.error(errorMessage);
|
|
167
183
|
}
|
|
168
|
-
//
|
|
184
|
+
// Parse the response for GUID map
|
|
169
185
|
const responseText = await response.text();
|
|
186
|
+
let guidMap = [];
|
|
170
187
|
if (responseText && responseText !== 'null') {
|
|
171
|
-
|
|
188
|
+
try {
|
|
189
|
+
const responseJson = JSON.parse(responseText);
|
|
190
|
+
if (responseJson?.guid_map && Array.isArray(responseJson.guid_map)) {
|
|
191
|
+
guidMap = responseJson.guid_map;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
catch {
|
|
195
|
+
// Response is not JSON (e.g., older server version)
|
|
196
|
+
if (flags.verbose) {
|
|
197
|
+
this.log('Server response is not JSON; skipping GUID sync');
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
// Write GUIDs back to local files
|
|
202
|
+
if (flags['sync-guids'] && guidMap.length > 0) {
|
|
203
|
+
let updatedCount = 0;
|
|
204
|
+
for (const entry of guidMap) {
|
|
205
|
+
if (!entry.guid)
|
|
206
|
+
continue;
|
|
207
|
+
const key = buildDocumentKey(entry.type, entry.name, entry.verb, entry.api_group);
|
|
208
|
+
const filePath = documentFileMap.get(key);
|
|
209
|
+
if (!filePath) {
|
|
210
|
+
if (flags.verbose) {
|
|
211
|
+
this.log(` No local file found for ${entry.type} "${entry.name}", skipping GUID sync`);
|
|
212
|
+
}
|
|
213
|
+
continue;
|
|
214
|
+
}
|
|
215
|
+
try {
|
|
216
|
+
const updated = syncGuidToFile(filePath, entry.guid);
|
|
217
|
+
if (updated)
|
|
218
|
+
updatedCount++;
|
|
219
|
+
}
|
|
220
|
+
catch (error) {
|
|
221
|
+
this.warn(`Failed to sync GUID to ${filePath}: ${error.message}`);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
if (updatedCount > 0) {
|
|
225
|
+
this.log(`Synced ${updatedCount} GUIDs to local files`);
|
|
226
|
+
}
|
|
172
227
|
}
|
|
173
228
|
}
|
|
174
229
|
catch (error) {
|
|
@@ -179,7 +234,7 @@ Push schema only, skip records and environment variables
|
|
|
179
234
|
this.error(`Failed to push multidoc: ${String(error)}`);
|
|
180
235
|
}
|
|
181
236
|
}
|
|
182
|
-
this.log(`Pushed ${
|
|
237
|
+
this.log(`Pushed ${documentEntries.length} documents from ${args.directory}`);
|
|
183
238
|
}
|
|
184
239
|
/**
|
|
185
240
|
* Recursively collect all .xs files from a directory, sorted by
|
|
@@ -220,3 +275,52 @@ Push schema only, skip records and environment variables
|
|
|
220
275
|
}
|
|
221
276
|
}
|
|
222
277
|
}
|
|
278
|
+
const GUID_REGEX = /guid\s*=\s*(["'])([^"']*)\1/;
|
|
279
|
+
/**
|
|
280
|
+
* Sync a GUID into a local .xs file. Returns true if the file was modified.
|
|
281
|
+
*
|
|
282
|
+
* - If the file already has a matching GUID, returns false (no change).
|
|
283
|
+
* - If the file has a different GUID, updates it.
|
|
284
|
+
* - If the file has no GUID, inserts one before the final closing brace.
|
|
285
|
+
*/
|
|
286
|
+
function syncGuidToFile(filePath, guid) {
|
|
287
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
288
|
+
const existingMatch = content.match(GUID_REGEX);
|
|
289
|
+
if (existingMatch) {
|
|
290
|
+
// Already has a GUID
|
|
291
|
+
if (existingMatch[2] === guid) {
|
|
292
|
+
return false; // Already matches
|
|
293
|
+
}
|
|
294
|
+
// Update existing GUID
|
|
295
|
+
const updated = content.replace(GUID_REGEX, `guid = "${guid}"`);
|
|
296
|
+
fs.writeFileSync(filePath, updated, 'utf8');
|
|
297
|
+
return true;
|
|
298
|
+
}
|
|
299
|
+
// No GUID line exists — insert before the final closing brace of the top-level block
|
|
300
|
+
const lines = content.split('\n');
|
|
301
|
+
let insertIndex = -1;
|
|
302
|
+
// Find the last closing brace (top-level block end)
|
|
303
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
304
|
+
if (lines[i].trim() === '}') {
|
|
305
|
+
insertIndex = i;
|
|
306
|
+
break;
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
if (insertIndex === -1) {
|
|
310
|
+
return false; // Could not find insertion point
|
|
311
|
+
}
|
|
312
|
+
// Determine indentation from the line above the closing brace
|
|
313
|
+
let indent = ' ';
|
|
314
|
+
for (let i = insertIndex - 1; i >= 0; i--) {
|
|
315
|
+
if (lines[i].trim()) {
|
|
316
|
+
const indentMatch = lines[i].match(/^(\s+)/);
|
|
317
|
+
if (indentMatch) {
|
|
318
|
+
indent = indentMatch[1];
|
|
319
|
+
}
|
|
320
|
+
break;
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
lines.splice(insertIndex, 0, `${indent}guid = "${guid}"`);
|
|
324
|
+
fs.writeFileSync(filePath, lines.join('\n'), 'utf8');
|
|
325
|
+
return true;
|
|
326
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export interface ParsedDocument {
|
|
2
|
+
apiGroup?: string;
|
|
3
|
+
content: string;
|
|
4
|
+
name: string;
|
|
5
|
+
type: string;
|
|
6
|
+
verb?: string;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Parse a single XanoScript document to extract its type, name, and optional verb/api_group.
|
|
10
|
+
* Skips leading comment lines (starting with //) to find the first meaningful line.
|
|
11
|
+
*/
|
|
12
|
+
export declare function parseDocument(content: string): null | ParsedDocument;
|
|
13
|
+
/**
|
|
14
|
+
* Build a unique key for a document based on its type, name, verb, and api_group.
|
|
15
|
+
* Used to match server GUID map entries back to local files.
|
|
16
|
+
*/
|
|
17
|
+
export declare function buildDocumentKey(type: string, name: string, verb?: string, apiGroup?: string): string;
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parse a single XanoScript document to extract its type, name, and optional verb/api_group.
|
|
3
|
+
* Skips leading comment lines (starting with //) to find the first meaningful line.
|
|
4
|
+
*/
|
|
5
|
+
export function parseDocument(content) {
|
|
6
|
+
const lines = content.split('\n');
|
|
7
|
+
// Find the first non-comment line
|
|
8
|
+
let firstLine = null;
|
|
9
|
+
for (const line of lines) {
|
|
10
|
+
const trimmedLine = line.trim();
|
|
11
|
+
if (trimmedLine && !trimmedLine.startsWith('//')) {
|
|
12
|
+
firstLine = trimmedLine;
|
|
13
|
+
break;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
if (!firstLine) {
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
// Parse the type keyword and name from the first meaningful line
|
|
20
|
+
// Expected formats:
|
|
21
|
+
// type name {
|
|
22
|
+
// type name verb=GET {
|
|
23
|
+
// type "name with spaces" {
|
|
24
|
+
// type "name with spaces" verb=PATCH {
|
|
25
|
+
const match = firstLine.match(/^(\w+)\s+("(?:[^"\\]|\\.)*"|\S+)(?:\s+(.*))?/);
|
|
26
|
+
if (!match) {
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
const type = match[1];
|
|
30
|
+
let name = match[2];
|
|
31
|
+
const rest = match[3] || '';
|
|
32
|
+
// Strip surrounding quotes from the name
|
|
33
|
+
if (name.startsWith('"') && name.endsWith('"')) {
|
|
34
|
+
name = name.slice(1, -1);
|
|
35
|
+
}
|
|
36
|
+
// Extract verb if present (e.g., verb=GET)
|
|
37
|
+
let verb;
|
|
38
|
+
const verbMatch = rest.match(/verb=(\S+)/);
|
|
39
|
+
if (verbMatch) {
|
|
40
|
+
verb = verbMatch[1];
|
|
41
|
+
}
|
|
42
|
+
// Extract api_group if present (e.g., api_group = "test")
|
|
43
|
+
let apiGroup;
|
|
44
|
+
const apiGroupMatch = content.match(/api_group\s*=\s*"([^"]*)"/);
|
|
45
|
+
if (apiGroupMatch) {
|
|
46
|
+
apiGroup = apiGroupMatch[1];
|
|
47
|
+
}
|
|
48
|
+
return { apiGroup, content, name, type, verb };
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Build a unique key for a document based on its type, name, verb, and api_group.
|
|
52
|
+
* Used to match server GUID map entries back to local files.
|
|
53
|
+
*/
|
|
54
|
+
export function buildDocumentKey(type, name, verb, apiGroup) {
|
|
55
|
+
const parts = [type, name];
|
|
56
|
+
if (verb)
|
|
57
|
+
parts.push(verb);
|
|
58
|
+
if (apiGroup)
|
|
59
|
+
parts.push(apiGroup);
|
|
60
|
+
return parts.join(':');
|
|
61
|
+
}
|