@xano/cli 1.0.3-beta.3 → 1.0.3-beta.6

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
@@ -527,11 +527,6 @@ xano static_host create marketing --description "Marketing site"
527
527
  xano static_host get marketing
528
528
  xano static_host edit marketing --name marketing-v2 --description "Updated"
529
529
 
530
- # Create a build (name optional — auto-generated from the timestamp if omitted).
531
- # For package.json builds, the CLI waits for the build to finish (--no-wait to skip).
532
- xano static_host build create default -f ./build.zip -n "v1.0.0"
533
- xano static_host build create default -f ./build.zip # name: 20260531-143022
534
-
535
530
  # List builds
536
531
  xano static_host build list default
537
532
 
@@ -546,10 +541,12 @@ xano static_host build pull default --latest # Latest build
546
541
  xano static_host build pull default --env dev # Build currently deployed to dev
547
542
  xano static_host build pull default --env prod -d ./prod-release
548
543
 
549
- # Push a directory as a new build (name optional — auto-generated if omitted).
544
+ # Push a build (name optional — auto-generated from the timestamp if omitted).
545
+ # Accepts a directory (-d) or a zip file (-f). Defaults to the current directory.
550
546
  # For package.json builds, the CLI waits for the build to finish (--no-wait to skip).
551
547
  xano static_host build push default -d ./dist -n "v1.0.0"
552
548
  xano static_host build push default # current dir, auto-name
549
+ xano static_host build push default -f ./build.zip -n "v1.0.0" # from zip file
553
550
  xano static_host build push default -n "release" --description "Production build"
554
551
 
555
552
  # Delete a build (prompts for confirmation; --force to skip)
