-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
3a6e549
commit b334ac2
Showing
26 changed files
with
1,456 additions
and
1,299 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,66 +1,135 @@ | ||
import chalk from 'chalk'; | ||
import * as E from 'fp-ts/lib/Either'; | ||
import { pipe } from 'fp-ts/lib/function'; | ||
import * as TE from 'fp-ts/lib/TaskEither'; | ||
|
||
import { BaseCommand } from './base-command'; | ||
import { | ||
CommandError, | ||
CommandResult, | ||
createCommand, | ||
createCommandError, | ||
createCommandLoop, | ||
taskEitherFromPromise | ||
} from './base-command'; | ||
import { createInteractivePrompts, MenuChoice } from './interactive'; | ||
import { Config, getConfig, setConfig } from '../../shared/config'; | ||
|
||
class ConfigCommand extends BaseCommand { | ||
constructor() { | ||
super('config', 'Manage CLI configuration'); | ||
this.action(this.execute.bind(this)); | ||
// Type definitions | ||
type ConfigAction = 'view' | 'set' | 'back'; | ||
|
||
interface ConfigCommandResult extends CommandResult { | ||
// readonly action: ConfigAction; | ||
readonly key?: keyof Config; | ||
readonly value?: string; | ||
} | ||
|
||
// Create interactive prompts instance | ||
const prompts = createInteractivePrompts(); | ||
// Pure functions for config operations | ||
const viewConfig = (): TE.TaskEither<CommandError, ConfigCommandResult> => | ||
pipe( | ||
taskEitherFromPromise( | ||
() => Promise.resolve(getConfig()), | ||
(error) => createCommandError('CONFIG_ERROR', 'Failed to read config', error) | ||
), | ||
TE.map((config) => { | ||
displayConfig(config); | ||
return { | ||
completed: false, | ||
action: 'view' as const | ||
}; | ||
}) | ||
); | ||
const setConfigValue = (key: keyof Config, value: string): TE.TaskEither<CommandError, ConfigCommandResult> => | ||
pipe( | ||
taskEitherFromPromise( | ||
() => { | ||
setConfig(key, value); | ||
return Promise.resolve(getConfig()); | ||
}, | ||
(error) => createCommandError('CONFIG_ERROR', 'Failed to set config value', error) | ||
), | ||
TE.map(() => ({ | ||
completed: false, | ||
action: 'set' as const, | ||
key, | ||
value | ||
})) | ||
); | ||
// Menu choices generators | ||
const createConfigMenuChoices = (): ReadonlyArray<MenuChoice<ConfigAction>> => [ | ||
{ name: 'View current configuration', value: 'view' }, | ||
{ name: 'Set a configuration value', value: 'set' } | ||
]; | ||
const createConfigKeyChoices = (config: Config): ReadonlyArray<MenuChoice<keyof Config>> => | ||
(Object.keys(config) as Array<keyof Config>).map((key) => ({ | ||
name: key, | ||
value: key | ||
})); | ||
// Helper functions for displaying config values | ||
const formatConfigValue = (key: keyof Config, value: unknown): string => | ||
key === 'ANTHROPIC_API_KEY' ? '********' : String(value); | ||
const displayConfig = (config: Config): void => { | ||
console.log(chalk.cyan('Current configuration:')); | ||
|
||
if (Object.keys(config).length === 0) { | ||
console.log(chalk.yellow('The configuration is empty.')); | ||
return; | ||
} | ||
|
||
async execute(): Promise<void> { | ||
while (true) { | ||
try { | ||
const action = await this.showMenu<'view' | 'set' | 'back'>('Select an action:', [ | ||
{ name: 'View current configuration', value: 'view' }, | ||
{ name: 'Set a configuration value', value: 'set' } | ||
]); | ||
const maxKeyLength = Math.max(...Object.keys(config).map((key) => key.length)); | ||
|
||
Object.entries(config).forEach(([key, value]) => { | ||
const paddingLength = maxKeyLength - key.length; | ||
const paddedSpaces = paddingLength > 0 ? ' '.repeat(paddingLength) : ''; | ||
console.log(`${key}${paddedSpaces} -> ${chalk.green(formatConfigValue(key as keyof Config, value))}`); | ||
}); | ||
}; | ||
// Validation helpers | ||
const validateConfigValue = (key: keyof Config, value: string): E.Either<CommandError, string> => | ||
value.trim() === '' | ||
? E.left(createCommandError('INVALID_CONFIG_VALUE', 'Configuration value cannot be empty')) | ||
: E.right(value); | ||
// Interactive workflow functions | ||
const handleSetConfig = (config: Config): TE.TaskEither<CommandError, ConfigCommandResult> => | ||
pipe( | ||
prompts.showMenu<keyof Config | 'back'>('Select the configuration key:', createConfigKeyChoices(config)), | ||
TE.chain((key) => { | ||
if (key === 'back') { | ||
return TE.right({ completed: true }); | ||
} | ||
return pipe( | ||
prompts.getInput(`Enter the value for ${chalk.cyan(key)}:`), | ||
TE.chain((value) => | ||
pipe( | ||
validateConfigValue(key, value), | ||
TE.fromEither, | ||
TE.chain(() => setConfigValue(key, value)) | ||
) | ||
) | ||
); | ||
}) | ||
); | ||
// Main command execution | ||
const executeConfigCommand = (): TE.TaskEither<CommandError, ConfigCommandResult> => { | ||
const loop = (): TE.TaskEither<CommandError, ConfigCommandResult> => | ||
pipe( | ||
prompts.showMenu<ConfigAction>('Select an action:', createConfigMenuChoices()), | ||
TE.chain((action) => { | ||
switch (action) { | ||
case 'view': | ||
this.viewConfig(); | ||
await this.pressKeyToContinue(); | ||
break; | ||
return viewConfig(); | ||
case 'set': | ||
await this.setConfigValue(); | ||
break; | ||
return handleSetConfig(getConfig()); | ||
case 'back': | ||
return; | ||
return TE.right({ completed: true, action }); | ||
default: | ||
return TE.left(createCommandError('INVALID_ACTION', `Invalid action: ${action}`)); | ||
} | ||
} catch (error) { | ||
this.handleError(error, 'config command'); | ||
await this.pressKeyToContinue(); | ||
} | ||
} | ||
} | ||
|
||
private viewConfig(): void { | ||
const currentConfig = getConfig(); | ||
console.log(chalk.cyan('Current configuration:')); | ||
|
||
if (Object.keys(currentConfig).length === 0) { | ||
console.log(chalk.yellow('The configuration is empty.')); | ||
} else { | ||
Object.entries(currentConfig).forEach(([key, value]) => { | ||
console.log(chalk.green(`${key}:`), chalk.yellow(key === 'ANTHROPIC_API_KEY' ? '********' : value)); | ||
}); | ||
} | ||
} | ||
|
||
private async setConfigValue(): Promise<void> { | ||
const currentConfig = getConfig(); | ||
const configKeys = Object.keys(currentConfig) as Array<keyof Config>; | ||
const key = await this.showMenu<keyof Config | 'back'>( | ||
'Select the configuration key:', | ||
configKeys.map((k) => ({ value: k, name: k })) | ||
}) | ||
); | ||
return createCommandLoop(loop); | ||
}; | ||
|
||
if (key === 'back') return; | ||
|
||
const value = await this.getInput(`Enter the value for ${chalk.cyan(key)}:`); | ||
setConfig(key, value); | ||
console.log(chalk.green(`Configuration updated: ${key} = ${value}`)); | ||
} | ||
} | ||
|
||
export default new ConfigCommand(); | ||
// Command export | ||
export const configCommand = createCommand('config', 'Manage CLI configuration', executeConfigCommand); |
Oops, something went wrong.