"use client"; import { useState, useEffect, useCallback } from "react"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { Card, CardContent, CardDescription, CardHeader, CardTitle, } from "@/components/ui/card"; import { useSetupStore, type CodexAuthStatus } from "@/store/setup-store"; import { useAppStore } from "@/store/app-store"; import { getElectronAPI } from "@/lib/electron"; import { CheckCircle2, XCircle, Loader2, Terminal, Key, Sparkles, ArrowRight, ArrowLeft, ExternalLink, Copy, AlertCircle, RefreshCw, Download, Shield, } from "lucide-react"; import { toast } from "sonner"; import { SetupTokenModal } from "./setup-token-modal"; // Step indicator component function StepIndicator({ currentStep, totalSteps, }: { currentStep: number; totalSteps: number; }) { return (
{Array.from({ length: totalSteps }).map((_, index) => (
))}
); } // CLI Status Badge function StatusBadge({ status, label, }: { status: | "installed" | "not_installed" | "checking" | "authenticated" | "not_authenticated"; label: string; }) { const getStatusConfig = () => { switch (status) { case "installed": case "authenticated": return { icon: , className: "bg-green-500/10 text-green-500 border-green-500/20", }; case "not_installed": case "not_authenticated": return { icon: , className: "bg-red-500/10 text-red-500 border-red-500/20", }; case "checking": return { icon: , className: "bg-yellow-500/10 text-yellow-500 border-yellow-500/20", }; } }; const config = getStatusConfig(); return (
{config.icon} {label}
); } // Terminal Output Component function TerminalOutput({ lines }: { lines: string[] }) { return (
{lines.map((line, index) => (
$ {line}
))} {lines.length === 0 && (
Waiting for output...
)}
); } // Welcome Step function WelcomeStep({ onNext }: { onNext: () => void }) { return (
{/* eslint-disable-next-line @next/next/no-img-element */} Automaker Logo

Welcome to Automaker

Let's set up your development environment. We'll check for required CLI tools and help you configure them.

Claude CLI

Anthropic's powerful AI assistant for code generation and analysis

Codex CLI

