Skip to content

Commit 59dc191

Browse files
anrgctclaude
andcommitted
refactor: reorganize config commands with metadata-driven architecture
BREAKING CHANGE: Config command internal architecture refactored This commit introduces a new architecture for the config command subsystem, centralizing configuration metadata and eliminating code duplication. New modules: - metadata.ts: Single source of truth for all configuration key metadata * Defines type, enum values, validation constraints for each config key * Provides getValidConfigKeys(), getConfigKeyMetadata(), isValidConfigKey() - parser.ts: Configuration value parsing and validation * parseConfigValue(): Type-driven value parsing with validation * parseConfigPairs(): Parses comma-separated key=value pairs - file-loader.ts: Shared configuration file loading * loadConfigLayers(): Loads default, global, and project config layers * Eliminates duplicate file reading logic between get.ts and set.ts Refactored modules: - get.ts: Reduced from 177 to 122 lines (-31%) * Now uses loadConfigLayers() for file loading * Simplified print functions using ConfigLayers type - set.ts: Reduced from 199 to 90 lines (-55%) * Uses parseConfigPairs() and parseConfigValue() from parser.ts * Uses loadConfigLayers() for configuration loading * Extracted saveConfig() helper function Benefits: - Eliminates 3 instances of duplicate code - Single source of truth for configuration metadata - Type-driven validation reduces hardcoding - Easier to add new configuration keys (just update metadata.ts) - Better separation of concerns Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 2f74406 commit 59dc191

5 files changed

Lines changed: 555 additions & 339 deletions

File tree

src/commands/config/file-loader.ts

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
/**
2+
* Shared configuration file loading utilities
3+
*
4+
* Provides common functions for loading configuration from files,
5+
* used by both get.ts and set.ts commands.
6+
*/
7+
8+
import * as fs from 'fs'
9+
import * as jsoncParser from 'jsonc-parser'
10+
11+
/**
12+
* Single configuration layer result
13+
*/
14+
export interface ConfigLayer {
15+
/** Parsed configuration object (or null if file doesn't exist) */
16+
config: Record<string, any> | null
17+
/** File path for this layer */
18+
path: string
19+
}
20+
21+
/**
22+
* All configuration layers
23+
*/
24+
export interface ConfigLayers {
25+
/** Default built-in configuration */
26+
defaultConfig: Record<string, any>
27+
/** Global configuration layer */
28+
global: ConfigLayer
29+
/** Project configuration layer */
30+
project: ConfigLayer
31+
/** Effective configuration (merged: default → global → project) */
32+
effective: Record<string, any>
33+
}
34+
35+
/**
36+
* Load a single configuration layer from a file
37+
*
38+
* @param filePath - Path to the configuration file
39+
* @param layerName - Name of the layer (for error messages)
40+
* @returns Parsed configuration object, or null if file doesn't exist
41+
*/
42+
function loadConfigLayer(filePath: string, layerName: string): Record<string, any> | null {
43+
try {
44+
if (fs.existsSync(filePath)) {
45+
const content = fs.readFileSync(filePath, 'utf-8')
46+
return jsoncParser.parse(content)
47+
}
48+
return null
49+
} catch (error) {
50+
console.error(`Failed to read ${layerName} configuration: ${error}`)
51+
console.error(`Path: ${filePath}`)
52+
process.exit(1)
53+
}
54+
}
55+
56+
/**
57+
* Load all configuration layers
58+
*
59+
* Loads configuration from default, global, and project sources,
60+
* then merges them in priority order (project > global > default).
61+
*
62+
* @param globalConfigPath - Path to global configuration file
63+
* @param projectConfigPath - Path to project configuration file
64+
* @param defaultConfig - Default built-in configuration
65+
* @returns All configuration layers
66+
*/
67+
export function loadConfigLayers(
68+
globalConfigPath: string,
69+
projectConfigPath: string,
70+
defaultConfig: Record<string, any>
71+
): ConfigLayers {
72+
const globalConfig = loadConfigLayer(globalConfigPath, 'global')
73+
const projectConfig = loadConfigLayer(projectConfigPath, 'project')
74+
75+
const effectiveConfig = {
76+
...defaultConfig,
77+
...(globalConfig ?? {}),
78+
...(projectConfig ?? {})
79+
}
80+
81+
return {
82+
defaultConfig,
83+
global: { config: globalConfig, path: globalConfigPath },
84+
project: { config: projectConfig, path: projectConfigPath },
85+
effective: effectiveConfig
86+
}
87+
}

