@xano/cli 0.0.84 → 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 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 # No workspace block required
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
- const multidoc = documentEntries.map((d) => d.content).join('\n---\n');
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
- // --partial implies --no-delete
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
- // Always request delete info in dry-run so we can show remote-only items
201
- dryRunParams.set('delete', 'true');
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,10 @@ 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);
253
259
  // Check for critical errors that must block the push
254
260
  const criticalOps = preview.operations.filter((op) => op.details?.includes('exception:') || op.details?.includes('mvp:placeholder'));
255
261
  if (criticalOps.length > 0) {
@@ -367,6 +373,25 @@ Truncate all table records before importing
367
373
  }
368
374
  }
369
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
+ }
370
395
  const apiUrl = `${profile.instance_origin}/api:meta/workspace/${workspaceId}/multidoc?${queryParams.toString()}`;
371
396
  const startTime = Date.now();
372
397
  try {
@@ -474,7 +499,8 @@ Truncate all table records before importing
474
499
  }
475
500
  }
476
501
  const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
477
- this.log(`Pushed ${documentEntries.length} documents from ${args.directory} in ${elapsed}s`);
502
+ const pushedCount = multidoc.split('\n---\n').length;
503
+ this.log(`Pushed ${pushedCount} documents from ${args.directory} in ${elapsed}s`);
478
504
  }
479
505
  async confirm(message) {
480
506
  const readline = await import('node:readline');
@@ -495,7 +521,7 @@ Truncate all table records before importing
495
521
  });
496
522
  });
497
523
  }
498
- renderPreview(result, willDelete, workspaceId, verbose = false) {
524
+ renderPreview(result, willDelete, workspaceId, verbose = false, partial = false) {
499
525
  const typeLabels = {
500
526
  addon: 'Addons',
501
527
  agent: 'Agents',
@@ -593,8 +619,8 @@ Truncate all table records before importing
593
619
  this.log(ux.colorize('yellow', ' If this is intended to be a field rename, use the Xano Admin — renaming is not'));
594
620
  this.log(ux.colorize('yellow', ' currently available through the CLI or Metadata API.'));
595
621
  }
596
- // Show remote-only items when not using --delete
597
- 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) {
598
624
  this.log('');
599
625
  this.log(ux.colorize('dim', '--- Remote Only (not included in push) ---'));
600
626
  this.log('');