/** * @license * Copyright 2025 Vybestack LLC * SPDX-License-Identifier: Apache-2.0 */ import type React from 'react'; import { useState, useCallback } from 'react'; import { Box, Text } from 'ink'; import { Colors } from '../../colors.js'; import { RadioButtonSelect } from '../shared/RadioButtonSelect.js'; import { TextInput } from './TextInput.js'; import { useKeypress } from '../../hooks/useKeypress.js'; import { PROVIDER_OPTIONS } from './constants.js'; import { validateKeyFile } from './validation.js'; import { getStepPosition } from './utils.js'; import type { WizardState } from './types.js'; import { firstNonEmptyString } from '../../../utils/coalesce.js'; const buildAuthOptions = ( providerOption: { supportsOAuth?: boolean } | undefined, ): Array<{ label: string; value: string; key: string }> => { const options: Array<{ label: string; value: string; key: string }> = [ { label: 'Enter API key now', value: 'apikey', key: 'apikey' }, { label: 'Use key file (provide path)', value: 'keyfile', key: 'keyfile' }, ]; if (providerOption?.supportsOAuth ?? false) { options.push({ label: 'OAuth (authenticate when needed)', value: 'oauth', key: 'oauth', }); } options.push( { label: 'Skip for now (configure manually later)', value: 'skip', key: 'skip', }, { label: '← Back', value: '__back__', key: '__back__' }, ); return options; }; const AuthApiKeyInput: React.FC<{ authInput: string; validationError: string | null; handleAuthInputChange: (value: string) => void; handleAuthInputSubmit: () => void; }> = ({ authInput, validationError, handleAuthInputChange, handleAuthInputSubmit, }) => ( <> API Key: {validationError && ( ✗ {validationError} )} ℹ Your key will be stored in the profile JSON file. For better security, consider using a key file instead. Enter Continue Esc Back to list ); const AuthKeyFileInput: React.FC<{ authInput: string; validationError: string | null; isPathValidated: boolean; handleAuthInputChange: (value: string) => void; handleAuthInputSubmit: () => void; }> = ({ authInput, validationError, isPathValidated, handleAuthInputChange, handleAuthInputSubmit, }) => ( <> Key file path: {validationError && ( ✗ {validationError} )} {!validationError && isPathValidated && ( ✓ Valid path )} ℹ Supports ~ expansion for home directory ← → Move cursor Enter Continue Esc Back to list ); const AuthOAuthInput: React.FC<{ oauthBuckets: string; onOauthBucketsChange: (value: string) => void; handleAuthInputSubmit: () => void; }> = ({ oauthBuckets, onOauthBucketsChange, handleAuthInputSubmit }) => ( <> OAuth Buckets (optional): Enter comma-separated bucket names, or leave empty for default ℹ You'll authenticate when you first load this profile Enter Continue Esc Back to list ); const getAuthHeaderTitle = ( focusedComponent: 'select' | 'input', authMethod: AuthMethod, ): string => { if (focusedComponent !== 'input') { return 'Authentication:'; } if (authMethod === 'apikey') { return 'Enter API Key:'; } if (authMethod === 'keyfile') { return 'Specify Key File:'; } return 'Configure OAuth:'; }; const getAuthHeaderDetail = ( focusedComponent: 'select' | 'input', authMethod: AuthMethod, providerLabel: string | null, ): string => { if (focusedComponent !== 'input') { return `Choose how to authenticate with ${providerLabel}`; } if (authMethod === 'apikey') { return `Enter your ${providerLabel} API key:`; } if (authMethod === 'keyfile') { return 'Enter the path to your API key file:'; } return 'OAuth authentication will be set up when you load this profile'; }; const AuthHeaderView: React.FC<{ focusedComponent: 'select' | 'input'; authMethod: AuthMethod; providerLabel: string | null; }> = ({ focusedComponent, authMethod, providerLabel }) => ( <> {getAuthHeaderTitle(focusedComponent, authMethod)} {getAuthHeaderDetail(focusedComponent, authMethod, providerLabel)} ); const processAuthSubmit = async ( authMethod: 'apikey' | 'keyfile' | 'oauth' | 'skip' | null, authInput: string, oauthBuckets: string, onUpdateAuth: (auth: WizardState['config']['auth']) => void, onContinue: () => void, setValidationError: (v: string | null) => void, setIsPathValidated: (v: boolean) => void, ) => { if (authMethod === 'oauth') { const buckets = oauthBuckets .split(',') .map((b) => b.trim()) .filter((b) => b.length > 0); onUpdateAuth({ type: 'oauth', buckets: buckets.length > 0 ? buckets : ['default'], }); onContinue(); return; } if (!authInput.trim()) { setValidationError('This field cannot be empty'); return; } if (authMethod === 'keyfile') { const validation = await validateKeyFile(authInput); if (!validation.valid) { setValidationError( firstNonEmptyString(validation.error, 'Invalid file path'), ); return; } setIsPathValidated(true); } setValidationError(null); if (authMethod === 'apikey') { onUpdateAuth({ type: 'apikey', value: authInput }); } else if (authMethod === 'keyfile') { onUpdateAuth({ type: 'keyfile', value: authInput }); } onContinue(); }; type AuthMethod = 'apikey' | 'keyfile' | 'oauth' | 'skip' | null; const AuthSelectView: React.FC<{ authOptions: Array<{ label: string; value: string; key: string }>; handleAuthSelect: (value: string) => void; }> = ({ authOptions, handleAuthSelect }) => ( <> Esc Back ); const AuthInputView: React.FC<{ authMethod: AuthMethod; authInput: string; validationError: string | null; isPathValidated: boolean; oauthBuckets: string; handleAuthInputChange: (value: string) => void; handleAuthInputSubmit: () => void; onOauthBucketsChange: (value: string) => void; }> = ({ authMethod, authInput, validationError, isPathValidated, oauthBuckets, handleAuthInputChange, handleAuthInputSubmit, onOauthBucketsChange, }) => ( <> {authMethod === 'apikey' && ( )} {authMethod === 'keyfile' && ( )} {authMethod === 'oauth' && ( )} ); const useEscapeHandler = ( focusedComponent: 'select' | 'input', setFocusedComponent: (v: 'select' | 'input') => void, setValidationError: (v: string | null) => void, onBack: () => void, ) => { useKeypress( (key) => { if (key.name === 'escape') { if (focusedComponent === 'input') { setFocusedComponent('select'); setValidationError(null); } else { onBack(); } } }, { isActive: true }, ); }; const applyAuthSelection = ( value: string, setAuthMethod: (m: AuthMethod) => void, setFocusedComponent: (v: 'select' | 'input') => void, onUpdateAuth: (auth: WizardState['config']['auth']) => void, onContinue: () => void, onBack: () => void, ) => { if (value === 'oauth') { setAuthMethod('oauth'); setFocusedComponent('input'); } else if (value === 'skip') { setAuthMethod('skip'); onUpdateAuth({ type: null }); onContinue(); } else if (value === 'apikey') { setAuthMethod('apikey'); setFocusedComponent('input'); } else if (value === 'keyfile') { setAuthMethod('keyfile'); setFocusedComponent('input'); } else if (value === '__back__') { onBack(); } }; const AuthStepHeader: React.FC<{ current: number; total: number; focusedComponent: 'select' | 'input'; authMethod: AuthMethod; providerLabel: string | null; }> = ({ current, total, focusedComponent, authMethod, providerLabel }) => ( <> Create New Profile - Step {current} of {total} ); const AuthContentView: React.FC<{ focusedComponent: 'select' | 'input'; authOptions: Array<{ label: string; value: string; key: string }>; handleAuthSelect: (value: string) => void; authMethod: AuthMethod; authInput: string; validationError: string | null; isPathValidated: boolean; oauthBuckets: string; handleAuthInputChange: (value: string) => void; handleAuthInputSubmit: () => void; onOauthBucketsChange: (value: string) => void; }> = ({ focusedComponent, authOptions, handleAuthSelect, authMethod, authInput, validationError, isPathValidated, oauthBuckets, handleAuthInputChange, handleAuthInputSubmit, onOauthBucketsChange, }) => ( <> {focusedComponent === 'select' && ( )} {focusedComponent === 'input' && ( )} ); const useAuthState = (state: WizardState) => { const [focusedComponent, setFocusedComponent] = useState<'select' | 'input'>( 'select', ); const [authMethod, setAuthMethod] = useState(null); const [authInput, setAuthInput] = useState(''); const [oauthBuckets, setOauthBuckets] = useState('default'); const [validationError, setValidationError] = useState(null); const [isPathValidated, setIsPathValidated] = useState(false); const providerOption = PROVIDER_OPTIONS.find( (p) => p.value === state.config.provider, ); return { focusedComponent, setFocusedComponent, authMethod, setAuthMethod, authInput, setAuthInput, oauthBuckets, setOauthBuckets, validationError, setValidationError, isPathValidated, setIsPathValidated, providerOption, }; }; const useAuthHandlers = ( setAuthMethod: React.Dispatch>, setFocusedComponent: React.Dispatch>, setAuthInput: React.Dispatch>, setValidationError: React.Dispatch>, setIsPathValidated: React.Dispatch>, authMethod: AuthMethod, authInput: string, oauthBuckets: string, onUpdateAuth: (auth: WizardState['config']['auth']) => void, onContinue: () => void, onBack: () => void, ) => { const handleAuthSelect = useCallback( (value: string) => { applyAuthSelection( value, setAuthMethod, setFocusedComponent, onUpdateAuth, onContinue, onBack, ); }, [setAuthMethod, setFocusedComponent, onUpdateAuth, onContinue, onBack], ); const handleAuthInputChange = useCallback( (value: string) => { setAuthInput(value); setValidationError(null); setIsPathValidated(false); }, [setAuthInput, setValidationError, setIsPathValidated], ); const handleAuthInputSubmit = useCallback(() => { void (async () => { try { await processAuthSubmit( authMethod, authInput, oauthBuckets, onUpdateAuth, onContinue, setValidationError, setIsPathValidated, ); } catch (error) { setValidationError( error instanceof Error ? error.message : 'Authentication setup failed', ); } })(); }, [ authInput, authMethod, oauthBuckets, onUpdateAuth, onContinue, setValidationError, setIsPathValidated, ]); return { handleAuthSelect, handleAuthInputChange, handleAuthInputSubmit }; }; const AuthReturnView: React.FC<{ current: number; total: number; focusedComponent: 'select' | 'input'; authMethod: AuthMethod; providerLabel: string | null; authOptions: Array<{ label: string; value: string; key: string }>; handleAuthSelect: (value: string) => void; authInput: string; validationError: string | null; isPathValidated: boolean; oauthBuckets: string; handleAuthInputChange: (value: string) => void; handleAuthInputSubmit: () => void; onOauthBucketsChange: (value: string) => void; }> = ({ current, total, focusedComponent, authMethod, providerLabel, authOptions, handleAuthSelect, authInput, validationError, isPathValidated, oauthBuckets, handleAuthInputChange, handleAuthInputSubmit, onOauthBucketsChange, }) => ( ); interface AuthenticationStepProps { state: WizardState; onUpdateAuth: (auth: WizardState['config']['auth']) => void; onContinue: () => void; onBack: () => void; onCancel: () => void; } export const AuthenticationStep: React.FC = ({ state, onUpdateAuth, onContinue, onBack, onCancel: _onCancel, }) => { const { focusedComponent, setFocusedComponent, authMethod, setAuthMethod, authInput, setAuthInput, oauthBuckets, setOauthBuckets, validationError, setValidationError, isPathValidated, setIsPathValidated, providerOption, } = useAuthState(state); useEscapeHandler( focusedComponent, setFocusedComponent, setValidationError, onBack, ); const { handleAuthSelect, handleAuthInputChange, handleAuthInputSubmit } = useAuthHandlers( setAuthMethod, setFocusedComponent, setAuthInput, setValidationError, setIsPathValidated, authMethod, authInput, oauthBuckets, onUpdateAuth, onContinue, onBack, ); const authOptions = buildAuthOptions(providerOption); const { current, total } = getStepPosition(state); const providerLabel = firstNonEmptyString(providerOption?.label, state.config.provider) ?? null; return ( ); };