@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 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,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
- 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`);
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('');