@xano/cli 1.0.3-beta.9 → 1.0.3

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.
Files changed (29) hide show
  1. package/README.md +3 -69
  2. package/dist/base-command.d.ts +0 -27
  3. package/dist/base-command.js +46 -125
  4. package/dist/commands/auth/index.d.ts +0 -10
  5. package/dist/commands/auth/index.js +9 -146
  6. package/dist/commands/static_host/build/create/index.d.ts +1 -9
  7. package/dist/commands/static_host/build/create/index.js +4 -54
  8. package/dist/commands/static_host/build/get/index.d.ts +1 -1
  9. package/dist/commands/static_host/build/get/index.js +10 -16
  10. package/dist/utils/multidoc-push.js +94 -27
  11. package/dist/utils/reference-checker.js +2 -2
  12. package/oclif.manifest.json +2421 -3382
  13. package/package.json +3 -4
  14. package/dist/commands/static_host/build/delete/index.d.ts +0 -19
  15. package/dist/commands/static_host/build/delete/index.js +0 -114
  16. package/dist/commands/static_host/build/pull/index.d.ts +0 -52
  17. package/dist/commands/static_host/build/pull/index.js +0 -300
  18. package/dist/commands/static_host/build/push/index.d.ts +0 -23
  19. package/dist/commands/static_host/build/push/index.js +0 -225
  20. package/dist/commands/static_host/create/index.d.ts +0 -17
  21. package/dist/commands/static_host/create/index.js +0 -86
  22. package/dist/commands/static_host/deploy/index.d.ts +0 -18
  23. package/dist/commands/static_host/deploy/index.js +0 -105
  24. package/dist/commands/static_host/edit/index.d.ts +0 -23
  25. package/dist/commands/static_host/edit/index.js +0 -151
  26. package/dist/commands/static_host/get/index.d.ts +0 -18
  27. package/dist/commands/static_host/get/index.js +0 -94
  28. package/dist/commands/static_host/migrate/index.d.ts +0 -44
  29. package/dist/commands/static_host/migrate/index.js +0 -205
