@xano/cli 0.0.84 → 0.0.86

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
@@ -114,17 +114,17 @@ xano workspace pull ./my-workspace -b dev # Specific branch
114
114
  xano workspace pull ./my-workspace --env --records # Include env vars and table records
115
115
  xano workspace pull ./my-workspace --draft # Include draft changes
116
116
 
117
- # Push local files to workspace
117
+ # Push local files to workspace (only changed files by default)
118
118
  xano workspace push ./my-workspace
119
119
  xano workspace push ./my-workspace -b dev
120
+ xano workspace push ./my-workspace --sync # Full push — send all files, not just changed ones
121
+ xano workspace push ./my-workspace --sync --delete # Full push + delete remote objects not included
120
122
  xano workspace push ./my-workspace --dry-run # Preview changes without pushing
121
- xano workspace push ./my-workspace --partial # No workspace block required
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
125
125
  xano workspace push ./my-workspace --truncate # Truncate tables before import
126
126
  xano workspace push ./my-workspace --no-transaction # Disable database transaction wrapping
127
- xano workspace push ./my-workspace --no-sync-guids # Skip writing GUIDs back to local files
127
+ xano workspace push ./my-workspace --no-guids # Skip writing GUIDs back to local files
128
128
  xano workspace push ./my-workspace --force # Skip preview and confirmation (for CI/CD)
129
129
 
130
130
  # Pull from a git repository to local files
@@ -10,9 +10,9 @@ export default class Push extends BaseCommand {
10
10
  delete: import("@oclif/core/interfaces").BooleanFlag<boolean>;
11
11
  'dry-run': import("@oclif/core/interfaces").BooleanFlag<boolean>;
12
12
  env: import("@oclif/core/interfaces").BooleanFlag<boolean>;
13
- partial: import("@oclif/core/interfaces").BooleanFlag<boolean>;
13
+ sync: import("@oclif/core/interfaces").BooleanFlag<boolean>;
14
14
  records: import("@oclif/core/interfaces").BooleanFlag<boolean>;
15
- 'sync-guids': import("@oclif/core/interfaces").BooleanFlag<boolean>;
15
+ guids: import("@oclif/core/interfaces").BooleanFlag<boolean>;
16
16
  transaction: import("@oclif/core/interfaces").BooleanFlag<boolean>;
17
17
  truncate: import("@oclif/core/interfaces").BooleanFlag<boolean>;
18
18
  workspace: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
@@ -12,19 +12,22 @@ export default class Push extends BaseCommand {
12
12
  required: true,
13
13
  }),
14
14
  };
