@xano/cli 1.0.2 → 1.0.3-beta.5

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
@@ -522,14 +522,46 @@ xano sandbox reset --force
522
522
  # List static hosts
523
523
  xano static_host list
524
524
 
525
- # Create a build
526
- xano static_host build create default -f ./build.zip -n "v1.0.0"
525
+ # Create / get / edit a static host
526
+ xano static_host create marketing --description "Marketing site"
527
+ xano static_host get marketing
528
+ xano static_host edit marketing --name marketing-v2 --description "Updated"
527
529
 
528
530
  # List builds
529
531
  xano static_host build list default
530
532
 
531
533
  # Get build details
532
- xano static_host build get default 52
534
+ xano static_host build get default --build_id 52
535
+
536
+ # Pull a build to disk. Defaults to the original uploaded source
537
+ # (including package.json). Use --source built for the compiled/served output.
538
+ xano static_host build pull default --build_id 52 # By build ID (original source)
539
+ xano static_host build pull default --build_id 52 --source built # Compiled output
540
+ xano static_host build pull default --latest # Latest build
541
+ xano static_host build pull default --env dev # Build currently deployed to dev
542
+ xano static_host build pull default --env prod -d ./prod-release
543
+
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.
546
+ # For package.json builds, the CLI waits for the build to finish (--no-wait to skip).
547
+ xano static_host build push default -d ./dist -n "v1.0.0"
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
550
+ xano static_host build push default -n "release" --description "Production build"
551
+
552
+ # Delete a build (prompts for confirmation; --force to skip)
553
+ xano static_host build delete default --build_id 52
554
+ xano static_host build delete default --build_id 52 --force
555
+
556
+ # Deploy a build to an environment
557
+ xano static_host deploy default --build_id 52 --env dev
558
+ xano static_host deploy default --build_id 52 --env prod
559
+
560
+ # Migrate a host to instance-managed (v2) hosting
561
+ xano static_host migrate newsite # one host (both envs)
562
+ xano static_host migrate newsite --env dev # one env
563
+ xano static_host migrate --all # every v1 host in the workspace
564
+ xano static_host migrate --all --dry-run # preview without changing anything
533
565
  ```
534
566
 
535
567
  ## Global Options
@@ -103,4 +103,31 @@ export default abstract class BaseCommand extends Command {
103
103
  * Use this for all Metadata API calls to support the --verbose flag.
104
104
  */
105
105
  protected verboseFetch(url: string, options: RequestInit, verbose: boolean, authToken?: string): Promise<Response>;
106
+ /**
107
+ * Poll a static-host build until it reaches a terminal status (ok | error),
108
+ * showing a live, ticking spinner with the current stage and elapsed time —
109
+ * mirroring the UI's build progress for async (package.json) builds, which
110
+ * keep running after the upload returns.
111
+ *
112
+ * On a TTY it renders an animated spinner via ux.action; when quiet (JSON
113
+ * output) or non-interactive it falls back to plain one-line status updates.
114
+ *
115
+ * Returns the final status. Resolves to the last-seen status on timeout.
116
+ */
117
+ protected logStaticHostUrls(opts: {
118
+ profile: ProfileConfig;
119
+ staticHost: string;
120
+ verbose: boolean;
121
+ workspaceId: string;
122
+ }): Promise<void>;
123
+ protected waitForBuild(opts: {
124
+ buildId: number | string;
125
+ intervalMs?: number;
126
+ profile: ProfileConfig;
127
+ quiet?: boolean;
128
+ staticHost: string;
129
+ timeoutMs?: number;
130
+ verbose: boolean;
131
+ workspaceId: string;
132
+ }): Promise<string>;
106
133
  }
@@ -1,4 +1,4 @@
1
- import { Command, Flags } from '@oclif/core';
1
+ import { Command, Flags, ux } from '@oclif/core';
2
2
  import * as yaml from 'js-yaml';
3
3
  import * as fs from 'node:fs';
4
4
  import * as os from 'node:os';
@@ -314,4 +314,127 @@ export default class BaseCommand extends Command {
314
314
  }
315
315
  return response;
316
316
  }
317
+ /**
318
+ * Poll a static-host build until it reaches a terminal status (ok | error),
319
+ * showing a live, ticking spinner with the current stage and elapsed time —
320
+ * mirroring the UI's build progress for async (package.json) builds, which
321
+ * keep running after the upload returns.
322
+ *
323
+ * On a TTY it renders an animated spinner via ux.action; when quiet (JSON
324
+ * output) or non-interactive it falls back to plain one-line status updates.
325
+ *
326
+ * Returns the final status. Resolves to the last-seen status on timeout.
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
+ }
352
+ async waitForBuild(opts) {
353
+ const { buildId, profile, quiet, staticHost, verbose, workspaceId } = opts;
354
+ const intervalMs = opts.intervalMs ?? 2000;
355
+ const timeoutMs = opts.timeoutMs ?? 600_000; // 10 min
356
+ const terminal = new Set(['error', 'ok']);
357
+ const url = `${profile.instance_origin}/api:meta/workspace/${workspaceId}/static_host/${staticHost}/build/${buildId}`;
358
+ // Spinner only on an interactive TTY and when not emitting JSON. Verbose mode
359
+ // also disables it (the spinner would interleave with request/response logs).
360
+ const animate = Boolean(process.stdout.isTTY) && !quiet && !verbose;
361
+ const start = Date.now();
362
+ const elapsed = () => Math.round((Date.now() - start) / 1000);
363
+ let stage = 'pending';
364
+ let ticker;
365
+ // Reflect the current stage: live spinner status on a TTY, else a plain line.
366
+ const render = () => {
367
+ if (animate) {
368
+ ux.action.status = `${stageLabel(stage)} (${elapsed()}s)`;
369
+ }
370
+ else if (!quiet && !terminal.has(stage)) {
371
+ this.log(`Build status: ${stage}`);
372
+ }
373
+ };
374
+ // Stop the spinner/ticker and emit a final line.
375
+ const conclude = (message) => {
376
+ if (ticker)
377
+ clearInterval(ticker);
378
+ if (animate) {
379
+ ux.action.stop(message);
380
+ }
381
+ else if (!quiet) {
382
+ this.log(message);
383
+ }
384
+ };
385
+ if (animate) {
386
+ ux.action.start('Building', stageLabel(stage));
387
+ // Re-render every 120ms so the elapsed counter ticks even between polls.
388
+ ticker = setInterval(render, 120);
389
+ }
390
+ else if (!quiet) {
391
+ this.log(`Build status: ${stage}`);
392
+ }
393
+ /* eslint-disable no-await-in-loop */
394
+ while (Date.now() - start < timeoutMs) {
395
+ const response = await this.verboseFetch(url, {
396
+ headers: { accept: 'application/json', Authorization: `Bearer ${profile.access_token}` },
397
+ method: 'GET',
398
+ }, verbose, profile.access_token);
399
+ if (response.ok) {
400
+ const build = (await response.json());
401
+ const status = build.status ?? 'pending';
402
+ if (status !== stage) {
403
+ stage = status;
404
+ render();
405
+ }
406
+ if (terminal.has(status)) {
407
+ const took = `${elapsed()}s`;
408
+ conclude(status === 'ok' ? `done in ${took}` : `failed after ${took}`);
409
+ return status;
410
+ }
411
+ }
412
+ await new Promise((resolve) => {
413
+ setTimeout(resolve, intervalMs);
414
+ });
415
+ }
416
+ /* eslint-enable no-await-in-loop */
417
+ conclude(`stopped waiting after ${Math.round(timeoutMs / 1000)}s (last status: ${stage || 'unknown'})`);
418
+ return stage;
419
+ }
420
+ }
421
+ /** Human-friendly label for a build status stage. */
422
+ function stageLabel(status) {
423
+ switch (status) {
424
+ case 'building': {
425
+ return 'Installing & building (npm)';
426
+ }
427
+ case 'ok': {
428
+ return 'Finishing';
429
+ }
430
+ case 'pending': {
431
+ return 'Queued';
432
+ }
433
+ case 'publishing': {
434
+ return 'Publishing files';
435
+ }
436
+ default: {
437
+ return status;
438
+ }
439
+ }
317
440
  }
@@ -1,5 +1,12 @@
1
1
  import BaseCommand from '../../../../base-command.js';
2
+ /**
3
+ * Generate a default build name from a compact timestamp: `YYYYMMDD-HHmmss`
4
+ * (e.g. `20260531-143022`). Sortable, distinct down to the second, and uses
5
+ * local time so it lines up with when the user ran the command.
6
+ */
7
+ export declare function generateBuildName(date?: Date): string;
2
8
  export default class StaticHostBuildCreate extends BaseCommand {
9
+ static hidden: boolean;
3
10
  static args: {
4
11
  static_host: import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
5
12
  };
@@ -8,7 +15,8 @@ export default class StaticHostBuildCreate extends BaseCommand {
8
15
  static flags: {
9
16
  description: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
10
17
  file: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
11
- name: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
18
+ name: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
19
+ 'no-wait': import("@oclif/core/interfaces").BooleanFlag<boolean>;
12
20
  output: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
13
21
  workspace: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
14
22
  config: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
@@ -2,20 +2,42 @@ import { Args, Flags } from '@oclif/core';
2
2
  import * as fs from 'node:fs';
3
3
  import * as path from 'node:path';
4
4
  import BaseCommand from '../../../../base-command.js';
5
+ const pad2 = (n) => String(n).padStart(2, '0');
6
+ /**
7
+ * Generate a default build name from a compact timestamp: `YYYYMMDD-HHmmss`
8
+ * (e.g. `20260531-143022`). Sortable, distinct down to the second, and uses
9
+ * local time so it lines up with when the user ran the command.
10
+ */
11
+ export function generateBuildName(date = new Date()) {
12
+ const y = date.getFullYear();
13
+ const mo = pad2(date.getMonth() + 1);
14
+ const d = pad2(date.getDate());
15
+ const h = pad2(date.getHours());
16
+ const mi = pad2(date.getMinutes());
17
+ const s = pad2(date.getSeconds());
18
+ return `${y}${mo}${d}-${h}${mi}${s}`;
19
+ }
5
20
  export default class StaticHostBuildCreate extends BaseCommand {
21
+ static hidden = true;
6
22
  static args = {
7
23
  static_host: Args.string({
8
24
  description: 'Static Host name',
9
25
  required: true,
10
26
  }),
11
27
  };
12
- 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';
13
29
  static examples = [
14
30
  `$ xano static_host:build:create default -f ./build.zip -n "v1.0.0"