OpenAI's GPT-5.1 Codex for advanced code generation tasks

); } // Claude Setup Step - 2 Authentication Options: // 1. OAuth Token (Subscription): User runs `claude setup-token` and provides the token // 2. API Key (Pay-per-use): User provides their Anthropic API key directly function ClaudeSetupStep({ onNext, onBack, onSkip, }: { onNext: () => void; onBack: () => void; onSkip: () => void; }) { const { claudeCliStatus, claudeAuthStatus, claudeInstallProgress, setClaudeCliStatus, setClaudeAuthStatus, setClaudeInstallProgress, } = useSetupStore(); const { setApiKeys, apiKeys } = useAppStore(); const [isChecking, setIsChecking] = useState(false); const [isInstalling, setIsInstalling] = useState(false); const [authMethod, setAuthMethod] = useState<"token" | "api_key" | null>( null ); const [oauthToken, setOAuthToken] = useState(""); const [apiKey, setApiKey] = useState(""); const [isSaving, setIsSaving] = useState(false); const [showTokenModal, setShowTokenModal] = useState(false); const checkStatus = useCallback(async () => { console.log("[Claude Setup] Starting status check..."); setIsChecking(true); try { const api = getElectronAPI(); const setupApi = api.setup; // Debug: Check what's available console.log( "[Claude Setup] isElectron:", typeof window !== "undefined" && (window as any).isElectron ); console.log( "[Claude Setup] electronAPI exists:", typeof window !== "undefined" && !!(window as any).electronAPI ); console.log( "[Claude Setup] electronAPI.setup exists:", typeof window !== "undefined" && !!(window as any).electronAPI?.setup ); console.log("[Claude Setup] Setup API available:", !!setupApi); if (setupApi?.getClaudeStatus) { const result = await setupApi.getClaudeStatus(); console.log("[Claude Setup] Raw status result:", result); if (result.success) { const cliStatus = { installed: result.status === "installed", path: result.path || null, version: result.version || null, method: result.method || "none", }; console.log("[Claude Setup] CLI Status:", cliStatus); setClaudeCliStatus(cliStatus); if (result.auth) { // Validate method is one of the expected values, default to "none" const validMethods = [ "oauth_token_env", "oauth_token", "api_key", "api_key_env", "none", ] as const; type AuthMethod = (typeof validMethods)[number]; const method: AuthMethod = validMethods.includes( result.auth.method as AuthMethod ) ? (result.auth.method as AuthMethod) : "none"; const authStatus = { authenticated: result.auth.authenticated, method, hasCredentialsFile: false, oauthTokenValid: result.auth.hasStoredOAuthToken || result.auth.hasEnvOAuthToken, apiKeyValid: result.auth.hasStoredApiKey || result.auth.hasEnvApiKey, hasEnvOAuthToken: result.auth.hasEnvOAuthToken, hasEnvApiKey: result.auth.hasEnvApiKey, }; setClaudeAuthStatus(authStatus); } } } } catch (error) { console.error("[Claude Setup] Failed to check Claude status:", error); } finally { setIsChecking(false); } }, [setClaudeCliStatus, setClaudeAuthStatus]); useEffect(() => { checkStatus(); }, [checkStatus]); const handleInstall = async () => { setIsInstalling(true); setClaudeInstallProgress({ isInstalling: true, currentStep: "Downloading Claude CLI...", progress: 0, output: [], }); try { const api = getElectronAPI(); const setupApi = api.setup; if (setupApi?.installClaude) { const unsubscribe = setupApi.onInstallProgress?.( (progress: { cli?: string; data?: string; type?: string }) => { if (progress.cli === "claude") { setClaudeInstallProgress({ output: [ ...claudeInstallProgress.output, progress.data || progress.type || "", ], }); } } ); const result = await setupApi.installClaude(); unsubscribe?.(); if (result.success) { // Installation script completed, but CLI might not be immediately detectable // Wait a bit for installation to complete and PATH to update, then retry status check let retries = 5; let detected = false; // Initial delay to let the installation script finish setting up await new Promise((resolve) => setTimeout(resolve, 1500)); for (let i = 0; i < retries; i++) { // Check status await checkStatus(); // Small delay to let state update await new Promise((resolve) => setTimeout(resolve, 300)); // Check if CLI is now detected by re-reading from store const currentStatus = useSetupStore.getState().claudeCliStatus; if (currentStatus?.installed) { detected = true; toast.success("Claude CLI installed and detected successfully"); break; } // Wait before next retry (longer delays for later retries) if (i < retries - 1) { await new Promise((resolve) => setTimeout(resolve, 2000 + i * 500) ); } } // Show appropriate message based on detection if (!detected) { // Installation completed but CLI not detected - this is common if PATH wasn't updated in current process toast.success("Claude CLI installation completed", { description: "The CLI was installed but may need a terminal restart to be detected. You can continue with authentication if you have a token.", duration: 7000, }); } } else { toast.error("Installation failed", { description: result.error }); } } } catch (error) { console.error("Failed to install Claude:", error); toast.error("Installation failed"); } finally { setIsInstalling(false); setClaudeInstallProgress({ isInstalling: false }); } }; const handleSaveOAuthToken = async () => { console.log("[Claude Setup] Saving OAuth token..."); if (!oauthToken.trim()) { toast.error("Please enter the token from claude setup-token"); return; } setIsSaving(true); try { const api = getElectronAPI(); const setupApi = api.setup; if (setupApi?.storeApiKey) { const result = await setupApi.storeApiKey( "anthropic_oauth_token", oauthToken ); console.log("[Claude Setup] Store OAuth token result:", result); if (result.success) { setClaudeAuthStatus({ authenticated: true, method: "oauth_token", hasCredentialsFile: false, oauthTokenValid: true, }); toast.success("Claude subscription token saved"); setAuthMethod(null); await checkStatus(); } else { toast.error("Failed to save token", { description: result.error }); } } } catch (error) { console.error("[Claude Setup] Failed to save OAuth token:", error); toast.error("Failed to save token"); } finally { setIsSaving(false); } }; const handleSaveApiKey = async () => { console.log("[Claude Setup] Saving API key..."); if (!apiKey.trim()) { toast.error("Please enter an API key"); return; } setIsSaving(true); try { const api = getElectronAPI(); const setupApi = api.setup; if (setupApi?.storeApiKey) { const result = await setupApi.storeApiKey("anthropic", apiKey); console.log("[Claude Setup] Store API key result:", result); if (result.success) { setApiKeys({ ...apiKeys, anthropic: apiKey }); setClaudeAuthStatus({ authenticated: true, method: "api_key", hasCredentialsFile: false, apiKeyValid: true, }); toast.success("Anthropic API key saved"); setAuthMethod(null); await checkStatus(); } else { toast.error("Failed to save API key", { description: result.error }); } } else { // Web mode fallback setApiKeys({ ...apiKeys, anthropic: apiKey }); setClaudeAuthStatus({ authenticated: true, method: "api_key", hasCredentialsFile: false, apiKeyValid: true, }); toast.success("Anthropic API key saved"); setAuthMethod(null); } } catch (error) { console.error("[Claude Setup] Failed to save API key:", error); toast.error("Failed to save API key"); } finally { setIsSaving(false); } }; const copyCommand = (command: string) => { navigator.clipboard.writeText(command); toast.success("Command copied to clipboard"); }; // Handle token obtained from the OAuth modal const handleTokenFromModal = useCallback( async (token: string) => { setOAuthToken(token); setShowTokenModal(false); // Auto-save the token setIsSaving(true); try { const api = getElectronAPI(); const setupApi = api.setup; if (setupApi?.storeApiKey) { const result = await setupApi.storeApiKey( "anthropic_oauth_token", token ); console.log("[Claude Setup] Store OAuth token result:", result); if (result.success) { setClaudeAuthStatus({ authenticated: true, method: "oauth_token", hasCredentialsFile: false, oauthTokenValid: true, }); toast.success("Claude subscription token saved"); setAuthMethod(null); await checkStatus(); } else { toast.error("Failed to save token", { description: result.error }); } } } catch (error) { console.error("[Claude Setup] Failed to save OAuth token:", error); toast.error("Failed to save token"); } finally { setIsSaving(false); } }, [checkStatus, setClaudeAuthStatus] ); const isAuthenticated = claudeAuthStatus?.authenticated || apiKeys.anthropic; const getAuthMethodLabel = () => { if (!isAuthenticated) return null; if ( claudeAuthStatus?.method === "oauth_token_env" || claudeAuthStatus?.method === "oauth_token" ) return "Subscription Token"; if ( apiKeys.anthropic || claudeAuthStatus?.method === "api_key" || claudeAuthStatus?.method === "api_key_env" ) return "API Key"; return "Authenticated"; }; return (

