Skip to content

Commit 3ddc783

Browse files
committed
Implement hierarchical configuration system with 4 levels
1 parent e236289 commit 3ddc783

File tree

4 files changed

+542
-69
lines changed

4 files changed

+542
-69
lines changed

packages/cli/src/commands/config.ts

Lines changed: 171 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,10 @@ import {
99
getDefaultConfig,
1010
updateConfig,
1111
clearAllConfig,
12+
getConfigAtLevel,
13+
clearConfigAtLevel,
14+
clearConfigKey,
15+
ConfigLevel,
1216
} from '../settings/config.js';
1317
import { nameToLogIndex } from '../utils/nameToLogIndex.js';
1418

@@ -38,6 +42,8 @@ export interface ConfigOptions extends SharedOptions {
3842
key?: string;
3943
value?: string;
4044
all?: boolean;
45+
global?: boolean;
46+
g?: boolean; // Alias for global
4147
}
4248

4349
export const command: CommandModule<SharedOptions, ConfigOptions> = {
@@ -64,6 +70,12 @@ export const command: CommandModule<SharedOptions, ConfigOptions> = {
6470
type: 'boolean',
6571
default: false,
6672
})
73+
.option('global', {
74+
alias: 'g',
75+
describe: 'Use global configuration instead of project-level',
76+
type: 'boolean',
77+
default: false,
78+
})
6779
.example('$0 config list', 'List all configuration values')
6880
.example(
6981
'$0 config get githubMode',
@@ -80,7 +92,23 @@ export const command: CommandModule<SharedOptions, ConfigOptions> = {
8092
)
8193
.example(
8294
'$0 config clear --all',
83-
'Clear all configuration settings',
95+
'Clear all project-level configuration settings',
96+
)
97+
.example(
98+
'$0 config set githubMode true --global',
99+
'Enable GitHub mode in global configuration',
100+
)
101+
.example(
102+
'$0 config set model claude-3-haiku-20240307 -g',
103+
'Set model in global configuration using short flag',
104+
)
105+
.example(
106+
'$0 config list --global',
107+
'List all global configuration settings',
108+
)
109+
.example(
110+
'$0 config clear --all --global',
111+
'Clear all global configuration settings',
84112
) as any; // eslint-disable-line @typescript-eslint/no-explicit-any
85113
},
86114
handler: async (argv: ArgumentsCamelCase<ConfigOptions>) => {
@@ -89,31 +117,61 @@ export const command: CommandModule<SharedOptions, ConfigOptions> = {
89117
logLevel: nameToLogIndex(argv.logLevel),
90118
});
91119

92-
const config = getConfig();
120+
// Determine which config level to use based on flags
121+
const configLevel =
122+
argv.global || argv.g ? ConfigLevel.GLOBAL : ConfigLevel.PROJECT;
123+
const levelName = configLevel === ConfigLevel.GLOBAL ? 'global' : 'project';
124+
125+
// Get merged config for display
126+
const mergedConfig = getConfig();
127+
128+
// Get level-specific configs for reference
129+
const defaultConfig = getConfigAtLevel(ConfigLevel.DEFAULT);
130+
const globalConfig = getConfigAtLevel(ConfigLevel.GLOBAL);
131+
const projectConfig = getConfigAtLevel(ConfigLevel.PROJECT);
93132

