/**
* @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 (
);
};