@@ -2,42 +2,20 @@ 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
- }
20
5
  export default class StaticHostBuildCreate extends BaseCommand {
21
- static hidden = true;
22
6
  static args = {
23
7
  static_host: Args.string({
24
8
  description: 'Static Host name',
25
9
  required: true,
26
10
  }),
27
11
  };
28
- static description = '[Deprecated: use "static_host build push -f <file>" instead] Create a new build from a zip file';
12
+ static description = 'Create a new build for a static host';
29
13
  static examples = [
30
14
  `$ xano static_host:build:create default -f ./build.zip -n "v1.0.0"
31
15
  Build created successfully!
32
16
  ID: 123
33
17
  Name: v1.0.0
34
18
  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
41
19
  `,
42
20
  `$ xano static_host:build:create default -w 40 -f ./dist.zip -n "production" -d "Production build"
43
21
  Build created successfully!
@@ -67,13 +45,8 @@ Description: Production build
67
45
  }),
68
46
  name: Flags.string({
69
47
  char: 'n',
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,
48
+ description: 'Build name',
49
+ required: true,
77
50
  }),
78
51
  output: Flags.string({
79
52
  char: 'o',
@@ -89,7 +62,6 @@ Description: Production build
89
62
  }),
90
63
  };
91
64
  async run() {
92
- this.warn('`static_host build create` is deprecated. Use `static_host build push -f <file>` instead.');
93
65
  const { args, flags } = await this.parse(StaticHostBuildCreate);
94
66
  const { profile, profileName } = this.resolveProfile(flags);
95
67
  // Determine workspace_id from flag or profile
@@ -130,10 +102,7 @@ Description: Production build
130
102
  const fileBuffer = fs.readFileSync(filePath);
131
103
  const blob = new Blob([fileBuffer], { type: 'application/zip' });
132
104
  formData.append('file', blob, path.basename(filePath));
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);
105
+ formData.append('name', flags.name);
137
106
  if (flags.description) {
138
107
  formData.append('description', flags.description);
139
108
  }
@@ -172,25 +141,6 @@ Description: Production build
172
141
  this.log(`Description: ${flags.description}`);
173
142
  }
174
143
  }
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
- }
194
144
  }
195
145
  catch (error) {
196
146
  if (error instanceof Error) {
@@ -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>>;
4
5
  static_host: import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
5
6
  };
6
7
  static description: string;
7
8
  static examples: string[];
8
9
  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,6 +2,10 @@ 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
+ }),
5
9
  static_host: Args.string({
6
10
  description: 'Static Host name',
7
11
  required: true,
@@ -9,24 +13,24 @@ export default class StaticHostBuildGet extends BaseCommand {
9
13
  };
10
14
  static description = 'Get details of a specific build for a static host';
11
15
  static examples = [
12
- `$ xano static_host:build:get default --build_id 52
16
+ `$ xano static_host:build:get default 52
13
17
  Build Details:
14
18
  ID: 52
15
19
  Name: v1.0.0
16
20
  Status: completed
17
21
  `,
18
- `$ xano static_host:build:get default --build_id 52 -w 40
22
+ `$ xano static_host:build:get default 52 -w 40
19
23
  Build Details:
20
24
  ID: 52
21
25
  Name: v1.0.0
22
26
  Status: completed
23
27
  `,
24
- `$ xano static_host:build:get myhost --build_id 123 --profile production
28
+ `$ xano static_host:build:get myhost 123 --profile production
25
29
  Build Details:
26
30
  ID: 123
27
31
  Name: production-build
28
32
  `,
29
- `$ xano static_host:build:get default --build_id 52 -o json
33
+ `$ xano static_host:build:get default 52 -o json
30
34
  {
31
35
  "id": 52,
32
36
  "name": "v1.0.0",
@@ -36,10 +40,6 @@ Name: production-build
36
40
  ];
37
41
  static flags = {
38
42
  ...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 <id> -w <workspace_id>\n` +
69
+ ` 1. Provide it as a flag: xano static_host:build:get <static_host> <build_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/${flags.build_id}`;
73
+ const apiUrl = `${profile.instance_origin}/api:meta/workspace/${workspaceId}/static_host/${args.static_host}/build/${args.build_id}`;
74
74
  // Fetch build from the API
75
75
  try {
76
76
  const response = await this.verboseFetch(apiUrl, {
@@ -104,12 +104,6 @@ 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
- }
113
107
  if (build.created_at) {
114
108
  this.log(`Created: ${build.created_at}`);
115
109
  }
@@ -476,7 +476,6 @@ 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;
480
479
  if (target.warnOnWorkspaceMismatch && preview.workspace_name) {
481
480
  const localWorkspaceName = findLocalWorkspaceName(documentEntries);
482
481
  const totalChanges = countSummaryChanges(preview.summary, shouldDelete);
@@ -495,33 +494,30 @@ export async function executePush(ctx, target, flags) {
495
494
  log('Push cancelled. Run `xano sandbox reset` then retry.');
496
495
  return;
497
496
  }
498
- mismatchConfirmed = true;
499
497
  }
500
498
  else {
501
499
  command.error('Workspace mismatch detected in non-interactive mode. Run `xano sandbox reset` first to start clean.');
502
500
  }
503
501
  }
504
502
  }
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.');
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;
523
516
  }
524
517
  }
518
+ else {
519
+ command.error('Non-interactive environment detected. Use --force to skip confirmation.');
520
+ }
525
521
  }
526
522
  else {
527
523
  // Server returned unexpected response
@@ -593,7 +589,7 @@ export async function executePush(ctx, target, flags) {
593
589
  }
594
590
  catch {
595
591
  if (flags.verbose) {
596
- log(`Server response is not JSON; skipping GUID sync\n${responseText}`);
592
+ log('Server response is not JSON; skipping GUID sync');
597
593
  }
598
594
  }
599
595
  }
@@ -645,18 +641,89 @@ export async function executePush(ctx, target, flags) {
645
641
  catch (error) {
646
642
  if (error instanceof Error && 'oclif' in error)
647
643
  throw error;
648
- if (error instanceof Error) {
649
- command.error(`Failed to push multidoc: ${error.message}`);
650
- }
651
- else {
652
- command.error(`Failed to push multidoc: ${String(error)}`);
653
- }
644
+ const elapsedMs = Date.now() - startTime;
645
+ command.error(`Failed to push multidoc: ${describeNetworkError(error, apiUrl, elapsedMs)}`);
654
646
  }
655
647
  const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
656
648
  const pushedCount = multidoc.split('\n---\n').length;
657
649
  log(`Pushed ${pushedCount} documents to ${target.label} from ${relative(process.cwd(), inputDir) || inputDir} in ${elapsed}s`);
658
650
  }
659
651
  // ── Error Handlers ──────────────────────────────────────────────────────────
652
+ /**
653
+ * Turn a thrown fetch/network error into an actionable message.
654
+ *
655
+ * Node's native fetch throws a TypeError with the unhelpful message "fetch
656
+ * failed" for all transport-level failures (DNS, connection refused, TLS,
657
+ * resets, timeouts). The real reason lives in `error.cause` as a system error
658
+ * with a `code` (ECONNREFUSED, ENOTFOUND, ETIMEDOUT, etc.). This unwraps it so
659
+ * the user sees what actually went wrong and where.
660
+ *
661
+ * `elapsedMs` is appended so the user can see how long the request ran before
662
+ * failing — a failure landing near a round boundary (e.g. ~300s) is a strong
663
+ * signal of a server-side or proxy timeout rather than a local network blip.
664
+ */
665
+ function describeNetworkError(error, url, elapsedMs) {
666
+ if (!(error instanceof Error))
667
+ return String(error);
668
+ let host = url;
669
+ try {
670
+ host = new URL(url).host;
671
+ }
672
+ catch { }
673
+ // AbortSignal.timeout() fires our explicit request-timeout ceiling.
674
+ if (error.name === 'TimeoutError' || error.name === 'AbortError') {
675
+ return `request to ${host} exceeded the CLI timeout. Raise it with XANO_CLI_REQUEST_TIMEOUT_MS (ms; 0 disables), or split the push into smaller batches.${formatFailureDuration(elapsedMs)}`;
676
+ }
677
+ const { cause } = error;
678
+ const code = cause && typeof cause === 'object' && 'code' in cause ? String(cause.code) : undefined;
679
+ const causeMessage = cause instanceof Error ? cause.message : undefined;
680
+ const hints = {
681
+ ECONNREFUSED: `Connection refused by ${host}. The instance may be down or starting up.`,
682
+ ECONNRESET: `Connection to ${host} was reset. The request may have been too large or the server restarted mid-push.`,
683
+ ENOTFOUND: `Could not resolve host "${host}". Check the instance origin and your network/DNS.`,
684
+ ETIMEDOUT: `Connection to ${host} timed out. Check your network or VPN, then retry.`,
685
+ UND_ERR_CONNECT_TIMEOUT: `Connection to ${host} timed out. Check your network or VPN, then retry.`,
686
+ UND_ERR_HEADERS_TIMEOUT: `${host} accepted the connection but did not respond in time. The push may be too large; try splitting it or retrying.`,
687
+ };
688
+ let base;
689
+ if (code && hints[code]) {
690
+ base = `${hints[code]} (${code})`;
691
+ }
692
+ else if (code?.startsWith('ERR_TLS') || code?.startsWith('CERT_') || /certificate|tls|ssl/i.test(error.message)) {
693
+ // TLS/cert failures surface their reason on the cause message.
694
+ base = `TLS/certificate error connecting to ${host}: ${causeMessage ?? error.message}`;
695
+ }
696
+ else if (error.message === 'fetch failed') {
697
+ // "fetch failed" with no recognized code — surface the underlying cause if any.
698
+ base = causeMessage
699
+ ? `network error connecting to ${host}: ${causeMessage}${code ? ` (${code})` : ''}`
700
+ : `network error connecting to ${host}${code ? ` (${code})` : ''}. Run with --verbose for more detail.`;
701
+ }
702
+ else {
703
+ base = error.message;
704
+ }
705
+ return base + formatFailureDuration(elapsedMs);
706
+ }
707
+ /**
708
+ * Render how long the request ran before failing, e.g. " (after 5m 0s)".
709
+ * Flags durations sitting near a common timeout boundary (30/60/120/300/600s),
710
+ * which usually points at a server-side or proxy/load-balancer timeout rather
711
+ * than a local network problem.
712
+ */
713
+ function formatFailureDuration(elapsedMs) {
714
+ if (elapsedMs === undefined || elapsedMs < 0)
715
+ return '';
716
+ const totalSeconds = elapsedMs / 1000;
717
+ const human = totalSeconds < 60
718
+ ? `${totalSeconds.toFixed(1)}s`
719
+ : `${Math.floor(totalSeconds / 60)}m ${Math.round(totalSeconds % 60)}s`;
720
+ // Within 5% of a common timeout boundary → likely a hard cutoff, not a blip.
721
+ const boundaries = [30, 60, 120, 300, 600];
722
+ const nearTimeout = boundaries.some((b) => Math.abs(totalSeconds - b) <= b * 0.05);
723
+ return nearTimeout
724
+ ? ` (failed after ~${human}, near a common ${boundaries.find((b) => Math.abs(totalSeconds - b) <= b * 0.05)}s timeout — likely a server or proxy cutoff)`
725
+ : ` (failed after ${human})`;
726
+ }
660
727
  async function handleDryRunError(response, command, flags, target) {
661
728
  const log = command.log.bind(command);
662
729
  if (response.status === 404) {
@@ -170,8 +170,8 @@ export function checkTableIndexes(documents) {
170
170
  return badIndexes;
171
171
  }
172
172
  function extractSchemaFields(content) {
173
- // id, created_at, and xdo are system fields not declared in the schema
174
- const fields = new Set(['id', 'created_at', 'xdo']);
173
+ // id and created_at are auto-added during import
174
+ const fields = new Set(['id', 'created_at']);
175
175
  // Find the schema block by matching braces
176
176
  const schemaStart = content.match(/\bschema\s*\{/);
177
177
  if (!schemaStart || schemaStart.index === undefined)