Skip to content

Commit bf25c16

Browse files
authored
feat(cli): easier agentic login (#15050)
Support better login experience from cursor / claude <!-- VADE_RISK_START --> > [!NOTE] > Low Risk Change > > This PR modifies CLI login UX to auto-open browser for authentication instead of waiting for user input, which is a flow change but does not alter authentication security - the same device authorization flow is used. > > - Removes manual [ENTER] prompt, auto-opens browser for device auth flow > - Adds Cursor agent detection to control browser-opening behavior in CI > - Graceful fallback with instructions when browser fails to open > > <sup>Risk assessment for [commit 265367b](https://github.com/vercel/vercel/commit/265367b4bc1c8ac0a658080a147da04e192d13fb).</sup> <!-- VADE_RISK_END -->
1 parent 53d78ca commit bf25c16

2 files changed

Lines changed: 43 additions & 24 deletions

File tree

.changeset/shaggy-weeks-brake.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'vercel': minor
3+
---
4+
5+
Support easier auth from cursor / claude

packages/cli/src/commands/login/future.ts

Lines changed: 38 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import readline from 'node:readline';
22
import chalk from 'chalk';
33
import * as open from 'open';
44
import { eraseLines } from 'ansi-escapes';
5+
import { KNOWN_AGENTS } from '@vercel/detect-agent';
56
import type Client from '../../util/client';
67
import { printError } from '../../util/error';
78
import { updateCurrentTeamAfterLogin } from '../../util/login/update-current-team-after-login';
@@ -47,7 +48,42 @@ export async function login(
4748
interval,
4849
} = deviceAuthorization;
4950

50-
let rlClosed = false;
51+
// Determine if we should skip opening the browser (only in CI, but not in Cursor)
52+
const isCursorAgent =
53+
client.agentName === KNOWN_AGENTS.CURSOR ||
54+
client.agentName === KNOWN_AGENTS.CURSOR_CLI;
55+
const shouldSkipBrowser = process.env.CI && !isCursorAgent;
56+
57+
o.log(
58+
`\n Visit ${chalk.bold(
59+
o.link(
60+
verification_uri.replace('https://', ''),
61+
verification_uri_complete,
62+
{ color: false, fallback: () => verification_uri_complete }
63+
)
64+
)}${o.supportsHyperlink ? ` and enter ${chalk.bold(user_code)}` : ''}\n`
65+
);
66+
67+
// Open browser automatically unless we're in CI (excluding Cursor)
68+
if (!shouldSkipBrowser) {
69+
try {
70+
await open.default(verification_uri_complete);
71+
} catch (error) {
72+
// Fail gracefully if browser can't be opened
73+
o.debug(`Failed to open browser: ${error}`);
74+
75+
// If in non-interactive agent mode, provide specific instructions
76+
if (client.isAgent && client.nonInteractive) {
77+
o.log(
78+
`\n${chalk.yellow('⚠')} ${chalk.bold('Browser could not be opened automatically.')}\n`
79+
);
80+
o.log(
81+
`Please ask the user to manually visit the URL above and complete the authentication process.\n`
82+
);
83+
}
84+
}
85+
}
86+
5187
const rl = readline
5288
.createInterface({
5389
input: process.stdin,
@@ -59,26 +95,6 @@ export async function login(
5995
process.exit(0);
6096
});
6197

62-
rl.question(
63-
`
64-
Visit ${chalk.bold(
65-
o.link(
66-
verification_uri.replace('https://', ''),
67-
verification_uri_complete,
68-
{ color: false, fallback: () => verification_uri_complete }
69-
)
70-
)}${o.supportsHyperlink ? ` and enter ${chalk.bold(user_code)}` : ''}
71-
${chalk.grey('Press [ENTER] to open the browser')}
72-
`,
73-
() => {
74-
open.default(verification_uri_complete);
75-
o.print(eraseLines(2)); // "Waiting for authentication..." gets printed twice, this removes one when Enter is pressed
76-
o.spinner('Waiting for authentication...');
77-
rl.close();
78-
rlClosed = true;
79-
}
80-
);
81-
8298
o.spinner('Waiting for authentication...');
8399

84100
let intervalMs = interval * 1000;
@@ -171,9 +187,7 @@ export async function login(
171187
error = await pollForToken();
172188

173189
o.stopSpinner();
174-
if (!rlClosed) {
175-
rl.close();
176-
}
190+
rl.close();
177191

178192
if (!error) {
179193
telemetry.trackState('success');

0 commit comments

Comments
 (0)