15
31
  Build created successfully!
16
32
  ID: 123
17
33
  Name: v1.0.0
18
34
  Status: pending
35
+ `,
36
+ `$ xano static_host:build:create default -f ./build.zip
37
+ Build created successfully!
38
+ ID: 123
39
+ Name: 20260531-143022
40
+ Status: pending
19
41
  `,
20
42
  `$ xano static_host:build:create default -w 40 -f ./dist.zip -n "production" -d "Production build"
21
43
  Build created successfully!
@@ -45,8 +67,13 @@ Description: Production build
45
67
  }),
46
68
  name: Flags.string({
47
69
  char: 'n',
48
- description: 'Build name',
49
- required: true,
70
+ description: 'Build name (auto-generated from the current timestamp if omitted)',
71
+ required: false,
72
+ }),
73
+ 'no-wait': Flags.boolean({
74
+ default: false,
75
+ description: 'Return immediately after upload instead of waiting for the build to finish',
76
+ required: false,
50
77
  }),
51
78
  output: Flags.string({
52
79
  char: 'o',
@@ -62,6 +89,7 @@ Description: Production build
62
89
  }),
63
90
  };
64
91
  async run() {
92
+ this.warn('`static_host build create` is deprecated. Use `static_host build push -f <file>` instead.');
65
93
  const { args, flags } = await this.parse(StaticHostBuildCreate);
66
94
  const { profile, profileName } = this.resolveProfile(flags);
67
95
  // Determine workspace_id from flag or profile
@@ -102,7 +130,10 @@ Description: Production build
102
130
  const fileBuffer = fs.readFileSync(filePath);
103
131
  const blob = new Blob([fileBuffer], { type: 'application/zip' });
104
132
  formData.append('file', blob, path.basename(filePath));
105
- formData.append('name', flags.name);
133
+ // Name is optional — fall back to a timestamped name so builds can be
134
+ // created without thinking up a label each time.
135
+ const buildName = flags.name ?? generateBuildName();
136
+ formData.append('name', buildName);
106
137
  if (flags.description) {
107
138
  formData.append('description', flags.description);
108
139
  }
@@ -141,6 +172,25 @@ Description: Production build
141
172
  this.log(`Description: ${flags.description}`);
142
173
  }