94133
// Handle 'list' command
95134
if (argv.command === 'list') {
96135
logger.info('Current configuration:');
97-
const defaultConfig = getDefaultConfig();
98136

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

102140
// Filter and sort config entries
103-
const configEntries = Object.entries(config)
141+
const configEntries = Object.entries(mergedConfig)
104142
.filter(([key]) => validKeys.includes(key))
105143
.sort(([keyA], [keyB]) => keyA.localeCompare(keyB));
106144

107-
// Display config entries with default indicators
145+
// Display config entries with source indicators
108146
configEntries.forEach(([key, value]) => {
109-
const isDefault =
110-
JSON.stringify(value) ===
111-
JSON.stringify(defaultConfig[key as keyof typeof defaultConfig]);
112-
const valueDisplay = isDefault
113-
? chalk.dim(`${value} (default)`)
114-
: chalk.green(value);
115-
logger.info(` ${key}: ${valueDisplay}`);
147+
const inProject = key in projectConfig;
148+
const inGlobal = key in globalConfig;
149+
const isDefault = !inProject && !inGlobal;
150+
151+
let valueDisplay = '';
152+
let sourceDisplay = '';
153+
154+
if (isDefault) {
155+
valueDisplay = chalk.dim(`${value} (default)`);
156+
} else if (inProject) {
157+
valueDisplay = chalk.green(`${value}`);
158+
sourceDisplay = chalk.blue(' [project]');
159+
} else if (inGlobal) {
160+
valueDisplay = chalk.yellow(`${value}`);
161+
sourceDisplay = chalk.magenta(' [global]');
162+
}
163+
164+
logger.info(` ${key}: ${valueDisplay}${sourceDisplay}`);
116165
});
166+
167+
logger.info('');
168+
logger.info('Configuration levels (in order of precedence):');
169+
logger.info(' CLI options (highest)');
170+
logger.info(
171+
' Project config (.mycoder/config.json in current directory)',
172+
);
173+
logger.info(' Global config (~/.mycoder/config.json)');
174+
logger.info(' Default values (lowest)');
117175
return;
118176
}
119177

@@ -124,10 +182,29 @@ export const command: CommandModule<SharedOptions, ConfigOptions> = {
124182
return;
125183
}
126184

