UNPKG

@hhoangphuoc/escape-room-cli

Version:

A CLI for playing AI-generated escape room games. Install globally with: npm install -g @hhoangphuoc/escape-room-cli

304 lines (303 loc) 16 kB
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; import { useState, useEffect } from 'react'; import { Box, Text, useInput } from 'ink'; import TextInput from 'ink-text-input'; import Spinner from 'ink-spinner'; import fs from 'fs'; import path from 'path'; import os from 'os'; import { getApiUrl } from '../utils/apiConfig.js'; import { useAuth } from '../hooks/useAuth.js'; import { validateName, validateEmail, validateOpenAIApiKey, validateAnthropicApiKey, validateRegistrationData } from '../utils/validation.js'; const USER_CONFIG_FILE = path.join(os.homedir(), '.escape-room-config.json'); export async function handleLogin(userId, apiKey, provider) { const apiUrl = getApiUrl(); const loginPayload = { userId }; if (apiKey && provider) { loginPayload.apiKey = apiKey; loginPayload.provider = provider; } const loginResponse = await fetch(`${apiUrl}/api/users/login`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(loginPayload) }); return loginResponse; } const UserRegistration = ({ onRegistrationComplete, onCancel, username = '', email = '' }) => { const { login } = useAuth(); const [step, setStep] = useState('verifying'); const [name, setName] = useState(username); const [userEmail, setUserEmail] = useState(email); // Can be empty string const [currentApiKeyProvider, setCurrentApiKeyProvider] = useState('openai'); const [currentCliApiKey, setCurrentCliApiKey] = useState(''); // Can be empty string const [message, setMessage] = useState('Checking for existing configuration...'); const [isSubmitting, setIsSubmitting] = useState(false); // Validation states const [validationErrors, setValidationErrors] = useState({}); const [isValidating, setIsValidating] = useState(false); const [validationMessage, setValidationMessage] = useState(''); // Handle ESC key to cancel registration useInput((_, key) => { if (key.escape && onCancel) { // Don't allow cancellation during submission/loading states if (step !== 'loading' && step !== 'verifying' && !isSubmitting) { onCancel(); } } }); useEffect(() => { setMessage('Please register to continue.'); let initialStep = 'name'; if (username) { setName(username); initialStep = 'email'; if (email) { setUserEmail(email); initialStep = 'apiKey'; } } const openaiEnvKey = process.env['OPENAI_API_KEY']; const anthropicEnvKey = process.env['ANTHROPIC_API_KEY']; if (openaiEnvKey) { setCurrentCliApiKey(openaiEnvKey); setCurrentApiKeyProvider('openai'); } else if (anthropicEnvKey) { setCurrentCliApiKey(anthropicEnvKey); setCurrentApiKeyProvider('anthropic'); } setStep(initialStep); }, []); // Validation functions const validateNameInput = async (nameValue) => { const validation = validateName(nameValue); if (!validation.isValid) { setValidationErrors(prev => ({ ...prev, name: validation.error })); return false; } setValidationErrors(prev => ({ ...prev, name: undefined })); return true; }; const validateEmailInput = async (emailValue) => { if (!emailValue.trim()) { // Email is optional setValidationErrors(prev => ({ ...prev, email: undefined })); return true; } setIsValidating(true); setValidationMessage('Validating email...'); try { const validation = await validateEmail(emailValue); if (!validation.isValid) { setValidationErrors(prev => ({ ...prev, email: validation.error })); return false; } setValidationErrors(prev => ({ ...prev, email: undefined })); return true; } catch (error) { setValidationErrors(prev => ({ ...prev, email: 'Email validation failed' })); return false; } finally { setIsValidating(false); setValidationMessage(''); } }; const validateApiKeyInput = async (apiKeyValue) => { if (!apiKeyValue.trim()) { // API key is optional setValidationErrors(prev => ({ ...prev, apiKey: undefined })); return true; } setIsValidating(true); setValidationMessage('Validating API key...'); try { let validation; if (currentApiKeyProvider === 'openai' || apiKeyValue.startsWith('sk-proj-')) { validation = await validateOpenAIApiKey(apiKeyValue); } else { validation = validateAnthropicApiKey(apiKeyValue); } if (!validation.isValid) { setValidationErrors(prev => ({ ...prev, apiKey: validation.error })); return false; } setValidationErrors(prev => ({ ...prev, apiKey: undefined })); return true; } catch (error) { setValidationErrors(prev => ({ ...prev, apiKey: 'API key validation failed' })); return false; } finally { setIsValidating(false); setValidationMessage(''); } }; // Step navigation with validation const handleNameSubmit = async () => { const isValid = await validateNameInput(name); if (isValid) { setStep('email'); } }; const handleEmailSubmit = async () => { const isValid = await validateEmailInput(userEmail); if (isValid) { setStep('apiKey'); } }; const saveUserConfig = async () => { if (isSubmitting) return; setIsSubmitting(true); setStep('loading'); setMessage('Validating registration data...'); // Validation before submission const validationResult = await validateRegistrationData({ name, email: userEmail || undefined, apiKey: currentCliApiKey || undefined, provider: currentApiKeyProvider }); if (!validationResult.isValid) { setValidationErrors(validationResult.errors); setMessage('Please fix validation errors and try again.'); setStep('apiKey'); setIsSubmitting(false); return; } setMessage('Registering and saving configuration...'); const finalRegApiKey = currentCliApiKey || undefined; const finalRegProvider = currentApiKeyProvider; // Modified: Don't send API key to backend - keep it local only const registrationPayload = { name, provider: finalRegProvider }; if (userEmail) registrationPayload.email = userEmail; // API key is NOT included in the payload to backend - UNCOMMENTED //if (finalRegApiKey) registrationPayload.apiKey = finalRegApiKey; let receivedUserId; let receivedToken; try { const apiUrl = getApiUrl(); const response = await fetch(`${apiUrl}/api/users/register`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(registrationPayload) }); const data = await response.json(); if (response.ok && data.userId && data.token) { receivedUserId = data.userId; receivedToken = data.token; setMessage('Registered with backend server!'); try { // API key saved locally only const configToSave = { name, userId: receivedUserId, registeredAt: new Date().toISOString(), apiKeys: finalRegApiKey && finalRegProvider ? { [finalRegProvider]: finalRegApiKey } : undefined }; if (userEmail) configToSave.email = userEmail; fs.writeFileSync(USER_CONFIG_FILE, JSON.stringify(configToSave, null, 2)); setMessage('Configuration saved! Registration complete.'); login({ userName: name, userId: receivedUserId, sessionToken: receivedToken, apiKey: finalRegApiKey, apiKeyProvider: finalRegProvider }); onRegistrationComplete(); } catch (saveError) { setMessage('Registered successfully, but could not save configuration locally.'); login({ userName: name, userId: receivedUserId, sessionToken: receivedToken, apiKey: finalRegApiKey, apiKeyProvider: finalRegProvider }); onRegistrationComplete(); } } else { // Enhanced error handling for different HTTP status codes let errorMessage = `Registration failed: ${data.error || 'Unknown error'}`; if (response.status === 409) { errorMessage = `Email already registered: ${data.error || 'Try using a different email.'}`; } else if (response.status === 400) { errorMessage = `Invalid registration data: ${data.error || 'Please check your inputs.'}`; } else if (response.status >= 500) { errorMessage = `Server error (${response.status}): ${data.error || 'Please try again later.'}`; } setMessage(errorMessage); setStep('apiKey'); setIsSubmitting(false); } } catch (networkError) { let errorMessage = 'Network error. Could not register.'; if (networkError instanceof Error) { if (networkError.message.includes('fetch')) { errorMessage = 'Connection failed. Please check your internet connection and server status.'; } else if (networkError.message.includes('timeout')) { errorMessage = 'Request timed out. Please try again.'; } } setMessage(errorMessage); setStep('apiKey'); setIsSubmitting(false); } }; if (step === 'verifying' || step === 'loading') { return (_jsx(Box, { padding: 1, children: _jsxs(Text, { color: "yellow", children: [_jsx(Spinner, { type: "dots" }), " ", message] }) })); } if (step === 'name') { return (_jsxs(Box, { flexDirection: "column", rowGap: 0.5, padding: 0.5, children: [_jsxs(Box, { flexDirection: "column", padding: 0.5, marginBottom: 1, borderStyle: "round", borderColor: "cyan", rowGap: 0.5, children: [_jsx(Text, { bold: true, color: "cyan", children: "What's your name?" }), _jsx(TextInput, { value: name, onChange: (value) => { setName(value); setValidationErrors(prev => ({ ...prev, name: undefined })); // Clear error on change }, onSubmit: handleNameSubmit, placeholder: "Your Name" }), validationErrors.name && (_jsxs(Text, { color: "red", children: ["\u2717 ", validationErrors.name] }))] }), _jsx(Box, { marginTop: 0.5, children: _jsxs(Text, { color: "gray", children: ["Press Enter to continue ", onCancel ? ' • ESC to cancel' : ''] }) })] })); } if (step === 'email') { return (_jsxs(Box, { flexDirection: "column", rowGap: 0.5, padding: 0.5, children: [_jsxs(Box, { flexDirection: "column", padding: 0.5, marginBottom: 1, borderStyle: "round", borderColor: "cyan", rowGap: 0.5, children: [_jsx(Text, { bold: true, color: "cyan", children: "Your email (optional):" }), _jsx(TextInput, { value: userEmail, onChange: (value) => { setUserEmail(value); setValidationErrors(prev => ({ ...prev, email: undefined })); // Clear error on change }, onSubmit: handleEmailSubmit, placeholder: "your.email@nedap.com" }), validationErrors.email && (_jsxs(Text, { color: "red", children: ["\u2717 ", validationErrors.email] })), isValidating && validationMessage && (_jsxs(Text, { color: "yellow", children: [_jsx(Spinner, { type: "dots" }), " ", validationMessage] }))] }), _jsx(Box, { marginTop: 0.5, children: _jsxs(Text, { color: "gray", children: ["Press Enter to continue ", onCancel ? ' • ESC to cancel' : ''] }) })] })); } if (step === 'apiKey') { const handleApiKeySubmit = async () => { if (currentCliApiKey.trim()) { const isValid = await validateApiKeyInput(currentCliApiKey); if (!isValid) { return; } } await saveUserConfig(); }; return (_jsxs(Box, { flexDirection: "column", rowGap: 0.5, padding: 0.5, children: [_jsxs(Box, { flexDirection: "column", padding: 0.5, marginBottom: 1, borderStyle: "round", borderColor: "cyan", rowGap: 0.5, children: [_jsx(Text, { bold: true, color: "cyan", children: "Your AI API Key (optional - OpenAI or Anthropic):" }), _jsxs(Box, { marginTop: 1, flexDirection: "row", alignItems: "center", children: [_jsx(Text, { color: "cyan", children: "Current AI Provider: " }), _jsxs(Text, { children: [" [", currentApiKeyProvider.toUpperCase(), "] "] })] }), _jsxs(Box, { children: [_jsx(Text, { color: "cyan", children: "API Key: " }), _jsx(TextInput, { value: currentCliApiKey, onChange: (value) => { setCurrentCliApiKey(value); setValidationErrors(prev => ({ ...prev, apiKey: undefined })); // Clear error on change // Auto-detect provider based on key format if (value.startsWith('sk-proj-')) { setCurrentApiKeyProvider('openai'); } else if (value.startsWith('sk-ant-') || value.startsWith('anthropic')) { setCurrentApiKeyProvider('anthropic'); } }, onSubmit: handleApiKeySubmit, placeholder: "sk-proj-... or sk-ant-...", mask: "*" })] }), validationErrors.apiKey && (_jsx(Box, { marginTop: 1, children: _jsxs(Text, { color: "red", children: ["\u2717 ", validationErrors.apiKey] }) })), isValidating && validationMessage && (_jsx(Box, { marginTop: 1, children: _jsxs(Text, { color: "yellow", children: [_jsx(Spinner, { type: "dots" }), " ", validationMessage] }) })), _jsxs(Box, { marginTop: 1, children: [currentCliApiKey && _jsxs(Text, { color: "gray", children: ["Your key will be stored locally only in ", USER_CONFIG_FILE] }), !currentCliApiKey && _jsx(Text, { color: "gray", children: "You can skip this and add an API key later for AI features" })] })] }), _jsx(Box, { marginTop: 0.5, children: _jsxs(Text, { color: "gray", children: ["Press Enter to register and save", onCancel ? ' • ESC to cancel' : '', "."] }) })] })); } if (step === 'complete') { return (_jsx(Box, { padding: 1, children: _jsxs(Text, { color: "green", children: ["\u2713 ", message || 'Ready! Starting game...'] }) })); } return _jsx(Text, { children: "Loading registration..." }); }; export default UserRegistration;