@xano/cli 1.0.3 → 1.0.4-beta.1
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 +69 -3
- package/dist/base-command.d.ts +27 -0
- package/dist/base-command.js +124 -1
- package/dist/commands/auth/index.d.ts +10 -0
- package/dist/commands/auth/index.js +146 -9
- package/dist/commands/static_host/build/create/index.d.ts +9 -1
- package/dist/commands/static_host/build/create/index.js +54 -4
- package/dist/commands/static_host/build/delete/index.d.ts +19 -0
- package/dist/commands/static_host/build/delete/index.js +114 -0
- package/dist/commands/static_host/build/get/index.d.ts +1 -1
- package/dist/commands/static_host/build/get/index.js +16 -10
- package/dist/commands/static_host/build/pull/index.d.ts +52 -0
- package/dist/commands/static_host/build/pull/index.js +300 -0
- package/dist/commands/static_host/build/push/index.d.ts +23 -0
- package/dist/commands/static_host/build/push/index.js +225 -0
- package/dist/commands/static_host/create/index.d.ts +17 -0
- package/dist/commands/static_host/create/index.js +86 -0
- package/dist/commands/static_host/deploy/index.d.ts +18 -0
- package/dist/commands/static_host/deploy/index.js +105 -0
- package/dist/commands/static_host/edit/index.d.ts +23 -0
- package/dist/commands/static_host/edit/index.js +151 -0
- package/dist/commands/static_host/get/index.d.ts +18 -0
- package/dist/commands/static_host/get/index.js +94 -0
- package/dist/commands/static_host/migrate/index.d.ts +44 -0
- package/dist/commands/static_host/migrate/index.js +205 -0
- package/dist/commands/tenant/snapshot/create/index.d.ts +17 -0
- package/dist/commands/tenant/snapshot/create/index.js +78 -0
- package/dist/commands/tenant/snapshot/delete/index.d.ts +19 -0
- package/dist/commands/tenant/snapshot/delete/index.js +102 -0
- package/dist/commands/tenant/snapshot/list/index.d.ts +16 -0
- package/dist/commands/tenant/snapshot/list/index.js +96 -0
- package/dist/commands/tenant/snapshot/swap/index.d.ts +19 -0
- package/dist/commands/tenant/snapshot/swap/index.js +103 -0
- package/dist/utils/multidoc-push.js +21 -17
- package/dist/utils/reference-checker.js +2 -2
- package/oclif.manifest.json +3831 -2484
- package/package.json +3 -1
package/README.md
CHANGED
|
@@ -46,8 +46,42 @@ These warnings are layer 1 of broader push-safety work; ephemeral sandbox enviro
|
|
|
46
46
|
xano auth
|
|
47
47
|
xano auth --origin https://custom.xano.com
|
|
48
48
|
xano auth --insecure # Skip TLS verification (self-signed certs)
|
|
49
|
+
xano auth --no-browser # Headless login (no local callback server)
|
|
50
|
+
|
|
51
|
+
# Pre-select instance/workspace/branch and profile name (skips the pickers)
|
|
52
|
+
xano auth -i my-instance -w 5 -b dev -p staging
|
|
53
|
+
xano auth --instance my-instance --workspace "My Workspace" --branch dev --profile staging
|
|
54
|
+
|
|
55
|
+
# Pass "" to take a picker's default: skip workspace, use live branch, default profile name
|
|
56
|
+
xano auth -i my-instance -w 5 -b "" -p ""
|
|
49
57
|
```
|
|
50
58
|
|
|
59
|
+
The default flow starts a temporary callback server on `127.0.0.1` and waits
|
|
60
|
+
for the browser to redirect back to it. On remote/SSH sessions, Docker
|
|
61
|
+
containers, or locked-down networks where the browser can't reach the CLI's
|
|
62
|
+
loopback address, use `--no-browser`: the CLI prints a login URL, you open it
|
|
63
|
+
in any browser, and paste back the code it displays. No local server required.
|
|
64
|
+
|
|
65
|
+
Each picker can be pre-answered with a flag: `-i/--instance` (instance name),
|
|
66
|
+
`-w/--workspace` (workspace ID or name), `-b/--branch` (branch label), and
|
|
67
|
+
`-p/--profile` (profile name to save). An empty value (`""`) takes the
|
|
68
|
+
picker's default answer: `-w ""` skips workspace selection, `-b ""` skips and
|
|
69
|
+
uses the live branch, and `-p ""` uses the default profile name. With all four
|
|
70
|
+
set alongside `--no-browser`, the only input is pasting the code from the
|
|
71
|
+
browser — useful for scripted or remote setups.
|
|
72
|
+
|
|
73
|
+
When stdin is piped (not a TTY), `--no-browser` reads the code directly from
|
|
74
|
+
stdin instead of prompting, so scripts and AI agents can complete the flow
|
|
75
|
+
without an interactive terminal:
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
echo "$CODE" | xano auth --no-browser -i my-instance -w 5 -b dev -p staging
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
If you can't run `xano auth` at all, you can always create a profile manually
|
|
82
|
+
with a Metadata API token from the Xano dashboard — see
|
|
83
|
+
[Profiles](#profiles) below.
|
|
84
|
+
|
|
51
85
|
### Profiles
|
|
52
86
|
|
|
53
87
|
Profiles store your Xano credentials and default workspace settings.
|
|
@@ -522,14 +556,46 @@ xano sandbox reset --force
|
|
|
522
556
|
# List static hosts
|
|
523
557
|
xano static_host list
|
|
524
558
|
|
|
525
|
-
# Create a
|
|
526
|
-
xano static_host
|
|
559
|
+
# Create / get / edit a static host
|
|
560
|
+
xano static_host create marketing --description "Marketing site"
|
|
561
|
+
xano static_host get marketing
|
|
562
|
+
xano static_host edit marketing --name marketing-v2 --description "Updated"
|
|
527
563
|
|
|
528
564
|
# List builds
|
|
529
565
|
xano static_host build list default
|
|
530
566
|
|
|
531
567
|
# Get build details
|
|
532
|
-
xano static_host build get default 52
|
|
568
|
+
xano static_host build get default --build_id 52
|
|
569
|
+
|
|
570
|
+
# Pull a build to disk. Defaults to the original uploaded source
|
|
571
|
+
# (including package.json). Use --source built for the compiled/served output.
|
|
572
|
+
xano static_host build pull default --build_id 52 # By build ID (original source)
|
|
573
|
+
xano static_host build pull default --build_id 52 --source built # Compiled output
|
|
574
|
+
xano static_host build pull default --latest # Latest build
|
|
575
|
+
xano static_host build pull default --env dev # Build currently deployed to dev
|
|
576
|
+
xano static_host build pull default --env prod -d ./prod-release
|
|
577
|
+
|
|
578
|
+
# Push a build (name optional — auto-generated from the timestamp if omitted).
|
|
579
|
+
# Accepts a directory (-d) or a zip file (-f). Defaults to the current directory.
|
|
580
|
+
# For package.json builds, the CLI waits for the build to finish (--no-wait to skip).
|
|
581
|
+
xano static_host build push default -d ./dist -n "v1.0.0"
|
|
582
|
+
xano static_host build push default # current dir, auto-name
|
|
583
|
+
xano static_host build push default -f ./build.zip -n "v1.0.0" # from zip file
|
|
584
|
+
xano static_host build push default -n "release" --description "Production build"
|
|
585
|
+
|
|
586
|
+
# Delete a build (prompts for confirmation; --force to skip)
|
|
587
|
+
xano static_host build delete default --build_id 52
|
|
588
|
+
xano static_host build delete default --build_id 52 --force
|
|
589
|
+
|
|
590
|
+
# Deploy a build to an environment
|
|
591
|
+
xano static_host deploy default --build_id 52 --env dev
|
|
592
|
+
xano static_host deploy default --build_id 52 --env prod
|
|
593
|
+
|
|
594
|
+
# Migrate a host to instance-managed (v2) hosting
|
|
595
|
+
xano static_host migrate newsite # one host (both envs)
|
|
596
|
+
xano static_host migrate newsite --env dev # one env
|
|
597
|
+
xano static_host migrate --all # every v1 host in the workspace
|
|
598
|
+
xano static_host migrate --all --dry-run # preview without changing anything
|
|
533
599
|
```
|
|
534
600
|
|
|
535
601
|
## Global Options
|
package/dist/base-command.d.ts
CHANGED
|
@@ -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
|
}
|
package/dist/base-command.js
CHANGED
|
@@ -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';
|
|
@@ -358,4 +358,127 @@ export default class BaseCommand extends Command {
|
|
|
358
358
|
}
|
|
359
359
|
return response;
|
|
360
360
|
}
|
|
361
|
+
/**
|
|
362
|
+
* Poll a static-host build until it reaches a terminal status (ok | error),
|
|
363
|
+
* showing a live, ticking spinner with the current stage and elapsed time —
|
|
364
|
+
* mirroring the UI's build progress for async (package.json) builds, which
|
|
365
|
+
* keep running after the upload returns.
|
|
366
|
+
*
|
|
367
|
+
* On a TTY it renders an animated spinner via ux.action; when quiet (JSON
|
|
368
|
+
* output) or non-interactive it falls back to plain one-line status updates.
|
|
369
|
+
*
|
|
370
|
+
* Returns the final status. Resolves to the last-seen status on timeout.
|
|
371
|
+
*/
|
|
372
|
+
async logStaticHostUrls(opts) {
|
|
373
|
+
const { profile, staticHost, verbose, workspaceId } = opts;
|
|
374
|
+
const url = `${profile.instance_origin}/api:meta/workspace/${workspaceId}/static_host/${staticHost}`;
|
|
375
|
+
try {
|
|
376
|
+
const response = await this.verboseFetch(url, {
|
|
377
|
+
headers: { accept: 'application/json', Authorization: `Bearer ${profile.access_token}` },
|
|
378
|
+
method: 'GET',
|
|
379
|
+
}, verbose, profile.access_token);
|
|
380
|
+
if (!response.ok)
|
|
381
|
+
return;
|
|
382
|
+
const host = (await response.json());
|
|
383
|
+
if (host.dev?.default_url)
|
|
384
|
+
this.log(`Dev URL: ${host.dev.default_url}`);
|
|
385
|
+
if (host.dev?.custom_url)
|
|
386
|
+
this.log(`Dev Custom URL: ${host.dev.custom_url}`);
|
|
387
|
+
if (host.prod?.default_url)
|
|
388
|
+
this.log(`Prod URL: ${host.prod.default_url}`);
|
|
389
|
+
if (host.prod?.custom_url)
|
|
390
|
+
this.log(`Prod Custom URL: ${host.prod.custom_url}`);
|
|
391
|
+
}
|
|
392
|
+
catch {
|
|
393
|
+
// Non-fatal — the build succeeded, we just can't show the URL.
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
async waitForBuild(opts) {
|
|
397
|
+
const { buildId, profile, quiet, staticHost, verbose, workspaceId } = opts;
|
|
398
|
+
const intervalMs = opts.intervalMs ?? 2000;
|
|
399
|
+
const timeoutMs = opts.timeoutMs ?? 600_000; // 10 min
|
|
400
|
+
const terminal = new Set(['error', 'ok']);
|
|
401
|
+
const url = `${profile.instance_origin}/api:meta/workspace/${workspaceId}/static_host/${staticHost}/build/${buildId}`;
|
|
402
|
+
// Spinner only on an interactive TTY and when not emitting JSON. Verbose mode
|
|
403
|
+
// also disables it (the spinner would interleave with request/response logs).
|
|
404
|
+
const animate = Boolean(process.stdout.isTTY) && !quiet && !verbose;
|
|
405
|
+
const start = Date.now();
|
|
406
|
+
const elapsed = () => Math.round((Date.now() - start) / 1000);
|
|
407
|
+
let stage = 'pending';
|
|
408
|
+
let ticker;
|
|
409
|
+
// Reflect the current stage: live spinner status on a TTY, else a plain line.
|
|
410
|
+
const render = () => {
|
|
411
|
+
if (animate) {
|
|
412
|
+
ux.action.status = `${stageLabel(stage)} (${elapsed()}s)`;
|
|
413
|
+
}
|
|
414
|
+
else if (!quiet && !terminal.has(stage)) {
|
|
415
|
+
this.log(`Build status: ${stage}`);
|
|
416
|
+
}
|
|
417
|
+
};
|
|
418
|
+
// Stop the spinner/ticker and emit a final line.
|
|
419
|
+
const conclude = (message) => {
|
|
420
|
+
if (ticker)
|
|
421
|
+
clearInterval(ticker);
|
|
422
|
+
if (animate) {
|
|
423
|
+
ux.action.stop(message);
|
|
424
|
+
}
|
|
425
|
+
else if (!quiet) {
|
|
426
|
+
this.log(message);
|
|
427
|
+
}
|
|
428
|
+
};
|
|
429
|
+
if (animate) {
|
|
430
|
+
ux.action.start('Building', stageLabel(stage));
|
|
431
|
+
// Re-render every 120ms so the elapsed counter ticks even between polls.
|
|
432
|
+
ticker = setInterval(render, 120);
|
|
433
|
+
}
|
|
434
|
+
else if (!quiet) {
|
|
435
|
+
this.log(`Build status: ${stage}`);
|
|
436
|
+
}
|
|
437
|
+
/* eslint-disable no-await-in-loop */
|
|
438
|
+
while (Date.now() - start < timeoutMs) {
|
|
439
|
+
const response = await this.verboseFetch(url, {
|
|
440
|
+
headers: { accept: 'application/json', Authorization: `Bearer ${profile.access_token}` },
|
|
441
|
+
method: 'GET',
|
|
442
|
+
}, verbose, profile.access_token);
|
|
443
|
+
if (response.ok) {
|
|
444
|
+
const build = (await response.json());
|
|
445
|
+
const status = build.status ?? 'pending';
|
|
446
|
+
if (status !== stage) {
|
|
447
|
+
stage = status;
|
|
448
|
+
render();
|
|
449
|
+
}
|
|
450
|
+
if (terminal.has(status)) {
|
|
451
|
+
const took = `${elapsed()}s`;
|
|
452
|
+
conclude(status === 'ok' ? `done in ${took}` : `failed after ${took}`);
|
|
453
|
+
return status;
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
await new Promise((resolve) => {
|
|
457
|
+
setTimeout(resolve, intervalMs);
|
|
458
|
+
});
|
|
459
|
+
}
|
|
460
|
+
/* eslint-enable no-await-in-loop */
|
|
461
|
+
conclude(`stopped waiting after ${Math.round(timeoutMs / 1000)}s (last status: ${stage || 'unknown'})`);
|
|
462
|
+
return stage;
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
/** Human-friendly label for a build status stage. */
|
|
466
|
+
function stageLabel(status) {
|
|
467
|
+
switch (status) {
|
|
468
|
+
case 'building': {
|
|
469
|
+
return 'Installing & building (npm)';
|
|
470
|
+
}
|
|
471
|
+
case 'ok': {
|
|
472
|
+
return 'Finishing';
|
|
473
|
+
}
|
|
474
|
+
case 'pending': {
|
|
475
|
+
return 'Queued';
|
|
476
|
+
}
|
|
477
|
+
case 'publishing': {
|
|
478
|
+
return 'Publishing files';
|
|
479
|
+
}
|
|
480
|
+
default: {
|
|
481
|
+
return status;
|
|
482
|
+
}
|
|
483
|
+
}
|
|
361
484
|
}
|
|
@@ -3,16 +3,26 @@ export default class Auth extends Command {
|
|
|
3
3
|
static description: string;
|
|
4
4
|
static examples: string[];
|
|
5
5
|
static flags: {
|
|
6
|
+
branch: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
6
7
|
config: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
7
8
|
insecure: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
9
|
+
instance: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
10
|
+
'no-browser': import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
8
11
|
origin: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
12
|
+
profile: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
13
|
+
workspace: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
9
14
|
};
|
|
10
15
|
run(): Promise<void>;
|
|
11
16
|
private getHeaders;
|
|
12
17
|
private fetchBranches;
|
|
13
18
|
private fetchInstances;
|
|
14
19
|
private fetchWorkspaces;
|
|
20
|
+
private promptForToken;
|
|
15
21
|
private promptProfileName;
|
|
22
|
+
private readTokenFromStdin;
|
|
23
|
+
private resolveBranch;
|
|
24
|
+
private resolveInstance;
|
|
25
|
+
private resolveWorkspace;
|
|
16
26
|
private saveProfile;
|
|
17
27
|
private selectBranch;
|
|
18
28
|
private selectInstance;
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { ExitPromptError } from '@inquirer/core';
|
|
2
1
|
import { Command, Flags } from '@oclif/core';
|
|
3
2
|
import inquirer from 'inquirer';
|
|
4
3
|
import * as yaml from 'js-yaml';
|
|
@@ -20,8 +19,21 @@ Authenticated as John Doe (john@example.com)
|
|
|
20
19
|
Profile 'default' created successfully!`,
|
|
21
20
|
`$ xano auth --origin https://custom.xano.com
|
|
22
21
|
Opening browser for Xano login at https://custom.xano.com...`,
|
|
22
|
+
`$ xano auth --no-browser
|
|
23
|
+
To authenticate, open the following URL in any browser:
|
|
24
|
+
https://app.xano.com/login?dest=cli&display=code
|
|
25
|
+
? Paste the code shown in your browser: ****`,
|
|
26
|
+
`$ xano auth --no-browser --instance my-instance --workspace 5 --branch dev --profile staging
|
|
27
|
+
(non-interactive: only the pasted code is prompted for)`,
|
|
28
|
+
`$ echo "$CODE" | xano auth --no-browser --instance my-instance --workspace 5 --branch dev --profile staging
|
|
29
|
+
(fully scripted: the code is read from piped stdin, no prompt at all)`,
|
|
23
30
|
];
|
|
24
31
|
static flags = {
|
|
32
|
+
branch: Flags.string({
|
|
33
|
+
char: 'b',
|
|
34
|
+
description: 'Pre-select a branch by label (skips the branch picker); pass "" to skip and use the live branch',
|
|
35
|
+
required: false,
|
|
36
|
+
}),
|
|
25
37
|
config: Flags.string({
|
|
26
38
|
char: 'c',
|
|
27
39
|
description: 'Path to credentials file (default: ~/.xano/credentials.yaml)',
|
|
@@ -33,11 +45,30 @@ Opening browser for Xano login at https://custom.xano.com...`,
|
|
|
33
45
|
default: false,
|
|
34
46
|
description: 'Skip TLS certificate verification (for self-signed certificates)',
|
|
35
47
|
}),
|
|
48
|
+
instance: Flags.string({
|
|
49
|
+
char: 'i',
|
|
50
|
+
description: 'Pre-select an instance by name (skips the instance picker)',
|
|
51
|
+
required: false,
|
|
52
|
+
}),
|
|
53
|
+
'no-browser': Flags.boolean({
|
|
54
|
+
default: false,
|
|
55
|
+
description: 'Headless login: print a URL and paste back the code shown in the browser, instead of starting a local callback server (use on remote/SSH/Docker hosts where 127.0.0.1 is not reachable from the browser)',
|
|
56
|
+
}),
|
|
36
57
|
origin: Flags.string({
|
|
37
58
|
char: 'o',
|
|
38
59
|
default: 'https://app.xano.com',
|
|
39
60
|
description: 'Xano account origin URL',
|
|
40
61
|
}),
|
|
62
|
+
profile: Flags.string({
|
|
63
|
+
char: 'p',
|
|
64
|
+
description: 'Profile name to save (skips the profile name prompt); pass "" to use the default name',
|
|
65
|
+
required: false,
|
|
66
|
+
}),
|
|
67
|
+
workspace: Flags.string({
|
|
68
|
+
char: 'w',
|
|
69
|
+
description: 'Pre-select a workspace by ID or name (skips the workspace picker); pass "" to skip workspace',
|
|
70
|
+
required: false,
|
|
71
|
+
}),
|
|
41
72
|
};
|
|
42
73
|
async run() {
|
|
43
74
|
const { flags } = await this.parse(Auth);
|
|
@@ -48,7 +79,9 @@ Opening browser for Xano login at https://custom.xano.com...`,
|
|
|
48
79
|
try {
|
|
49
80
|
// Step 1: Get token via browser auth
|
|
50
81
|
this.log('Starting authentication flow...');
|
|
51
|
-
const token =
|
|
82
|
+
const token = flags['no-browser']
|
|
83
|
+
? await this.promptForToken(flags.origin)
|
|
84
|
+
: await this.startAuthServer(flags.origin);
|
|
52
85
|
// Step 2: Validate token and get user info
|
|
53
86
|
this.log('');
|
|
54
87
|
this.log('Validating authentication...');
|
|
@@ -60,6 +93,9 @@ Opening browser for Xano login at https://custom.xano.com...`,
|
|
|
60
93
|
const isSelfHosted = !/^https:\/\/app\.(.*\.)?xano\.com$/.test(flags.origin);
|
|
61
94
|
let instance;
|
|
62
95
|
if (isSelfHosted) {
|
|
96
|
+
if (flags.instance) {
|
|
97
|
+
this.warn('Ignoring --instance: the origin itself is the instance for self-hosted Xano.');
|
|
98
|
+
}
|
|
63
99
|
instance = {
|
|
64
100
|
display: flags.origin,
|
|
65
101
|
id: 'self-hosted',
|
|
@@ -74,7 +110,7 @@ Opening browser for Xano login at https://custom.xano.com...`,
|
|
|
74
110
|
if (instances.length === 0) {
|
|
75
111
|
this.error('No instances found. Please check your account.');
|
|
76
112
|
}
|
|
77
|
-
instance = await this.
|
|
113
|
+
instance = await this.resolveInstance(instances, flags.instance);
|
|
78
114
|
}
|
|
79
115
|
// Step 4: Workspace selection
|
|
80
116
|
let workspace;
|
|
@@ -83,20 +119,25 @@ Opening browser for Xano login at https://custom.xano.com...`,
|
|
|
83
119
|
this.log('Fetching available workspaces...');
|
|
84
120
|
const workspaces = await this.fetchWorkspaces(token, instance.origin);
|
|
85
121
|
if (workspaces.length > 0) {
|
|
86
|
-
workspace = await this.
|
|
122
|
+
workspace = await this.resolveWorkspace(workspaces, flags.workspace);
|
|
87
123
|
if (workspace) {
|
|
88
124
|
// Step 5: Branch selection
|
|
89
125
|
this.log('');
|
|
90
126
|
this.log('Fetching available branches...');
|
|
91
127
|
const branches = await this.fetchBranches(token, instance.origin, workspace.id);
|
|
92
|
-
|
|
93
|
-
branch = await this.selectBranch(branches);
|
|
94
|
-
}
|
|
128
|
+
branch = await this.resolveBranch(branches, flags.branch);
|
|
95
129
|
}
|
|
96
130
|
}
|
|
131
|
+
else if (flags.workspace) {
|
|
132
|
+
this.error(`Workspace '${flags.workspace}' not found: no workspaces are available on this instance.`);
|
|
133
|
+
}
|
|
134
|
+
if (flags.branch && !workspace) {
|
|
135
|
+
this.warn('Ignoring --branch: no workspace selected.');
|
|
136
|
+
}
|
|
97
137
|
// Step 6: Profile name
|
|
98
138
|
this.log('');
|
|
99
|
-
|
|
139
|
+
// An empty --profile value means "use the default name" (same as accepting the prompt's default)
|
|
140
|
+
const profileName = flags.profile === undefined ? await this.promptProfileName() : flags.profile.trim() || 'default';
|
|
100
141
|
// Step 7: Save profile
|
|
101
142
|
await this.saveProfile({
|
|
102
143
|
access_token: token,
|
|
@@ -113,7 +154,10 @@ Opening browser for Xano login at https://custom.xano.com...`,
|
|
|
113
154
|
process.exit(0);
|
|
114
155
|
}
|
|
115
156
|
catch (error) {
|
|
116
|
-
|
|
157
|
+
// Ctrl+C at an inquirer prompt throws ExitPromptError. Match on the name
|
|
158
|
+
// rather than `instanceof`: inquirer bundles its own copy of
|
|
159
|
+
// @inquirer/core, so the thrown class won't match an imported one.
|
|
160
|
+
if (error?.name === 'ExitPromptError') {
|
|
117
161
|
this.log('Authentication cancelled.');
|
|
118
162
|
return;
|
|
119
163
|
}
|
|
@@ -196,6 +240,45 @@ Opening browser for Xano login at https://custom.xano.com...`,
|
|
|
196
240
|
return [];
|
|
197
241
|
}
|
|
198
242
|
}
|
|
243
|
+
async promptForToken(origin) {
|
|
244
|
+
// Headless flow: no local callback server. The login page, when opened
|
|
245
|
+
// without a `callback` param, renders the access token on screen for the
|
|
246
|
+
// user to copy. We point the browser there (best-effort) and prompt for
|
|
247
|
+
// the pasted code.
|
|
248
|
+
const authUrl = `${origin}/login?dest=cli&display=code`;
|
|
249
|
+
this.log('To authenticate, open the following URL in any browser:');
|
|
250
|
+
this.log('');
|
|
251
|
+
this.log(` ${authUrl}`);
|
|
252
|
+
this.log('');
|
|
253
|
+
// Piped (non-TTY) stdin: read the code directly instead of prompting, so
|
|
254
|
+
// scripts and agents can do `echo $CODE | xano auth --no-browser ...`.
|
|
255
|
+
// The masked inquirer prompt below requires an interactive terminal.
|
|
256
|
+
if (!process.stdin.isTTY) {
|
|
257
|
+
const piped = await this.readTokenFromStdin();
|
|
258
|
+
if (!piped) {
|
|
259
|
+
this.error('No code received on stdin. Pipe the code shown in the browser, e.g. `echo "$CODE" | xano auth --no-browser ...`');
|
|
260
|
+
}
|
|
261
|
+
this.log('Read code from stdin.');
|
|
262
|
+
return piped;
|
|
263
|
+
}
|
|
264
|
+
try {
|
|
265
|
+
await open(authUrl);
|
|
266
|
+
}
|
|
267
|
+
catch {
|
|
268
|
+
// Best-effort only; the URL is already printed above for manual use.
|
|
269
|
+
}
|
|
270
|
+
const { token } = await inquirer.prompt([
|
|
271
|
+
{
|
|
272
|
+
message: 'Paste the code shown in your browser',
|
|
273
|
+
name: 'token',
|
|
274
|
+
type: 'password',
|
|
275
|
+
validate(input) {
|
|
276
|
+
return input.trim() === '' ? 'A code is required' : true;
|
|
277
|
+
},
|
|
278
|
+
},
|
|
279
|
+
]);
|
|
280
|
+
return token.trim();
|
|
281
|
+
}
|
|
199
282
|
async promptProfileName() {
|
|
200
283
|
const { profileName } = await inquirer.prompt([
|
|
201
284
|
{
|
|
@@ -214,6 +297,60 @@ Opening browser for Xano login at https://custom.xano.com...`,
|
|
|
214
297
|
]);
|
|
215
298
|
return profileName.trim() || 'default';
|
|
216
299
|
}
|
|
300
|
+
readTokenFromStdin() {
|
|
301
|
+
return new Promise((resolve) => {
|
|
302
|
+
let data = '';
|
|
303
|
+
process.stdin.setEncoding('utf8');
|
|
304
|
+
process.stdin.on('data', (chunk) => {
|
|
305
|
+
data += chunk;
|
|
306
|
+
});
|
|
307
|
+
process.stdin.on('end', () => resolve(data.trim()));
|
|
308
|
+
process.stdin.on('error', () => resolve(data.trim()));
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
async resolveBranch(branches, flagValue) {
|
|
312
|
+
if (flagValue !== undefined) {
|
|
313
|
+
// An empty value means "skip and use live branch" (same as the picker's skip option)
|
|
314
|
+
if (flagValue.trim() === '') {
|
|
315
|
+
this.log('Using live branch');
|
|
316
|
+
return undefined;
|
|
317
|
+
}
|
|
318
|
+
const match = branches.find((br) => br.label === flagValue || br.id === flagValue);
|
|
319
|
+
if (!match) {
|
|
320
|
+
this.error(`Branch '${flagValue}' not found. Available branches: ${branches.map((br) => br.label).join(', ')}`);
|
|
321
|
+
}
|
|
322
|
+
this.log(`Using branch: ${match.label}`);
|
|
323
|
+
return match.id;
|
|
324
|
+
}
|
|
325
|
+
return branches.length > 1 ? this.selectBranch(branches) : undefined;
|
|
326
|
+
}
|
|
327
|
+
async resolveInstance(instances, flagValue) {
|
|
328
|
+
if (flagValue) {
|
|
329
|
+
const match = instances.find((inst) => inst.name === flagValue || inst.id === flagValue);
|
|
330
|
+
if (!match) {
|
|
331
|
+
this.error(`Instance '${flagValue}' not found. Available instances: ${instances.map((inst) => inst.name).join(', ')}`);
|
|
332
|
+
}
|
|
333
|
+
this.log(`Using instance: ${match.name} (${match.display})`);
|
|
334
|
+
return match;
|
|
335
|
+
}
|
|
336
|
+
return this.selectInstance(instances);
|
|
337
|
+
}
|
|
338
|
+
async resolveWorkspace(workspaces, flagValue) {
|
|
339
|
+
if (flagValue !== undefined) {
|
|
340
|
+
// An empty value means "skip workspace" (same as the picker's skip option)
|
|
341
|
+
if (flagValue.trim() === '') {
|
|
342
|
+
this.log('Skipping workspace selection');
|
|
343
|
+
return undefined;
|
|
344
|
+
}
|
|
345
|
+
const match = workspaces.find((ws) => String(ws.id) === flagValue || ws.name === flagValue);
|
|
346
|
+
if (!match) {
|
|
347
|
+
this.error(`Workspace '${flagValue}' not found. Available workspaces: ${workspaces.map((ws) => `${ws.name} (${ws.id})`).join(', ')}`);
|
|
348
|
+
}
|
|
349
|
+
this.log(`Using workspace: ${match.name} (${match.id})`);
|
|
350
|
+
return match;
|
|
351
|
+
}
|
|
352
|
+
return this.selectWorkspace(workspaces);
|
|
353
|
+
}
|
|
217
354
|
async saveProfile(profile, configPath) {
|
|
218
355
|
const credentialsPath = resolveCredentialsPath(configPath);
|
|
219
356
|
const credDir = dirname(credentialsPath);
|
|
@@ -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
|
|
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:
|
|
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
|
-
|
|
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
|
+
}
|