UNPKG

ccusage-byobu

Version:

Real-time Claude Code API usage monitoring for byobu/tmux status bars with colored indicators, persistent caching, and automated integration

307 lines (273 loc) 9.19 kB
/** * Configuration module for ccusage-byobu * Handles environment variable validation and default values */ import { readFileSync } from 'fs'; import { resolve } from 'path'; // Default configuration values const DEFAULT_CONFIG = { format: 'full', colors: 'auto', threshold: 80, refresh: 60, maxWidth: 50, }; // Valid values for each configuration option const VALID_VALUES = { format: ['compact', 'full'], colors: ['auto', 'true', 'false'], threshold: (value) => { // Use strict regex to ensure entire string is a valid integer if (!/^(0|[1-9]\d*)$/.test(value)) return false; const num = Number(value); return num >= 0 && num <= 100; }, refresh: (value) => { // Use strict regex to ensure entire string is a valid positive integer if (!/^[1-9]\d*$/.test(value)) return false; const num = Number(value); return num > 0; }, maxWidth: (value) => { // Use strict regex to ensure entire string is a valid positive integer if (!/^[1-9]\d*$/.test(value)) return false; const num = Number(value); return num >= 10 && num <= 200; // Reasonable bounds for display width }, }; /** * Validates a configuration value * @param {string} key - Configuration key * @param {string} value - Value to validate * @returns {boolean} - True if valid, false otherwise */ function validateValue(key, value) { const validator = VALID_VALUES[key]; if (Array.isArray(validator)) { return validator.includes(value); } if (typeof validator === 'function') { return validator(value); } return false; } /** * Gets configuration from environment variables with validation and defaults * @returns {Object} Configuration object */ export function getConfig() { const config = {}; const errors = []; // Process each configuration option for (const [key, defaultValue] of Object.entries(DEFAULT_CONFIG)) { const envKey = `CCUSAGE_BYOBU_${key.toUpperCase()}`; const envValue = process.env[envKey]; if (envValue !== undefined) { if (validateValue(key, envValue)) { // Convert numeric values if (key === 'threshold' || key === 'refresh') { config[key] = Number(envValue); } else { config[key] = envValue; } } else { errors.push(`Invalid value for ${envKey}: "${envValue}"`); config[key] = defaultValue; } } else { config[key] = defaultValue; } } // Store validation errors for potential reporting config._errors = errors; return config; } /** * Validates plan type and provides migration guidance if needed * @param {string} planType - The plan type to validate * @returns {Object} Validation result with warnings if applicable */ function validatePlanType(planType) { if (!planType) { return { isValid: true, warnings: [] }; } const validPlans = ['free', 'pro', 'max_5x', 'max_20x', 'enterprise']; const legacyPlans = ['team']; if (validPlans.includes(planType)) { return { isValid: true, warnings: [] }; } if (legacyPlans.includes(planType)) { return { isValid: false, isLegacy: true, warnings: [ `⚠️ Legacy plan type detected: "${planType}"`, `The "${planType}" plan has been replaced with new Max tier options:`, ` • max_5x - For moderate usage scaling`, ` • max_20x - For high usage scaling`, `Please update your CLAUDE_PLAN_TYPE environment variable.`, `Run: ./install-ccusage-byobu.sh --migrate`, ], }; } return { isValid: false, isLegacy: false, warnings: [ `⚠️ Invalid plan type: "${planType}"`, `Valid plan types: ${validPlans.join(', ')}`, ], }; } /** * Gets pass-through environment variables for ccusage * @returns {Object} Environment variables to pass through */ export function getPassThroughEnv() { const passThroughVars = [ 'CLAUDE_CONFIG_DIR', 'CCUSAGE_DATA_DIR', 'CLAUDE_PLAN_TYPE', 'CLAUDE_API_KEY', ]; const env = {}; for (const varName of passThroughVars) { if (process.env[varName] !== undefined) { env[varName] = process.env[varName]; } } // Validate plan type if present if (env.CLAUDE_PLAN_TYPE) { const validation = validatePlanType(env.CLAUDE_PLAN_TYPE); if (!validation.isValid) { // For legacy plans, still pass through but show warnings if (validation.isLegacy) { console.warn('\n' + validation.warnings.join('\n')); console.warn(''); // Empty line for better readability } else { // For invalid plans, remove from pass-through delete env.CLAUDE_PLAN_TYPE; console.warn('\n' + validation.warnings.join('\n')); console.warn('CLAUDE_PLAN_TYPE has been removed from environment.\n'); } } } return env; } /** * Displays current configuration (for --config flag) * @returns {string} Formatted configuration display */ export function displayConfig() { const config = getConfig(); const passThroughEnv = getPassThroughEnv(); let output = 'Current Configuration:\n'; output += '=====================\n\n'; output += 'ccusage-byobu Settings:\n'; output += `-----------------------\n`; output += `Format: ${config.format} (CCUSAGE_BYOBU_FORMAT)\n`; output += `Colors: ${config.colors} (CCUSAGE_BYOBU_COLORS)\n`; output += `Threshold: ${config.threshold}% (CCUSAGE_BYOBU_THRESHOLD)\n`; output += `Refresh: ${config.refresh}s (CCUSAGE_BYOBU_REFRESH)\n`; output += `Max Width: ${config.maxWidth} chars (CCUSAGE_BYOBU_MAXWIDTH)\n`; if (config._errors.length > 0) { output += '\nValidation Errors:\n'; output += '------------------\n'; for (const error of config._errors) { output += `⚠️ ${error}\n`; } } output += '\nPass-through Variables:\n'; output += '-----------------------\n'; const passThroughVarNames = [ 'CLAUDE_CONFIG_DIR', 'CCUSAGE_DATA_DIR', 'CLAUDE_PLAN_TYPE', 'CLAUDE_API_KEY', ]; for (const varName of passThroughVarNames) { const value = passThroughEnv[varName]; if (value !== undefined) { // Mask sensitive values const displayValue = varName === 'CLAUDE_API_KEY' ? `${value.substring(0, 8)}...` : value; output += `${varName}: ${displayValue}\n`; } else { output += `${varName}: (not set)\n`; } } return output; } /** * Loads configuration from a file and sets environment variables * @param {string} filePath - Path to the configuration file */ export function loadConfigFile(filePath) { const resolvedPath = resolve(filePath); try { const fileContent = readFileSync(resolvedPath, 'utf-8'); // Support different file formats let config; if (filePath.endsWith('.json')) { config = JSON.parse(fileContent); } else { // Parse as .env format (KEY=VALUE lines) config = {}; const lines = fileContent.split('\n'); for (const line of lines) { const trimmed = line.trim(); // Skip empty lines and comments if (!trimmed || trimmed.startsWith('#')) continue; const equalIndex = trimmed.indexOf('='); if (equalIndex > 0) { const key = trimmed.substring(0, equalIndex).trim(); const value = trimmed.substring(equalIndex + 1).trim(); // Remove matching quotes if present config[key] = value.replace(/^(['"])(.*)\1$/g, '$2'); } } } // Set environment variables from config const configPrefix = 'CCUSAGE_BYOBU_'; const passThroughVars = [ 'CLAUDE_CONFIG_DIR', 'CCUSAGE_DATA_DIR', 'CLAUDE_PLAN_TYPE', 'CLAUDE_API_KEY', ]; for (const [key, value] of Object.entries(config)) { // Handle ccusage-byobu specific variables (with or without prefix) if ( key.startsWith(configPrefix) || ['FORMAT', 'COLORS', 'THRESHOLD', 'REFRESH', 'MAXWIDTH'].includes(key.toUpperCase()) ) { const envKey = key.startsWith(configPrefix) ? key : `${configPrefix}${key.toUpperCase()}`; process.env[envKey] = String(value); } // Handle pass-through variables else if (passThroughVars.includes(key)) { process.env[key] = String(value); } } // Validate the loaded configuration const validatedConfig = getConfig(); if (validatedConfig._errors.length > 0) { throw new Error(`Configuration validation errors:\n${validatedConfig._errors.join('\n')}`); } } catch (error) { if (error.code === 'ENOENT') { throw new Error(`Configuration file not found: ${resolvedPath}`); } else if (error.code === 'EACCES') { throw new Error( `Permission denied reading configuration file: ${resolvedPath}. Please check file permissions.` ); } else if (error.code === 'EPERM') { throw new Error( `Insufficient permissions to read configuration file: ${resolvedPath}. Please check file permissions or run with appropriate privileges.` ); } else if (error instanceof SyntaxError) { throw new Error(`Invalid JSON in configuration file: ${error.message}`); } else { throw error; } } }