@@ -4,6 +4,7 @@ import Conf from 'conf';
44import chalk from 'chalk' ;
55import ora from 'ora' ;
66import { getAuthToken } from "@heyputer/puter.js/src/init.cjs" ;
7+ import { puter } from "@heyputer/puter.js" ;
78
89// project
910import { BASE_URL , NULL_UUID , PROJECT_NAME , getHeaders , reconfigureURLs } from '../commons.js'
@@ -109,10 +110,9 @@ class ProfileModule {
109110 async switchProfileWizard ( args = { } ) {
110111 const profiles = this . getProfiles ( ) ;
111112 if ( profiles . length < 1 ) {
112- return this . addProfileWizard ( ) ;
113+ return this . addProfileWizard ( args ) ;
113114 }
114115
115- // console.log('doing this branch');
116116 const answer = await inquirer . prompt ( [
117117 {
118118 name : 'profile' ,
@@ -134,77 +134,188 @@ class ProfileModule {
134134 ] ) ;
135135
136136 if ( answer . profile === 'new' ) {
137- return await this . addProfileWizard ( ) ;
137+ return await this . addProfileWizard ( args ) ;
138138 }
139139
140140 this . selectProfile ( answer . profile ) ;
141141 }
142142
143143 async addProfileWizard ( args = { } ) {
144+ const host = args . host || 'https://puter.com' ;
145+
146+ if ( args . withCredentials ) {
147+ return await this . credentialLogin ( { ...args , host } ) ;
148+ }
149+
150+ // Browser-based login (default)
151+ return await this . browserLogin ( { ...args , host } ) ;
152+ }
153+
154+ async browserLogin ( args ) {
155+ const { host, save } = args ;
156+ const TIMEOUT_MS = 60000 ; // 1 minute timeout
157+ let spinner ;
158+
159+ try {
160+ spinner = ora ( 'Opening browser for login...' ) . start ( ) ;
161+
162+ const timeoutPromise = new Promise ( ( _ , reject ) => {
163+ setTimeout ( ( ) => reject ( new Error ( 'Login timed out after 60 seconds' ) ) , TIMEOUT_MS ) ;
164+ } ) ;
165+
166+ const authToken = await Promise . race ( [
167+ getAuthToken ( ) ,
168+ timeoutPromise
169+ ] ) ;
170+
171+ if ( ! authToken ) {
172+ spinner . fail ( chalk . red ( 'Login failed or was cancelled.' ) ) ;
173+ return ;
174+ }
175+
176+ spinner . text = 'Fetching user info...' ;
177+
178+ // Set token and fetch user info
179+ puter . setAuthToken ( authToken ) ;
180+ const userInfo = await puter . auth . getUser ( ) ;
181+
182+ const profileUUID = crypto . randomUUID ( ) ;
183+ const profile = {
184+ host,
185+ username : userInfo . username ,
186+ cwd : `/${ userInfo . username } ` ,
187+ token : authToken ,
188+ uuid : profileUUID ,
189+ } ;
190+
191+ this . addProfile ( profile ) ;
192+ this . selectProfile ( profile ) ;
193+ spinner . succeed ( chalk . green ( `Successfully logged in as ${ userInfo . username } !` ) ) ;
194+
195+ // Handle --save option
196+ this . saveTokenToEnv ( authToken , save ) ;
197+ } catch ( error ) {
198+ if ( spinner ) {
199+ spinner . fail ( chalk . red ( `Failed to login: ${ error . message } ` ) ) ;
200+ } else {
201+ console . error ( chalk . red ( `Failed to login: ${ error . message } ` ) ) ;
202+ }
203+ }
204+ }
205+
206+ async credentialLogin ( args ) {
207+ const { host, save } = args ;
208+
144209 const answers = await inquirer . prompt ( [
145- {
146- type : 'input' ,
147- name : 'host' ,
148- message : 'Host (leave blank for puter.com):' ,
149- default : 'https://puter.com' ,
150- validate : input => input . length >= 1 || 'Host is required'
151- } ,
152210 {
153211 type : 'input' ,
154212 name : 'username' ,
155213 message : 'Username:' ,
156214 validate : input => input . length >= 1 || 'Username is required'
157215 } ,
216+ {
217+ type : 'password' ,
218+ name : 'password' ,
219+ message : 'Password:' ,
220+ mask : '*' ,
221+ validate : input => input . length >= 1 || 'Password is required'
222+ }
158223 ] ) ;
159224
160225 let spinner ;
161226 try {
162227 spinner = ora ( 'Logging in to Puter...' ) . start ( ) ;
163- const authToken = await getAuthToken ( ) ;
164228
165- if ( authToken ) {
166- const profileUUID = crypto . randomUUID ( ) ;
167- const profile = {
168- host : answers . host ,
229+ const apiHost = toApiSubdomain ( host ) ;
230+ const response = await fetch ( `${ apiHost } /login` , {
231+ method : 'POST' ,
232+ headers : getHeaders ( ) ,
233+ body : JSON . stringify ( {
169234 username : answers . username ,
170- cwd : `/${ answers . username } ` ,
171- token : authToken ,
172- uuid : profileUUID ,
173- } ;
174- this . addProfile ( profile ) ;
175- this . selectProfile ( profile ) ;
176- if ( spinner ) {
177- spinner . succeed ( chalk . green ( 'Successfully logged in to Puter!' ) ) ;
178- }
179- // Save token
180- if ( args . save ) {
181- const localEnvFile = '.env' ;
182- try {
183- // Check if the file exists, if so then append the api key to the EOF.
184- if ( fs . existsSync ( localEnvFile ) ) {
185- console . log ( chalk . yellow ( `File "${ localEnvFile } " already exists... Adding token.` ) ) ;
186- fs . appendFileSync ( localEnvFile , `\nPUTER_API_KEY="${ authToken } "` , 'utf8' ) ;
187- } else {
188- console . log ( chalk . cyan ( `Saving token to ${ chalk . green ( localEnvFile ) } file.` ) ) ;
189- fs . writeFileSync ( localEnvFile , `PUTER_API_KEY="${ authToken } "` , 'utf8' ) ;
190- }
191- } catch ( error ) {
192- console . error ( chalk . red ( `Cannot save token to .env file. Error: ${ error . message } ` ) ) ;
193- console . log ( chalk . cyan ( `PUTER_API_KEY="${ authToken } "` ) ) ;
235+ password : answers . password ,
236+ } ) ,
237+ } ) ;
238+
239+ const data = await response . json ( ) ;
240+
241+ if ( data . proceed && data . next_step === 'otp' ) {
242+ // Handle 2FA
243+ spinner . stop ( ) ;
244+ const otpAnswer = await inquirer . prompt ( [
245+ {
246+ type : 'input' ,
247+ name : 'otp' ,
248+ message : 'Enter your 2FA code:' ,
249+ validate : input => input . length >= 1 || '2FA code is required'
194250 }
251+ ] ) ;
252+
253+ spinner = ora ( 'Verifying 2FA code...' ) . start ( ) ;
254+ const otpResponse = await fetch ( `${ apiHost } /login/otp` , {
255+ method : 'POST' ,
256+ headers : getHeaders ( ) ,
257+ body : JSON . stringify ( {
258+ token : data . otp_jwt_token ,
259+ code : otpAnswer . otp ,
260+ } ) ,
261+ } ) ;
262+
263+ const otpData = await otpResponse . json ( ) ;
264+
265+ if ( otpData . token ) {
266+ this . createProfileFromToken ( otpData . token , answers . username , host , spinner , save ) ;
267+ } else {
268+ spinner . fail ( chalk . red ( '2FA verification failed.' ) ) ;
195269 }
270+ } else if ( data . token ) {
271+ this . createProfileFromToken ( data . token , answers . username , host , spinner , save ) ;
196272 } else {
197- spinner . fail ( chalk . red ( 'Login failed. Please check your credentials.' ) ) ;
273+ spinner . fail ( chalk . red ( data . error ?. message || 'Login failed. Please check your credentials.' ) ) ;
198274 }
199275 } catch ( error ) {
200276 if ( spinner ) {
201277 spinner . fail ( chalk . red ( `Failed to login: ${ error . message } ` ) ) ;
202- console . log ( error ) ;
203278 } else {
204279 console . error ( chalk . red ( `Failed to login: ${ error . message } ` ) ) ;
205280 }
206281 }
207282 }
283+
284+ createProfileFromToken ( token , username , host , spinner , save ) {
285+ const profileUUID = crypto . randomUUID ( ) ;
286+ const profile = {
287+ host,
288+ username,
289+ cwd : `/${ username } ` ,
290+ token,
291+ uuid : profileUUID ,
292+ } ;
293+
294+ this . addProfile ( profile ) ;
295+ this . selectProfile ( profile ) ;
296+ spinner . succeed ( chalk . green ( `Successfully logged in as ${ username } !` ) ) ;
297+
298+ // Handle --save option
299+ this . saveTokenToEnv ( token , save ) ;
300+ }
301+
302+ saveTokenToEnv ( token , save ) {
303+ if ( ! save ) return ;
304+
305+ const localEnvFile = '.env' ;
306+ try {
307+ if ( fs . existsSync ( localEnvFile ) ) {
308+ console . log ( chalk . yellow ( `File "${ localEnvFile } " already exists... Adding token.` ) ) ;
309+ fs . appendFileSync ( localEnvFile , `\nPUTER_API_KEY="${ token } "` , 'utf8' ) ;
310+ } else {
311+ console . log ( chalk . cyan ( `Saving token to ${ chalk . green ( localEnvFile ) } file.` ) ) ;
312+ fs . writeFileSync ( localEnvFile , `PUTER_API_KEY="${ token } "` , 'utf8' ) ;
313+ }
314+ } catch ( error ) {
315+ console . error ( chalk . red ( `Cannot save token to .env file. Error: ${ error . message } ` ) ) ;
316+ console . log ( chalk . cyan ( `PUTER_API_KEY="${ token } "` ) ) ;
317+ }
318+ }
208319}
209320
210321export const initProfileModule = ( ) => {
0 commit comments