15
- static description = 'Push local documents to a workspace. Shows a preview of changes before pushing unless --force is specified. Use --dry-run to preview only.';
15
+ static description = 'Push local documents to a workspace. By default, only changed files are pushed (partial mode). Use --sync to push all files. Shows a preview of changes before pushing unless --force is specified. Use --dry-run to preview only.';
16
16
  static examples = [
17
17
  `$ xano workspace push ./my-workspace
18
- Shows preview of changes, requires confirmation
18
+ Push only changed files (default partial mode)
19
+ `,
20
+ `$ xano workspace push ./my-workspace --sync
21
+ Push all files to the workspace
22
+ `,
23
+ `$ xano workspace push ./my-workspace --sync --delete
24
+ Push all files and delete remote objects not included
19
25
  `,
20
26
  `$ xano workspace push ./my-workspace --dry-run
21
27
  Preview changes without pushing
22
28
  `,
23
29
  `$ xano workspace push ./my-workspace --force
24
30
  Skip preview and push immediately (for CI/CD)
25
- `,
26
- `$ xano workspace push ./my-workspace --delete
27
- Shows preview including deletions, requires confirmation
28
31
  `,
29
32
  `$ xano workspace push ./output -w 40
30
33
  Pushed 15 documents from ./output
@@ -34,9 +37,6 @@ Pushed 58 documents from ./backup
34
37
  `,
35
38
  `$ xano workspace push ./my-workspace -b dev
36
39
  Pushed 42 documents from ./my-workspace
37
- `,
38
- `$ xano workspace push ./my-functions --partial
39
- Push some files without a workspace block (implies --no-delete)
40
40
  `,
41
41
  `$ xano workspace push ./my-workspace --no-records
42
42
  Push schema only, skip importing table records
@@ -57,7 +57,7 @@ Truncate all table records before importing
57
57
  }),
58
58
  delete: Flags.boolean({
59
59
  default: false,
60
- description: 'Delete workspace objects not included in the push',
60
+ description: 'Delete workspace objects not included in the push (requires --sync)',
61
61
  required: false,
62
62
  }),
63
63
  'dry-run': Flags.boolean({
@@ -70,9 +70,9 @@ Truncate all table records before importing
70
70
  description: 'Include environment variables in import',
71
71
  required: false,
72
72
  }),
73
- partial: Flags.boolean({
73
+ sync: Flags.boolean({
74
74
  default: false,
75
- description: 'Partial push — workspace block is not required, existing objects are kept (implies --no-delete)',
75
+ description: 'Full push — send all files, not just changed ones. Required for --delete.',
76
76
  required: false,
77
77
  }),
78
78
  records: Flags.boolean({
@@ -80,10 +80,10 @@ Truncate all table records before importing
80
80
  description: 'Include records in import',
81
81
  required: false,
82
82
  }),
83
- 'sync-guids': Flags.boolean({
83
+ guids: Flags.boolean({
84
84
  allowNo: true,
85
85
  default: true,
86
- description: 'Write server-assigned GUIDs back to local files (use --no-sync-guids to skip)',
86
+ description: 'Write server-assigned GUIDs back to local files (use --no-guids to skip)',
87
87
  required: false,
88
88
  }),
89
89
  transaction: Flags.boolean({
@@ -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,14 +176,17 @@ 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
180
- const shouldDelete = flags.partial ? false : flags.delete;
179
+ const isPartial = !flags.sync;
180
+ if (flags.delete && isPartial) {
181
+ this.error('Cannot use --delete without --sync');
182
+ }
183
+ const shouldDelete = isPartial ? false : flags.delete;
181
184
  // Construct the API URL
182
185
  const queryParams = new URLSearchParams({
183
186
  branch,
184
187
  delete: shouldDelete.toString(),
185
188
  env: flags.env.toString(),
186
- partial: flags.partial.toString(),
189
+ partial: isPartial.toString(),
187
190
  records: flags.records.toString(),
188
191
  transaction: flags.transaction.toString(),
189
192
  truncate: flags.truncate.toString(),
@@ -195,10 +198,13 @@ Truncate all table records before importing
195
198
  'Content-Type': 'text/x-xanoscript',
196
199
  };
197
200
  // Preview mode: show what would change before pushing
201
+ let dryRunPreview = null;
198
202
  if (flags['dry-run'] || !flags.force) {
199
203
  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');
204
+ // Request delete info in dry-run so we can show remote-only items (skip for partial)
205
+ if (!isPartial) {
206
+ dryRunParams.set('delete', 'true');
207
+ }
202
208
  const dryRunUrl = `${profile.instance_origin}/api:meta/workspace/${workspaceId}/multidoc/dry-run?${dryRunParams.toString()}`;
203
209
  try {
204
210
  const dryRunResponse = await this.verboseFetch(dryRunUrl, {
@@ -247,9 +253,10 @@ Truncate all table records before importing
247
253
  else {
248
254
  const dryRunText = await dryRunResponse.text();
249
255
  const preview = JSON.parse(dryRunText);
256
+ dryRunPreview = preview;
250
257
  // Check if the server returned a valid dry-run response
251
258
  if (preview && preview.summary) {
252
- this.renderPreview(preview, shouldDelete, workspaceId, flags.verbose);
259
+ this.renderPreview(preview, shouldDelete, workspaceId, flags.verbose, isPartial);
253
260
  // Check for critical errors that must block the push
254
261
  const criticalOps = preview.operations.filter((op) => op.details?.includes('exception:') || op.details?.includes('mvp:placeholder'));
255
262
  if (criticalOps.length > 0) {
@@ -367,6 +374,25 @@ Truncate all table records before importing
367
374
  }
368
375
  }
369
376
  }
377
+ // For partial pushes, filter to only changed documents
378
+ if (isPartial && dryRunPreview) {
379
+ const changedKeys = new Set(dryRunPreview.operations
380
+ .filter((op) => op.action !== 'unchanged' && op.action !== 'delete' && op.action !== 'cascade_delete')
381
+ .map((op) => `${op.type}:${op.name}`));
382
+ const filteredEntries = documentEntries.filter((entry) => {
383
+ const parsed = parseDocument(entry.content);
384
+ if (!parsed)
385
+ return true;
386
+ // For queries, operation name includes verb (e.g., "path/{id} DELETE")
387
+ const opName = parsed.verb ? `${parsed.name} ${parsed.verb}` : parsed.name;
388
+ return changedKeys.has(`${parsed.type}:${opName}`);
389
+ });
390
+ if (filteredEntries.length === 0) {
391
+ this.log('No changes to push.');
392
+ return;
393
+ }
394
+ multidoc = filteredEntries.map((d) => d.content).join('\n---\n');
395
+ }
370
396
  const apiUrl = `${profile.instance_origin}/api:meta/workspace/${workspaceId}/multidoc?${queryParams.toString()}`;
371
397
  const startTime = Date.now();
372
398
  try {
@@ -417,7 +443,7 @@ Truncate all table records before importing
417
443
  }
418
444
  }
419
445
  // Write GUIDs back to local files
420
- if (flags['sync-guids'] && guidMap.length > 0) {
446
+ if (flags.guids && guidMap.length > 0) {
421
447
  // Build a secondary lookup by type:name only (without verb/api_group)
422
448
  // for cases where the server omits those fields
423
449
  const baseKeyMap = new Map();
@@ -474,7 +500,8 @@ Truncate all table records before importing
474
500
  }
475
501
  }
476
502
  const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
477
- this.log(`Pushed ${documentEntries.length} documents from ${args.directory} in ${elapsed}s`);
503
+ const pushedCount = multidoc.split('\n---\n').length;
504
+ this.log(`Pushed ${pushedCount} documents from ${args.directory} in ${elapsed}s`);
478
505
  }
479
506
  async confirm(message) {
480
507
  const readline = await import('node:readline');
@@ -495,7 +522,7 @@ Truncate all table records before importing
495
522
  });
496
523
  });
497
524
  }
498
- renderPreview(result, willDelete, workspaceId, verbose = false) {
525
+ renderPreview(result, willDelete, workspaceId, verbose = false, partial = false) {
499
526
  const typeLabels = {
500
527
  addon: 'Addons',
501
528
  agent: 'Agents',
@@ -593,8 +620,8 @@ Truncate all table records before importing
593
620
  this.log(ux.colorize('yellow', ' If this is intended to be a field rename, use the Xano Admin — renaming is not'));
594
621
  this.log(ux.colorize('yellow', ' currently available through the CLI or Metadata API.'));
595
622
  }
596
- // Show remote-only items when not using --delete
597
- if (!willDelete && deleteOps.length > 0) {
623
+ // Show remote-only items when not using --delete (skip for partial pushes)
624
+ if (!willDelete && !partial && deleteOps.length > 0) {
598
625
  this.log('');
599
626
  this.log(ux.colorize('dim', '--- Remote Only (not included in push) ---'));
600
627
  this.log('');