@@ -114,6 +114,12 @@ export default abstract class BaseCommand extends Command {
114
114
  *
115
115
  * Returns the final status. Resolves to the last-seen status on timeout.
116
116
  */
117
+ protected logStaticHostUrls(opts: {
118
+ profile: ProfileConfig;
119
+ staticHost: string;
120
+ verbose: boolean;
121
+ workspaceId: string;
122
+ }): Promise<void>;
117
123
  protected waitForBuild(opts: {
118
124
  buildId: number | string;
119
125
  intervalMs?: number;
@@ -325,6 +325,30 @@ export default class BaseCommand extends Command {
325
325
  *
326
326
  * Returns the final status. Resolves to the last-seen status on timeout.
327
327
  */
328
+ async logStaticHostUrls(opts) {
329
+ const { profile, staticHost, verbose, workspaceId } = opts;
330
+ const url = `${profile.instance_origin}/api:meta/workspace/${workspaceId}/static_host/${staticHost}`;
331
+ try {
332
+ const response = await this.verboseFetch(url, {
333
+ headers: { accept: 'application/json', Authorization: `Bearer ${profile.access_token}` },
334
+ method: 'GET',
335
+ }, verbose, profile.access_token);
336
+ if (!response.ok)
337
+ return;
338
+ const host = (await response.json());
339
+ if (host.dev?.default_url)
340
+ this.log(`Dev URL: ${host.dev.default_url}`);
341
+ if (host.dev?.custom_url)
342
+ this.log(`Dev Custom URL: ${host.dev.custom_url}`);
343
+ if (host.prod?.default_url)
344
+ this.log(`Prod URL: ${host.prod.default_url}`);
345
+ if (host.prod?.custom_url)
346
+ this.log(`Prod Custom URL: ${host.prod.custom_url}`);
347
+ }
348
+ catch {
349
+ // Non-fatal — the build succeeded, we just can't show the URL.
350
+ }
351
+ }
328
352
  async waitForBuild(opts) {
329
353
  const { buildId, profile, quiet, staticHost, verbose, workspaceId } = opts;
330
354
  const intervalMs = opts.intervalMs ?? 2000;
@@ -6,6 +6,7 @@ import BaseCommand from '../../../../base-command.js';
6
6
  */
7
7
  export declare function generateBuildName(date?: Date): string;
8
8
  export default class StaticHostBuildCreate extends BaseCommand {
9
+ static hidden: boolean;
9
10
  static args: {
10
11
  static_host: import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
11
12
  };
@@ -18,13 +18,14 @@ export function generateBuildName(date = new Date()) {
18
18
  return `${y}${mo}${d}-${h}${mi}${s}`;
19
19
  }
20
20
  export default class StaticHostBuildCreate extends BaseCommand {
21
+ static hidden = true;
21
22
  static args = {
22
23
  static_host: Args.string({
23
24
  description: 'Static Host name',
24
25
  required: true,
25
26
  }),
26
27
  };
27
- static description = 'Create a new build for a static host';
28
+ static description = '[Deprecated: use "static_host build push -f <file>" instead] Create a new build from a zip file';
28
29
  static examples = [
29
30
  `$ xano static_host:build:create default -f ./build.zip -n "v1.0.0"
30
31
  Build created successfully!
@@ -88,6 +89,7 @@ Description: Production build
88
89
  }),
89
90
  };
90
91
  async run() {
92
+ this.warn('`static_host build create` is deprecated. Use `static_host build push -f <file>` instead.');
91
93
  const { args, flags } = await this.parse(StaticHostBuildCreate);
92
94
  const { profile, profileName } = this.resolveProfile(flags);
93
95
  // Determine workspace_id from flag or profile
@@ -186,6 +188,9 @@ Description: Production build
186
188
  this.error(`Build ${result.id} failed (status: error). Check the build log with: xano static_host build get ${args.static_host} --build_id ${result.id}`);
187
189
  }
188
190
  }
191
+ if (flags.output !== 'json') {
192
+ await this.logStaticHostUrls({ profile, staticHost: args.static_host, verbose: flags.verbose, workspaceId });
193
+ }
189
194
  }
190
195
  catch (error) {
191
196
  if (error instanceof Error) {
@@ -7,7 +7,8 @@ export default class StaticHostBuildPush extends BaseCommand {
7
7
  static examples: string[];
8
8
  static flags: {
9
9
  description: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
10
- directory: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
10
+ directory: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
11
+ file: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
11
12
  name: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
12
13
  'no-wait': import("@oclif/core/interfaces").BooleanFlag<boolean>;
13
14
  output: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
@@ -11,23 +11,23 @@ export default class StaticHostBuildPush extends BaseCommand {
11
11
  required: true,
12
12
  }),
13
13
  };
14
- static description = 'Push a directory as a new static host build';
14
+ static description = 'Push a directory or zip file as a new static host build';
15
15
  static examples = [
16
- `$ xano static_host build push default -n "v1.0.0" -d ./dist
16
+ `$ xano static_host build push default -d ./dist -n "v1.0.0"
17
17
  Pushed 15 files as build "v1.0.0"
18
18
  ID: 123
19
- `,
20
- `$ xano static_host build push default -n "v1.0.0"
21
- Pushed 8 files as build "v1.0.0" (from current directory)
22
19
  `,
23
20
  `$ xano static_host build push default
24
21
  Pushed 8 files as build "20260531-143022"
22
+ `,
23
+ `$ xano static_host build push default -f ./build.zip -n "v1.0.0"
24
+ Pushed build.zip as build "v1.0.0"
25
+ ID: 124
25
26
  `,
26
27
  `$ xano static_host build push myhost -n "production" --description "Production build" -w 40
27
28
  Pushed 22 files as build "production"
28
- ID: 124
29
+ ID: 125
29
30
  `,
30
- `$ xano static_host build push default -n "release-1.2" -d ./build -o json`,
31
31
  ];
32
32
  static flags = {
33
33
  ...BaseCommand.baseFlags,
@@ -37,8 +37,14 @@ ID: 124
37
37
  }),
38
38
  directory: Flags.string({
39
39
  char: 'd',
40
- default: '.',
41
40
  description: 'Directory to push (defaults to current directory)',
41
+ exclusive: ['file'],
42
+ required: false,
43
+ }),
44
+ file: Flags.string({
45
+ char: 'f',
46
+ description: 'Path to a zip file to upload (alternative to -d)',
47
+ exclusive: ['directory'],
42
48
  required: false,
43
49
  }),
44
50
  name: Flags.string({
@@ -79,32 +85,48 @@ ID: 124
79
85
  ` 1. Provide it as a flag: xano static_host build push <static_host> -n <name> -w <workspace_id>\n` +
80
86
  ` 2. Set it in your profile using: xano profile edit ${profileName} -w <workspace_id>`);
81
87
  }
82
- const sourceDir = path.resolve(flags.directory);
83
- if (!fs.existsSync(sourceDir)) {
84
- this.error(`Directory not found: ${sourceDir}`);
85
- }
86
- const stats = fs.statSync(sourceDir);
87
- if (!stats.isDirectory()) {
88
- this.error(`Path is not a directory: ${sourceDir}`);
89
- }
90
88
  const animate = Boolean(process.stdout.isTTY) && flags.output !== 'json' && !flags.verbose;
91
- const fileCount = this.countFiles(sourceDir);
92
- if (animate) {
93
- ux.action.start('Packaging', `${fileCount} files`);
89
+ const buildName = flags.name ?? generateBuildName();
90
+ let zipBuffer;
91
+ let fileCount = 0;
92
+ let fileName;
93
+ if (flags.file) {
94
+ const filePath = path.resolve(flags.file);
95
+ if (!fs.existsSync(filePath)) {
96
+ this.error(`File not found: ${filePath}`);
97
+ }
98
+ const fileStats = fs.statSync(filePath);
99
+ if (!fileStats.isFile()) {
100
+ this.error(`Path is not a file: ${filePath}`);
101
+ }
102
+ fileName = path.basename(filePath);
103
+ if (animate)
104
+ ux.action.start('Uploading', fileName);
105
+ zipBuffer = fs.readFileSync(filePath);
94
106
  }
95
- const zipBuffer = await this.createZipBuffer(sourceDir);
96
- const sizeMB = (zipBuffer.length / (1024 * 1024)).toFixed(1);
97
- if (animate) {
98
- ux.action.stop(`${fileCount} files (${sizeMB} MB)`);
99
- ux.action.start('Uploading');
107
+ else {
108
+ const sourceDir = path.resolve(flags.directory ?? '.');
109
+ if (!fs.existsSync(sourceDir)) {
110
+ this.error(`Directory not found: ${sourceDir}`);
111
+ }
112
+ const dirStats = fs.statSync(sourceDir);
113
+ if (!dirStats.isDirectory()) {
114
+ this.error(`Path is not a directory: ${sourceDir}`);
115
+ }
116
+ fileCount = this.countFiles(sourceDir);
117
+ if (animate)
118
+ ux.action.start('Packaging', `${fileCount} files`);
119
+ zipBuffer = await this.createZipBuffer(sourceDir);
120
+ if (animate) {
121
+ ux.action.stop(`${fileCount} files (${(zipBuffer.length / (1024 * 1024)).toFixed(1)} MB)`);
122
+ ux.action.start('Uploading');
123
+ }
100
124
  }
125
+ const sizeMB = (zipBuffer.length / (1024 * 1024)).toFixed(1);
101
126
  const apiUrl = `${profile.instance_origin}/api:meta/workspace/${workspaceId}/static_host/${args.static_host}/build`;
102
- // Name is optional — fall back to a timestamped name so builds can be
103
- // pushed without thinking up a label each time.
104
- const buildName = flags.name ?? generateBuildName();
105
127
  const formData = new globalThis.FormData();
106
128
  const blob = new Blob([new Uint8Array(zipBuffer)], { type: 'application/zip' });
107
- formData.append('file', blob, 'build.zip');
129
+ formData.append('file', blob, fileName ?? 'build.zip');
108
130
  formData.append('name', buildName);
109
131
  if (flags.description) {
110
132
  formData.append('description', flags.description);
@@ -133,7 +155,9 @@ ID: 124
133
155
  this.log(JSON.stringify(result, null, 2));
134
156
  }
135
157
  else {
136
- this.log(`Pushed ${fileCount} files as build "${buildName}" (${sizeMB} MB)`);
158
+ this.log(fileName
159
+ ? `Pushed ${fileName} as build "${buildName}" (${sizeMB} MB)`
160
+ : `Pushed ${fileCount} files as build "${buildName}" (${sizeMB} MB)`);
137
161
  this.log(`ID: ${result.id}`);
138
162
  if (result.status) {
139
163
  this.log(`Status: ${result.status}`);
@@ -155,6 +179,9 @@ ID: 124
155
179
  this.error(`Build ${result.id} failed (status: error). Check the build log with: xano static_host build get ${args.static_host} --build_id ${result.id}`);
156
180
  }
157
181
  }
182
+ if (flags.output !== 'json') {
183
+ await this.logStaticHostUrls({ profile, staticHost: args.static_host, verbose: flags.verbose, workspaceId });
184
+ }
158
185
  }
159
186
  catch (error) {
160
187
  if (error instanceof Error) {
@@ -476,6 +476,7 @@ export async function executePush(ctx, target, flags) {
476
476
  }
477
477
  // Warn when the sandbox currently holds a different workspace than the one being
478
478
  // pushed and the change set is large enough that stale state is a real risk.
479
+ let mismatchConfirmed = false;
479
480
  if (target.warnOnWorkspaceMismatch && preview.workspace_name) {
480
481
  const localWorkspaceName = findLocalWorkspaceName(documentEntries);
481
482
  const totalChanges = countSummaryChanges(preview.summary, shouldDelete);
@@ -494,29 +495,32 @@ export async function executePush(ctx, target, flags) {
494
495
  log('Push cancelled. Run `xano sandbox reset` then retry.');
495
496
  return;
496
497
  }
498
+ mismatchConfirmed = true;
497
499
  }
498
500
  else {
499
501
  command.error('Workspace mismatch detected in non-interactive mode. Run `xano sandbox reset` first to start clean.');
500
502
  }
501
503
  }
502
504
  }
503
- // Confirm with user
504
- const hasDestructive = preview.operations.some((op) => (shouldDelete && (op.action === 'delete' || op.action === 'cascade_delete')) ||
505
- op.action === 'truncate' ||
506
- op.action === 'drop_field' ||
507
- op.action === 'alter_field');
508
- const message = hasDestructive
509
- ? 'Proceed with push? This includes DESTRUCTIVE operations listed above.'
510
- : 'Proceed with push?';
511
- if (process.stdin.isTTY) {
512
- const confirmed = await confirm(message);
513
- if (!confirmed) {
514
- log('Push cancelled.');
515
- return;
505
+ // Confirm with user (skip if workspace mismatch prompt already obtained confirmation)
506
+ if (!mismatchConfirmed) {
507
+ const hasDestructive = preview.operations.some((op) => (shouldDelete && (op.action === 'delete' || op.action === 'cascade_delete')) ||
508
+ op.action === 'truncate' ||
509
+ op.action === 'drop_field' ||
510
+ op.action === 'alter_field');
511
+ const message = hasDestructive
512
+ ? 'Proceed with push? This includes DESTRUCTIVE operations listed above.'
513
+ : 'Proceed with push?';
514
+ if (process.stdin.isTTY) {
515
+ const confirmed = await confirm(message);
516
+ if (!confirmed) {
517
+ log('Push cancelled.');
518
+ return;
519
+ }
520
+ }
521
+ else {
522
+ command.error('Non-interactive environment detected. Use --force to skip confirmation.');
516
523
  }
517
- }
518
- else {
519
- command.error('Non-interactive environment detected. Use --force to skip confirmation.');
520
524
  }
521
525
  }
522
526
  else {