Claude Setup

Configure Claude for code generation

{/* Status Card */}
Status
CLI Installation {isChecking ? ( ) : claudeCliStatus?.installed ? ( ) : ( )}
{claudeCliStatus?.version && (
Version {claudeCliStatus.version}
)}
Authentication {isAuthenticated ? (
{getAuthMethodLabel() && ( ({getAuthMethodLabel()}) )}
) : ( )}
{/* Installation Section */} {!claudeCliStatus?.installed && ( Install Claude CLI Required for subscription-based authentication
curl -fsSL https://claude.ai/install.sh | bash
irm https://claude.ai/install.ps1 | iex
{claudeInstallProgress.isInstalling && ( )}
)} {/* Authentication Section */} {!isAuthenticated && ( Authentication Choose your authentication method {/* Option 1: Subscription Token */} {authMethod === "token" ? (

Subscription Token

Use your Claude subscription (no API charges)

{claudeCliStatus?.installed ? ( <> {/* Primary: Automated OAuth setup */} {/* Divider */}
or paste manually
{/* Fallback: Manual token entry */}
setOAuthToken(e.target.value)} className="bg-input border-border text-foreground" data-testid="oauth-token-input" />
) : (

Install Claude CLI first to use subscription authentication

)}
) : authMethod === "api_key" ? ( /* Option 2: API Key */

API Key

Pay-per-use with your Anthropic API key

setApiKey(e.target.value)} className="bg-input border-border text-foreground" data-testid="anthropic-api-key-input" />

Get your API key from{" "} console.anthropic.com

) : ( /* Auth Method Selection */
)}
)} {/* Success State */} {isAuthenticated && (

Claude is ready to use!

{getAuthMethodLabel() && `Using ${getAuthMethodLabel()}. `}You can proceed to the next step

)} {/* Navigation */}
{/* OAuth Setup Modal */} setShowTokenModal(false)} onTokenObtained={handleTokenFromModal} />
); } // Codex Setup Step function CodexSetupStep({ onNext, onBack, onSkip, }: { onNext: () => void; onBack: () => void; onSkip: () => void; }) { const { codexCliStatus, codexAuthStatus, codexInstallProgress, setCodexCliStatus, setCodexAuthStatus, setCodexInstallProgress, } = useSetupStore(); const { setApiKeys, apiKeys } = useAppStore(); const [isChecking, setIsChecking] = useState(false); const [isInstalling, setIsInstalling] = useState(false); const [showApiKeyInput, setShowApiKeyInput] = useState(false); const [apiKey, setApiKey] = useState(""); const [isSavingKey, setIsSavingKey] = useState(false); // Normalize CLI auth method strings to our store-friendly values const mapAuthMethod = (method?: string): CodexAuthStatus["method"] => { switch (method) { case "cli_verified": return "cli_verified"; case "cli_tokens": return "cli_tokens"; case "auth_file": return "api_key"; case "env_var": return "env"; default: return "none"; } }; const checkStatus = useCallback(async () => { console.log("[Codex Setup] Starting status check..."); setIsChecking(true); try { const api = getElectronAPI(); const setupApi = api.setup; console.log("[Codex Setup] Setup API available:", !!setupApi); console.log( "[Codex Setup] getCodexStatus available:", !!setupApi?.getCodexStatus ); if (setupApi?.getCodexStatus) { const result = await setupApi.getCodexStatus(); console.log("[Codex Setup] Raw status result:", result); if (result.success) { const cliStatus = { installed: result.status === "installed", path: result.path || null, version: result.version || null, method: result.method || "none", }; console.log("[Codex Setup] CLI Status:", cliStatus); setCodexCliStatus(cliStatus); if (result.auth) { const method = mapAuthMethod(result.auth.method); const authStatus: CodexAuthStatus = { authenticated: result.auth.authenticated, method, // Only set apiKeyValid for actual API key methods, not CLI login apiKeyValid: method === "cli_verified" || method === "cli_tokens" ? undefined : result.auth.authenticated, }; console.log("[Codex Setup] Auth Status:", authStatus); setCodexAuthStatus(authStatus); } else { console.log("[Codex Setup] No auth info in result"); } } else { console.log("[Codex Setup] Status check failed:", result.error); } } else { console.log("[Codex Setup] Setup API not available (web mode?)"); } } catch (error) { console.error("[Codex Setup] Failed to check Codex status:", error); } finally { setIsChecking(false); console.log("[Codex Setup] Status check complete"); } }, [setCodexCliStatus, setCodexAuthStatus]); useEffect(() => { checkStatus(); }, [checkStatus]); const handleInstall = async () => { setIsInstalling(true); setCodexInstallProgress({ isInstalling: true, currentStep: "Installing Codex CLI via npm...", progress: 0, output: [], }); try { const api = getElectronAPI(); const setupApi = api.setup; if (setupApi?.installCodex) { const unsubscribe = setupApi.onInstallProgress?.( (progress: { cli?: string; data?: string; type?: string }) => { if (progress.cli === "codex") { setCodexInstallProgress({ output: [ ...codexInstallProgress.output, progress.data || progress.type || "", ], }); } } ); const result = await setupApi.installCodex(); unsubscribe?.(); if (result.success) { toast.success("Codex CLI installed successfully"); await checkStatus(); } else { toast.error("Installation failed", { description: result.error, }); } } } catch (error) { console.error("Failed to install Codex:", error); toast.error("Installation failed"); } finally { setIsInstalling(false); setCodexInstallProgress({ isInstalling: false }); } }; const handleSaveApiKey = async () => { console.log("[Codex Setup] Saving API key..."); if (!apiKey.trim()) { console.log("[Codex Setup] API key is empty"); toast.error("Please enter an API key"); return; } setIsSavingKey(true); try { const api = getElectronAPI(); const setupApi = api.setup; console.log( "[Codex Setup] storeApiKey available:", !!setupApi?.storeApiKey ); if (setupApi?.storeApiKey) { console.log("[Codex Setup] Calling storeApiKey for openai..."); const result = await setupApi.storeApiKey("openai", apiKey); console.log("[Codex Setup] storeApiKey result:", result); if (result.success) { console.log( "[Codex Setup] API key stored successfully, updating state..." ); setApiKeys({ ...apiKeys, openai: apiKey }); setCodexAuthStatus({ authenticated: true, method: "api_key", apiKeyValid: true, }); toast.success("OpenAI API key saved"); setShowApiKeyInput(false); } else { console.log("[Codex Setup] Failed to store API key:", result.error); } } else { console.log( "[Codex Setup] Web mode - storing API key in app state only" ); setApiKeys({ ...apiKeys, openai: apiKey }); setCodexAuthStatus({ authenticated: true, method: "api_key", apiKeyValid: true, }); toast.success("OpenAI API key saved"); setShowApiKeyInput(false); } } catch (error) { console.error("[Codex Setup] Failed to save API key:", error); toast.error("Failed to save API key"); } finally { setIsSavingKey(false); } }; const copyCommand = (command: string) => { navigator.clipboard.writeText(command); toast.success("Command copied to clipboard"); }; const isAuthenticated = codexAuthStatus?.authenticated || apiKeys.openai; const getAuthMethodLabel = () => { if (!isAuthenticated) return null; if (apiKeys.openai) return "API Key (Manual)"; if (codexAuthStatus?.method === "api_key") return "API Key (Auth File)"; if (codexAuthStatus?.method === "env") return "API Key (Environment)"; if (codexAuthStatus?.method === "cli_verified") return "CLI Login (ChatGPT)"; return "Authenticated"; }; return (

