@xano/cli 0.0.83 → 0.0.85
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 +1 -1
- package/dist/commands/workspace/push/index.js +57 -9
- package/oclif.manifest.json +1306 -1306
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -118,7 +118,7 @@ xano workspace pull ./my-workspace --draft # Include draft changes
|
|
|
118
118
|
xano workspace push ./my-workspace
|
|
119
119
|
xano workspace push ./my-workspace -b dev
|
|
120
120
|
xano workspace push ./my-workspace --dry-run # Preview changes without pushing
|
|
121
|
-
xano workspace push ./my-workspace --partial #
|
|
121
|
+
xano workspace push ./my-workspace --partial # Push only changed files, workspace block not required
|
|
122
122
|
xano workspace push ./my-workspace --delete # Delete objects not in the push
|
|
123
123
|
xano workspace push ./my-workspace --records # Include table records
|
|
124
124
|
xano workspace push ./my-workspace --env # Include environment variables
|
|
@@ -164,7 +164,7 @@ Truncate all table records before importing
|
|
|
164
164
|
if (documentEntries.length === 0) {
|
|
165
165
|
this.error(`All .xs files in ${args.directory} are empty`);
|
|
166
166
|
}
|
|
167
|
-
|
|
167
|
+
let multidoc = documentEntries.map((d) => d.content).join('\n---\n');
|
|
168
168
|
// Build lookup map from document key to file path (for GUID writeback)
|
|
169
169
|
const documentFileMap = new Map();
|
|
170
170
|
for (const entry of documentEntries) {
|
|
@@ -176,7 +176,9 @@ Truncate all table records before importing
|
|
|
176
176
|
}
|
|
177
177
|
// Determine branch from flag or profile
|
|
178
178
|
const branch = flags.branch || profile.branch || '';
|
|
179
|
-
|
|
179
|
+
if (flags.partial && flags.delete) {
|
|
180
|
+
this.error('Cannot use --delete with --partial');
|
|
181
|
+
}
|
|
180
182
|
const shouldDelete = flags.partial ? false : flags.delete;
|
|
181
183
|
// Construct the API URL
|
|
182
184
|
const queryParams = new URLSearchParams({
|
|
@@ -195,10 +197,13 @@ Truncate all table records before importing
|
|
|
195
197
|
'Content-Type': 'text/x-xanoscript',
|
|
196
198
|
};
|
|
197
199
|
// Preview mode: show what would change before pushing
|
|
200
|
+
let dryRunPreview = null;
|
|
198
201
|
if (flags['dry-run'] || !flags.force) {
|
|
199
202
|
const dryRunParams = new URLSearchParams(queryParams);
|
|
200
|
-
//
|
|
201
|
-
|
|
203
|
+
// Request delete info in dry-run so we can show remote-only items (skip for partial)
|
|
204
|
+
if (!flags.partial) {
|
|
205
|
+
dryRunParams.set('delete', 'true');
|
|
206
|
+
}
|
|
202
207
|
const dryRunUrl = `${profile.instance_origin}/api:meta/workspace/${workspaceId}/multidoc/dry-run?${dryRunParams.toString()}`;
|
|
203
208
|
try {
|
|
204
209
|
const dryRunResponse = await this.verboseFetch(dryRunUrl, {
|
|
@@ -247,9 +252,32 @@ Truncate all table records before importing
|
|
|
247
252
|
else {
|
|
248
253
|
const dryRunText = await dryRunResponse.text();
|
|
249
254
|
const preview = JSON.parse(dryRunText);
|
|
255
|
+
dryRunPreview = preview;
|
|
250
256
|
// Check if the server returned a valid dry-run response
|
|
251
257
|
if (preview && preview.summary) {
|
|
252
|
-
this.renderPreview(preview, shouldDelete, workspaceId, flags.verbose);
|
|
258
|
+
this.renderPreview(preview, shouldDelete, workspaceId, flags.verbose, flags.partial);
|
|
259
|
+
// Check for critical errors that must block the push
|
|
260
|
+
const criticalOps = preview.operations.filter((op) => op.details?.includes('exception:') || op.details?.includes('mvp:placeholder'));
|
|
261
|
+
if (criticalOps.length > 0) {
|
|
262
|
+
this.log('');
|
|
263
|
+
this.log(ux.colorize('red', ux.colorize('bold', '=== CRITICAL ERRORS ===')));
|
|
264
|
+
this.log('');
|
|
265
|
+
this.log(ux.colorize('red', 'The following items contain syntax errors or unresolved placeholder statements'));
|
|
266
|
+
this.log(ux.colorize('red', 'that would corrupt data if pushed. These must be resolved first:'));
|
|
267
|
+
this.log('');
|
|
268
|
+
for (const op of criticalOps) {
|
|
269
|
+
this.log(` ${ux.colorize('red', 'BLOCKED'.padEnd(16))} ${op.type.padEnd(18)} ${op.name}`);
|
|
270
|
+
if (op.details) {
|
|
271
|
+
this.log(` ${' '.repeat(16)} ${' '.repeat(18)} ${ux.colorize('dim', op.details)}`);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
this.log('');
|
|
275
|
+
this.log(ux.colorize('red', `Push blocked: ${criticalOps.length} critical error(s) found.`));
|
|
276
|
+
if (!flags.force) {
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
this.log(ux.colorize('yellow', 'Proceeding anyway due to --force flag.'));
|
|
280
|
+
}
|
|
253
281
|
// Check if there are any actual changes (exclude deletes when --delete is off)
|
|
254
282
|
const hasChanges = Object.values(preview.summary).some((c) => c.created > 0 || c.updated > 0 || (shouldDelete && c.deleted > 0) || c.truncated > 0);
|
|
255
283
|
// Detect if local files contain records that would be imported
|
|
@@ -345,6 +373,25 @@ Truncate all table records before importing
|
|
|
345
373
|
}
|
|
346
374
|
}
|
|
347
375
|
}
|
|
376
|
+
// For partial pushes, filter to only changed documents
|
|
377
|
+
if (flags.partial && dryRunPreview) {
|
|
378
|
+
const changedKeys = new Set(dryRunPreview.operations
|
|
379
|
+
.filter((op) => op.action !== 'unchanged' && op.action !== 'delete' && op.action !== 'cascade_delete')
|
|
380
|
+
.map((op) => `${op.type}:${op.name}`));
|
|
381
|
+
const filteredEntries = documentEntries.filter((entry) => {
|
|
382
|
+
const parsed = parseDocument(entry.content);
|
|
383
|
+
if (!parsed)
|
|
384
|
+
return true;
|
|
385
|
+
// For queries, operation name includes verb (e.g., "path/{id} DELETE")
|
|
386
|
+
const opName = parsed.verb ? `${parsed.name} ${parsed.verb}` : parsed.name;
|
|
387
|
+
return changedKeys.has(`${parsed.type}:${opName}`);
|
|
388
|
+
});
|
|
389
|
+
if (filteredEntries.length === 0) {
|
|
390
|
+
this.log('No changes to push.');
|
|
391
|
+
return;
|
|
392
|
+
}
|
|
393
|
+
multidoc = filteredEntries.map((d) => d.content).join('\n---\n');
|
|
394
|
+
}
|
|
348
395
|
const apiUrl = `${profile.instance_origin}/api:meta/workspace/${workspaceId}/multidoc?${queryParams.toString()}`;
|
|
349
396
|
const startTime = Date.now();
|
|
350
397
|
try {
|
|
@@ -452,7 +499,8 @@ Truncate all table records before importing
|
|
|
452
499
|
}
|
|
453
500
|
}
|
|
454
501
|
const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
|
|
455
|
-
|
|
502
|
+
const pushedCount = multidoc.split('\n---\n').length;
|
|
503
|
+
this.log(`Pushed ${pushedCount} documents from ${args.directory} in ${elapsed}s`);
|
|
456
504
|
}
|
|
457
505
|
async confirm(message) {
|
|
458
506
|
const readline = await import('node:readline');
|
|
@@ -473,7 +521,7 @@ Truncate all table records before importing
|
|
|
473
521
|
});
|
|
474
522
|
});
|
|
475
523
|
}
|
|
476
|
-
renderPreview(result, willDelete, workspaceId, verbose = false) {
|
|
524
|
+
renderPreview(result, willDelete, workspaceId, verbose = false, partial = false) {
|
|
477
525
|
const typeLabels = {
|
|
478
526
|
addon: 'Addons',
|
|
479
527
|
agent: 'Agents',
|
|
@@ -571,8 +619,8 @@ Truncate all table records before importing
|
|
|
571
619
|
this.log(ux.colorize('yellow', ' If this is intended to be a field rename, use the Xano Admin — renaming is not'));
|
|
572
620
|
this.log(ux.colorize('yellow', ' currently available through the CLI or Metadata API.'));
|
|
573
621
|
}
|
|
574
|
-
// Show remote-only items when not using --delete
|
|
575
|
-
if (!willDelete && deleteOps.length > 0) {
|
|
622
|
+
// Show remote-only items when not using --delete (skip for partial pushes)
|
|
623
|
+
if (!willDelete && !partial && deleteOps.length > 0) {
|
|
576
624
|
this.log('');
|
|
577
625
|
this.log(ux.colorize('dim', '--- Remote Only (not included in push) ---'));
|
|
578
626
|
this.log('');
|