claude-buddy
Version:
Claude Buddy installation and setup script
376 lines (342 loc) • 9.68 kB
JavaScript
/**
* Configuration Loader for Claude Buddy Installation Script
*
* Handles loading and merging configuration from multiple sources with proper precedence:
* CLI flags > Environment variables > Project config > User config > Defaults
*/
const fs = require('fs').promises;
const path = require('path');
const os = require('os');
const { ValidationError, VALIDATION_ERROR_CODES } = require('./errors');
/**
* Default configuration values
*/
const DEFAULT_CONFIG = {
installation: {
targetDir: process.cwd(),
skipHooks: false,
force: false
},
update: {
preserveAll: false,
mergeConfig: true,
createBackup: true
},
uninstall: {
preserveCustomizations: true,
purge: false
},
logging: {
level: 'info',
verbose: false,
quiet: false,
jsonOutput: false,
logFile: null
},
execution: {
dryRun: false,
nonInteractive: false,
timeout: 300000 // 5 minutes
},
environment: {
requireGit: false,
requireUV: false,
requirePython: false,
minDiskSpace: 52428800 // 50MB in bytes
}
};
/**
* Configuration schema for validation
*/
const CONFIG_SCHEMA = {
installation: {
type: 'object',
properties: {
targetDir: { type: 'string' },
skipHooks: { type: 'boolean' },
force: { type: 'boolean' }
}
},
update: {
type: 'object',
properties: {
preserveAll: { type: 'boolean' },
mergeConfig: { type: 'boolean' },
createBackup: { type: 'boolean' }
}
},
uninstall: {
type: 'object',
properties: {
preserveCustomizations: { type: 'boolean' },
purge: { type: 'boolean' }
}
},
logging: {
type: 'object',
properties: {
level: { type: 'string', enum: ['error', 'warn', 'info', 'debug', 'verbose'] },
verbose: { type: 'boolean' },
quiet: { type: 'boolean' },
jsonOutput: { type: 'boolean' },
logFile: { type: ['string', 'null'] }
}
},
execution: {
type: 'object',
properties: {
dryRun: { type: 'boolean' },
nonInteractive: { type: 'boolean' },
timeout: { type: 'number', min: 0 }
}
},
environment: {
type: 'object',
properties: {
requireGit: { type: 'boolean' },
requireUV: { type: 'boolean' },
requirePython: { type: 'boolean' },
minDiskSpace: { type: 'number', min: 0 }
}
}
};
/**
* Load configuration from a JSON file
*
* @param {string} filePath - Path to configuration file
* @returns {Promise<Object|null>} Configuration object or null if file doesn't exist
*/
async function loadConfigFile(filePath) {
try {
const content = await fs.readFile(filePath, 'utf8');
return JSON.parse(content);
} catch (error) {
if (error.code === 'ENOENT') {
return null; // File doesn't exist
}
throw new ValidationError(
`Failed to load configuration file: ${filePath}`,
VALIDATION_ERROR_CODES.INVALID_CONFIG,
{ filePath, error: error.message }
);
}
}
/**
* Get user-level configuration file path
*
* @returns {string} Path to user config file
*/
function getUserConfigPath() {
return path.join(os.homedir(), '.claude-buddy-rc.json');
}
/**
* Get project-level configuration file path
*
* @param {string} projectDir - Project directory
* @returns {string} Path to project config file
*/
function getProjectConfigPath(projectDir) {
return path.join(projectDir, '.claude-buddy-rc.json');
}
/**
* Load configuration from environment variables
*
* @returns {Object} Configuration from environment variables
*/
function loadEnvConfig() {
const config = {};
// Installation settings
if (process.env.CLAUDE_BUDDY_HOME) {
config.installation = config.installation || {};
config.installation.targetDir = process.env.CLAUDE_BUDDY_HOME;
}
// Logging settings
if (process.env.CLAUDE_BUDDY_VERBOSE === 'true') {
config.logging = config.logging || {};
config.logging.verbose = true;
}
if (process.env.CLAUDE_BUDDY_NO_COLOR === 'true') {
config.logging = config.logging || {};
config.logging.noColor = true;
}
if (process.env.CLAUDE_BUDDY_LOG_LEVEL) {
config.logging = config.logging || {};
config.logging.level = process.env.CLAUDE_BUDDY_LOG_LEVEL;
}
// Execution settings
if (process.env.CLAUDE_BUDDY_NON_INTERACTIVE === 'true') {
config.execution = config.execution || {};
config.execution.nonInteractive = true;
}
return config;
}
/**
* Deep merge two configuration objects
*
* @param {Object} target - Target object
* @param {Object} source - Source object
* @returns {Object} Merged object
*/
function deepMerge(target, source) {
const result = { ...target };
for (const key in source) {
if (Object.prototype.hasOwnProperty.call(source, key)) {
if (
source[key] &&
typeof source[key] === 'object' &&
!Array.isArray(source[key])
) {
result[key] = deepMerge(target[key] || {}, source[key]);
} else {
result[key] = source[key];
}
}
}
return result;
}
/**
* Validate configuration value against schema
*
* @param {*} value - Value to validate
* @param {Object} schema - Schema definition
* @param {string} path - Path to value (for error messages)
* @throws {ValidationError} If validation fails
*/
function validateValue(value, schema, path) {
// Type validation
if (schema.type) {
const types = Array.isArray(schema.type) ? schema.type : [schema.type];
const actualType = Array.isArray(value) ? 'array' : typeof value;
const valueIsNull = value === null;
if (!types.includes(actualType) && !(valueIsNull && types.includes('null'))) {
throw new ValidationError(
`Invalid type for ${path}: expected ${types.join(' or ')}, got ${actualType}`,
VALIDATION_ERROR_CODES.INVALID_FIELD_TYPE,
{ path, expectedTypes: types, actualType, value }
);
}
}
// Enum validation
if (schema.enum && !schema.enum.includes(value)) {
throw new ValidationError(
`Invalid value for ${path}: must be one of ${schema.enum.join(', ')}`,
VALIDATION_ERROR_CODES.INVALID_FIELD_VALUE,
{ path, allowedValues: schema.enum, value }
);
}
// Min/max validation for numbers
if (typeof value === 'number') {
if (schema.min !== undefined && value < schema.min) {
throw new ValidationError(
`Value for ${path} is too small: minimum is ${schema.min}`,
VALIDATION_ERROR_CODES.INVALID_FIELD_VALUE,
{ path, min: schema.min, value }
);
}
if (schema.max !== undefined && value > schema.max) {
throw new ValidationError(
`Value for ${path} is too large: maximum is ${schema.max}`,
VALIDATION_ERROR_CODES.INVALID_FIELD_VALUE,
{ path, max: schema.max, value }
);
}
}
// Nested object validation
if (schema.properties && typeof value === 'object' && value !== null) {
for (const key in value) {
if (Object.prototype.hasOwnProperty.call(value, key)) {
const propertySchema = schema.properties[key];
if (propertySchema) {
validateValue(value[key], propertySchema, `${path}.${key}`);
}
}
}
}
}
/**
* Validate configuration against schema
*
* @param {Object} config - Configuration to validate
* @throws {ValidationError} If validation fails
*/
function validateConfig(config) {
for (const section in CONFIG_SCHEMA) {
if (config[section]) {
validateValue(config[section], CONFIG_SCHEMA[section], section);
}
}
}
/**
* Load and merge configuration from all sources
*
* Configuration precedence (highest to lowest):
* 1. CLI flags (passed as options)
* 2. Environment variables
* 3. Project-level config file (.claude-buddy-rc.json in project dir)
* 4. User-level config file (.claude-buddy-rc.json in home dir)
* 5. Default configuration
*
* @param {Object} options - Options from CLI flags
* @param {string} options.projectDir - Project directory for project config
* @returns {Promise<Object>} Merged configuration
*/
async function loadConfig(options = {}) {
const projectDir = options.projectDir || process.cwd();
// Start with defaults
let config = { ...DEFAULT_CONFIG };
// Layer 4: User-level config file
const userConfig = await loadConfigFile(getUserConfigPath());
if (userConfig) {
config = deepMerge(config, userConfig);
}
// Layer 3: Project-level config file
const projectConfig = await loadConfigFile(getProjectConfigPath(projectDir));
if (projectConfig) {
config = deepMerge(config, projectConfig);
}
// Layer 2: Environment variables
const envConfig = loadEnvConfig();
config = deepMerge(config, envConfig);
// Layer 1: CLI flags (highest precedence)
config = deepMerge(config, options);
// Validate final configuration
validateConfig(config);
return config;
}
/**
* Save configuration to file
*
* @param {Object} config - Configuration to save
* @param {string} filePath - Path to save configuration
* @returns {Promise<void>}
*/
async function saveConfig(config, filePath) {
// Validate before saving
validateConfig(config);
try {
await fs.writeFile(
filePath,
JSON.stringify(config, null, 2),
'utf8'
);
} catch (error) {
throw new ValidationError(
`Failed to save configuration file: ${filePath}`,
VALIDATION_ERROR_CODES.INVALID_CONFIG,
{ filePath, error: error.message }
);
}
}
/**
* Export configuration functions
*/
module.exports = {
loadConfig,
saveConfig,
validateConfig,
getUserConfigPath,
getProjectConfigPath,
deepMerge,
DEFAULT_CONFIG,
CONFIG_SCHEMA
};