diff --git a/packages/cli/README.md b/packages/cli/README.md index 3f90e4f..502645f 100644 --- a/packages/cli/README.md +++ b/packages/cli/README.md @@ -75,6 +75,9 @@ mycoder config get githubMode # Set a configuration value mycoder config set githubMode true +# Reset a configuration value to its default +mycoder config clear customPrompt + # Configure model provider and model name mycoder config set modelProvider openai mycoder config set modelName gpt-4o-2024-05-13 diff --git a/packages/cli/src/commands/config.ts b/packages/cli/src/commands/config.ts index 53ee5ad..190c252 100644 --- a/packages/cli/src/commands/config.ts +++ b/packages/cli/src/commands/config.ts @@ -2,13 +2,17 @@ import chalk from 'chalk'; import { Logger } from 'mycoder-agent'; import { SharedOptions } from '../options.js'; -import { getConfig, updateConfig } from '../settings/config.js'; +import { + getConfig, + getDefaultConfig, + updateConfig, +} from '../settings/config.js'; import { nameToLogIndex } from '../utils/nameToLogIndex.js'; import type { CommandModule, ArgumentsCamelCase } from 'yargs'; export interface ConfigOptions extends SharedOptions { - command: 'get' | 'set' | 'list'; + command: 'get' | 'set' | 'list' | 'clear'; key?: string; value?: string; } @@ -20,7 +24,7 @@ export const command: CommandModule = { return yargs .positional('command', { describe: 'Config command to run', - choices: ['get', 'set', 'list'], + choices: ['get', 'set', 'list', 'clear'], type: 'string', demandOption: true, }) @@ -37,7 +41,11 @@ export const command: CommandModule = { '$0 config get githubMode', 'Get the value of githubMode setting', ) - .example('$0 config set githubMode true', 'Enable GitHub mode') as any; // eslint-disable-line @typescript-eslint/no-explicit-any + .example('$0 config set githubMode true', 'Enable GitHub mode') + .example( + '$0 config clear customPrompt', + 'Reset customPrompt to default value', + ) as any; // eslint-disable-line @typescript-eslint/no-explicit-any }, handler: async (argv: ArgumentsCamelCase) => { const logger = new Logger({ @@ -50,8 +58,16 @@ export const command: CommandModule = { // Handle 'list' command if (argv.command === 'list') { logger.info('Current configuration:'); + const defaultConfig = getDefaultConfig(); Object.entries(config).forEach(([key, value]) => { - logger.info(` ${key}: ${chalk.green(value)}`); + const isDefault = + JSON.stringify(value) === + JSON.stringify(defaultConfig[key as keyof typeof defaultConfig]); + const valueDisplay = chalk.green(value); + const statusIndicator = isDefault + ? chalk.dim(' (default)') + : chalk.blue(' (custom)'); + logger.info(` ${key}: ${valueDisplay}${statusIndicator}`); }); return; } @@ -116,8 +132,51 @@ export const command: CommandModule = { return; } + // Handle 'clear' command + if (argv.command === 'clear') { + if (!argv.key) { + logger.error('Key is required for clear command'); + return; + } + + const defaultConfig = getDefaultConfig(); + + // Check if the key exists in the config + if (!(argv.key in config)) { + logger.error(`Configuration key '${argv.key}' not found`); + return; + } + + // Check if the key exists in the default config + if (!(argv.key in defaultConfig)) { + logger.error( + `Configuration key '${argv.key}' does not have a default value`, + ); + return; + } + + // Get the current config, create a new object without the specified key + const currentConfig = getConfig(); + const { [argv.key]: _, ...newConfig } = currentConfig as Record< + string, + any + >; + + // Update the config file with the new object + updateConfig(newConfig); + + // Get the default value that will now be used + const defaultValue = + defaultConfig[argv.key as keyof typeof defaultConfig]; + + logger.info( + `Cleared ${argv.key}, now using default value: ${chalk.green(defaultValue)}`, + ); + return; + } + // If command not recognized logger.error(`Unknown config command: ${argv.command}`); - logger.info('Available commands: get, set, list'); + logger.info('Available commands: get, set, list, clear'); }, }; diff --git a/packages/cli/src/settings/config.ts b/packages/cli/src/settings/config.ts index 30cd925..87a4f7d 100644 --- a/packages/cli/src/settings/config.ts +++ b/packages/cli/src/settings/config.ts @@ -24,6 +24,11 @@ const defaultConfig = { export type Config = typeof defaultConfig; +// Export the default config for use in other functions +export const getDefaultConfig = (): Config => { + return { ...defaultConfig }; +}; + export const getConfig = (): Config => { if (!fs.existsSync(configFile)) { return defaultConfig; diff --git a/packages/cli/tests/commands/config.test.ts b/packages/cli/tests/commands/config.test.ts index 3071a14..d07ff31 100644 --- a/packages/cli/tests/commands/config.test.ts +++ b/packages/cli/tests/commands/config.test.ts @@ -2,11 +2,16 @@ import { Logger } from 'mycoder-agent'; import { beforeEach, afterEach, describe, expect, it, vi } from 'vitest'; import { command } from '../../src/commands/config.js'; -import { getConfig, updateConfig } from '../../src/settings/config.js'; +import { + getConfig, + getDefaultConfig, + updateConfig, +} from '../../src/settings/config.js'; // Mock dependencies vi.mock('../../src/settings/config.js', () => ({ getConfig: vi.fn(), + getDefaultConfig: vi.fn(), updateConfig: vi.fn(), })); @@ -39,6 +44,10 @@ describe.skip('Config Command', () => { }; vi.mocked(Logger).mockImplementation(() => mockLogger as unknown as Logger); vi.mocked(getConfig).mockReturnValue({ githubMode: false }); + vi.mocked(getDefaultConfig).mockReturnValue({ + githubMode: false, + customPrompt: '', + }); vi.mocked(updateConfig).mockImplementation((config) => ({ githubMode: false, ...config, @@ -150,4 +159,87 @@ describe.skip('Config Command', () => { expect.stringContaining('Unknown config command'), ); }); + + it('should list all configuration values with default indicators', async () => { + // Mock getConfig to return a mix of default and custom values + vi.mocked(getConfig).mockReturnValue({ + githubMode: false, // default value + customPrompt: 'custom value', // custom value + }); + + // Mock getDefaultConfig to return the default values + vi.mocked(getDefaultConfig).mockReturnValue({ + githubMode: false, + customPrompt: '', + }); + + await command.handler!({ + _: ['config', 'config', 'list'], + logLevel: 'info', + interactive: false, + command: 'list', + } as any); + + expect(getConfig).toHaveBeenCalled(); + expect(getDefaultConfig).toHaveBeenCalled(); + expect(mockLogger.info).toHaveBeenCalledWith('Current configuration:'); + + // Check for default indicator + expect(mockLogger.info).toHaveBeenCalledWith( + expect.stringContaining('githubMode') && + expect.stringContaining('(default)'), + ); + + // Check for custom indicator + expect(mockLogger.info).toHaveBeenCalledWith( + expect.stringContaining('customPrompt') && + expect.stringContaining('(custom)'), + ); + }); + + it('should clear a configuration value', async () => { + await command.handler!({ + _: ['config', 'config', 'clear', 'customPrompt'], + logLevel: 'info', + interactive: false, + command: 'clear', + key: 'customPrompt', + } as any); + + // Verify updateConfig was called with an object that doesn't include the key + expect(updateConfig).toHaveBeenCalled(); + + // Verify success message + expect(mockLogger.info).toHaveBeenCalledWith( + expect.stringContaining('Cleared customPrompt'), + ); + }); + + it('should handle missing key for clear command', async () => { + await command.handler!({ + _: ['config', 'config', 'clear'], + logLevel: 'info', + interactive: false, + command: 'clear', + key: undefined, + } as any); + + expect(mockLogger.error).toHaveBeenCalledWith( + expect.stringContaining('Key is required'), + ); + }); + + it('should handle non-existent key for clear command', async () => { + await command.handler!({ + _: ['config', 'config', 'clear', 'nonExistentKey'], + logLevel: 'info', + interactive: false, + command: 'clear', + key: 'nonExistentKey', + } as any); + + expect(mockLogger.error).toHaveBeenCalledWith( + expect.stringContaining('not found'), + ); + }); });