@xano/cli 1.0.2-beta.0 → 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 CHANGED
@@ -151,9 +151,10 @@ All branch commands use **branch labels** (e.g., `v1`, `dev`), not IDs.
151
151
  The `v1` branch is the default branch and always exists. It cannot be created, edited, or deleted.
152
152
 
153
153
  ```bash
154
- # List branches
154
+ # List branches (backup branches are hidden by default)
155
155
  xano branch list
156
156
  xano branch list -w <workspace_id>
157
+ xano branch list --backups # include backup branches
157
158
 
158
159
  # Get branch details
159
160
  xano branch get <branch_label>
@@ -481,8 +482,6 @@ xano sandbox push --records --env # Include records and e
481
482
  xano sandbox push --truncate # Truncate tables before import
482
483
  xano sandbox push --no-guids # Skip writing GUIDs back to local files
483
484
  xano sandbox push --force # Skip preview and confirmation
484
- xano sandbox push -i "function/*" # Push only matching files
485
- xano sandbox push -e "table/*" # Push all files except tables
486
485
  xano sandbox push --review # Push and open sandbox review in the browser
487
486
 
488
487
  # Review (open in browser)
@@ -1,8 +1,16 @@
1
1
  import BaseCommand from '../../../base-command.js';
2
+ export interface Branch {
3
+ backup: boolean;
4
+ created_at: string;
5
+ label: string;
6
+ live: boolean;
7
+ }
8
+ export declare function filterBackups(branches: Branch[], includeBackups: boolean): Branch[];
2
9
  export default class BranchList extends BaseCommand {
3
10
  static description: string;
4
11
  static examples: string[];
5
12
  static flags: {
13
+ backups: import("@oclif/core/interfaces").BooleanFlag<boolean>;
6
14
  output: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
7
15
  workspace: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
8
16
  config: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
@@ -2,6 +2,9 @@ import { Flags } from '@oclif/core';
2
2
  import * as yaml from 'js-yaml';
3
3
  import * as fs from 'node:fs';
4
4
  import BaseCommand from '../../../base-command.js';
5
+ export function filterBackups(branches, includeBackups) {
6
+ return includeBackups ? branches : branches.filter((b) => !b.backup);
7
+ }
5
8
  export default class BranchList extends BaseCommand {
6
9
  static description = 'List all branches in a workspace';
7
10
  static examples = [
@@ -15,6 +18,12 @@ Available branches:
15
18
  Available branches:
16
19
  - v1 (live)
17
20
  - feature-auth
21
+ `,
22
+ `$ xano branch list --backups
23
+ Available branches:
24
+ - v1 (live)
25
+ - dev
26
+ - backup_2024_01_15 (backup)
18
27
  `,
19
28
  `$ xano branch list --output json
20
29
  [
@@ -29,6 +38,11 @@ Available branches:
29
38
  ];
30
39
  static flags = {
31
40
  ...BaseCommand.baseFlags,
41
+ backups: Flags.boolean({
42
+ default: false,
43
+ description: 'Include backup branches in the output',
44
+ required: false,
45
+ }),
32
46
  output: Flags.string({
33
47
  char: 'o',
34
48
  default: 'summary',
@@ -82,7 +96,8 @@ Available branches:
82
96
  const errorText = await response.text();
83
97
  this.error(`API request failed with status ${response.status}: ${response.statusText}\n${errorText}`);
84
98
  }
85
- const branches = await response.json();
99
+ const allBranches = await response.json();
100
+ const branches = filterBackups(allBranches, flags.backups);
86
101
  // Output results
87
102
  if (flags.output === 'json') {
88
103
  this.log(JSON.stringify(branches, null, 2));
@@ -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' ||