/** * @license * Copyright 2025 Vybestack LLC * SPDX-License-Identifier: Apache-2.0 */ import type React from 'react'; import { useState, useCallback, useEffect } from 'react'; import { promises as fs } from 'node:fs'; import path from 'node:path'; import { Storage } from '@vybestack/llxprt-code-settings'; 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 { generateProfileNameSuggestions, buildProfileJSON, saveProfile, getStepPosition, } from './utils.js'; import type { WizardState } from './types.js'; import { firstNonEmptyString } from '../../../utils/coalesce.js'; const useExistingProfiles = () => { const [existingProfiles, setExistingProfiles] = useState([]); useEffect(() => { const loadExistingProfiles = async () => { try { const profilesDir = path.join(Storage.getGlobalConfigDir(), 'profiles'); const files = await fs.readdir(profilesDir); const profileNames = files .filter((f) => f.endsWith('.json')) .map((f) => f.replace(/\.json$/, '')); setExistingProfiles(profileNames); } catch { setExistingProfiles([]); } }; void loadExistingProfiles(); }, []); return existingProfiles; }; const ConflictDialog: React.FC<{ profileNameInput: string; handleConflictResolution: (value: string) => void; }> = ({ profileNameInput, handleConflictResolution }) => ( <> Profile Name Conflict A profile named '{profileNameInput}' already exists. Choose a different name or overwrite the existing profile: ); const ProfileNameInput: React.FC<{ profileNameInput: string; saveError: string | null; validationError: string | null; suggestions: string[]; handleProfileNameChange: (value: string) => void; handleProfileNameSubmit: (forceSave?: boolean) => void; }> = ({ profileNameInput, saveError, validationError, suggestions, handleProfileNameChange, handleProfileNameSubmit, }) => ( <> Profile name: {saveError && ✗ {saveError}} {validationError && ( ✗ {validationError} )} {!validationError && !saveError && profileNameInput && ( ✓ Name is available )} ℹ Suggested names: {suggestions.map((suggestion) => ( • {suggestion} ))} ← → Move cursor Enter Continue Esc Back ); const validateProfileName = ( value: string, existingProfiles: string[], ): string | null => { if (!value.trim()) { return 'Profile name cannot be empty'; } if (value.includes('/') || value.includes('\\')) { return 'Profile name cannot contain path separators'; } if (existingProfiles.includes(value)) { return 'Profile name already exists'; } return null; }; const executeSave = async ( profileNameInput: string, state: WizardState, forceSave: boolean, onContinue: () => void, setFocusedComponent: (v: 'input' | 'saving' | 'conflict') => void, setSaveError: (v: string | null) => void, setValidationError: (v: string | null) => void, ) => { setFocusedComponent('saving'); setSaveError(null); const profileJSON = buildProfileJSON(state); const result = await saveProfile(profileNameInput, profileJSON, { overwrite: forceSave, }); if (result.success) { onContinue(); } else if (result.alreadyExists ?? false) { setValidationError('Profile name already exists'); setFocusedComponent('conflict'); } else { setSaveError(firstNonEmptyString(result.error, 'Failed to save profile')); setFocusedComponent('input'); } }; const useSaveEscapeHandler = ( focusedComponent: 'input' | 'saving' | 'conflict', setFocusedComponent: (v: 'input' | 'saving' | 'conflict') => void, onBack: () => void, ) => { useKeypress( (key) => { if (key.name === 'escape') { if (focusedComponent === 'input') { onBack(); } else if (focusedComponent === 'conflict') { setFocusedComponent('input'); } } }, { isActive: true }, ); }; const useProfileNameHandler = ( existingProfiles: string[], onUpdateProfileName: (name: string) => void, setProfileNameInput: (v: string) => void, setValidationError: (v: string | null) => void, ) => useCallback( (value: string) => { setProfileNameInput(value); const error = validateProfileName(value, existingProfiles); setValidationError(error); if (!error) { onUpdateProfileName(value); } }, [ onUpdateProfileName, existingProfiles, setProfileNameInput, setValidationError, ], ); const useProfileNameSubmitHandler = ( profileNameInput: string, validationError: string | null, state: WizardState, onContinue: () => void, setFocusedComponent: (v: 'input' | 'saving' | 'conflict') => void, setSaveError: (v: string | null) => void, setValidationError: (v: string | null) => void, ) => useCallback( (forceSave: boolean = false) => { void (async () => { try { if (!profileNameInput.trim()) { setValidationError('Profile name cannot be empty'); return; } if (validationError && !forceSave) { if (validationError === 'Profile name already exists') { setFocusedComponent('conflict'); return; } return; } await executeSave( profileNameInput, state, forceSave, onContinue, setFocusedComponent, setSaveError, setValidationError, ); } catch (error) { setSaveError( error instanceof Error ? error.message : 'Failed to save profile', ); setFocusedComponent('input'); } })(); }, [ profileNameInput, validationError, state, onContinue, setFocusedComponent, setSaveError, setValidationError, ], ); const FocusedContentView: React.FC<{ focusedComponent: 'input' | 'saving' | 'conflict'; profileNameInput: string; saveError: string | null; validationError: string | null; suggestions: string[]; handleProfileNameChange: (value: string) => void; handleProfileNameSubmit: (forceSave?: boolean) => void; handleConflictResolution: (value: string) => void; }> = ({ focusedComponent, profileNameInput, saveError, validationError, suggestions, handleProfileNameChange, handleProfileNameSubmit, handleConflictResolution, }) => ( <> {focusedComponent === 'saving' && ( Saving profile... )} {focusedComponent === 'conflict' && ( )} {focusedComponent === 'input' && ( )} ); interface ProfileSaveStepProps { state: WizardState; onUpdateProfileName: (name: string) => void; onContinue: () => void; onBack: () => void; onCancel: () => void; } export const ProfileSaveStep: React.FC = ({ state, onUpdateProfileName, onContinue, onBack, onCancel, }) => { const [focusedComponent, setFocusedComponent] = useState< 'input' | 'saving' | 'conflict' >('input'); const [saveError, setSaveError] = useState(null); const [profileNameInput, setProfileNameInput] = useState( state.profileName ?? '', ); const [validationError, setValidationError] = useState(null); const existingProfiles = useExistingProfiles(); const [suggestions] = useState(() => generateProfileNameSuggestions(state.config), ); useSaveEscapeHandler(focusedComponent, setFocusedComponent, onBack); const handleProfileNameChange = useProfileNameHandler( existingProfiles, onUpdateProfileName, setProfileNameInput, setValidationError, ); const handleProfileNameSubmit = useProfileNameSubmitHandler( profileNameInput, validationError, state, onContinue, setFocusedComponent, setSaveError, setValidationError, ); const handleConflictResolution = useCallback( (value: string) => { if (value === 'rename') { setFocusedComponent('input'); } else if (value === 'overwrite') { setValidationError(null); handleProfileNameSubmit(true); } else if (value === 'back') { onBack(); } else if (value === 'cancel') { onCancel(); } }, [onBack, onCancel, handleProfileNameSubmit], ); const { current, total } = getStepPosition(state); return ( Create New Profile - Step {current} of {total} Name Your Profile: Enter a name for this profile ); };