Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions packages/cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
71 changes: 65 additions & 6 deletions packages/cli/src/commands/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand All @@ -20,7 +24,7 @@ export const command: CommandModule<SharedOptions, ConfigOptions> = {
return yargs
.positional('command', {
describe: 'Config command to run',
choices: ['get', 'set', 'list'],
choices: ['get', 'set', 'list', 'clear'],
type: 'string',
demandOption: true,
})
Expand All @@ -37,7 +41,11 @@ export const command: CommandModule<SharedOptions, ConfigOptions> = {
'$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<ConfigOptions>) => {
const logger = new Logger({
Expand All @@ -50,8 +58,16 @@ export const command: CommandModule<SharedOptions, ConfigOptions> = {
// 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;
}
Expand Down Expand Up @@ -116,8 +132,51 @@ export const command: CommandModule<SharedOptions, ConfigOptions> = {
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');
},
};
5 changes: 5 additions & 0 deletions packages/cli/src/settings/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
94 changes: 93 additions & 1 deletion packages/cli/tests/commands/config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
}));

Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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'),
);
});
});
Loading