@xano/cli 0.0.40 → 0.0.42
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 +6 -3
- package/dist/commands/profile/me/index.js +33 -14
- 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 +3 -0
- package/dist/commands/workspace/push/index.js +154 -8
- package/dist/utils/document-parser.d.ts +17 -0
- package/dist/utils/document-parser.js +61 -0
- package/oclif.manifest.json +1497 -1474
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -106,9 +106,12 @@ xano workspace pull ./my-workspace --draft # Include draft changes
|
|
|
106
106
|
# Push local files to workspace
|
|
107
107
|
xano workspace push ./my-workspace
|
|
108
108
|
xano workspace push ./my-workspace -b dev
|
|
109
|
-
xano workspace push ./my-workspace --
|
|
110
|
-
xano workspace push ./my-workspace --
|
|
111
|
-
xano workspace push ./my-workspace --
|
|
109
|
+
xano workspace push ./my-workspace --partial # No workspace block required
|
|
110
|
+
xano workspace push ./my-workspace --delete # Delete objects not in the push
|
|
111
|
+
xano workspace push ./my-workspace --no-records # Schema only
|
|
112
|
+
xano workspace push ./my-workspace --no-env # Skip env vars
|
|
113
|
+
xano workspace push ./my-workspace --truncate # Truncate tables before import
|
|
114
|
+
xano workspace push ./my-workspace --no-sync-guids # Skip writing GUIDs back to local files
|
|
112
115
|
```
|
|
113
116
|
|
|
114
117
|
### Branches
|
|
@@ -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 {
|
|
@@ -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
|
|
@@ -7,8 +7,11 @@ export default class Push extends BaseCommand {
|
|
|
7
7
|
static examples: string[];
|
|
8
8
|
static flags: {
|
|
9
9
|
branch: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
10
|
+
delete: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
10
11
|
env: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
12
|
+
partial: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
11
13
|
records: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
14
|
+
'sync-guids': import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
12
15
|
truncate: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
13
16
|
workspace: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
14
17
|
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({
|
|
@@ -24,6 +25,12 @@ Pushed 58 documents from ./backup
|
|
|
24
25
|
`,
|
|
25
26
|
`$ xano workspace push ./my-workspace -b dev
|
|
26
27
|
Pushed 42 documents from ./my-workspace
|
|
28
|
+
`,
|
|
29
|
+
`$ xano workspace push ./my-functions --partial
|
|
30
|
+
Push some files without a workspace block (implies --no-delete)
|
|
31
|
+
`,
|
|
32
|
+
`$ xano workspace push ./my-workspace --no-delete
|
|
33
|
+
Patch files without deleting existing workspace objects
|
|
27
34
|
`,
|
|
28
35
|
`$ xano workspace push ./my-workspace --no-records
|
|
29
36
|
Push schema only, skip importing table records
|
|
@@ -48,18 +55,35 @@ Push schema only, skip records and environment variables
|
|
|
48
55
|
description: 'Branch name (optional if set in profile, defaults to live)',
|
|
49
56
|
required: false,
|
|
50
57
|
}),
|
|
58
|
+
delete: Flags.boolean({
|
|
59
|
+
allowNo: true,
|
|
60
|
+
default: false,
|
|
61
|
+
description: 'Delete workspace objects not included in the push (default: false)',
|
|
62
|
+
required: false,
|
|
63
|
+
}),
|
|
51
64
|
env: Flags.boolean({
|
|
52
65
|
allowNo: true,
|
|
53
66
|
default: true,
|
|
54
67
|
description: 'Include environment variables in import (default: true, use --no-env to exclude)',
|
|
55
68
|
required: false,
|
|
56
69
|
}),
|
|
70
|
+
partial: Flags.boolean({
|
|
71
|
+
default: false,
|
|
72
|
+
description: 'Partial push — workspace block is not required, existing objects are kept (implies --no-delete)',
|
|
73
|
+
required: false,
|
|
74
|
+
}),
|
|
57
75
|
records: Flags.boolean({
|
|
58
76
|
allowNo: true,
|
|
59
77
|
default: true,
|
|
60
78
|
description: 'Include records in import (default: true, use --no-records to exclude)',
|
|
61
79
|
required: false,
|
|
62
80
|
}),
|
|
81
|
+
'sync-guids': Flags.boolean({
|
|
82
|
+
allowNo: true,
|
|
83
|
+
default: true,
|
|
84
|
+
description: 'Write server-assigned GUIDs back to local files (use --no-sync-guids to skip)',
|
|
85
|
+
required: false,
|
|
86
|
+
}),
|
|
63
87
|
truncate: Flags.boolean({
|
|
64
88
|
default: false,
|
|
65
89
|
description: 'Truncate all table records before importing',
|
|
@@ -116,24 +140,37 @@ Push schema only, skip records and environment variables
|
|
|
116
140
|
if (files.length === 0) {
|
|
117
141
|
this.error(`No .xs files found in ${args.directory}`);
|
|
118
142
|
}
|
|
119
|
-
// Read each file and
|
|
120
|
-
const
|
|
143
|
+
// Read each file and track file path alongside content
|
|
144
|
+
const documentEntries = [];
|
|
121
145
|
for (const filePath of files) {
|
|
122
146
|
const content = fs.readFileSync(filePath, 'utf8').trim();
|
|
123
147
|
if (content) {
|
|
124
|
-
|
|
148
|
+
documentEntries.push({ content, filePath });
|
|
125
149
|
}
|
|
126
150
|
}
|
|
127
|
-
if (
|
|
151
|
+
if (documentEntries.length === 0) {
|
|
128
152
|
this.error(`All .xs files in ${args.directory} are empty`);
|
|
129
153
|
}
|
|
130
|
-
const multidoc =
|
|
154
|
+
const multidoc = documentEntries.map((d) => d.content).join('\n---\n');
|
|
155
|
+
// Build lookup map from document key to file path (for GUID writeback)
|
|
156
|
+
const documentFileMap = new Map();
|
|
157
|
+
for (const entry of documentEntries) {
|
|
158
|
+
const parsed = parseDocument(entry.content);
|
|
159
|
+
if (parsed) {
|
|
160
|
+
const key = buildDocumentKey(parsed.type, parsed.name, parsed.verb, parsed.apiGroup);
|
|
161
|
+
documentFileMap.set(key, entry.filePath);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
131
164
|
// Determine branch from flag or profile
|
|
132
165
|
const branch = flags.branch || profile.branch || '';
|
|
166
|
+
// --partial implies --no-delete
|
|
167
|
+
const shouldDelete = flags.partial ? false : flags.delete;
|
|
133
168
|
// Construct the API URL
|
|
134
169
|
const queryParams = new URLSearchParams({
|
|
135
170
|
branch,
|
|
171
|
+
delete: shouldDelete.toString(),
|
|
136
172
|
env: flags.env.toString(),
|
|
173
|
+
partial: flags.partial.toString(),
|
|
137
174
|
records: flags.records.toString(),
|
|
138
175
|
truncate: flags.truncate.toString(),
|
|
139
176
|
});
|
|
@@ -165,10 +202,70 @@ Push schema only, skip records and environment variables
|
|
|
165
202
|
}
|
|
166
203
|
this.error(errorMessage);
|
|
167
204
|
}
|
|
168
|
-
//
|
|
205
|
+
// Parse the response for GUID map
|
|
169
206
|
const responseText = await response.text();
|
|
207
|
+
let guidMap = [];
|
|
170
208
|
if (responseText && responseText !== 'null') {
|
|
171
|
-
|
|
209
|
+
try {
|
|
210
|
+
const responseJson = JSON.parse(responseText);
|
|
211
|
+
if (responseJson?.guid_map && Array.isArray(responseJson.guid_map)) {
|
|
212
|
+
guidMap = responseJson.guid_map;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
catch {
|
|
216
|
+
// Response is not JSON (e.g., older server version)
|
|
217
|
+
if (flags.verbose) {
|
|
218
|
+
this.log('Server response is not JSON; skipping GUID sync');
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
// Write GUIDs back to local files
|
|
223
|
+
if (flags['sync-guids'] && guidMap.length > 0) {
|
|
224
|
+
// Build a secondary lookup by type:name only (without verb/api_group)
|
|
225
|
+
// for cases where the server omits those fields
|
|
226
|
+
const baseKeyMap = new Map();
|
|
227
|
+
for (const [key, fp] of documentFileMap) {
|
|
228
|
+
const baseKey = key.split(':').slice(0, 2).join(':');
|
|
229
|
+
// Only use base key if there's no ambiguity (single entry per base key)
|
|
230
|
+
if (baseKeyMap.has(baseKey)) {
|
|
231
|
+
baseKeyMap.set(baseKey, ''); // Mark as ambiguous
|
|
232
|
+
}
|
|
233
|
+
else {
|
|
234
|
+
baseKeyMap.set(baseKey, fp);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
let updatedCount = 0;
|
|
238
|
+
for (const entry of guidMap) {
|
|
239
|
+
if (!entry.guid)
|
|
240
|
+
continue;
|
|
241
|
+
const key = buildDocumentKey(entry.type, entry.name, entry.verb, entry.api_group);
|
|
242
|
+
let filePath = documentFileMap.get(key);
|
|
243
|
+
// Fallback: try type:name only if full key didn't match
|
|
244
|
+
if (!filePath) {
|
|
245
|
+
const baseKey = `${entry.type}:${entry.name}`;
|
|
246
|
+
const basePath = baseKeyMap.get(baseKey);
|
|
247
|
+
if (basePath) {
|
|
248
|
+
filePath = basePath;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
if (!filePath) {
|
|
252
|
+
if (flags.verbose) {
|
|
253
|
+
this.log(` No local file found for ${entry.type} "${entry.name}", skipping GUID sync`);
|
|
254
|
+
}
|
|
255
|
+
continue;
|
|
256
|
+
}
|
|
257
|
+
try {
|
|
258
|
+
const updated = syncGuidToFile(filePath, entry.guid);
|
|
259
|
+
if (updated)
|
|
260
|
+
updatedCount++;
|
|
261
|
+
}
|
|
262
|
+
catch (error) {
|
|
263
|
+
this.warn(`Failed to sync GUID to ${filePath}: ${error.message}`);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
if (updatedCount > 0) {
|
|
267
|
+
this.log(`Synced ${updatedCount} GUIDs to local files`);
|
|
268
|
+
}
|
|
172
269
|
}
|
|
173
270
|
}
|
|
174
271
|
catch (error) {
|
|
@@ -179,7 +276,7 @@ Push schema only, skip records and environment variables
|
|
|
179
276
|
this.error(`Failed to push multidoc: ${String(error)}`);
|
|
180
277
|
}
|
|
181
278
|
}
|
|
182
|
-
this.log(`Pushed ${
|
|
279
|
+
this.log(`Pushed ${documentEntries.length} documents from ${args.directory}`);
|
|
183
280
|
}
|
|
184
281
|
/**
|
|
185
282
|
* Recursively collect all .xs files from a directory, sorted by
|
|
@@ -220,3 +317,52 @@ Push schema only, skip records and environment variables
|
|
|
220
317
|
}
|
|
221
318
|
}
|
|
222
319
|
}
|
|
320
|
+
const GUID_REGEX = /guid\s*=\s*(["'])([^"']*)\1/;
|
|
321
|
+
/**
|
|
322
|
+
* Sync a GUID into a local .xs file. Returns true if the file was modified.
|
|
323
|
+
*
|
|
324
|
+
* - If the file already has a matching GUID, returns false (no change).
|
|
325
|
+
* - If the file has a different GUID, updates it.
|
|
326
|
+
* - If the file has no GUID, inserts one before the final closing brace.
|
|
327
|
+
*/
|
|
328
|
+
function syncGuidToFile(filePath, guid) {
|
|
329
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
330
|
+
const existingMatch = content.match(GUID_REGEX);
|
|
331
|
+
if (existingMatch) {
|
|
332
|
+
// Already has a GUID
|
|
333
|
+
if (existingMatch[2] === guid) {
|
|
334
|
+
return false; // Already matches
|
|
335
|
+
}
|
|
336
|
+
// Update existing GUID
|
|
337
|
+
const updated = content.replace(GUID_REGEX, `guid = "${guid}"`);
|
|
338
|
+
fs.writeFileSync(filePath, updated, 'utf8');
|
|
339
|
+
return true;
|
|
340
|
+
}
|
|
341
|
+
// No GUID line exists — insert before the final closing brace of the top-level block
|
|
342
|
+
const lines = content.split('\n');
|
|
343
|
+
let insertIndex = -1;
|
|
344
|
+
// Find the last closing brace (top-level block end)
|
|
345
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
346
|
+
if (lines[i].trim() === '}') {
|
|
347
|
+
insertIndex = i;
|
|
348
|
+
break;
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
if (insertIndex === -1) {
|
|
352
|
+
return false; // Could not find insertion point
|
|
353
|
+
}
|
|
354
|
+
// Determine indentation from the line above the closing brace
|
|
355
|
+
let indent = ' ';
|
|
356
|
+
for (let i = insertIndex - 1; i >= 0; i--) {
|
|
357
|
+
if (lines[i].trim()) {
|
|
358
|
+
const indentMatch = lines[i].match(/^(\s+)/);
|
|
359
|
+
if (indentMatch) {
|
|
360
|
+
indent = indentMatch[1];
|
|
361
|
+
}
|
|
362
|
+
break;
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
lines.splice(insertIndex, 0, `${indent}guid = "${guid}"`);
|
|
366
|
+
fs.writeFileSync(filePath, lines.join('\n'), 'utf8');
|
|
367
|
+
return true;
|
|
368
|
+
}
|
|
@@ -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
|
+
}
|