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
92 changes: 85 additions & 7 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,25 @@ export const command: CommandModule<SharedOptions, ConfigOptions> = {
// Handle 'list' command
if (argv.command === 'list') {
logger.info('Current configuration:');
Object.entries(config).forEach(([key, value]) => {
logger.info(` ${key}: ${chalk.green(value)}`);
const defaultConfig = getDefaultConfig();

// Get all valid config keys
const validKeys = Object.keys(defaultConfig);

// Filter and sort config entries
const configEntries = Object.entries(config)
.filter(([key]) => validKeys.includes(key))
.sort(([keyA], [keyB]) => keyA.localeCompare(keyB));

// Display config entries with default indicators
configEntries.forEach(([key, value]) => {
const isDefault =
JSON.stringify(value) ===
JSON.stringify(defaultConfig[key as keyof typeof defaultConfig]);
const valueDisplay = isDefault
? chalk.dim(`${value} (default)`)
: chalk.green(value);
logger.info(` ${key}: ${valueDisplay}`);
});
return;
}
Expand Down Expand Up @@ -85,6 +110,16 @@ export const command: CommandModule<SharedOptions, ConfigOptions> = {
return;
}

// Validate that the key exists in default config
const defaultConfig = getDefaultConfig();
if (!(argv.key in defaultConfig)) {
logger.error(`Invalid configuration key '${argv.key}'`);
logger.info(
`Valid configuration keys: ${Object.keys(defaultConfig).join(', ')}`,
);
return;
}

// Parse the value based on current type or infer boolean/number
let parsedValue: string | boolean | number = argv.value;

Expand Down Expand Up @@ -116,8 +151,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
140 changes: 139 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 All @@ -56,6 +65,37 @@ describe.skip('Config Command', () => {
interactive: false,
command: 'list',
} as any);
it('should filter out invalid config keys in list command', async () => {
// Mock getConfig to return config with invalid keys
vi.mocked(getConfig).mockReturnValue({
githubMode: false,
invalidKey: 'some value',
} as any);

// Mock getDefaultConfig to return only valid keys
vi.mocked(getDefaultConfig).mockReturnValue({
githubMode: false,
});

await command.handler!({
_: ['config', 'config', 'list'],
logLevel: 'info',
interactive: false,
command: 'list',
} as any);

expect(getConfig).toHaveBeenCalled();
expect(getDefaultConfig).toHaveBeenCalled();

// Should show the valid key
expect(mockLogger.info).toHaveBeenCalledWith(
expect.stringContaining('githubMode'),
);

// Should not show the invalid key
const infoCallArgs = mockLogger.info.mock.calls.flat();
expect(infoCallArgs.join()).not.toContain('invalidKey');
});

expect(getConfig).toHaveBeenCalled();
expect(mockLogger.info).toHaveBeenCalledWith('Current configuration:');
Expand Down Expand Up @@ -138,6 +178,21 @@ describe.skip('Config Command', () => {
);
});

it('should validate key exists in default config for set command', async () => {
await command.handler!({
_: ['config', 'config', 'set', 'invalidKey', 'value'],
logLevel: 'info',
interactive: false,
command: 'set',
key: 'invalidKey',
value: 'value',
} as any);

expect(mockLogger.error).toHaveBeenCalledWith(
expect.stringContaining('Invalid configuration key'),
);
});

it('should handle unknown command', async () => {
await command.handler!({
_: ['config', 'config', 'unknown'],
Expand All @@ -150,4 +205,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