@xano/cli 1.0.2-beta.1 → 1.0.2-beta.2
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 +0 -2
- package/dist/commands/sandbox/push/index.d.ts +0 -2
- package/dist/commands/sandbox/push/index.js +2 -24
- package/dist/utils/multidoc-push.d.ts +21 -0
- package/dist/utils/multidoc-push.js +44 -0
- package/oclif.manifest.json +2235 -2256
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -482,8 +482,6 @@ xano sandbox push --records --env # Include records and e
|
|
|
482
482
|
xano sandbox push --truncate # Truncate tables before import
|
|
483
483
|
xano sandbox push --no-guids # Skip writing GUIDs back to local files
|
|
484
484
|
xano sandbox push --force # Skip preview and confirmation
|
|
485
|
-
xano sandbox push -i "function/*" # Push only matching files
|
|
486
|
-
xano sandbox push -e "table/*" # Push all files except tables
|
|
487
485
|
xano sandbox push --review # Push and open sandbox review in the browser
|
|
488
486
|
|
|
489
487
|
# Review (open in browser)
|
|
@@ -7,10 +7,8 @@ export default class SandboxPush extends BaseCommand {
|
|
|
7
7
|
delete: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
8
8
|
'dry-run': import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
9
9
|
env: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
10
|
-
exclude: import("@oclif/core/interfaces").OptionFlag<string[] | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
11
10
|
force: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
12
11
|
guids: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
13
|
-
include: import("@oclif/core/interfaces").OptionFlag<string[] | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
14
12
|
records: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
15
13
|
review: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
16
14
|
sync: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
@@ -5,7 +5,7 @@ import open from 'open';
|
|
|
5
5
|
import BaseCommand from '../../../base-command.js';
|
|
6
6
|
import { executePush } from '../../../utils/multidoc-push.js';
|
|
7
7
|
export default class SandboxPush extends BaseCommand {
|
|
8
|
-
static description = 'Push local documents to your sandbox environment via multidoc import. 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.';
|
|
8
|
+
static description = 'Push local documents to your sandbox environment via multidoc import. 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. Include/exclude glob filters are intentionally not supported on sandbox push — partial pushes can hide deletions during review and lead to data loss when promoted to the workspace. Large pushes against a sandbox loaded with a different workspace will prompt for confirmation; run `xano sandbox reset` first to start clean.';
|
|
9
9
|
static examples = [
|
|
10
10
|
`$ xano sandbox push
|
|
11
11
|
Push from current directory (default partial mode)
|
|
@@ -27,15 +27,6 @@ Skip preview and push immediately
|
|
|
27
27
|
`,
|
|
28
28
|
`$ xano sandbox push --records --env`,
|
|
29
29
|
`$ xano sandbox push --truncate`,
|
|
30
|
-
`$ xano sandbox push -i "**/func*"
|
|
31
|
-
Push only files matching the glob pattern
|
|
32
|
-
`,
|
|
33
|
-
`$ xano sandbox push -i "function/*" -i "table/*"
|
|
34
|
-
Push files matching multiple patterns
|
|
35
|
-
`,
|
|
36
|
-
`$ xano sandbox push -e "table/*"
|
|
37
|
-
Push all files except tables
|
|
38
|
-
`,
|
|
39
30
|
`$ xano sandbox push --review
|
|
40
31
|
Push and open sandbox review in the browser
|
|
41
32
|
`,
|
|
@@ -63,12 +54,6 @@ Push and open sandbox review in the browser
|
|
|
63
54
|
description: 'Include environment variables in import',
|
|
64
55
|
required: false,
|
|
65
56
|
}),
|
|
66
|
-
exclude: Flags.string({
|
|
67
|
-
char: 'e',
|
|
68
|
-
description: 'Glob pattern to exclude files (e.g. "table/*", "**/test*"). Matched against relative paths from the push directory.',
|
|
69
|
-
multiple: true,
|
|
70
|
-
required: false,
|
|
71
|
-
}),
|
|
72
57
|
force: Flags.boolean({
|
|
73
58
|
default: false,
|
|
74
59
|
description: 'Skip preview and confirmation prompt (for CI/CD pipelines)',
|
|
@@ -80,12 +65,6 @@ Push and open sandbox review in the browser
|
|
|
80
65
|
description: 'Write server-assigned GUIDs back to local files (use --no-guids to skip)',
|
|
81
66
|
required: false,
|
|
82
67
|
}),
|
|
83
|
-
include: Flags.string({
|
|
84
|
-
char: 'i',
|
|
85
|
-
description: 'Glob pattern to include files (e.g. "**/func*", "table/*.xs"). Matched against relative paths from the push directory.',
|
|
86
|
-
multiple: true,
|
|
87
|
-
required: false,
|
|
88
|
-
}),
|
|
89
68
|
records: Flags.boolean({
|
|
90
69
|
default: false,
|
|
91
70
|
description: 'Include records in import',
|
|
@@ -132,15 +111,14 @@ Push and open sandbox review in the browser
|
|
|
132
111
|
label: 'sandbox environment',
|
|
133
112
|
supportsBranches: false,
|
|
134
113
|
supportsPartial: true,
|
|
114
|
+
warnOnWorkspaceMismatch: true,
|
|
135
115
|
};
|
|
136
116
|
const pushFlags = {
|
|
137
117
|
delete: flags.delete,
|
|
138
118
|
'dry-run': flags['dry-run'],
|
|
139
119
|
env: flags.env,
|
|
140
|
-
exclude: flags.exclude,
|
|
141
120
|
force: flags.force,
|
|
142
121
|
guids: flags.guids,
|
|
143
|
-
include: flags.include,
|
|
144
122
|
records: flags.records,
|
|
145
123
|
sync: flags.sync,
|
|
146
124
|
transaction: flags.transaction,
|
|
@@ -29,6 +29,12 @@ export interface PushTarget {
|
|
|
29
29
|
supportsBranches: boolean;
|
|
30
30
|
/** Does this target support the partial query param? */
|
|
31
31
|
supportsPartial: boolean;
|
|
32
|
+
/**
|
|
33
|
+
* Warn when the workspace embedded in the local push differs from the workspace currently
|
|
34
|
+
* loaded on the target (per dry-run). Used by sandbox push because the sandbox is shared
|
|
35
|
+
* across workspaces and pushing onto a different workspace can leave stale state behind.
|
|
36
|
+
*/
|
|
37
|
+
warnOnWorkspaceMismatch?: boolean;
|
|
32
38
|
}
|
|
33
39
|
export interface PushContext {
|
|
34
40
|
accessToken: string;
|
|
@@ -37,6 +43,17 @@ export interface PushContext {
|
|
|
37
43
|
inputDir: string;
|
|
38
44
|
verboseFetch: (url: string, options: RequestInit, verbose: boolean, authToken?: string) => Promise<Response>;
|
|
39
45
|
}
|
|
46
|
+
export declare const WORKSPACE_MISMATCH_THRESHOLD = 10;
|
|
47
|
+
/**
|
|
48
|
+
* Sum all impactful operations in a dry-run summary. `deleted` is only counted when the
|
|
49
|
+
* caller actually intends to apply deletions (sync mode), matching what the user will see.
|
|
50
|
+
*/
|
|
51
|
+
export declare function countSummaryChanges(summary: Record<string, {
|
|
52
|
+
created: number;
|
|
53
|
+
deleted: number;
|
|
54
|
+
truncated: number;
|
|
55
|
+
updated: number;
|
|
56
|
+
}>, shouldDelete: boolean): number;
|
|
40
57
|
/**
|
|
41
58
|
* Recursively collect all .xs files from a directory, sorted for deterministic ordering.
|
|
42
59
|
*/
|
|
@@ -55,6 +72,10 @@ export declare function readDocuments(files: string[]): Array<{
|
|
|
55
72
|
}>;
|
|
56
73
|
export declare function renderBadReferences(badRefs: BadReference[], log: (msg: string) => void): void;
|
|
57
74
|
export declare function renderBadIndexes(badIndexes: BadIndex[], log: (msg: string) => void): void;
|
|
75
|
+
export declare function findLocalWorkspaceName(entries: Array<{
|
|
76
|
+
content: string;
|
|
77
|
+
filePath: string;
|
|
78
|
+
}>): null | string;
|
|
58
79
|
export declare function confirm(message: string): Promise<boolean>;
|
|
59
80
|
/**
|
|
60
81
|
* Execute a multidoc push with preview, validation, partial mode, and GUID sync.
|
|
@@ -4,6 +4,16 @@ import * as fs from 'node:fs';
|
|
|
4
4
|
import { join, relative } from 'node:path';
|
|
5
5
|
import { buildDocumentKey, findFilesWithGuid, parseDocument } from './document-parser.js';
|
|
6
6
|
import { checkReferences, checkTableIndexes } from './reference-checker.js';
|
|
7
|
+
// Minimum total operations before a workspace mismatch is treated as worth interrupting for.
|
|
8
|
+
// Small change sets (e.g., editing a single function) aren't worth a reset prompt.
|
|
9
|
+
export const WORKSPACE_MISMATCH_THRESHOLD = 10;
|
|
10
|
+
/**
|
|
11
|
+
* Sum all impactful operations in a dry-run summary. `deleted` is only counted when the
|
|
12
|
+
* caller actually intends to apply deletions (sync mode), matching what the user will see.
|
|
13
|
+
*/
|
|
14
|
+
export function countSummaryChanges(summary, shouldDelete) {
|
|
15
|
+
return Object.values(summary).reduce((sum, c) => sum + c.created + c.updated + (shouldDelete ? c.deleted : 0) + c.truncated, 0);
|
|
16
|
+
}
|
|
7
17
|
// ── File Collection ─────────────────────────────────────────────────────────
|
|
8
18
|
/**
|
|
9
19
|
* Recursively collect all .xs files from a directory, sorted for deterministic ordering.
|
|
@@ -211,6 +221,14 @@ function renderPreview(result, willDelete, target, verbose, partial, log) {
|
|
|
211
221
|
}
|
|
212
222
|
log('');
|
|
213
223
|
}
|
|
224
|
+
export function findLocalWorkspaceName(entries) {
|
|
225
|
+
for (const entry of entries) {
|
|
226
|
+
const parsed = parseDocument(entry.content);
|
|
227
|
+
if (parsed?.type === 'workspace')
|
|
228
|
+
return parsed.name;
|
|
229
|
+
}
|
|
230
|
+
return null;
|
|
231
|
+
}
|
|
214
232
|
// ── Confirmation ────────────────────────────────────────────────────────────
|
|
215
233
|
export async function confirm(message) {
|
|
216
234
|
const readline = await import('node:readline');
|
|
@@ -418,6 +436,32 @@ export async function executePush(ctx, target, flags) {
|
|
|
418
436
|
if (flags['dry-run']) {
|
|
419
437
|
return;
|
|
420
438
|
}
|
|
439
|
+
// Warn when the sandbox currently holds a different workspace than the one being
|
|
440
|
+
// pushed and the change set is large enough that stale state is a real risk.
|
|
441
|
+
if (target.warnOnWorkspaceMismatch && preview.workspace_name) {
|
|
442
|
+
const localWorkspaceName = findLocalWorkspaceName(documentEntries);
|
|
443
|
+
const totalChanges = countSummaryChanges(preview.summary, shouldDelete);
|
|
444
|
+
if (localWorkspaceName &&
|
|
445
|
+
localWorkspaceName !== preview.workspace_name &&
|
|
446
|
+
totalChanges >= WORKSPACE_MISMATCH_THRESHOLD) {
|
|
447
|
+
log('');
|
|
448
|
+
log(ux.colorize('yellow', ux.colorize('bold', '=== Workspace Mismatch ===')));
|
|
449
|
+
log('');
|
|
450
|
+
log(ux.colorize('yellow', `Sandbox currently holds workspace "${preview.workspace_name}", but you're pushing "${localWorkspaceName}" with ${totalChanges} changes.`));
|
|
451
|
+
log(ux.colorize('yellow', 'Pushing on top of a different workspace can leave stale data behind. Run `xano sandbox reset` first to start clean.'));
|
|
452
|
+
log('');
|
|
453
|
+
if (process.stdin.isTTY) {
|
|
454
|
+
const proceed = await confirm('Continue with push anyway?');
|
|
455
|
+
if (!proceed) {
|
|
456
|
+
log('Push cancelled. Run `xano sandbox reset` then retry.');
|
|
457
|
+
return;
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
else {
|
|
461
|
+
command.error('Workspace mismatch detected in non-interactive mode. Run `xano sandbox reset` first to start clean.');
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
}
|
|
421
465
|
// Confirm with user
|
|
422
466
|
const hasDestructive = preview.operations.some((op) => (shouldDelete && (op.action === 'delete' || op.action === 'cascade_delete')) ||
|
|
423
467
|
op.action === 'truncate' ||
|