| import { useState, useEffect, useCallback, useRef } from 'react'; |
| import { createLogger } from '@automaker/utils/logger'; |
| import { Button } from '@/components/ui/button'; |
| import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; |
| import { Badge } from '@/components/ui/badge'; |
| import { useSetupStore } from '@/store/setup-store'; |
| import { getElectronAPI } from '@/lib/electron'; |
| import { |
| CheckCircle2, |
| ArrowRight, |
| ArrowLeft, |
| ExternalLink, |
| Copy, |
| RefreshCw, |
| AlertTriangle, |
| XCircle, |
| } from 'lucide-react'; |
| import { Spinner } from '@/components/ui/spinner'; |
| import { toast } from 'sonner'; |
| import { StatusBadge } from '../components'; |
| import { CursorIcon } from '@/components/ui/provider-icon'; |
|
|
| const logger = createLogger('CursorSetupStep'); |
|
|
| interface CursorSetupStepProps { |
| onNext: () => void; |
| onBack: () => void; |
| onSkip: () => void; |
| } |
|
|
| interface CursorCliStatus { |
| installed: boolean; |
| version?: string | null; |
| path?: string | null; |
| auth?: { |
| authenticated: boolean; |
| method: string; |
| }; |
| installCommand?: string; |
| loginCommand?: string; |
| } |
|
|
| export function CursorSetupStep({ onNext, onBack, onSkip }: CursorSetupStepProps) { |
| const { cursorCliStatus, setCursorCliStatus } = useSetupStore(); |
| const [isChecking, setIsChecking] = useState(false); |
| const [isLoggingIn, setIsLoggingIn] = useState(false); |
| const pollIntervalRef = useRef<NodeJS.Timeout | null>(null); |
|
|
| const checkStatus = useCallback(async () => { |
| setIsChecking(true); |
| try { |
| const api = getElectronAPI(); |
| if (!api.setup?.getCursorStatus) { |
| return; |
| } |
| const result = await api.setup.getCursorStatus(); |
| if (result.success) { |
| const status: CursorCliStatus = { |
| installed: result.installed ?? false, |
| version: result.version, |
| path: result.path, |
| auth: result.auth, |
| installCommand: result.installCommand, |
| loginCommand: result.loginCommand, |
| }; |
| setCursorCliStatus(status); |
|
|
| if (result.auth?.authenticated) { |
| toast.success('Cursor CLI is ready!'); |
| } |
| } |
| } catch (error) { |
| logger.error('Failed to check Cursor status:', error); |
| } finally { |
| setIsChecking(false); |
| } |
| }, [setCursorCliStatus]); |
|
|
| useEffect(() => { |
| checkStatus(); |
| |
| return () => { |
| if (pollIntervalRef.current) { |
| clearInterval(pollIntervalRef.current); |
| } |
| }; |
| }, [checkStatus]); |
|
|
| const copyCommand = (command: string) => { |
| navigator.clipboard.writeText(command); |
| toast.success('Command copied to clipboard'); |
| }; |
|
|
| const handleLogin = async () => { |
| setIsLoggingIn(true); |
|
|
| try { |
| |
| const loginCommand = cursorCliStatus?.loginCommand || 'cursor-agent login'; |
| await navigator.clipboard.writeText(loginCommand); |
| toast.info('Login command copied! Paste in terminal to authenticate.'); |
|
|
| |
| let attempts = 0; |
| const maxAttempts = 60; |
|
|
| pollIntervalRef.current = setInterval(async () => { |
| attempts++; |
|
|
| try { |
| const api = getElectronAPI(); |
| if (!api.setup?.getCursorStatus) { |
| return; |
| } |
| const result = await api.setup.getCursorStatus(); |
|
|
| if (result.auth?.authenticated) { |
| if (pollIntervalRef.current) { |
| clearInterval(pollIntervalRef.current); |
| pollIntervalRef.current = null; |
| } |
| setCursorCliStatus({ |
| ...cursorCliStatus, |
| installed: result.installed ?? true, |
| version: result.version, |
| path: result.path, |
| auth: result.auth, |
| } as CursorCliStatus); |
| setIsLoggingIn(false); |
| toast.success('Successfully logged in to Cursor!'); |
| } |
| } catch { |
| |
| } |
|
|
| if (attempts >= maxAttempts) { |
| if (pollIntervalRef.current) { |
| clearInterval(pollIntervalRef.current); |
| pollIntervalRef.current = null; |
| } |
| setIsLoggingIn(false); |
| toast.error('Login timed out. Please try again.'); |
| } |
| }, 2000); |
| } catch (error) { |
| logger.error('Login failed:', error); |
| toast.error('Failed to start login process'); |
| setIsLoggingIn(false); |
| } |
| }; |
|
|
| const isReady = cursorCliStatus?.installed && cursorCliStatus?.auth?.authenticated; |
|
|
| const getStatusBadge = () => { |
| if (isChecking) { |
| return <StatusBadge status="checking" label="Checking..." />; |
| } |
| if (cursorCliStatus?.auth?.authenticated) { |
| return <StatusBadge status="authenticated" label="Ready" />; |
| } |
| if (cursorCliStatus?.installed) { |
| return <StatusBadge status="unverified" label="Not Logged In" />; |
| } |
| return <StatusBadge status="not_installed" label="Not Installed" />; |
| }; |
|
|
| return ( |
| <div className="space-y-6"> |
| <div className="text-center mb-8"> |
| <div className="w-16 h-16 rounded-xl bg-cyan-500/10 flex items-center justify-center mx-auto mb-4"> |
| <CursorIcon className="w-8 h-8 text-cyan-500" /> |
| </div> |
| <h2 className="text-2xl font-bold text-foreground mb-2">Cursor CLI Setup</h2> |
| <p className="text-muted-foreground">Optional - Use Cursor as an AI provider</p> |
| </div> |
| |
| {/* Info Banner */} |
| <Card className="bg-cyan-500/10 border-cyan-500/20"> |
| <CardContent className="pt-4"> |
| <div className="flex items-start gap-3"> |
| <AlertTriangle className="w-5 h-5 text-cyan-500 shrink-0 mt-0.5" /> |
| <div> |
| <p className="font-medium text-foreground">This step is optional</p> |
| <p className="text-sm text-muted-foreground mt-1"> |
| Configure Cursor CLI as an alternative AI provider. You can skip this and use Claude |
| instead, or configure it later in Settings. |
| </p> |
| </div> |
| </div> |
| </CardContent> |
| </Card> |
| |
| {/* Status Card */} |
| <Card className="bg-card border-border"> |
| <CardHeader> |
| <div className="flex items-center justify-between"> |
| <CardTitle className="text-lg flex items-center gap-2"> |
| <CursorIcon className="w-5 h-5" /> |
| Cursor CLI Status |
| <Badge variant="outline" className="ml-2"> |
| Optional |
| </Badge> |
| </CardTitle> |
| <div className="flex items-center gap-2"> |
| {getStatusBadge()} |
| <Button variant="ghost" size="sm" onClick={checkStatus} disabled={isChecking}> |
| {isChecking ? <Spinner size="sm" /> : <RefreshCw className="w-4 h-4" />} |
| </Button> |
| </div> |
| </div> |
| <CardDescription> |
| {cursorCliStatus?.installed |
| ? cursorCliStatus.auth?.authenticated |
| ? `Authenticated via ${cursorCliStatus.auth.method === 'api_key' ? 'API Key' : 'Browser Login'}${cursorCliStatus.version ? ` (v${cursorCliStatus.version})` : ''}` |
| : 'Installed but not authenticated' |
| : 'Not installed on your system'} |
| </CardDescription> |
| </CardHeader> |
| <CardContent className="space-y-4"> |
| {/* Success State */} |
| {isReady && ( |
| <div className="flex items-center gap-3 p-4 rounded-lg bg-green-500/10 border border-green-500/20"> |
| <CheckCircle2 className="w-5 h-5 text-green-500" /> |
| <div> |
| <p className="font-medium text-foreground">Cursor CLI is ready!</p> |
| <p className="text-sm text-muted-foreground"> |
| You can use Cursor models for AI tasks. |
| {cursorCliStatus?.version && ( |
| <span className="ml-1">Version: {cursorCliStatus.version}</span> |
| )} |
| </p> |
| </div> |
| </div> |
| )} |
| |
| {/* Not Installed */} |
| {!cursorCliStatus?.installed && !isChecking && ( |
| <div className="space-y-4"> |
| <div className="flex items-start gap-3 p-4 rounded-lg bg-muted/30 border border-border"> |
| <XCircle className="w-5 h-5 text-muted-foreground shrink-0 mt-0.5" /> |
| <div className="flex-1"> |
| <p className="font-medium text-foreground">Cursor CLI not found</p> |
| <p className="text-sm text-muted-foreground mt-1"> |
| Install the Cursor CLI to use Cursor models. |
| </p> |
| </div> |
| </div> |
| |
| <div className="space-y-3 p-4 rounded-lg bg-muted/30 border border-border"> |
| <p className="font-medium text-foreground text-sm">Install Cursor CLI:</p> |
| <div className="flex items-center gap-2"> |
| <code className="flex-1 bg-muted px-3 py-2 rounded text-sm font-mono text-foreground overflow-x-auto"> |
| {cursorCliStatus?.installCommand || |
| 'curl https://cursor.com/install -fsS | bash'} |
| </code> |
| <Button |
| variant="ghost" |
| size="icon" |
| onClick={() => |
| copyCommand( |
| cursorCliStatus?.installCommand || |
| 'curl https://cursor.com/install -fsS | bash' |
| ) |
| } |
| > |
| <Copy className="w-4 h-4" /> |
| </Button> |
| </div> |
| <a |
| href="https://cursor.com/docs/cli" |
| target="_blank" |
| rel="noopener noreferrer" |
| className="inline-flex items-center text-sm text-brand-500 hover:underline mt-2" |
| > |
| View installation docs |
| <ExternalLink className="w-3 h-3 ml-1" /> |
| </a> |
| </div> |
| </div> |
| )} |
| |
| {/* Installed but not authenticated */} |
| {cursorCliStatus?.installed && !cursorCliStatus?.auth?.authenticated && !isChecking && ( |
| <div className="space-y-4"> |
| <div className="flex items-start gap-3 p-4 rounded-lg bg-amber-500/10 border border-amber-500/20"> |
| <AlertTriangle className="w-5 h-5 text-amber-500 shrink-0 mt-0.5" /> |
| <div className="flex-1"> |
| <p className="font-medium text-foreground">Cursor CLI not authenticated</p> |
| <p className="text-sm text-muted-foreground mt-1"> |
| Run the login command to authenticate with Cursor. |
| </p> |
| </div> |
| </div> |
| |
| <div className="space-y-3 p-4 rounded-lg bg-muted/30 border border-border"> |
| <p className="text-sm text-muted-foreground"> |
| Run the login command in your terminal, then complete authentication in your |
| browser: |
| </p> |
| <div className="flex items-center gap-2"> |
| <code className="flex-1 bg-muted px-3 py-2 rounded text-sm font-mono text-foreground"> |
| {cursorCliStatus?.loginCommand || 'cursor-agent login'} |
| </code> |
| <Button |
| variant="ghost" |
| size="icon" |
| onClick={() => |
| copyCommand(cursorCliStatus?.loginCommand || 'cursor-agent login') |
| } |
| > |
| <Copy className="w-4 h-4" /> |
| </Button> |
| </div> |
| <Button |
| onClick={handleLogin} |
| disabled={isLoggingIn} |
| className="w-full bg-brand-500 hover:bg-brand-600 text-white" |
| > |
| {isLoggingIn ? ( |
| <> |
| <Spinner size="sm" variant="foreground" className="mr-2" /> |
| Waiting for login... |
| </> |
| ) : ( |
| 'Copy Command & Wait for Login' |
| )} |
| </Button> |
| </div> |
| </div> |
| )} |
| |
| {/* Loading State */} |
| {isChecking && ( |
| <div className="flex items-center gap-3 p-4 rounded-lg bg-blue-500/10 border border-blue-500/20"> |
| <Spinner size="md" /> |
| <div> |
| <p className="font-medium text-foreground">Checking Cursor CLI status...</p> |
| </div> |
| </div> |
| )} |
| </CardContent> |
| </Card> |
|
|
| {} |
| <div className="flex justify-between pt-4"> |
| <Button variant="ghost" onClick={onBack} className="text-muted-foreground"> |
| <ArrowLeft className="w-4 h-4 mr-2" /> |
| Back |
| </Button> |
| <div className="flex gap-2"> |
| <Button variant="ghost" onClick={onSkip} className="text-muted-foreground"> |
| {isReady ? 'Skip' : 'Skip for now'} |
| </Button> |
| <Button |
| onClick={onNext} |
| className="bg-brand-500 hover:bg-brand-600 text-white" |
| data-testid="cursor-next-button" |
| > |
| {isReady ? 'Continue' : 'Continue without Cursor'} |
| <ArrowRight className="w-4 h-4 ml-2" /> |
| </Button> |
| </div> |
| </div> |
|
|
| {} |
| <p className="text-xs text-muted-foreground text-center"> |
| You can always configure Cursor later in Settings |
| </p> |
| </div> |
| ); |
| } |
|
|