@xano/cli 1.0.4-beta.3 → 1.0.4
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 +4 -51
- package/dist/base-command.d.ts +0 -27
- package/dist/base-command.js +1 -124
- package/dist/commands/auth/index.d.ts +1 -45
- package/dist/commands/auth/index.js +29 -197
- package/dist/commands/{tenant/snapshot/list → knowledge/pull}/index.d.ts +4 -6
- package/dist/commands/knowledge/pull/index.js +86 -0
- package/dist/commands/knowledge/push/index.d.ts +20 -0
- package/dist/commands/knowledge/push/index.js +126 -0
- 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/knowledge-sync.d.ts +108 -0
- package/dist/utils/knowledge-sync.js +380 -0
- package/dist/utils/multidoc-push.js +17 -21
- package/dist/utils/reference-checker.js +2 -2
- package/oclif.manifest.json +2951 -4086
- package/package.json +1 -3
- 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
- package/dist/commands/tenant/snapshot/create/index.d.ts +0 -17
- package/dist/commands/tenant/snapshot/create/index.js +0 -78
- package/dist/commands/tenant/snapshot/delete/index.d.ts +0 -19
- package/dist/commands/tenant/snapshot/delete/index.js +0 -102
- package/dist/commands/tenant/snapshot/list/index.js +0 -96
- package/dist/commands/tenant/snapshot/swap/index.d.ts +0 -19
- package/dist/commands/tenant/snapshot/swap/index.js +0 -103
package/README.md
CHANGED
|
@@ -47,13 +47,6 @@ xano auth
|
|
|
47
47
|
xano auth --origin https://custom.xano.com
|
|
48
48
|
xano auth --insecure # Skip TLS verification (self-signed certs)
|
|
49
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 ""
|
|
57
50
|
```
|
|
58
51
|
|
|
59
52
|
The default flow starts a temporary callback server on `127.0.0.1` and waits
|
|
@@ -62,20 +55,12 @@ containers, or locked-down networks where the browser can't reach the CLI's
|
|
|
62
55
|
loopback address, use `--no-browser`: the CLI prints a login URL, you open it
|
|
63
56
|
in any browser, and paste back the code it displays. No local server required.
|
|
64
57
|
|
|
65
|
-
Each picker can be pre-answered with a flag: `-i/--instance` (instance name, or numeric instance ID),
|
|
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
58
|
When stdin is piped (not a TTY), `--no-browser` reads the code directly from
|
|
74
59
|
stdin instead of prompting, so scripts and AI agents can complete the flow
|
|
75
60
|
without an interactive terminal:
|
|
76
61
|
|
|
77
62
|
```bash
|
|
78
|
-
echo "$CODE" | xano auth --no-browser
|
|
63
|
+
echo "$CODE" | xano auth --no-browser
|
|
79
64
|
```
|
|
80
65
|
|
|
81
66
|
If you can't run `xano auth` at all, you can always create a profile manually
|
|
@@ -556,46 +541,14 @@ xano sandbox reset --force
|
|
|
556
541
|
# List static hosts
|
|
557
542
|
xano static_host list
|
|
558
543
|
|
|
559
|
-
# Create
|
|
560
|
-
xano static_host create
|
|
561
|
-
xano static_host get marketing
|
|
562
|
-
xano static_host edit marketing --name marketing-v2 --description "Updated"
|
|
544
|
+
# Create a build
|
|
545
|
+
xano static_host build create default -f ./build.zip -n "v1.0.0"
|
|
563
546
|
|
|
564
547
|
# List builds
|
|
565
548
|
xano static_host build list default
|
|
566
549
|
|
|
567
550
|
# Get build details
|
|
568
|
-
xano static_host build get default
|
|
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
|
|
551
|
+
xano static_host build get default 52
|
|
599
552
|
```
|
|
600
553
|
|
|
601
554
|
## Global Options
|
package/dist/base-command.d.ts
CHANGED
|
@@ -103,31 +103,4 @@ 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>;
|
|
133
106
|
}
|
package/dist/base-command.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Command, Flags
|
|
1
|
+
import { Command, Flags } 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,127 +358,4 @@ 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
|
-
}
|
|
484
361
|
}
|
|
@@ -1,53 +1,14 @@
|
|
|
1
1
|
import { Command } from '@oclif/core';
|
|
2
|
-
export interface Instance {
|
|
3
|
-
display: string;
|
|
4
|
-
id: string;
|
|
5
|
-
name: string;
|
|
6
|
-
origin: string;
|
|
7
|
-
}
|
|
8
|
-
export interface AuthResult {
|
|
9
|
-
branch: null | string;
|
|
10
|
-
credentialsPath: string;
|
|
11
|
-
instance: {
|
|
12
|
-
id: string;
|
|
13
|
-
name: string;
|
|
14
|
-
origin: string;
|
|
15
|
-
};
|
|
16
|
-
profile: string;
|
|
17
|
-
user: {
|
|
18
|
-
email: string;
|
|
19
|
-
id: string;
|
|
20
|
-
name: string;
|
|
21
|
-
};
|
|
22
|
-
workspace: null | {
|
|
23
|
-
id: string;
|
|
24
|
-
name: string;
|
|
25
|
-
};
|
|
26
|
-
}
|
|
27
|
-
/**
|
|
28
|
-
* Match a user-supplied --instance value against the instance list:
|
|
29
|
-
* numeric values match by ID, URL/hostname values match by the instance
|
|
30
|
-
* origin's hostname, anything else matches by name. Exported for tests.
|
|
31
|
-
*/
|
|
32
|
-
export declare function matchInstance(instances: Instance[], query: string): Instance | undefined;
|
|
33
2
|
export default class Auth extends Command {
|
|
34
3
|
static description: string;
|
|
35
|
-
static enableJsonFlag: boolean;
|
|
36
4
|
static examples: string[];
|
|
37
5
|
static flags: {
|
|
38
|
-
branch: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
39
|
-
code: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
40
6
|
config: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
41
7
|
insecure: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
42
|
-
instance: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
43
8
|
'no-browser': import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
44
9
|
origin: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
45
|
-
profile: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
46
|
-
workspace: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
47
10
|
};
|
|
48
|
-
run(): Promise<
|
|
49
|
-
protected toErrorJson(err: unknown): unknown;
|
|
50
|
-
private acquireToken;
|
|
11
|
+
run(): Promise<void>;
|
|
51
12
|
private getHeaders;
|
|
52
13
|
private fetchBranches;
|
|
53
14
|
private fetchInstances;
|
|
@@ -55,11 +16,6 @@ export default class Auth extends Command {
|
|
|
55
16
|
private promptForToken;
|
|
56
17
|
private promptProfileName;
|
|
57
18
|
private readTokenFromStdin;
|
|
58
|
-
private resolveBranch;
|
|
59
|
-
private resolveInstance;
|
|
60
|
-
private resolveProfileName;
|
|
61
|
-
private resolveWorkspace;
|
|
62
|
-
private resolveWorkspaceAndBranch;
|
|
63
19
|
private saveProfile;
|
|
64
20
|
private selectBranch;
|
|
65
21
|
private selectInstance;
|
|
@@ -6,37 +6,9 @@ import * as http from 'node:http';
|
|
|
6
6
|
import { dirname } from 'node:path';
|
|
7
7
|
import open from 'open';
|
|
8
8
|
import { buildUserAgent, resolveCredentialsPath } from '../../base-command.js';
|
|
9
|
-
function originHostname(value) {
|
|
10
|
-
try {
|
|
11
|
-
return new URL(value.includes('://') ? value : `https://${value}`).hostname.toLowerCase();
|
|
12
|
-
}
|
|
13
|
-
catch {
|
|
14
|
-
return undefined;
|
|
15
|
-
}
|
|
16
|
-
}
|
|
17
|
-
/**
|
|
18
|
-
* Match a user-supplied --instance value against the instance list:
|
|
19
|
-
* numeric values match by ID, URL/hostname values match by the instance
|
|
20
|
-
* origin's hostname, anything else matches by name. Exported for tests.
|
|
21
|
-
*/
|
|
22
|
-
export function matchInstance(instances, query) {
|
|
23
|
-
const q = query.trim();
|
|
24
|
-
if (/^\d+$/.test(q)) {
|
|
25
|
-
return instances.find((inst) => inst.id === q);
|
|
26
|
-
}
|
|
27
|
-
if (q.includes('://') || q.includes('.')) {
|
|
28
|
-
const queryHost = originHostname(q);
|
|
29
|
-
const match = queryHost ? instances.find((inst) => originHostname(inst.origin) === queryHost) : undefined;
|
|
30
|
-
if (match) {
|
|
31
|
-
return match;
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
return instances.find((inst) => inst.name === q);
|
|
35
|
-
}
|
|
36
9
|
const AUTH_TIMEOUT_MS = 5 * 60 * 1000; // 5 minutes
|
|
37
10
|
export default class Auth extends Command {
|
|
38
11
|
static description = 'Authenticate with Xano via browser login';
|
|
39
|
-
static enableJsonFlag = true;
|
|
40
12
|
static examples = [
|
|
41
13
|
`$ xano auth
|
|
42
14
|
Opening browser for Xano login...
|
|
@@ -51,24 +23,10 @@ Opening browser for Xano login at https://custom.xano.com...`,
|
|
|
51
23
|
To authenticate, open the following URL in any browser:
|
|
52
24
|
https://app.xano.com/login?dest=cli&display=code
|
|
53
25
|
? Paste the code shown in your browser: ****`,
|
|
54
|
-
`$
|
|
55
|
-
(
|
|
56
|
-
`$ echo "$CODE" | xano auth --no-browser --instance my-instance --workspace 5 --branch dev --profile staging
|
|
57
|
-
(fully scripted: the code is read from piped stdin, no prompt at all)`,
|
|
58
|
-
`$ xano auth --code "$CODE" --instance 42 --workspace 5 --json
|
|
59
|
-
(machine-readable: prints the created profile as JSON)`,
|
|
26
|
+
`$ echo "$CODE" | xano auth --no-browser
|
|
27
|
+
(the code is read from piped stdin, no prompt at all)`,
|
|
60
28
|
];
|
|
61
29
|
static flags = {
|
|
62
|
-
branch: Flags.string({
|
|
63
|
-
char: 'b',
|
|
64
|
-
description: 'Pre-select a branch by label (skips the branch picker); pass "" to skip and use the live branch',
|
|
65
|
-
required: false,
|
|
66
|
-
}),
|
|
67
|
-
code: Flags.string({
|
|
68
|
-
description: 'Login code copied from the browser (implies --no-browser and runs fully non-interactively). Get the code at <origin>/login?dest=cli&display=code',
|
|
69
|
-
env: 'XANO_AUTH_CODE',
|
|
70
|
-
required: false,
|
|
71
|
-
}),
|
|
72
30
|
config: Flags.string({
|
|
73
31
|
char: 'c',
|
|
74
32
|
description: 'Path to credentials file (default: ~/.xano/credentials.yaml)',
|
|
@@ -80,11 +38,6 @@ To authenticate, open the following URL in any browser:
|
|
|
80
38
|
default: false,
|
|
81
39
|
description: 'Skip TLS certificate verification (for self-signed certificates)',
|
|
82
40
|
}),
|
|
83
|
-
instance: Flags.string({
|
|
84
|
-
char: 'i',
|
|
85
|
-
description: 'Pre-select an instance by name, numeric ID, or instance URL/hostname (skips the instance picker)',
|
|
86
|
-
required: false,
|
|
87
|
-
}),
|
|
88
41
|
'no-browser': Flags.boolean({
|
|
89
42
|
default: false,
|
|
90
43
|
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)',
|
|
@@ -94,16 +47,6 @@ To authenticate, open the following URL in any browser:
|
|
|
94
47
|
default: 'https://app.xano.com',
|
|
95
48
|
description: 'Xano account origin URL',
|
|
96
49
|
}),
|
|
97
|
-
profile: Flags.string({
|
|
98
|
-
char: 'p',
|
|
99
|
-
description: 'Profile name to save (skips the profile name prompt); pass "" to use the default name',
|
|
100
|
-
required: false,
|
|
101
|
-
}),
|
|
102
|
-
workspace: Flags.string({
|
|
103
|
-
char: 'w',
|
|
104
|
-
description: 'Pre-select a workspace by ID or name (skips the workspace picker); pass "" to skip workspace',
|
|
105
|
-
required: false,
|
|
106
|
-
}),
|
|
107
50
|
};
|
|
108
51
|
async run() {
|
|
109
52
|
const { flags } = await this.parse(Auth);
|
|
@@ -111,13 +54,12 @@ To authenticate, open the following URL in any browser:
|
|
|
111
54
|
process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0';
|
|
112
55
|
this.warn('TLS certificate verification is disabled (insecure mode)');
|
|
113
56
|
}
|
|
114
|
-
// A supplied code (flag or env), piped stdin, or --json output means no
|
|
115
|
-
// prompt can ever open: every unanswered picker falls back to its default.
|
|
116
|
-
const nonInteractive = flags.code !== undefined || (flags['no-browser'] && !process.stdin.isTTY) || this.jsonEnabled();
|
|
117
57
|
try {
|
|
118
|
-
// Step 1: Get token via
|
|
58
|
+
// Step 1: Get token via browser auth
|
|
119
59
|
this.log('Starting authentication flow...');
|
|
120
|
-
const token =
|
|
60
|
+
const token = flags['no-browser']
|
|
61
|
+
? await this.promptForToken(flags.origin)
|
|
62
|
+
: await this.startAuthServer(flags.origin);
|
|
121
63
|
// Step 2: Validate token and get user info
|
|
122
64
|
this.log('');
|
|
123
65
|
this.log('Validating authentication...');
|
|
@@ -129,9 +71,6 @@ To authenticate, open the following URL in any browser:
|
|
|
129
71
|
const isSelfHosted = !/^https:\/\/app\.(.*\.)?xano\.com$/.test(flags.origin);
|
|
130
72
|
let instance;
|
|
131
73
|
if (isSelfHosted) {
|
|
132
|
-
if (flags.instance) {
|
|
133
|
-
this.warn('Ignoring --instance: the origin itself is the instance for self-hosted Xano.');
|
|
134
|
-
}
|
|
135
74
|
instance = {
|
|
136
75
|
display: flags.origin,
|
|
137
76
|
id: 'self-hosted',
|
|
@@ -146,15 +85,31 @@ To authenticate, open the following URL in any browser:
|
|
|
146
85
|
if (instances.length === 0) {
|
|
147
86
|
this.error('No instances found. Please check your account.');
|
|
148
87
|
}
|
|
149
|
-
instance = await this.
|
|
88
|
+
instance = await this.selectInstance(instances);
|
|
89
|
+
}
|
|
90
|
+
// Step 4: Workspace selection
|
|
91
|
+
let workspace;
|
|
92
|
+
let branch;
|
|
93
|
+
this.log('');
|
|
94
|
+
this.log('Fetching available workspaces...');
|
|
95
|
+
const workspaces = await this.fetchWorkspaces(token, instance.origin);
|
|
96
|
+
if (workspaces.length > 0) {
|
|
97
|
+
workspace = await this.selectWorkspace(workspaces);
|
|
98
|
+
if (workspace) {
|
|
99
|
+
// Step 5: Branch selection
|
|
100
|
+
this.log('');
|
|
101
|
+
this.log('Fetching available branches...');
|
|
102
|
+
const branches = await this.fetchBranches(token, instance.origin, workspace.id);
|
|
103
|
+
if (branches.length > 1) {
|
|
104
|
+
branch = await this.selectBranch(branches);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
150
107
|
}
|
|
151
|
-
// Steps 4 + 5: Workspace and branch selection
|
|
152
|
-
const { branch, workspace } = await this.resolveWorkspaceAndBranch(token, instance.origin, flags, nonInteractive);
|
|
153
108
|
// Step 6: Profile name
|
|
154
109
|
this.log('');
|
|
155
|
-
const profileName = await this.
|
|
110
|
+
const profileName = await this.promptProfileName();
|
|
156
111
|
// Step 7: Save profile
|
|
157
|
-
|
|
112
|
+
await this.saveProfile({
|
|
158
113
|
access_token: token,
|
|
159
114
|
account_origin: flags.origin,
|
|
160
115
|
branch,
|
|
@@ -165,17 +120,6 @@ To authenticate, open the following URL in any browser:
|
|
|
165
120
|
}, flags.config);
|
|
166
121
|
this.log('');
|
|
167
122
|
this.log(`Profile '${profileName}' created successfully!`);
|
|
168
|
-
const result = {
|
|
169
|
-
branch: branch ?? null,
|
|
170
|
-
credentialsPath,
|
|
171
|
-
instance: { id: instance.id, name: instance.name, origin: instance.origin },
|
|
172
|
-
profile: profileName,
|
|
173
|
-
user: { email: user.email, id: user.id, name: user.name },
|
|
174
|
-
workspace: workspace ? { id: workspace.id, name: workspace.name } : null,
|
|
175
|
-
};
|
|
176
|
-
if (this.jsonEnabled()) {
|
|
177
|
-
this.logJson(result);
|
|
178
|
-
}
|
|
179
123
|
// Ensure clean exit (the open() call can keep the event loop alive)
|
|
180
124
|
process.exit(0);
|
|
181
125
|
}
|
|
@@ -185,33 +129,11 @@ To authenticate, open the following URL in any browser:
|
|
|
185
129
|
// @inquirer/core, so the thrown class won't match an imported one.
|
|
186
130
|
if (error?.name === 'ExitPromptError') {
|
|
187
131
|
this.log('Authentication cancelled.');
|
|
188
|
-
|
|
132
|
+
return;
|
|
189
133
|
}
|
|
190
134
|
throw error;
|
|
191
135
|
}
|
|
192
136
|
}
|
|
193
|
-
// oclif's default toErrorJson serializes Error objects to {} (message is a
|
|
194
|
-
// non-enumerable property), leaving --json consumers with no error detail.
|
|
195
|
-
toErrorJson(err) {
|
|
196
|
-
const error = err;
|
|
197
|
-
return {
|
|
198
|
-
error: {
|
|
199
|
-
...(error.code ? { code: error.code } : {}),
|
|
200
|
-
message: error.message ?? String(err),
|
|
201
|
-
...(error.suggestions?.length ? { suggestions: error.suggestions } : {}),
|
|
202
|
-
},
|
|
203
|
-
};
|
|
204
|
-
}
|
|
205
|
-
async acquireToken(code, noBrowser, origin) {
|
|
206
|
-
if (code !== undefined) {
|
|
207
|
-
const token = code.trim();
|
|
208
|
-
if (token === '') {
|
|
209
|
-
this.error(`No code provided. Copy it from ${origin}/login?dest=cli&display=code and pass it via --code or XANO_AUTH_CODE.`);
|
|
210
|
-
}
|
|
211
|
-
return token;
|
|
212
|
-
}
|
|
213
|
-
return noBrowser ? this.promptForToken(origin) : this.startAuthServer(origin);
|
|
214
|
-
}
|
|
215
137
|
getHeaders(accessToken) {
|
|
216
138
|
return {
|
|
217
139
|
'User-Agent': buildUserAgent(this.config.version),
|
|
@@ -259,7 +181,7 @@ To authenticate, open the following URL in any browser:
|
|
|
259
181
|
if (Array.isArray(data)) {
|
|
260
182
|
return data.map((inst) => ({
|
|
261
183
|
display: inst.display,
|
|
262
|
-
id:
|
|
184
|
+
id: inst.id || inst.name,
|
|
263
185
|
name: inst.name,
|
|
264
186
|
origin: new URL(inst.meta_api).origin,
|
|
265
187
|
}));
|
|
@@ -356,95 +278,6 @@ To authenticate, open the following URL in any browser:
|
|
|
356
278
|
process.stdin.on('error', () => resolve(data.trim()));
|
|
357
279
|
});
|
|
358
280
|
}
|
|
359
|
-
async resolveBranch(branches, flagValue, nonInteractive) {
|
|
360
|
-
if (flagValue !== undefined) {
|
|
361
|
-
// An empty value means "skip and use live branch" (same as the picker's skip option)
|
|
362
|
-
if (flagValue.trim() === '') {
|
|
363
|
-
this.log('Using live branch');
|
|
364
|
-
return undefined;
|
|
365
|
-
}
|
|
366
|
-
const match = branches.find((br) => br.label === flagValue || br.id === flagValue);
|
|
367
|
-
if (!match) {
|
|
368
|
-
this.error(`Branch '${flagValue}' not found. Available branches: ${branches.map((br) => br.label).join(', ')}`);
|
|
369
|
-
}
|
|
370
|
-
this.log(`Using branch: ${match.label}`);
|
|
371
|
-
return match.id;
|
|
372
|
-
}
|
|
373
|
-
if (nonInteractive) {
|
|
374
|
-
this.log('Using live branch (non-interactive; pass --branch to select one)');
|
|
375
|
-
return undefined;
|
|
376
|
-
}
|
|
377
|
-
return branches.length > 1 ? this.selectBranch(branches) : undefined;
|
|
378
|
-
}
|
|
379
|
-
async resolveInstance(instances, flagValue, nonInteractive) {
|
|
380
|
-
if (flagValue) {
|
|
381
|
-
const match = matchInstance(instances, flagValue);
|
|
382
|
-
if (!match) {
|
|
383
|
-
this.error(`Instance '${flagValue}' not found (match by name, numeric ID, or instance URL). Available instances: ${instances.map((inst) => `${inst.name} (${inst.id})`).join(', ')}`);
|
|
384
|
-
}
|
|
385
|
-
this.log(`Using instance: ${match.name} (${match.display})`);
|
|
386
|
-
return match;
|
|
387
|
-
}
|
|
388
|
-
if (nonInteractive) {
|
|
389
|
-
if (instances.length === 1) {
|
|
390
|
-
this.log(`Using instance: ${instances[0].name} (${instances[0].display})`);
|
|
391
|
-
return instances[0];
|
|
392
|
-
}
|
|
393
|
-
this.error(`Multiple instances available; pass --instance to select one. Available instances: ${instances.map((inst) => `${inst.name} (${inst.id})`).join(', ')}`);
|
|
394
|
-
}
|
|
395
|
-
return this.selectInstance(instances);
|
|
396
|
-
}
|
|
397
|
-
async resolveProfileName(flagValue, nonInteractive) {
|
|
398
|
-
// An empty --profile value means "use the default name" (same as accepting the prompt's default);
|
|
399
|
-
// non-interactive runs with no --profile fall back to the default name too.
|
|
400
|
-
if (flagValue !== undefined) {
|
|
401
|
-
return flagValue.trim() || 'default';
|
|
402
|
-
}
|
|
403
|
-
return nonInteractive ? 'default' : this.promptProfileName();
|
|
404
|
-
}
|
|
405
|
-
async resolveWorkspace(workspaces, flagValue, nonInteractive) {
|
|
406
|
-
if (flagValue !== undefined) {
|
|
407
|
-
// An empty value means "skip workspace" (same as the picker's skip option)
|
|
408
|
-
if (flagValue.trim() === '') {
|
|
409
|
-
this.log('Skipping workspace selection');
|
|
410
|
-
return undefined;
|
|
411
|
-
}
|
|
412
|
-
const match = workspaces.find((ws) => String(ws.id) === flagValue || ws.name === flagValue);
|
|
413
|
-
if (!match) {
|
|
414
|
-
this.error(`Workspace '${flagValue}' not found. Available workspaces: ${workspaces.map((ws) => `${ws.name} (${ws.id})`).join(', ')}`);
|
|
415
|
-
}
|
|
416
|
-
this.log(`Using workspace: ${match.name} (${match.id})`);
|
|
417
|
-
return match;
|
|
418
|
-
}
|
|
419
|
-
if (nonInteractive) {
|
|
420
|
-
this.log('Skipping workspace selection (non-interactive; pass --workspace to select one)');
|
|
421
|
-
return undefined;
|
|
422
|
-
}
|
|
423
|
-
return this.selectWorkspace(workspaces);
|
|
424
|
-
}
|
|
425
|
-
async resolveWorkspaceAndBranch(token, instanceOrigin, flags, nonInteractive) {
|
|
426
|
-
let workspace;
|
|
427
|
-
let branch;
|
|
428
|
-
this.log('');
|
|
429
|
-
this.log('Fetching available workspaces...');
|
|
430
|
-
const workspaces = await this.fetchWorkspaces(token, instanceOrigin);
|
|
431
|
-
if (workspaces.length > 0) {
|
|
432
|
-
workspace = await this.resolveWorkspace(workspaces, flags.workspace, nonInteractive);
|
|
433
|
-
if (workspace) {
|
|
434
|
-
this.log('');
|
|
435
|
-
this.log('Fetching available branches...');
|
|
436
|
-
const branches = await this.fetchBranches(token, instanceOrigin, workspace.id);
|
|
437
|
-
branch = await this.resolveBranch(branches, flags.branch, nonInteractive);
|
|
438
|
-
}
|
|
439
|
-
}
|
|
440
|
-
else if (flags.workspace) {
|
|
441
|
-
this.error(`Workspace '${flags.workspace}' not found: no workspaces are available on this instance.`);
|
|
442
|
-
}
|
|
443
|
-
if (flags.branch && !workspace) {
|
|
444
|
-
this.warn('Ignoring --branch: no workspace selected.');
|
|
445
|
-
}
|
|
446
|
-
return { branch, workspace };
|
|
447
|
-
}
|
|
448
281
|
async saveProfile(profile, configPath) {
|
|
449
282
|
const credentialsPath = resolveCredentialsPath(configPath);
|
|
450
283
|
const credDir = dirname(credentialsPath);
|
|
@@ -484,7 +317,6 @@ To authenticate, open the following URL in any browser:
|
|
|
484
317
|
noRefs: true,
|
|
485
318
|
});
|
|
486
319
|
fs.writeFileSync(credentialsPath, yamlContent, 'utf8');
|
|
487
|
-
return credentialsPath;
|
|
488
320
|
}
|
|
489
321
|
async selectBranch(branches) {
|
|
490
322
|
const { selectedBranch } = await inquirer.prompt([
|
|
@@ -1,12 +1,10 @@
|
|
|
1
|
-
import BaseCommand from '
|
|
2
|
-
export default class
|
|
3
|
-
static args: {
|
|
4
|
-
tenant_name: import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
|
|
5
|
-
};
|
|
1
|
+
import BaseCommand from '../../../base-command.js';
|
|
2
|
+
export default class KnowledgePull extends BaseCommand {
|
|
6
3
|
static description: string;
|
|
7
4
|
static examples: string[];
|
|
8
5
|
static flags: {
|
|
9
|
-
|
|
6
|
+
branch: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
7
|
+
directory: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
10
8
|
workspace: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
11
9
|
config: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
12
10
|
profile: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|