@xano/cli 1.0.3-beta.7 → 1.0.3-beta.9
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 +23 -0
- package/dist/commands/auth/index.d.ts +8 -0
- package/dist/commands/auth/index.js +103 -6
- package/oclif.manifest.json +3120 -3082
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -47,6 +47,13 @@ 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 ""
|
|
50
57
|
```
|
|
51
58
|
|
|
52
59
|
The default flow starts a temporary callback server on `127.0.0.1` and waits
|
|
@@ -55,6 +62,22 @@ containers, or locked-down networks where the browser can't reach the CLI's
|
|
|
55
62
|
loopback address, use `--no-browser`: the CLI prints a login URL, you open it
|
|
56
63
|
in any browser, and paste back the code it displays. No local server required.
|
|
57
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
|
+
|
|
58
81
|
If you can't run `xano auth` at all, you can always create a profile manually
|
|
59
82
|
with a Metadata API token from the Xano dashboard — see
|
|
60
83
|
[Profiles](#profiles) below.
|
|
@@ -3,10 +3,14 @@ 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>;
|
|
8
10
|
'no-browser': import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
9
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>;
|
|
10
14
|
};
|
|
11
15
|
run(): Promise<void>;
|
|
12
16
|
private getHeaders;
|
|
@@ -15,6 +19,10 @@ export default class Auth extends Command {
|
|
|
15
19
|
private fetchWorkspaces;
|
|
16
20
|
private promptForToken;
|
|
17
21
|
private promptProfileName;
|
|
22
|
+
private readTokenFromStdin;
|
|
23
|
+
private resolveBranch;
|
|
24
|
+
private resolveInstance;
|
|
25
|
+
private resolveWorkspace;
|
|
18
26
|
private saveProfile;
|
|
19
27
|
private selectBranch;
|
|
20
28
|
private selectInstance;
|
|
@@ -23,8 +23,17 @@ Opening browser for Xano login at https://custom.xano.com...`,
|
|
|
23
23
|
To authenticate, open the following URL in any browser:
|
|
24
24
|
https://app.xano.com/login?dest=cli&display=code
|
|
25
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)`,
|
|
26
30
|
];
|
|
27
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
|
+
}),
|
|
28
37
|
config: Flags.string({
|
|
29
38
|
char: 'c',
|
|
30
39
|
description: 'Path to credentials file (default: ~/.xano/credentials.yaml)',
|
|
@@ -36,6 +45,11 @@ To authenticate, open the following URL in any browser:
|
|
|
36
45
|
default: false,
|
|
37
46
|
description: 'Skip TLS certificate verification (for self-signed certificates)',
|
|
38
47
|
}),
|
|
48
|
+
instance: Flags.string({
|
|
49
|
+
char: 'i',
|
|
50
|
+
description: 'Pre-select an instance by name (skips the instance picker)',
|
|
51
|
+
required: false,
|
|
52
|
+
}),
|
|
39
53
|
'no-browser': Flags.boolean({
|
|
40
54
|
default: false,
|
|
41
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)',
|
|
@@ -45,6 +59,16 @@ To authenticate, open the following URL in any browser:
|
|
|
45
59
|
default: 'https://app.xano.com',
|
|
46
60
|
description: 'Xano account origin URL',
|
|
47
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
|
+
}),
|
|
48
72
|
};
|
|
49
73
|
async run() {
|
|
50
74
|
const { flags } = await this.parse(Auth);
|
|
@@ -69,6 +93,9 @@ To authenticate, open the following URL in any browser:
|
|
|
69
93
|
const isSelfHosted = !/^https:\/\/app\.(.*\.)?xano\.com$/.test(flags.origin);
|
|
70
94
|
let instance;
|
|
71
95
|
if (isSelfHosted) {
|
|
96
|
+
if (flags.instance) {
|
|
97
|
+
this.warn('Ignoring --instance: the origin itself is the instance for self-hosted Xano.');
|
|
98
|
+
}
|
|
72
99
|
instance = {
|
|
73
100
|
display: flags.origin,
|
|
74
101
|
id: 'self-hosted',
|
|
@@ -83,7 +110,7 @@ To authenticate, open the following URL in any browser:
|
|
|
83
110
|
if (instances.length === 0) {
|
|
84
111
|
this.error('No instances found. Please check your account.');
|
|
85
112
|
}
|
|
86
|
-
instance = await this.
|
|
113
|
+
instance = await this.resolveInstance(instances, flags.instance);
|
|
87
114
|
}
|
|
88
115
|
// Step 4: Workspace selection
|
|
89
116
|
let workspace;
|
|
@@ -92,20 +119,25 @@ To authenticate, open the following URL in any browser:
|
|
|
92
119
|
this.log('Fetching available workspaces...');
|
|
93
120
|
const workspaces = await this.fetchWorkspaces(token, instance.origin);
|
|
94
121
|
if (workspaces.length > 0) {
|
|
95
|
-
workspace = await this.
|
|
122
|
+
workspace = await this.resolveWorkspace(workspaces, flags.workspace);
|
|
96
123
|
if (workspace) {
|
|
97
124
|
// Step 5: Branch selection
|
|
98
125
|
this.log('');
|
|
99
126
|
this.log('Fetching available branches...');
|
|
100
127
|
const branches = await this.fetchBranches(token, instance.origin, workspace.id);
|
|
101
|
-
|
|
102
|
-
branch = await this.selectBranch(branches);
|
|
103
|
-
}
|
|
128
|
+
branch = await this.resolveBranch(branches, flags.branch);
|
|
104
129
|
}
|
|
105
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
|
+
}
|
|
106
137
|
// Step 6: Profile name
|
|
107
138
|
this.log('');
|
|
108
|
-
|
|
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';
|
|
109
141
|
// Step 7: Save profile
|
|
110
142
|
await this.saveProfile({
|
|
111
143
|
access_token: token,
|
|
@@ -218,6 +250,17 @@ To authenticate, open the following URL in any browser:
|
|
|
218
250
|
this.log('');
|
|
219
251
|
this.log(` ${authUrl}`);
|
|
220
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
|
+
}
|
|
221
264
|
try {
|
|
222
265
|
await open(authUrl);
|
|
223
266
|
}
|
|
@@ -254,6 +297,60 @@ To authenticate, open the following URL in any browser:
|
|
|
254
297
|
]);
|
|
255
298
|
return profileName.trim() || 'default';
|
|
256
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
|
+
}
|
|
257
354
|
async saveProfile(profile, configPath) {
|
|
258
355
|
const credentialsPath = resolveCredentialsPath(configPath);
|
|
259
356
|
const credDir = dirname(credentialsPath);
|