@@ -1149,6 +1149,21 @@ def convert(self, value: str, param: click.Parameter | None, ctx: click.Context
11491149
11501150# Returns True if the user entered a new API key, False if they used an existing one
11511151def prompt_api_key () -> bool :
1152+ import threading
1153+ import socket
1154+ import http .server
1155+ import urllib .parse
1156+ import random
1157+ import string
1158+ import base64
1159+ import hashlib
1160+ import time
1161+ import json
1162+ import webbrowser
1163+ import requests
1164+
1165+ BASE_URL = "https://app.codeflash.ai/"
1166+
11521167 try :
11531168 existing_api_key = get_codeflash_api_key ()
11541169 except OSError :
@@ -1168,10 +1183,150 @@ def prompt_api_key() -> bool:
11681183 console .print (api_key_panel )
11691184 console .print ()
11701185 return False
1186+ auth_choices = [
1187+ "🔐 Sign in" ,
1188+ "🔑 Enter Api key"
1189+ ]
1190+ name = "auth_method"
1191+ questions = [
1192+ inquirer .List (
1193+ name ,
1194+ message = "How would you like to sign in?" ,
1195+ choices = auth_choices ,
1196+ default = auth_choices [0 ],
1197+ carousel = True ,
1198+ )
1199+ ]
11711200
1172- enter_api_key_and_save_to_rc ()
1173- ph ("cli-new-api-key-entered" )
1174- return True
1201+ answers = inquirer .prompt (questions , theme = CodeflashTheme ())
1202+ if not answers :
1203+ apologize_and_exit ()
1204+ method = answers [name ]
1205+ if method == "🔑 Enter Api key" :
1206+ enter_api_key_and_save_to_rc ()
1207+ ph ("cli-new-api-key-entered" )
1208+ return True
1209+ # OAuth PKCE Flow for "🔐 Sign in"
1210+ # 1. Start a local server on available port
1211+ class OAuthCallbackHandler (http .server .BaseHTTPRequestHandler ):
1212+ server_version = "CFHTTP"
1213+ code = None
1214+ state = None
1215+ error = None
1216+ def do_GET (self ):
1217+ parsed = urllib .parse .urlparse (self .path )
1218+ if parsed .path != "/callback" :
1219+ self .send_response (404 )
1220+ self .end_headers ()
1221+ return
1222+ params = urllib .parse .parse_qs (parsed .query )
1223+ OAuthCallbackHandler .code = params .get ("code" , [None ])[0 ]
1224+ OAuthCallbackHandler .state = params .get ("state" , [None ])[0 ]
1225+ OAuthCallbackHandler .error = params .get ("error" , [None ])[0 ]
1226+ self .send_response (200 )
1227+ self .send_header ("Content-type" , "text/html" )
1228+ self .end_headers ()
1229+ if OAuthCallbackHandler .code :
1230+ self .wfile .write (b"<html><body><h2>Sign-in successful!</h2>You may close this window.</body></html>" )
1231+ elif OAuthCallbackHandler .error :
1232+ self .wfile .write (b"<html><body><h2>Sign-in failed.</h2></body></html>" )
1233+ else :
1234+ self .wfile .write (b"<html><body><h2>Missing code.</h2></body></html>" )
1235+
1236+ def log_message (self , format , * args ):
1237+ # Silence HTTP logs
1238+ pass
1239+
1240+ # Find a free port
1241+ def get_free_port ():
1242+ with socket .socket (socket .AF_INET , socket .SOCK_STREAM ) as s :
1243+ s .bind (("" , 0 ))
1244+ return s .getsockname ()[1 ]
1245+
1246+ port = get_free_port ()
1247+ redirect_uri = f"http://localhost:{ port } /callback"
1248+ # PKCE code_verifier and code_challenge
1249+ def random_string (length = 64 ):
1250+ return '' .join (random .choices (string .ascii_letters + string .digits + "-._~" , k = length ))
1251+ code_verifier = random_string (64 )
1252+ code_challenge = base64 .urlsafe_b64encode (
1253+ hashlib .sha256 (code_verifier .encode ()).digest ()
1254+ ).rstrip (b'=' ).decode ()
1255+ state = random_string (16 )
1256+
1257+ # Compose auth URL
1258+ auth_url = (
1259+ f"{ BASE_URL } codeflash/auth?"
1260+ f"response_type=code"
1261+ f"&client_id=cf_vscode_app"
1262+ f"&redirect_uri={ urllib .parse .quote (redirect_uri )} "
1263+ f"&code_challenge={ code_challenge } "
1264+ f"&code_challenge_method=sha256"
1265+ f"&state={ state } "
1266+ )
1267+
1268+ # Start HTTP server in thread
1269+ handler_class = OAuthCallbackHandler
1270+ httpd = http .server .HTTPServer (("localhost" , port ), handler_class )
1271+ server_thread = threading .Thread (target = httpd .handle_request )
1272+ server_thread .daemon = True
1273+ server_thread .start ()
1274+ click .echo (f"🌐 Opening browser to sign in to Codeflash…" )
1275+ webbrowser .open (auth_url )
1276+ click .echo (f"If your browser did not open, visit:\n { auth_url } " )
1277+ # Wait for callback (with timeout)
1278+ max_wait = 120 # seconds
1279+ waited = 0
1280+ while handler_class .code is None and handler_class .error is None and waited < max_wait :
1281+ time .sleep (0.5 )
1282+ waited += 0.5
1283+ httpd .server_close ()
1284+ if handler_class .error :
1285+ click .echo (f"❌ Sign-in failed: { handler_class .error } " )
1286+ apologize_and_exit ()
1287+ if not handler_class .code or not handler_class .state :
1288+ click .echo ("❌ Did not receive code from sign-in. Please try again." )
1289+ apologize_and_exit ()
1290+ if handler_class .state != state :
1291+ click .echo ("❌ State mismatch in OAuth callback." )
1292+ apologize_and_exit ()
1293+ code = handler_class .code
1294+ console .print (code )
1295+ # Exchange code for token
1296+ token_url = f"{ BASE_URL } codeflash/auth/oauth/token"
1297+ data = {
1298+ "grant_type" : "authorization_code" ,
1299+ "code" : code ,
1300+ "code_verifier" : code_verifier ,
1301+ "redirect_uri" : redirect_uri ,
1302+ "client_id" : "cf_vscode_app"
1303+ }
1304+ try :
1305+ resp = requests .post (
1306+ token_url ,
1307+ headers = {"Content-Type" : "application/json" },
1308+ data = json .dumps (data ),
1309+ timeout = 10 ,
1310+ )
1311+ resp .raise_for_status ()
1312+ token_json = resp .json ()
1313+ api_key = token_json .get ("api_key" ) or token_json .get ("access_token" )
1314+ if not api_key :
1315+ click .echo ("❌ Could not retrieve API key from response." )
1316+ apologize_and_exit ()
1317+ result = save_api_key_to_rc (api_key )
1318+ if is_successful (result ):
1319+ click .echo (result .unwrap ())
1320+ click .echo ("✅ Signed in successfully and API key saved!" )
1321+ else :
1322+ click .echo (result .failure ())
1323+ click .pause ()
1324+ os .environ ["CODEFLASH_API_KEY" ] = api_key
1325+ ph ("cli-new-api-key-entered" )
1326+ return True
1327+ except Exception as e :
1328+ click .echo (f"❌ Failed to exchange code for API key: { e } " )
1329+ apologize_and_exit ()
11751330
11761331
11771332def enter_api_key_and_save_to_rc () -> None :
0 commit comments