Skip to content

Commit 93990f5

Browse files
committed
POC
1 parent daa7627 commit 93990f5

File tree

1 file changed

+158
-3
lines changed

1 file changed

+158
-3
lines changed

codeflash/cli_cmds/cmd_init.py

Lines changed: 158 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -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
11511151
def 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

11771332
def enter_api_key_and_save_to_rc() -> None:

0 commit comments

Comments
 (0)