143
174
  }
175
+ // Async (package.json) builds keep running after upload. Unless --no-wait,
176
+ // poll until the build finishes so the CLI mirrors the UI's progress.
177
+ const inProgress = result.status !== undefined && !['error', 'ok'].includes(result.status);
178
+ if (inProgress && !flags['no-wait']) {
179
+ const finalStatus = await this.waitForBuild({
180
+ buildId: result.id,
181
+ profile,
182
+ quiet: flags.output === 'json',
183
+ staticHost: args.static_host,
184
+ verbose: flags.verbose,
185
+ workspaceId,
186
+ });
187
+ if (finalStatus === 'error') {
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}`);
189
+ }
190
+ }
191
+ if (flags.output !== 'json') {
192
+ await this.logStaticHostUrls({ profile, staticHost: args.static_host, verbose: flags.verbose, workspaceId });
193
+ }
144
194
  }
145
195
  catch (error) {
146
196
  if (error instanceof Error) {
@@ -0,0 +1,19 @@
1
+ import BaseCommand from '../../../../base-command.js';
2
+ export default class StaticHostBuildDelete extends BaseCommand {
3
+ static args: {
4
+ static_host: import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
5
+ };
6
+ static description: string;
7
+ static examples: string[];
8
+ static flags: {
9
+ build_id: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
10
+ force: import("@oclif/core/interfaces").BooleanFlag<boolean>;
11
+ output: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
12
+ workspace: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
13
+ config: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
14
+ profile: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
15
+ verbose: import("@oclif/core/interfaces").BooleanFlag<boolean>;
16
+ };
17
+ run(): Promise<void>;
18
+ private confirm;
19
+ }
@@ -0,0 +1,114 @@
1
+ import { Args, Flags } from '@oclif/core';
2
+ import BaseCommand from '../../../../base-command.js';
3
+ export default class StaticHostBuildDelete extends BaseCommand {
4
+ static args = {
5
+ static_host: Args.string({
6
+ description: 'Static Host name',
7
+ required: true,
8
+ }),
9
+ };
10
+ static description = 'Delete a static host build permanently. This action cannot be undone.';
11
+ static examples = [
12
+ `$ xano static_host build delete default --build_id 52
13
+ Are you sure you want to delete build 52 from static host 'default'? This action cannot be undone. (y/N) y
14
+ Deleted build 52 from static host 'default'
15
+ `,
16
+ `$ xano static_host build delete default --build_id 52 --force
17
+ Deleted build 52 from static host 'default'
18
+ `,
19
+ `$ xano static_host build delete myhost --build_id 123 -w 40 -f
20
+ Deleted build 123 from static host 'myhost'
21
+ `,
22
+ `$ xano static_host build delete default --build_id 52 -f -o json`,
23
+ ];
24
+ static flags = {
25
+ ...BaseCommand.baseFlags,
26
+ build_id: Flags.string({
27
+ description: 'Build ID to delete',
28
+ required: true,
29
+ }),
30
+ force: Flags.boolean({
31
+ char: 'f',
32
+ default: false,
33
+ description: '[CRITICAL] NEVER run without explicit user confirmation. Skips the confirmation prompt.',
34
+ required: false,
35
+ }),
36
+ output: Flags.string({
37
+ char: 'o',
38
+ default: 'summary',
39
+ description: 'Output format',
40
+ options: ['summary', 'json'],
41
+ required: false,
42
+ }),
43
+ workspace: Flags.string({
44
+ char: 'w',
45
+ description: 'Workspace ID (optional if set in profile)',
46
+ required: false,
47
+ }),
48
+ };
49
+ async run() {
50
+ const { args, flags } = await this.parse(StaticHostBuildDelete);
51
+ const { profile, profileName } = this.resolveProfile(flags);
52
+ // Determine workspace_id from flag or profile
53
+ let workspaceId;
54
+ if (flags.workspace) {
55
+ workspaceId = flags.workspace;
56
+ }
57
+ else if (profile.workspace) {
58
+ workspaceId = profile.workspace;
59
+ }
60
+ else {
61
+ this.error(`Workspace ID is required. Either:\n` +
62
+ ` 1. Provide it as a flag: xano static_host build delete <static_host> --build_id <id> -w <workspace_id>\n` +
63
+ ` 2. Set it in your profile using: xano profile edit ${profileName} -w <workspace_id>`);
64
+ }
65
+ if (!flags.force) {
66
+ const confirmed = await this.confirm(`Are you sure you want to delete build ${flags.build_id} from static host '${args.static_host}'? This action cannot be undone.`);
67
+ if (!confirmed) {
68
+ this.log('Deletion cancelled.');
69
+ return;
70
+ }
71
+ }
72
+ const apiUrl = `${profile.instance_origin}/api:meta/workspace/${workspaceId}/static_host/${args.static_host}/build/${flags.build_id}`;
73
+ try {
74
+ const response = await this.verboseFetch(apiUrl, {
75
+ headers: {
76
+ 'accept': 'application/json',
77
+ 'Authorization': `Bearer ${profile.access_token}`,
78
+ },
79
+ method: 'DELETE',
80
+ }, flags.verbose, profile.access_token);
81
+ if (!response.ok) {
82
+ const errorText = await response.text();
83
+ this.error(`API request failed with status ${response.status}: ${response.statusText}\n${errorText}`);
84
+ }
85
+ if (flags.output === 'json') {
86
+ this.log(JSON.stringify({ build_id: flags.build_id, deleted: true, static_host: args.static_host }, null, 2));
87
+ }
88
+ else {
89
+ this.log(`Deleted build ${flags.build_id} from static host '${args.static_host}'`);
90
+ }
91
+ }
92
+ catch (error) {
93
+ if (error instanceof Error) {
94
+ this.error(`Failed to delete build: ${error.message}`);
95
+ }
96
+ else {
97
+ this.error(`Failed to delete build: ${String(error)}`);
98
+ }
99
+ }
100
+ }
101
+ async confirm(message) {
102
+ const readline = await import('node:readline');
103
+ const rl = readline.createInterface({
104
+ input: process.stdin,
105
+ output: process.stdout,
106
+ });
107
+ return new Promise((resolve) => {
108
+ rl.question(`${message} (y/N) `, (answer) => {
109
+ rl.close();
110
+ resolve(answer.toLowerCase() === 'y' || answer.toLowerCase() === 'yes');
111
+ });
112
+ });
113
+ }
114
+ }
@@ -1,12 +1,12 @@
1
1
  import BaseCommand from '../../../../base-command.js';
2
2
  export default class StaticHostBuildGet extends BaseCommand {
3
3
  static args: {
4
- build_id: import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
5
4
  static_host: import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
6
5
  };
7
6
  static description: string;
8
7
  static examples: string[];
9
8
  static flags: {
9
+ build_id: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
10
10
  output: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
11
11
  workspace: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
12
12
  config: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
@@ -2,10 +2,6 @@ import { Args, Flags } from '@oclif/core';
2
2
  import BaseCommand from '../../../../base-command.js';
3
3
  export default class StaticHostBuildGet extends BaseCommand {
4
4
  static args = {
5
- build_id: Args.string({
6
- description: 'Build ID',
7
- required: true,
8
- }),
9
5
  static_host: Args.string({
10
6
  description: 'Static Host name',
11
7
  required: true,
@@ -13,24 +9,24 @@ export default class StaticHostBuildGet extends BaseCommand {
13
9
  };
14
10
  static description = 'Get details of a specific build for a static host';
15
11
  static examples = [
16
- `$ xano static_host:build:get default 52
12
+ `$ xano static_host:build:get default --build_id 52
17
13
  Build Details:
18
14
  ID: 52
19
15
  Name: v1.0.0
20
16
  Status: completed
21
17
  `,
22
- `$ xano static_host:build:get default 52 -w 40
18
+ `$ xano static_host:build:get default --build_id 52 -w 40
23
19
  Build Details:
24
20
  ID: 52
25
21
  Name: v1.0.0
26
22
  Status: completed
27
23
  `,
28
- `$ xano static_host:build:get myhost 123 --profile production
24
+ `$ xano static_host:build:get myhost --build_id 123 --profile production
29
25
  Build Details:
30
26
  ID: 123
31
27
  Name: production-build
32
28
  `,
33
- `$ xano static_host:build:get default 52 -o json
29
+ `$ xano static_host:build:get default --build_id 52 -o json
34
30
  {
35
31
  "id": 52,
36
32
  "name": "v1.0.0",
@@ -40,6 +36,10 @@ Name: production-build
40
36
  ];
41
37
  static flags = {
42
38
  ...BaseCommand.baseFlags,
39
+ build_id: Flags.string({
40
+ description: 'Build ID',
41
+ required: true,
42
+ }),
43
43
  output: Flags.string({
44
44
  char: 'o',
45
45
  default: 'summary',
@@ -66,11 +66,11 @@ Name: production-build
66
66
  }
67
67
  else {
68
68
  this.error(`Workspace ID is required. Either:\n` +
69
- ` 1. Provide it as a flag: xano static_host:build:get <static_host> <build_id> -w <workspace_id>\n` +
69
+ ` 1. Provide it as a flag: xano static_host:build:get <static_host> --build_id <id> -w <workspace_id>\n` +
70
70
  ` 2. Set it in your profile using: xano profile:edit ${profileName} -w <workspace_id>`);
71
71
  }
72
72
  // Construct the API URL
73
- const apiUrl = `${profile.instance_origin}/api:meta/workspace/${workspaceId}/static_host/${args.static_host}/build/${args.build_id}`;
73
+ const apiUrl = `${profile.instance_origin}/api:meta/workspace/${workspaceId}/static_host/${args.static_host}/build/${flags.build_id}`;
74
74
  // Fetch build from the API
75
75
  try {
76
76
  const response = await this.verboseFetch(apiUrl, {
@@ -104,6 +104,12 @@ Name: production-build
104
104
  if (build.status) {
105
105
  this.log(`Status: ${build.status}`);
106
106
  }
107
+ if (typeof build.file_count === 'number') {
108
+ this.log(`Files: ${build.file_count}`);
109
+ }
110
+ if (typeof build.file_bytes === 'number') {
111
+ this.log(`Size: ${build.file_bytes} bytes`);
112
+ }
107
113
  if (build.created_at) {
108
114
  this.log(`Created: ${build.created_at}`);
109
115
  }
@@ -0,0 +1,52 @@
1
+ import BaseCommand from '../../../../base-command.js';
2
+ export interface StaticHostEnv {
3
+ canonical?: null | string;
4
+ }
5
+ export interface StaticHostSummary {
6
+ [k: string]: unknown;
7
+ dev?: StaticHostEnv;
8
+ name: string;
9
+ prod?: StaticHostEnv;
10
+ }
11
+ export interface BuildSummary {
12
+ canonical?: string;
13
+ id: number;
14
+ }
15
+ /**
16
+ * Find the deployed `canonical` for a static host's env from a list of hosts.
17
+ * Returns null when the host isn't present or nothing is deployed to that env.
18
+ */
19
+ export declare function extractEnvCanonical(hosts: StaticHostSummary[], staticHost: string, env: string): null | string;
20
+ /** Find the build whose unique `canonical` matches, or null if none do. */
21
+ export declare function findBuildByCanonical(builds: BuildSummary[], canonical: string): BuildSummary | null;
22
+ export default class StaticHostBuildPull extends BaseCommand {
23
+ static args: {
24
+ static_host: import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
25
+ };
26
+ static description: string;
27
+ static examples: string[];
28
+ static flags: {
29
+ build_id: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
30
+ directory: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
31
+ env: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
32
+ latest: import("@oclif/core/interfaces").BooleanFlag<boolean>;
33
+ source: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
34
+ workspace: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
35
+ config: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
36
+ profile: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
37
+ verbose: import("@oclif/core/interfaces").BooleanFlag<boolean>;
38
+ };
39
+ run(): Promise<void>;
40
+ private parseFileDocument;
41
+ /**
42
+ * Resolve the build ID currently deployed to a static host's dev/prod env.
43
+ *
44
+ * The deployed env stores the `canonical` of the build it was created from
45
+ * (static_host.{env}.canonical). Each build carries that same unique
46
+ * `canonical`, so we match the env's canonical against the build list to
47
+ * recover the build ID — a shortcut for "list builds, find the deployed one,
48
+ * then pull it".
49
+ */
50
+ private resolveEnvBuild;
51
+ private resolveLatestBuild;
52
+ }