@@ -6,60 +6,86 @@ import { Command } from "@commander-js/extra-typings";
66import { getApiClient , resetApiClient } from "../api/client.ts" ;
77import { saveTokens , clearTokens } from "../config/credentials.ts" ;
88import { setSigintExit } from "../cli/exit.ts" ;
9- import { promptHidden , promptInput } from "../cli/prompts.ts" ;
109import { terminal } from "../output/index.ts" ;
1110import { t } from "../i18n/index.ts" ;
1211import { getLogger } from "../logging/index.ts" ;
1312import { ErrorCode } from "../errors/codes.ts" ;
1413import { failCommand , handleCommandError } from "./error-handler.ts" ;
14+ import { runDeviceFlow } from "./device-flow.ts" ;
15+ import { getCurrentAbortSignal } from "../runtime/request-context.ts" ;
1516
1617const log = getLogger ( import . meta. url ) ;
1718
1819/**
1920 * Login command.
2021 *
21- * Authenticates via OAuth2 ROPC flow. Supports flags or interactive input.
22+ * Two authentication paths:
23+ * - No flags: Device Flow (RFC 8628) — opens browser for Google OAuth.
24+ * Works for both new registration and existing user recovery.
25+ * - With -a/-s flags: OAuth2 ROPC flow (backward compatible).
26+ *
2227 * Stores tokens in ~/.prompsit/credentials.json.
2328 */
2429export const loginCommand = new Command ( "login" )
25- . description ( "Authenticate with the Prompsit API" )
30+ . description ( "Sign in or register with the Prompsit API" )
2631 . option ( "-a, --account <email>" , "Account email address" )
2732 . option ( "-s, --secret <key>" , "API secret key" )
2833 . action ( async ( options ) => {
2934 const startMs = Date . now ( ) ;
3035 try {
3136 log . debug ( "Login action entered" ) ;
32- // Resolve account and secret (flags or interactive)
33- const account = options . account ?? ( await promptInput ( t ( "auth.login.prompt_account" ) ) ) ;
34- const secret = options . secret ?? ( await promptHidden ( t ( "auth.login.prompt_secret" ) ) ) ;
35-
36- if ( ! account || ! secret ) {
37- failCommand ( ErrorCode . AUTH_FAILED , t ( "auth.login.credentials_required" ) ) ;
38- return ;
39- }
4037
41- // Authenticate via OAuth2
42- terminal . info ( t ( "auth.login.authenticating" ) ) ;
43- log . debug ( "Sending auth request" , { account } ) ;
44- const response = await getApiClient ( ) . auth . getToken ( account , secret ) ;
45- log . info ( "Authentication completed" , { duration_ms : String ( Date . now ( ) - startMs ) } ) ;
38+ if ( options . account && options . secret ) {
39+ // ROPC path: login with existing credentials (backward compatible)
40+ terminal . info ( t ( "auth.login.authenticating" ) ) ;
41+ log . debug ( "Sending ROPC auth request" , { account : options . account } ) ;
42+ const response = await getApiClient ( ) . auth . getToken ( options . account , options . secret ) ;
43+ log . info ( "ROPC authentication completed" , {
44+ duration_ms : String ( Date . now ( ) - startMs ) ,
45+ } ) ;
4646
47- // Save tokens (normalize nullable -> undefined for credential store)
48- saveTokens ( {
49- accessToken : response . access_token ,
50- refreshToken : response . refresh_token ?? undefined ,
51- accountId : account ,
52- expiresIn : response . expires_in ?? undefined ,
53- plan : response . plan ,
54- } ) ;
47+ saveTokens ( {
48+ accessToken : response . access_token ,
49+ refreshToken : response . refresh_token ?? undefined ,
50+ accountId : options . account ,
51+ expiresIn : response . expires_in ?? undefined ,
52+ plan : response . plan ,
53+ } ) ;
54+ resetApiClient ( ) ;
55+ terminal . success ( t ( "auth.login.success" ) ) ;
56+ } else if ( options . account || options . secret ) {
57+ // Partial flags: require both
58+ failCommand ( ErrorCode . AUTH_FAILED , t ( "auth.login.credentials_required" ) ) ;
59+ } else {
60+ // Device Flow path: browser-based sign-in / registration
61+ log . debug ( "Starting device flow" ) ;
62+ const result = await runDeviceFlow (
63+ getApiClient ( ) . auth ,
64+ getCurrentAbortSignal ( )
65+ ) ;
5566
56- // Reset client to pick up new credentials
57- resetApiClient ( ) ;
67+ saveTokens ( {
68+ accessToken : result . accessToken ,
69+ refreshToken : result . refreshToken ,
70+ accountId : result . accountId ,
71+ expiresIn : result . expiresIn ,
72+ plan : result . plan ,
73+ prompsitSecret : result . prompsitSecret ,
74+ } ) ;
75+ resetApiClient ( ) ;
5876
59- terminal . success ( t ( "auth.login.success" ) ) ;
77+ // Show hint for future ROPC login
78+ terminal . dim (
79+ t ( "auth.device.secret_hint" , {
80+ cmd : "login" ,
81+ account : result . accountId ,
82+ secret : result . prompsitSecret ,
83+ } )
84+ ) ;
85+ }
6086 } catch ( error : unknown ) {
61- // Ctrl+C during interactive readline: POSIX SIGINT exit code
62- if ( ( error as Error ) . message === "Cancelled" ) {
87+ // Ctrl+C during interactive readline or device flow polling
88+ if ( ( error as Error ) . message === "Cancelled" || ( error as Error ) . message === "Request cancelled" ) {
6389 setSigintExit ( ) ;
6490 return ;
6591 }
0 commit comments