Codex CLI Setup

OpenAI's GPT-5.1 Codex for advanced code generation

{/* Status Card */}
Installation Status
CLI Installation {isChecking ? ( ) : codexCliStatus?.installed ? ( ) : ( )}
{codexCliStatus?.version && (
Version {codexCliStatus.version}
)}
Authentication {isAuthenticated ? (
{getAuthMethodLabel() && ( ({getAuthMethodLabel()}) )}
) : ( )}
{/* Installation Section */} {!codexCliStatus?.installed && ( Install Codex CLI Install via npm (Node.js required)
npm install -g @openai/codex
{codexInstallProgress.isInstalling && ( )}

Requires Node.js to be installed. If the auto-install fails, try running the command manually in your terminal.

)} {/* Authentication Section */} {!isAuthenticated && ( Authentication Codex requires an OpenAI API key {codexCliStatus?.installed && (

Authenticate via CLI

Run this command in your terminal:

codex auth login
)}
or enter API key
{showApiKeyInput ? (
setApiKey(e.target.value)} className="bg-input border-border text-foreground" data-testid="openai-api-key-input" />

Get your API key from{" "} platform.openai.com

) : ( )}
)} {/* Success State */} {isAuthenticated && (

Codex is ready to use!

{getAuthMethodLabel() && `Authenticated via ${getAuthMethodLabel()}. `} You can proceed to complete setup

)} {/* Navigation */}
); } // Complete Step function CompleteStep({ onFinish }: { onFinish: () => void }) { const { claudeCliStatus, claudeAuthStatus, codexCliStatus, codexAuthStatus } = useSetupStore(); const { apiKeys } = useAppStore(); const claudeReady = (claudeCliStatus?.installed && claudeAuthStatus?.authenticated) || apiKeys.anthropic; const codexReady = (codexCliStatus?.installed && codexAuthStatus?.authenticated) || apiKeys.openai; return (