127-
if (argv.key in config) {
128-
logger.info(
129-
`${argv.key}: ${chalk.green(config[argv.key as keyof typeof config])}`,
130-
);
185+
// Check if the key exists in the merged config
186+
if (argv.key in mergedConfig) {
187+
const value = mergedConfig[argv.key as keyof typeof mergedConfig];
188+
189+
// Determine the source of this value
190+
const inProject = argv.key in projectConfig;
191+
const inGlobal = argv.key in globalConfig;
192+
const isDefault = !inProject && !inGlobal;
193+
194+
let valueDisplay = '';
195+
let sourceDisplay = '';
196+
197+
if (isDefault) {
198+
valueDisplay = chalk.dim(`${value} (default)`);
199+
} else if (inProject) {
200+
valueDisplay = chalk.green(`${value}`);
201+
sourceDisplay = chalk.blue(' [project]');
202+
} else if (inGlobal) {
203+
valueDisplay = chalk.yellow(`${value}`);
204+
sourceDisplay = chalk.magenta(' [global]');
205+
}
206+
207+
logger.info(`${argv.key}: ${valueDisplay}${sourceDisplay}`);
131208
} else {
132209
logger.error(`Configuration key '${argv.key}' not found`);
133210
}
@@ -186,11 +263,15 @@ export const command: CommandModule<SharedOptions, ConfigOptions> = {
186263
let parsedValue: string | boolean | number = argv.value;
187264

188265
// Check if config already exists to determine type
189-
if (argv.key in config) {
190-
if (typeof config[argv.key as keyof typeof config] === 'boolean') {
266+
if (argv.key in mergedConfig) {
267+
if (
268+
typeof mergedConfig[argv.key as keyof typeof mergedConfig] ===
269+
'boolean'
270+
) {
191271
parsedValue = argv.value.toLowerCase() === 'true';
192272
} else if (
193-
typeof config[argv.key as keyof typeof config] === 'number'
273+
typeof mergedConfig[argv.key as keyof typeof mergedConfig] ===
274+
'number'
194275
) {
195276
parsedValue = Number(argv.value);
196277
}
@@ -206,9 +287,14 @@ export const command: CommandModule<SharedOptions, ConfigOptions> = {
206287
}
207288
}
208289

209-
const updatedConfig = updateConfig({ [argv.key]: parsedValue });
290+
// Update config at the specified level
291+
const updatedConfig = updateConfig(
292+
{ [argv.key]: parsedValue },
293+
configLevel,
294+
);
295+
210296
logger.info(
211-
`Updated ${argv.key}: ${chalk.green(updatedConfig[argv.key as keyof typeof updatedConfig])}`,
297+
`Updated ${argv.key}: ${chalk.green(updatedConfig[argv.key as keyof typeof updatedConfig])} at ${levelName} level`,
212298
);
213299
return;
214300
}
@@ -217,20 +303,47 @@ export const command: CommandModule<SharedOptions, ConfigOptions> = {
217303
if (argv.command === 'clear') {
218304
// Check if --all flag is provided
219305
if (argv.all) {
220-
// Confirm with the user before clearing all settings
221-
const isConfirmed = await confirm(
222-
'Are you sure you want to clear all configuration settings? This action cannot be undone.',
223-
);
306+
const confirmMessage = `Are you sure you want to clear all ${levelName} configuration settings? This action cannot be undone.`;
307+
308+
if (configLevel === ConfigLevel.GLOBAL && !argv.global && !argv.g) {
309+
// If no level was explicitly specified and we're using the default (project),
310+
// ask if they want to clear all levels
311+
const clearAllLevels = await confirm(
312+
'Do you want to clear both project and global configuration? (No will clear only project config)',
313+
);
314+
315+
if (clearAllLevels) {
316+
// Confirm before clearing all levels
317+
const isConfirmed = await confirm(
318+
'Are you sure you want to clear ALL configuration settings (both project and global)? This action cannot be undone.',
319+
);
320+
321+
if (!isConfirmed) {
322+
logger.info('Operation cancelled.');
323+
return;
324+
}
325+
326+
// Clear all settings at all levels
327+
clearAllConfig();
328+
logger.info(
329+
'All configuration settings (both project and global) have been cleared. Default values will be used.',
330+
);
331+
return;
332+
}
333+
}
334+
335+
// Confirm before clearing the specified level
336+
const isConfirmed = await confirm(confirmMessage);
224337

225338
if (!isConfirmed) {
226339
logger.info('Operation cancelled.');
227340
return;
228341
}
229342

230-
// Clear all settings
231-
clearAllConfig();
343+
// Clear settings at the specified level
344+
clearConfigAtLevel(configLevel);
232345
logger.info(
233-
'All configuration settings have been cleared. Default values will be used.',
346+
`All ${levelName} configuration settings have been cleared.`,
234347
);
235348
return;
236349
}
@@ -244,9 +357,12 @@ export const command: CommandModule<SharedOptions, ConfigOptions> = {
244357

245358
const defaultConfig = getDefaultConfig();
246359

247-
// Check if the key exists in the config
248-
if (!(argv.key in config)) {
249-
logger.error(`Configuration key '${argv.key}' not found`);
360+
// Check if the key exists at the specified level
361+
const levelConfig = getConfigAtLevel(configLevel);
362+
if (!(argv.key in levelConfig)) {
363+
logger.error(
364+
`Configuration key '${argv.key}' not found at ${levelName} level`,
365+
);
250366
return;
251367
}
252368

@@ -258,22 +374,32 @@ export const command: CommandModule<SharedOptions, ConfigOptions> = {
258374
return;
259375
}
260376

261-
// Get the current config, create a new object without the specified key
262-
const currentConfig = getConfig();
263-
const { [argv.key]: _, ...newConfig } = currentConfig as Record<
264-
string,
265-
any
266-
>;
267-
268-
// Update the config file with the new object
269-
updateConfig(newConfig);
270-
271-
// Get the default value that will now be used
272-
const defaultValue =
273-
defaultConfig[argv.key as keyof typeof defaultConfig];
377+
// Clear the key at the specified level
378+
clearConfigKey(argv.key, configLevel);
379+
380+
// Get the value that will now be used
381+
const mergedAfterClear = getConfig();
382+
const newValue =
383+
mergedAfterClear[argv.key as keyof typeof mergedAfterClear];
384+
385+
// Determine the source of the new value
386+
const afterClearInProject =
387+
argv.key in getConfigAtLevel(ConfigLevel.PROJECT);
388+
const afterClearInGlobal =
389+
argv.key in getConfigAtLevel(ConfigLevel.GLOBAL);
390+
const isDefaultAfterClear = !afterClearInProject && !afterClearInGlobal;
391+
392+
let sourceDisplay = '';
393+
if (isDefaultAfterClear) {
394+
sourceDisplay = '(default)';
395+
} else if (afterClearInProject) {
396+
sourceDisplay = '(from project config)';
397+
} else if (afterClearInGlobal) {
398+
sourceDisplay = '(from global config)';
399+
}
274400

275401
logger.info(
276-
`Cleared ${argv.key}, now using default value: ${chalk.green(defaultValue)}`,
402+
`Cleared ${argv.key} at ${levelName} level, now using: ${chalk.green(newValue)} ${sourceDisplay}`,
277403
);
278404
return;
279405
}

0 commit comments

Comments
 (0)