@xano/cli 1.0.3-beta.6 → 1.0.3-beta.8
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 +26 -0
- package/dist/commands/auth/index.d.ts +9 -0
- package/dist/commands/auth/index.js +122 -9
- package/oclif.manifest.json +2108 -2064
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -46,8 +46,34 @@ 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
|
+
If you can't run `xano auth` at all, you can always create a profile manually
|
|
74
|
+
with a Metadata API token from the Xano dashboard — see
|
|
75
|
+
[Profiles](#profiles) below.
|
|
76
|
+
|
|
51
77
|
### Profiles
|
|
52
78
|
|
|
53
79
|
Profiles store your Xano credentials and default workspace settings.
|
|
@@ -3,16 +3,25 @@ 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 resolveBranch;
|
|
23
|
+
private resolveInstance;
|
|
24
|
+
private resolveWorkspace;
|
|
16
25
|
private saveProfile;
|
|
17
26
|
private selectBranch;
|
|
18
27
|
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,19 @@ 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)`,
|
|
23
28
|
];
|
|
24
29
|
static flags = {
|
|
30
|
+
branch: Flags.string({
|
|
31
|
+
char: 'b',
|
|
32
|
+
description: 'Pre-select a branch by label (skips the branch picker); pass "" to skip and use the live branch',
|
|
33
|
+
required: false,
|
|
34
|
+
}),
|
|
25
35
|
config: Flags.string({
|
|
26
36
|
char: 'c',
|
|
27
37
|
description: 'Path to credentials file (default: ~/.xano/credentials.yaml)',
|
|
@@ -33,11 +43,30 @@ Opening browser for Xano login at https://custom.xano.com...`,
|
|
|
33
43
|
default: false,
|
|
34
44
|
description: 'Skip TLS certificate verification (for self-signed certificates)',
|
|
35
45
|
}),
|
|
46
|
+
instance: Flags.string({
|
|
47
|
+
char: 'i',
|
|
48
|
+
description: 'Pre-select an instance by name (skips the instance picker)',
|
|
49
|
+
required: false,
|
|
50
|
+
}),
|
|
51
|
+
'no-browser': Flags.boolean({
|
|
52
|
+
default: false,
|
|
53
|
+
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)',
|
|
54
|
+
}),
|
|
36
55
|
origin: Flags.string({
|
|
37
56
|
char: 'o',
|
|
38
57
|
default: 'https://app.xano.com',
|
|
39
58
|
description: 'Xano account origin URL',
|
|
40
59
|
}),
|
|
60
|
+
profile: Flags.string({
|
|
61
|
+
char: 'p',
|
|
62
|
+
description: 'Profile name to save (skips the profile name prompt); pass "" to use the default name',
|
|
63
|
+
required: false,
|
|
64
|
+
}),
|
|
65
|
+
workspace: Flags.string({
|
|
66
|
+
char: 'w',
|
|
67
|
+
description: 'Pre-select a workspace by ID or name (skips the workspace picker); pass "" to skip workspace',
|
|
68
|
+
required: false,
|
|
69
|
+
}),
|
|
41
70
|
};
|
|
42
71
|
async run() {
|
|
43
72
|
const { flags } = await this.parse(Auth);
|
|
@@ -48,7 +77,9 @@ Opening browser for Xano login at https://custom.xano.com...`,
|
|
|
48
77
|
try {
|
|
49
78
|
// Step 1: Get token via browser auth
|
|
50
79
|
this.log('Starting authentication flow...');
|
|
51
|
-
const token =
|
|
80
|
+
const token = flags['no-browser']
|
|
81
|
+
? await this.promptForToken(flags.origin)
|
|
82
|
+
: await this.startAuthServer(flags.origin);
|
|
52
83
|
// Step 2: Validate token and get user info
|
|
53
84
|
this.log('');
|
|
54
85
|
this.log('Validating authentication...');
|
|
@@ -60,6 +91,9 @@ Opening browser for Xano login at https://custom.xano.com...`,
|
|
|
60
91
|
const isSelfHosted = !/^https:\/\/app\.(.*\.)?xano\.com$/.test(flags.origin);
|
|
61
92
|
let instance;
|
|
62
93
|
if (isSelfHosted) {
|
|
94
|
+
if (flags.instance) {
|
|
95
|
+
this.warn('Ignoring --instance: the origin itself is the instance for self-hosted Xano.');
|
|
96
|
+
}
|
|
63
97
|
instance = {
|
|
64
98
|
display: flags.origin,
|
|
65
99
|
id: 'self-hosted',
|
|
@@ -74,7 +108,7 @@ Opening browser for Xano login at https://custom.xano.com...`,
|
|
|
74
108
|
if (instances.length === 0) {
|
|
75
109
|
this.error('No instances found. Please check your account.');
|
|
76
110
|
}
|
|
77
|
-
instance = await this.
|
|
111
|
+
instance = await this.resolveInstance(instances, flags.instance);
|
|
78
112
|
}
|
|
79
113
|
// Step 4: Workspace selection
|
|
80
114
|
let workspace;
|
|
@@ -83,20 +117,25 @@ Opening browser for Xano login at https://custom.xano.com...`,
|
|
|
83
117
|
this.log('Fetching available workspaces...');
|
|
84
118
|
const workspaces = await this.fetchWorkspaces(token, instance.origin);
|
|
85
119
|
if (workspaces.length > 0) {
|
|
86
|
-
workspace = await this.
|
|
120
|
+
workspace = await this.resolveWorkspace(workspaces, flags.workspace);
|
|
87
121
|
if (workspace) {
|
|
88
122
|
// Step 5: Branch selection
|
|
89
123
|
this.log('');
|
|
90
124
|
this.log('Fetching available branches...');
|
|
91
125
|
const branches = await this.fetchBranches(token, instance.origin, workspace.id);
|
|
92
|
-
|
|
93
|
-
branch = await this.selectBranch(branches);
|
|
94
|
-
}
|
|
126
|
+
branch = await this.resolveBranch(branches, flags.branch);
|
|
95
127
|
}
|
|
96
128
|
}
|
|
129
|
+
else if (flags.workspace) {
|
|
130
|
+
this.error(`Workspace '${flags.workspace}' not found: no workspaces are available on this instance.`);
|
|
131
|
+
}
|
|
132
|
+
if (flags.branch && !workspace) {
|
|
133
|
+
this.warn('Ignoring --branch: no workspace selected.');
|
|
134
|
+
}
|
|
97
135
|
// Step 6: Profile name
|
|
98
136
|
this.log('');
|
|
99
|
-
|
|
137
|
+
// An empty --profile value means "use the default name" (same as accepting the prompt's default)
|
|
138
|
+
const profileName = flags.profile === undefined ? await this.promptProfileName() : flags.profile.trim() || 'default';
|
|
100
139
|
// Step 7: Save profile
|
|
101
140
|
await this.saveProfile({
|
|
102
141
|
access_token: token,
|
|
@@ -113,7 +152,10 @@ Opening browser for Xano login at https://custom.xano.com...`,
|
|
|
113
152
|
process.exit(0);
|
|
114
153
|
}
|
|
115
154
|
catch (error) {
|
|
116
|
-
|
|
155
|
+
// Ctrl+C at an inquirer prompt throws ExitPromptError. Match on the name
|
|
156
|
+
// rather than `instanceof`: inquirer bundles its own copy of
|
|
157
|
+
// @inquirer/core, so the thrown class won't match an imported one.
|
|
158
|
+
if (error?.name === 'ExitPromptError') {
|
|
117
159
|
this.log('Authentication cancelled.');
|
|
118
160
|
return;
|
|
119
161
|
}
|
|
@@ -196,6 +238,34 @@ Opening browser for Xano login at https://custom.xano.com...`,
|
|
|
196
238
|
return [];
|
|
197
239
|
}
|
|
198
240
|
}
|
|
241
|
+
async promptForToken(origin) {
|
|
242
|
+
// Headless flow: no local callback server. The login page, when opened
|
|
243
|
+
// without a `callback` param, renders the access token on screen for the
|
|
244
|
+
// user to copy. We point the browser there (best-effort) and prompt for
|
|
245
|
+
// the pasted code.
|
|
246
|
+
const authUrl = `${origin}/login?dest=cli&display=code`;
|
|
247
|
+
this.log('To authenticate, open the following URL in any browser:');
|
|
248
|
+
this.log('');
|
|
249
|
+
this.log(` ${authUrl}`);
|
|
250
|
+
this.log('');
|
|
251
|
+
try {
|
|
252
|
+
await open(authUrl);
|
|
253
|
+
}
|
|
254
|
+
catch {
|
|
255
|
+
// Best-effort only; the URL is already printed above for manual use.
|
|
256
|
+
}
|
|
257
|
+
const { token } = await inquirer.prompt([
|
|
258
|
+
{
|
|
259
|
+
message: 'Paste the code shown in your browser',
|
|
260
|
+
name: 'token',
|
|
261
|
+
type: 'password',
|
|
262
|
+
validate(input) {
|
|
263
|
+
return input.trim() === '' ? 'A code is required' : true;
|
|
264
|
+
},
|
|
265
|
+
},
|
|
266
|
+
]);
|
|
267
|
+
return token.trim();
|
|
268
|
+
}
|
|
199
269
|
async promptProfileName() {
|
|
200
270
|
const { profileName } = await inquirer.prompt([
|
|
201
271
|
{
|
|
@@ -214,6 +284,49 @@ Opening browser for Xano login at https://custom.xano.com...`,
|
|
|
214
284
|
]);
|
|
215
285
|
return profileName.trim() || 'default';
|
|
216
286
|
}
|
|
287
|
+
async resolveBranch(branches, flagValue) {
|
|
288
|
+
if (flagValue !== undefined) {
|
|
289
|
+
// An empty value means "skip and use live branch" (same as the picker's skip option)
|
|
290
|
+
if (flagValue.trim() === '') {
|
|
291
|
+
this.log('Using live branch');
|
|
292
|
+
return undefined;
|
|
293
|
+
}
|
|
294
|
+
const match = branches.find((br) => br.label === flagValue || br.id === flagValue);
|
|
295
|
+
if (!match) {
|
|
296
|
+
this.error(`Branch '${flagValue}' not found. Available branches: ${branches.map((br) => br.label).join(', ')}`);
|
|
297
|
+
}
|
|
298
|
+
this.log(`Using branch: ${match.label}`);
|
|
299
|
+
return match.id;
|
|
300
|
+
}
|
|
301
|
+
return branches.length > 1 ? this.selectBranch(branches) : undefined;
|
|
302
|
+
}
|
|
303
|
+
async resolveInstance(instances, flagValue) {
|
|
304
|
+
if (flagValue) {
|
|
305
|
+
const match = instances.find((inst) => inst.name === flagValue || inst.id === flagValue);
|
|
306
|
+
if (!match) {
|
|
307
|
+
this.error(`Instance '${flagValue}' not found. Available instances: ${instances.map((inst) => inst.name).join(', ')}`);
|
|
308
|
+
}
|
|
309
|
+
this.log(`Using instance: ${match.name} (${match.display})`);
|
|
310
|
+
return match;
|
|
311
|
+
}
|
|
312
|
+
return this.selectInstance(instances);
|
|
313
|
+
}
|
|
314
|
+
async resolveWorkspace(workspaces, flagValue) {
|
|
315
|
+
if (flagValue !== undefined) {
|
|
316
|
+
// An empty value means "skip workspace" (same as the picker's skip option)
|
|
317
|
+
if (flagValue.trim() === '') {
|
|
318
|
+
this.log('Skipping workspace selection');
|
|
319
|
+
return undefined;
|
|
320
|
+
}
|
|
321
|
+
const match = workspaces.find((ws) => String(ws.id) === flagValue || ws.name === flagValue);
|
|
322
|
+
if (!match) {
|
|
323
|
+
this.error(`Workspace '${flagValue}' not found. Available workspaces: ${workspaces.map((ws) => `${ws.name} (${ws.id})`).join(', ')}`);
|
|
324
|
+
}
|
|
325
|
+
this.log(`Using workspace: ${match.name} (${match.id})`);
|
|
326
|
+
return match;
|
|
327
|
+
}
|
|
328
|
+
return this.selectWorkspace(workspaces);
|
|
329
|
+
}
|
|
217
330
|
async saveProfile(profile, configPath) {
|
|
218
331
|
const credentialsPath = resolveCredentialsPath(configPath);
|
|
219
332
|
const credDir = dirname(credentialsPath);
|