@xano/cli 1.0.3-beta.5 → 1.0.3-beta.7
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 +11 -0
- package/dist/commands/auth/index.d.ts +2 -0
- package/dist/commands/auth/index.js +43 -3
- package/dist/utils/multidoc-push.js +20 -16
- package/oclif.manifest.json +2494 -2487
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -46,8 +46,19 @@ 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)
|
|
49
50
|
```
|
|
50
51
|
|
|
52
|
+
The default flow starts a temporary callback server on `127.0.0.1` and waits
|
|
53
|
+
for the browser to redirect back to it. On remote/SSH sessions, Docker
|
|
54
|
+
containers, or locked-down networks where the browser can't reach the CLI's
|
|
55
|
+
loopback address, use `--no-browser`: the CLI prints a login URL, you open it
|
|
56
|
+
in any browser, and paste back the code it displays. No local server required.
|
|
57
|
+
|
|
58
|
+
If you can't run `xano auth` at all, you can always create a profile manually
|
|
59
|
+
with a Metadata API token from the Xano dashboard — see
|
|
60
|
+
[Profiles](#profiles) below.
|
|
61
|
+
|
|
51
62
|
### Profiles
|
|
52
63
|
|
|
53
64
|
Profiles store your Xano credentials and default workspace settings.
|
|
@@ -5,6 +5,7 @@ export default class Auth extends Command {
|
|
|
5
5
|
static flags: {
|
|
6
6
|
config: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
7
7
|
insecure: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
8
|
+
'no-browser': import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
8
9
|
origin: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
9
10
|
};
|
|
10
11
|
run(): Promise<void>;
|
|
@@ -12,6 +13,7 @@ export default class Auth extends Command {
|
|
|
12
13
|
private fetchBranches;
|
|
13
14
|
private fetchInstances;
|
|
14
15
|
private fetchWorkspaces;
|
|
16
|
+
private promptForToken;
|
|
15
17
|
private promptProfileName;
|
|
16
18
|
private saveProfile;
|
|
17
19
|
private selectBranch;
|
|
@@ -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,6 +19,10 @@ 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: ****`,
|
|
23
26
|
];
|
|
24
27
|
static flags = {
|
|
25
28
|
config: Flags.string({
|
|
@@ -33,6 +36,10 @@ Opening browser for Xano login at https://custom.xano.com...`,
|
|
|
33
36
|
default: false,
|
|
34
37
|
description: 'Skip TLS certificate verification (for self-signed certificates)',
|
|
35
38
|
}),
|
|
39
|
+
'no-browser': Flags.boolean({
|
|
40
|
+
default: false,
|
|
41
|
+
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)',
|
|
42
|
+
}),
|
|
36
43
|
origin: Flags.string({
|
|
37
44
|
char: 'o',
|
|
38
45
|
default: 'https://app.xano.com',
|
|
@@ -48,7 +55,9 @@ Opening browser for Xano login at https://custom.xano.com...`,
|
|
|
48
55
|
try {
|
|
49
56
|
// Step 1: Get token via browser auth
|
|
50
57
|
this.log('Starting authentication flow...');
|
|
51
|
-
const token =
|
|
58
|
+
const token = flags['no-browser']
|
|
59
|
+
? await this.promptForToken(flags.origin)
|
|
60
|
+
: await this.startAuthServer(flags.origin);
|
|
52
61
|
// Step 2: Validate token and get user info
|
|
53
62
|
this.log('');
|
|
54
63
|
this.log('Validating authentication...');
|
|
@@ -113,7 +122,10 @@ Opening browser for Xano login at https://custom.xano.com...`,
|
|
|
113
122
|
process.exit(0);
|
|
114
123
|
}
|
|
115
124
|
catch (error) {
|
|
116
|
-
|
|
125
|
+
// Ctrl+C at an inquirer prompt throws ExitPromptError. Match on the name
|
|
126
|
+
// rather than `instanceof`: inquirer bundles its own copy of
|
|
127
|
+
// @inquirer/core, so the thrown class won't match an imported one.
|
|
128
|
+
if (error?.name === 'ExitPromptError') {
|
|
117
129
|
this.log('Authentication cancelled.');
|
|
118
130
|
return;
|
|
119
131
|
}
|
|
@@ -196,6 +208,34 @@ Opening browser for Xano login at https://custom.xano.com...`,
|
|
|
196
208
|
return [];
|
|
197
209
|
}
|
|
198
210
|
}
|
|
211
|
+
async promptForToken(origin) {
|
|
212
|
+
// Headless flow: no local callback server. The login page, when opened
|
|
213
|
+
// without a `callback` param, renders the access token on screen for the
|
|
214
|
+
// user to copy. We point the browser there (best-effort) and prompt for
|
|
215
|
+
// the pasted code.
|
|
216
|
+
const authUrl = `${origin}/login?dest=cli&display=code`;
|
|
217
|
+
this.log('To authenticate, open the following URL in any browser:');
|
|
218
|
+
this.log('');
|
|
219
|
+
this.log(` ${authUrl}`);
|
|
220
|
+
this.log('');
|
|
221
|
+
try {
|
|
222
|
+
await open(authUrl);
|
|
223
|
+
}
|
|
224
|
+
catch {
|
|
225
|
+
// Best-effort only; the URL is already printed above for manual use.
|
|
226
|
+
}
|
|
227
|
+
const { token } = await inquirer.prompt([
|
|
228
|
+
{
|
|
229
|
+
message: 'Paste the code shown in your browser',
|
|
230
|
+
name: 'token',
|
|
231
|
+
type: 'password',
|
|
232
|
+
validate(input) {
|
|
233
|
+
return input.trim() === '' ? 'A code is required' : true;
|
|
234
|
+
},
|
|
235
|
+
},
|
|
236
|
+
]);
|
|
237
|
+
return token.trim();
|
|
238
|
+
}
|
|
199
239
|
async promptProfileName() {
|
|
200
240
|
const { profileName } = await inquirer.prompt([
|
|
201
241
|
{
|
|
@@ -476,6 +476,7 @@ export async function executePush(ctx, target, flags) {
|
|
|
476
476
|
}
|
|
477
477
|
// Warn when the sandbox currently holds a different workspace than the one being
|
|
478
478
|
// pushed and the change set is large enough that stale state is a real risk.
|
|
479
|
+
let mismatchConfirmed = false;
|
|
479
480
|
if (target.warnOnWorkspaceMismatch && preview.workspace_name) {
|
|
480
481
|
const localWorkspaceName = findLocalWorkspaceName(documentEntries);
|
|
481
482
|
const totalChanges = countSummaryChanges(preview.summary, shouldDelete);
|
|
@@ -494,29 +495,32 @@ export async function executePush(ctx, target, flags) {
|
|
|
494
495
|
log('Push cancelled. Run `xano sandbox reset` then retry.');
|
|
495
496
|
return;
|
|
496
497
|
}
|
|
498
|
+
mismatchConfirmed = true;
|
|
497
499
|
}
|
|
498
500
|
else {
|
|
499
501
|
command.error('Workspace mismatch detected in non-interactive mode. Run `xano sandbox reset` first to start clean.');
|
|
500
502
|
}
|
|
501
503
|
}
|
|
502
504
|
}
|
|
503
|
-
// Confirm with user
|
|
504
|
-
|
|
505
|
-
op.action === '
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
505
|
+
// Confirm with user (skip if workspace mismatch prompt already obtained confirmation)
|
|
506
|
+
if (!mismatchConfirmed) {
|
|
507
|
+
const hasDestructive = preview.operations.some((op) => (shouldDelete && (op.action === 'delete' || op.action === 'cascade_delete')) ||
|
|
508
|
+
op.action === 'truncate' ||
|
|
509
|
+
op.action === 'drop_field' ||
|
|
510
|
+
op.action === 'alter_field');
|
|
511
|
+
const message = hasDestructive
|
|
512
|
+
? 'Proceed with push? This includes DESTRUCTIVE operations listed above.'
|
|
513
|
+
: 'Proceed with push?';
|
|
514
|
+
if (process.stdin.isTTY) {
|
|
515
|
+
const confirmed = await confirm(message);
|
|
516
|
+
if (!confirmed) {
|
|
517
|
+
log('Push cancelled.');
|
|
518
|
+
return;
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
else {
|
|
522
|
+
command.error('Non-interactive environment detected. Use --force to skip confirmation.');
|
|
516
523
|
}
|
|
517
|
-
}
|
|
518
|
-
else {
|
|
519
|
-
command.error('Non-interactive environment detected. Use --force to skip confirmation.');
|
|
520
524
|
}
|
|
521
525
|
}
|
|
522
526
|
else {
|