src/commands/config/get.ts

Lines changed: 99 additions & 153 deletions
Original file line numberDiff line numberDiff line change
@@ -1,176 +1,122 @@
11
/**
22
* Config get command implementation
33
*/
4-
import { Command } from 'commander';
5-
import * as path from 'path';
6-
import * as fs from 'fs';
7-
import * as os from 'os';
8-
import * as jsoncParser from 'jsonc-parser';
9-
import { DEFAULT_CONFIG } from '../../code-index/constants';
10-
import { CodeIndexConfig } from '../../code-index/interfaces/config';
11-
import { resolveWorkspacePath } from '../shared';
4+
import * as path from 'path'
5+
import * as os from 'os'
6+
import { DEFAULT_CONFIG } from '../../code-index/constants'
7+
import { CodeIndexConfig } from '../../code-index/interfaces/config'
8+
import { resolveWorkspacePath } from '../shared'
9+
import { loadConfigLayers, type ConfigLayers } from './file-loader'
1210

1311
/**
1412
* Format configuration value for display
1513
*/
16-
function formatValue(value: any): string {
17-
if (value === undefined) return 'undefined';
18-
if (value === null) return 'null';
19-
if (typeof value === 'object') return JSON.stringify(value);
20-
return String(value);
21-
}
22-
23-
/**
24-
* Format config value for display (with sensitive value masking)
25-
*/
26-
function formatConfigValueForDisplay(key: string, value: any): string {
27-
return formatValue(value);
14+
function formatValue(value: unknown): string {
15+
if (value === undefined) return 'undefined'
16+
if (value === null) return 'null'
17+
if (typeof value === 'object') return JSON.stringify(value)
18+
return String(value)
2819
}
2920

3021
/**
3122
* Print all configuration layers in detail
3223
*/
33-
function printAllConfigLayers(
34-
defaultConfig: Record<string, any>,
35-
globalConfig: Record<string, any> | null,
36-
projectConfig: Record<string, any> | null,
37-
effectiveConfig: Record<string, any>,
38-
globalConfigPath: string,
39-
projectConfigPath: string
40-
): void {
41-
console.log('\n=== Configuration Layers (Highest Priority First) ===\n');
42-
43-
console.log('【1. Effective Configuration】(Final values after merging all layers)');
44-
console.log(JSON.stringify(effectiveConfig, null, 2));
45-
console.log();
46-
47-
console.log('【2. Project Configuration】(Overrides global and default values)');
48-
if (projectConfig) {
49-
console.log(`File path: ${projectConfigPath}`);
50-
console.log(JSON.stringify(projectConfig, null, 2));
51-
} else {
52-
console.log('(Not configured)');
53-
}
54-
console.log();
55-
56-
console.log('【3. Global Configuration】(Overrides default values)');
57-
if (globalConfig) {
58-
console.log(`File path: ${globalConfigPath}`);
59-
console.log(JSON.stringify(globalConfig, null, 2));
60-
} else {
61-
console.log('(Not configured)');
62-
}
63-
console.log();
64-
65-
console.log('【4. Default Values】(Built-in fallback values)');
66-
console.log(JSON.stringify(defaultConfig, null, 2));
24+
function printAllConfigLayers(layers: ConfigLayers): void {
25+
const { defaultConfig, global, project, effective } = layers
26+
27+
console.log('\n=== Configuration Layers (Highest Priority First) ===\n')
28+
29+
console.log('【1. Effective Configuration】(Final values after merging all layers)')
30+
console.log(JSON.stringify(effective, null, 2))
31+
console.log()
32+
33+
console.log('【2. Project Configuration】(Overrides global and default values)')
34+
if (project.config) {
35+
console.log(`File path: ${project.path}`)
36+
console.log(JSON.stringify(project.config, null, 2))
37+
} else {
38+
console.log('(Not configured)')
39+
}
40+
console.log()
41+
42+
console.log('【3. Global Configuration】(Overrides default values)')
43+
if (global.config) {
44+
console.log(`File path: ${global.path}`)
45+
console.log(JSON.stringify(global.config, null, 2))
46+
} else {
47+
console.log('(Not configured)')
48+
}
49+
console.log()
50+
51+
console.log('【4. Default Values】(Built-in fallback values)')
52+
console.log(JSON.stringify(defaultConfig, null, 2))
6753
}
6854

6955
/**
7056
* Print detailed layers for specific configuration items
7157
*/
72-
function printConfigItemLayers(
73-
keys: string[],
74-
defaultConfig: Record<string, any>,
75-
globalConfig: Record<string, any> | null,
76-
projectConfig: Record<string, any> | null,
77-
effectiveConfig: Record<string, any>
78-
): void {
79-
for (const key of keys) {
80-
console.log(`\n=== ${key} ===`);
81-
82-
const defaultValue = defaultConfig[key];
83-
const globalValue = globalConfig?.[key];
84-
const projectValue = projectConfig?.[key];
85-
const effectiveValue = effectiveConfig[key];
86-
87-
console.log(`Default: ${formatConfigValueForDisplay(key, defaultValue)}`);
88-
console.log(`Global: ${globalValue !== undefined ? formatConfigValueForDisplay(key, globalValue) : '(Not set)'}`);
89-
console.log(`Project: ${projectValue !== undefined ? formatConfigValueForDisplay(key, projectValue) : '(Not set)'}`);
90-
console.log(`Effective: ${formatConfigValueForDisplay(key, effectiveValue)}`);
91-
}
58+
function printConfigItemLayers(keys: string[], layers: ConfigLayers): void {
59+
const { defaultConfig, global, project, effective } = layers
60+
61+
for (const key of keys) {
62+
console.log(`\n=== ${key} ===`)
63+
64+
const defaultValue = defaultConfig[key]
65+
const globalValue = global.config?.[key]
66+
const projectValue = project.config?.[key]
67+
const effectiveValue = effective[key]
68+
69+
console.log(`Default: ${formatValue(defaultValue)}`)
70+
console.log(`Global: ${globalValue !== undefined ? formatValue(globalValue) : '(Not set)'}`)
71+
console.log(`Project: ${projectValue !== undefined ? formatValue(projectValue) : '(Not set)'}`)
72+
console.log(`Effective: ${formatValue(effectiveValue)}`)
73+
}
9274
}
9375

9476
/**
9577
* Config get handler
9678
*/
97-
export default async function configGetHandler(items: string[], options: any): Promise<void> {
98-
const workspacePath = resolveWorkspacePath(options.path, false);
99-
const projectConfigPath = options.config || path.join(workspacePath, 'autodev-config.json');
100-
const globalConfigPath = path.join(os.homedir(), '.autodev-cache', 'autodev-config.json');
101-
102-
const defaultConfig = DEFAULT_CONFIG;
103-
104-
let globalConfig: Record<string, any> | null = null;
105-
try {
106-
if (fs.existsSync(globalConfigPath)) {
107-
const content = fs.readFileSync(globalConfigPath, 'utf-8');
108-
globalConfig = jsoncParser.parse(content);
109-
}
110-
} catch (error) {
111-
console.error(`Failed to read global configuration: ${error}`);
112-
console.error(`Path: ${globalConfigPath}`);
113-
process.exit(1);
114-
}
115-
116-
let projectConfig: Record<string, any> | null = null;
117-
try {
118-
if (fs.existsSync(projectConfigPath)) {
119-
const content = fs.readFileSync(projectConfigPath, 'utf-8');
120-
projectConfig = jsoncParser.parse(content);
121-
}
122-
} catch (error) {
123-
console.error(`Failed to read project configuration: ${error}`);
124-
console.error(`Path: ${projectConfigPath}`);
125-
process.exit(1);
126-
}
127-
128-
const effectiveConfig = {
129-
...defaultConfig,
130-
...(globalConfig ?? {}),
131-
...(projectConfig ?? {})
132-
};
133-
134-
if (options.json) {
135-
if (items.length === 0) {
136-
console.log(JSON.stringify({
137-
paths: {
138-
default: '(Built-in)',
139-
global: globalConfigPath,
140-
project: projectConfigPath
141-
},
142-
default: defaultConfig,
143-
global: globalConfig || {},
144-
project: projectConfig || {},
145-
effective: effectiveConfig
146-
}, null, 2));
147-
} else {
148-
const result: Record<string, any> = {};
149-
for (const key of items) {
150-
const globalValue = globalConfig?.[key as keyof CodeIndexConfig] ?? null;
151-
const projectValue = projectConfig?.[key as keyof CodeIndexConfig] ?? null;
152-
const effectiveValue = effectiveConfig[key as keyof CodeIndexConfig];
153-
154-
result[key] = {
155-
default: defaultConfig[key as keyof CodeIndexConfig],
156-
global: globalValue,
157-
project: projectValue,
158-
effective: effectiveValue
159-
};
160-
}
161-
console.log(JSON.stringify(result, null, 2));
162-
}
163-
} else {
164-
if (items.length === 0) {
165-
printAllConfigLayers(defaultConfig, globalConfig, projectConfig, effectiveConfig, globalConfigPath, projectConfigPath);
166-
} else {
167-
printConfigItemLayers(
168-
items,
169-
defaultConfig,
170-
globalConfig,
171-
projectConfig,
172-
effectiveConfig
173-
);
174-
}
175-
}
79+
export default async function configGetHandler(items: string[], options: Record<string, unknown>): Promise<void> {
80+
const workspacePath = resolveWorkspacePath(options['path'] as string, false)
81+
const projectConfigPath = (options['config'] as string) || path.join(workspacePath, 'autodev-config.json')
82+
const globalConfigPath = path.join(os.homedir(), '.autodev-cache', 'autodev-config.json')
83+
84+
const layers = loadConfigLayers(globalConfigPath, projectConfigPath, DEFAULT_CONFIG)
85+
86+
if (options['json']) {
87+
if (items.length === 0) {
88+
console.log(
89+
JSON.stringify(
90+
{
91+
paths: {
92+
default: '(Built-in)',
93+
global: globalConfigPath,
94+
project: projectConfigPath
95+
},
96+
default: layers.defaultConfig,
97+
global: layers.global.config || {},
98+
project: layers.project.config || {},
99+
effective: layers.effective
100+
},
101+
null,
102+
2
103+
)
104+
)
105+
} else {
106+
const result: Record<string, any> = {}
107+
for (const key of items) {
108+
result[key] = {
109+
default: layers.defaultConfig[key as keyof CodeIndexConfig],
110+
global: layers.global.config?.[key as keyof CodeIndexConfig] ?? null,
111+
project: layers.project.config?.[key as keyof CodeIndexConfig] ?? null,
112+
effective: layers.effective[key as keyof CodeIndexConfig]
113+
}
114+
}
115+
console.log(JSON.stringify(result, null, 2))
116+
}
117+
} else if (items.length === 0) {
118+
printAllConfigLayers(layers)
119+
} else {
120+
printConfigItemLayers(items, layers)
121+
}
176122
}

0 commit comments

Comments
 (0)