@xano/cli 1.0.3-beta.8 → 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.
- package/README.md +3 -61
- package/dist/base-command.d.ts +0 -27
- package/dist/base-command.js +46 -125
- package/dist/commands/auth/index.d.ts +0 -9
- package/dist/commands/auth/index.js +9 -122
- package/dist/commands/static_host/build/create/index.d.ts +1 -9
- package/dist/commands/static_host/build/create/index.js +4 -54
- package/dist/commands/static_host/build/get/index.d.ts +1 -1
- package/dist/commands/static_host/build/get/index.js +10 -16
- package/dist/utils/multidoc-push.js +94 -27
- package/dist/utils/reference-checker.js +2 -2
- package/oclif.manifest.json +2425 -3385
- package/package.json +3 -4
- package/dist/commands/static_host/build/delete/index.d.ts +0 -19
- package/dist/commands/static_host/build/delete/index.js +0 -114
- package/dist/commands/static_host/build/pull/index.d.ts +0 -52
- package/dist/commands/static_host/build/pull/index.js +0 -300
- package/dist/commands/static_host/build/push/index.d.ts +0 -23
- package/dist/commands/static_host/build/push/index.js +0 -225
- package/dist/commands/static_host/create/index.d.ts +0 -17
- package/dist/commands/static_host/create/index.js +0 -86
- package/dist/commands/static_host/deploy/index.d.ts +0 -18
- package/dist/commands/static_host/deploy/index.js +0 -105
- package/dist/commands/static_host/edit/index.d.ts +0 -23
- package/dist/commands/static_host/edit/index.js +0 -151
- package/dist/commands/static_host/get/index.d.ts +0 -18
- package/dist/commands/static_host/get/index.js +0 -94
- package/dist/commands/static_host/migrate/index.d.ts +0 -44
- 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 = '
|
|
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
|
|
71
|
-
required:
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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>
|
|
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/${
|
|
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
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
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(
|
|
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
|
-
|
|
649
|
-
|
|
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
|
|
174
|
-
const fields = new Set(['id', 'created_at'
|
|
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)
|