@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 +4 -4
- package/dist/commands/workspace/push/index.d.ts +2 -2
- package/dist/commands/workspace/push/index.js +52 -25
- package/oclif.manifest.json +1375 -1375
- package/package.json +1 -1
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-
|
|
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
|
-
|
|
13
|
+
sync: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
14
14
|
records: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
15
|
-
|
|
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
|
-
|
|
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
|
-
|
|
73
|
+
sync: Flags.boolean({
|
|
74
74
|
default: false,
|
|
75
|
-
description: '
|
|
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
|
-
|
|
83
|
+
guids: Flags.boolean({
|
|
84
84
|
allowNo: true,
|
|
85
85
|
default: true,
|
|
86
|
-
description: 'Write server-assigned GUIDs back to local files (use --no-
|
|
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
|
-
|
|
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
|
-
|
|
180
|
-
|
|
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:
|
|
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
|
-
//
|
|
201
|
-
|
|
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
|
|
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
|
-
|
|
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('');
|