Setup Complete!

Your development environment is configured. You're ready to start building with AI-powered assistance.

{claudeReady ? ( ) : ( )}

Claude

{claudeReady ? "Ready to use" : "Configure later in settings"}

{codexReady ? ( ) : ( )}

Codex

{codexReady ? "Ready to use" : "Configure later in settings"}

Your credentials are secure

API keys are stored locally and never sent to our servers

); } // Main Setup View export function SetupView() { const { currentStep, setCurrentStep, completeSetup, setSkipClaudeSetup, setSkipCodexSetup, } = useSetupStore(); const { setCurrentView } = useAppStore(); const steps = ["welcome", "claude", "codex", "complete"] as const; type StepName = (typeof steps)[number]; const getStepName = (): StepName => { if (currentStep === "claude_detect" || currentStep === "claude_auth") return "claude"; if (currentStep === "codex_detect" || currentStep === "codex_auth") return "codex"; if (currentStep === "welcome") return "welcome"; return "complete"; }; const currentIndex = steps.indexOf(getStepName()); const handleNext = (from: string) => { console.log( "[Setup Flow] handleNext called from:", from, "currentStep:", currentStep ); switch (from) { case "welcome": console.log("[Setup Flow] Moving to claude_detect step"); setCurrentStep("claude_detect"); break; case "claude": console.log("[Setup Flow] Moving to codex_detect step"); setCurrentStep("codex_detect"); break; case "codex": console.log("[Setup Flow] Moving to complete step"); setCurrentStep("complete"); break; } }; const handleBack = (from: string) => { console.log("[Setup Flow] handleBack called from:", from); switch (from) { case "claude": setCurrentStep("welcome"); break; case "codex": setCurrentStep("claude_detect"); break; } }; const handleSkipClaude = () => { console.log("[Setup Flow] Skipping Claude setup"); setSkipClaudeSetup(true); setCurrentStep("codex_detect"); }; const handleSkipCodex = () => { console.log("[Setup Flow] Skipping Codex setup"); setSkipCodexSetup(true); setCurrentStep("complete"); }; const handleFinish = () => { console.log("[Setup Flow] handleFinish called - completing setup"); completeSetup(); console.log("[Setup Flow] Setup completed, redirecting to welcome view"); setCurrentView("welcome"); }; return (
{/* Header */}
{/* eslint-disable-next-line @next/next/no-img-element */} Automaker Automaker Setup
{/* Content */}
{currentStep === "welcome" && ( handleNext("welcome")} /> )} {(currentStep === "claude_detect" || currentStep === "claude_auth") && ( handleNext("claude")} onBack={() => handleBack("claude")} onSkip={handleSkipClaude} /> )} {(currentStep === "codex_detect" || currentStep === "codex_auth") && ( handleNext("codex")} onBack={() => handleBack("codex")} onSkip={handleSkipCodex} /> )} {currentStep === "complete" && ( )}
); }