/** * Ink UI components for OAuth setup flow */ import { hostname } from "node:os"; import { Box, Text, useApp, useInput } from "ink"; import { useState } from "react"; import { asciiLogo } from "../cli/components/AsciiArt.ts"; import { colors } from "../cli/components/colors"; import { settingsManager } from "../settings-manager"; import { pollForToken, requestDeviceCode } from "./oauth"; type SetupMode = "menu" | "device-code" | "auth-code" | "self-host" | "done"; interface SetupUIProps { onComplete: () => void; } export function SetupUI({ onComplete }: SetupUIProps) { const [mode, setMode] = useState("menu"); const [selectedOption, setSelectedOption] = useState(0); const [error, setError] = useState(null); const [_deviceCode, setDeviceCode] = useState(null); const [userCode, setUserCode] = useState(null); const [verificationUri, setVerificationUri] = useState(null); const { exit } = useApp(); // Handle menu navigation useInput( (_input, key) => { if (mode === "menu") { if (key.upArrow) { setSelectedOption((prev) => Math.max(0, prev - 1)); } else if (key.downArrow) { setSelectedOption((prev) => Math.min(2, prev + 1)); } else if (key.return) { if (selectedOption === 0) { // Login to Letta Cloud - start device code flow setMode("device-code"); startDeviceCodeFlow(); } else if (selectedOption === 1) { exit(); } } } }, { isActive: mode === "menu" }, ); const startDeviceCodeFlow = async () => { try { const deviceData = await requestDeviceCode(); setDeviceCode(deviceData.device_code); setUserCode(deviceData.user_code); setVerificationUri(deviceData.verification_uri_complete); // Auto-open browser (fire-and-forget, never crash) // Uses promise chaining to ensure error handler is attached immediately // after promise resolution, preventing race conditions with error events import("open") .then(({ default: open }) => open(deviceData.verification_uri_complete, { wait: false }), ) .then((subprocess) => { subprocess.on("error", () => { // Silently ignore - user can manually visit the URL shown above }); }) .catch(() => { // Silently ignore any failures (WSL PowerShell issues, missing xdg-open, etc.) }); // Get or generate device ID const deviceId = settingsManager.getOrCreateDeviceId(); const deviceName = hostname(); // Start polling in background pollForToken( deviceData.device_code, deviceData.interval, deviceData.expires_in, deviceId, deviceName, ) .then(async (tokens) => { // Save tokens using secrets for secure storage // Note: LETTA_BASE_URL is intentionally NOT saved to settings // It should only come from environment variables const now = Date.now(); try { // Update settings with non-sensitive data and tokens (secrets handles secure storage) settingsManager.updateSettings({ env: { ...settingsManager.getSettings().env, LETTA_API_KEY: tokens.access_token, }, refreshToken: tokens.refresh_token, tokenExpiresAt: now + tokens.expires_in * 1000, }); // Wait for all pending writes (keychain, disk) to complete before continuing // This prevents a race condition where main() validation runs before tokens are persisted await settingsManager.flush(); setMode("done"); setTimeout(() => onComplete(), 1000); } catch (err) { setError(err instanceof Error ? err.message : String(err)); } }) .catch((err) => { setError(err.message); }); } catch (err) { setError(err instanceof Error ? err.message : String(err)); } }; if (mode === "done") { return ( ✓ Setup complete! Starting Letta Code... ); } if (error) { return ( ✗ Error: {error} Press Ctrl+C to exit ); } if (mode === "device-code") { return ( {asciiLogo} Login to the Letta Developer Platform Opening browser for authorization... Your authorization code:{" "} {userCode} If browser didn't open, visit: {verificationUri} Waiting for you to authorize in the browser... ); } // Main menu return ( {asciiLogo} Welcome to Letta Code! Let's get you authenticated: {selectedOption === 0 ? "→" : " "} Login to the Letta Developer Platform {selectedOption === 1 ? "→" : " "} Exit Use ↑/↓ to navigate, Enter to select ); }