woaru
Version:
Universal Project Setup Autopilot - Analyze and automatically configure development tools for ANY programming language
620 lines (619 loc) • 24.3 kB
JavaScript
import fs from 'fs-extra';
import * as path from 'path';
import * as os from 'os';
import { APP_CONFIG } from './constants.js';
import chalk from 'chalk';
import { SchemaValidator, } from '../schemas/ai-config.schema.js';
import { triggerHook } from '../core/HookSystem.js';
/**
* ConfigManager - Secure management of WOARU configuration and API keys
* Handles global .env file creation, security measures, and key storage
*/
export class ConfigManager {
static instance;
woaruDir;
configDir;
envFile;
aiConfigFile;
userConfigFile;
constructor() {
this.woaruDir = path.join(os.homedir(), APP_CONFIG.DIRECTORIES.BASE);
this.configDir = path.join(this.woaruDir, 'config');
this.envFile = path.join(this.woaruDir, '.env');
this.aiConfigFile = path.join(this.configDir, 'ai_config.json');
this.userConfigFile = path.join(this.configDir, 'user.json');
}
static getInstance() {
if (!ConfigManager.instance) {
ConfigManager.instance = new ConfigManager();
}
return ConfigManager.instance;
}
/**
* Initialize the WOARU configuration directory and security measures
*/
async initialize() {
try {
// Ensure .woaru directory exists
await fs.ensureDir(this.woaruDir);
// Ensure config subdirectory exists
await fs.ensureDir(this.configDir);
// Set up security measures
await this.setupGitIgnoreProtection();
// Ensure .env file exists (create empty if not)
if (!(await fs.pathExists(this.envFile))) {
await this.createEmptyEnvFile();
}
// Migration: Check for legacy llm_config.json and migrate to ai_config.json
await this.migrateLegacyConfiguration();
// Ensure AI config file exists (create empty if not)
if (!(await fs.pathExists(this.aiConfigFile))) {
await this.createEmptyAiConfigFile();
}
// Set secure permissions
await this.setSecurePermissions();
}
catch (error) {
console.warn(chalk.yellow(`⚠️ Warning: Could not fully initialize config security: ${error instanceof Error ? error.message : error}`));
}
}
/**
* Store an API key securely in the global .env file
*/
async storeApiKey(provider, apiKey) {
try {
await this.initialize();
const envVarName = `${provider.toUpperCase()}_API_KEY`;
// Read existing .env content
let envContent = '';
if (await fs.pathExists(this.envFile)) {
envContent = await fs.readFile(this.envFile, 'utf-8');
}
// Remove existing entry for this provider if it exists
const lines = envContent
.split('\n')
.filter(line => !line.startsWith(`${envVarName}=`));
// Add new entry
lines.push(`${envVarName}="${apiKey}"`);
// Write back to file
await fs.writeFile(this.envFile, lines.join('\n') + '\n');
// Set secure permissions
await this.setSecurePermissions();
console.log(chalk.green(`✅ API key for ${provider} stored securely in ${this.envFile}`));
}
catch (error) {
throw new Error(`Failed to store API key: ${error instanceof Error ? error.message : error}`);
}
}
/**
* Load all environment variables from the global .env file
*/
async loadEnvironmentVariables() {
try {
if (await fs.pathExists(this.envFile)) {
const dotenv = await import('dotenv');
// Suppress dotenv console output
const originalLog = console.log;
console.log = () => { };
dotenv.config({ path: this.envFile });
console.log = originalLog;
}
}
catch (error) {
console.warn(chalk.yellow(`⚠️ Warning: Could not load environment variables: ${error instanceof Error ? error.message : error}`));
}
}
/**
* Get the path to the .env file
*/
getEnvFilePath() {
return this.envFile;
}
/**
* Check if an API key exists for a provider
*/
async hasApiKey(provider) {
await this.loadEnvironmentVariables();
const envVarName = `${provider.toUpperCase()}_API_KEY`;
return !!process.env[envVarName];
}
/**
* Alias for storeApiKey to maintain API compatibility
*/
async saveApiKey(provider, apiKey) {
return this.storeApiKey(provider, apiKey);
}
/**
* Get an API key for a provider
*/
async getApiKey(provider) {
await this.loadEnvironmentVariables();
const envVarName = `${provider.toUpperCase()}_API_KEY`;
return process.env[envVarName];
}
/**
* List all configured providers
*/
async getConfiguredProviders() {
try {
if (!(await fs.pathExists(this.envFile))) {
return [];
}
const envContent = await fs.readFile(this.envFile, 'utf-8');
const providers = [];
envContent.split('\n').forEach(line => {
const match = line.match(/^([A-Z_]+)_API_KEY=/);
if (match) {
providers.push(match[1].toLowerCase());
}
});
return providers;
}
catch (error) {
console.warn(chalk.yellow(`⚠️ Warning: Could not read providers: ${error instanceof Error ? error.message : error}`));
return [];
}
}
/**
* Remove an API key for a provider
*/
async removeApiKey(provider) {
try {
if (!(await fs.pathExists(this.envFile))) {
return;
}
const envVarName = `${provider.toUpperCase()}_API_KEY`;
const envContent = await fs.readFile(this.envFile, 'utf-8');
const lines = envContent
.split('\n')
.filter(line => !line.startsWith(`${envVarName}=`));
await fs.writeFile(this.envFile, lines.join('\n'));
await this.setSecurePermissions();
console.log(chalk.green(`✅ API key for ${provider} removed`));
}
catch (error) {
throw new Error(`Failed to remove API key: ${error instanceof Error ? error.message : error}`);
}
}
/**
* Store AI configuration in global config file with Zod schema validation
* 🛡️ REGEL: Alle Konfigurationsdateien MÜSSEN vor dem Speichern validiert werden
*/
async storeAiConfig(config) {
try {
await this.initialize();
// 🔒 Schema-Validierung vor dem Speichern - KI-freundliche Regelwelt
const validation = SchemaValidator.validateAIConfig(config);
if (!validation.success) {
console.error(chalk.red('❌ Cannot store invalid AI config:'));
validation.errors?.forEach(error => {
console.error(chalk.red(` • ${error}`));
});
throw new Error('AI configuration validation failed');
}
await fs.writeFile(this.aiConfigFile, JSON.stringify(validation.data, null, 2));
await this.setSecurePermissions();
console.log(chalk.green(`✅ AI configuration validated and stored in ${this.aiConfigFile}`));
}
catch (error) {
if (error instanceof Error &&
error.message.includes('validation failed')) {
throw error; // Re-throw validation errors
}
throw new Error(`Failed to store AI config: ${error instanceof Error ? error.message : error}`);
}
}
/**
* Load AI configuration from global config file with Zod schema validation
* 🛡️ REGEL: Alle Konfigurationsdateien MÜSSEN validiert werden
*/
async loadAiConfig() {
try {
if (!(await fs.pathExists(this.aiConfigFile))) {
console.log(chalk.gray('📄 No AI config file found, creating default config...'));
return {};
}
const content = await fs.readFile(this.aiConfigFile, 'utf-8');
const rawData = JSON.parse(content);
// 🔒 Schema-Validierung - KI-freundliche Regelwelt
const validation = SchemaValidator.validateAIConfig(rawData);
if (validation.success && validation.data) {
console.log(chalk.green('✅ AI config successfully validated against schema'));
return validation.data;
}
else {
console.error(chalk.red('❌ AI config validation failed:'));
validation.errors?.forEach(error => {
console.error(chalk.red(` • ${error}`));
});
console.error(chalk.yellow('⚠️ Using fallback empty config to prevent crashes'));
return {};
}
}
catch (error) {
if (error instanceof SyntaxError) {
console.error(chalk.red('❌ AI config contains invalid JSON:'));
console.error(chalk.red(` ${error.message}`));
console.error(chalk.yellow('💡 Please fix the JSON syntax in:'));
console.error(chalk.gray(` ${this.aiConfigFile}`));
}
else {
console.warn(chalk.yellow(`⚠️ Warning: Could not load AI config: ${error instanceof Error ? error.message : error}`));
}
return {};
}
}
/**
* Save AI configuration to global config file
*/
async saveAiConfig(config) {
try {
await this.initialize();
await fs.writeFile(this.aiConfigFile, JSON.stringify(config, null, 2));
await this.setSecurePermissions();
}
catch (error) {
console.warn(chalk.yellow(`⚠️ Warning: Could not save AI config: ${error instanceof Error ? error.message : error}`));
}
}
/**
* Check if a key is a metadata or configuration key (not a provider)
*/
isMetadataKey(key) {
return (key === '_metadata' ||
key === 'multi_ai_review_enabled' ||
key === 'primary_review_provider_id' ||
key === 'multiAi' ||
key === 'primaryProvider' ||
key === 'lastDataUpdate' ||
key.startsWith('_') // Any key starting with underscore is metadata
);
}
/**
* Get all configured AI providers
*/
async getConfiguredAiProviders() {
try {
const config = await this.loadAiConfig();
const providers = [];
for (const [key, value] of Object.entries(config)) {
// Skip metadata and configuration entries
if (this.isMetadataKey(key)) {
continue;
}
// Only include actual provider objects
if (value &&
typeof value === 'object' &&
Object.prototype.hasOwnProperty.call(value, 'enabled')) {
providers.push(key);
}
}
return providers;
}
catch (error) {
console.warn(chalk.yellow(`⚠️ Warning: Could not get AI providers: ${error instanceof Error ? error.message : error}`));
return [];
}
}
/**
* Get AI config file path
*/
getAiConfigFilePath() {
return this.aiConfigFile;
}
// Backward compatibility aliases
/**
* @deprecated Use storeAiConfig() instead
* Backward compatibility alias for storing AI configuration
*/
async storeLlmConfig(config) {
return this.storeAiConfig(config);
}
/**
* @deprecated Use loadAiConfig() instead
* Backward compatibility alias for loading AI configuration
*/
async loadLlmConfig() {
return this.loadAiConfig();
}
/**
* @deprecated Use getConfiguredAiProviders() instead
* Backward compatibility alias for getting configured providers
*/
async getConfiguredLlmProviders() {
return this.getConfiguredAiProviders();
}
/**
* @deprecated Use getAiConfigFilePath() instead
* Backward compatibility alias for getting config file path
*/
getLlmConfigFilePath() {
return this.getAiConfigFilePath();
}
/**
* Get config directory path
*/
getConfigDirPath() {
return this.configDir;
}
/**
* Get Multi-AI Review configuration
*/
async getMultiAiReviewConfig() {
const config = await this.loadAiConfig();
return {
enabled: Boolean(config.multi_ai_review_enabled) || false,
primaryProvider: config.primary_review_provider_id || null,
};
}
/**
* Update Multi-AI Review configuration
*/
async updateMultiAiReviewConfig(enabled, primaryProvider = null) {
const config = await this.loadAiConfig();
config.multi_ai_review_enabled = enabled;
config.primary_review_provider_id = primaryProvider;
await this.storeAiConfig(config);
}
/**
* Get all enabled AI providers
*/
async getEnabledAiProviders() {
const config = await this.loadAiConfig();
const enabledProviders = [];
for (const [providerId, providerConfig] of Object.entries(config)) {
// Skip metadata and configuration entries
if (this.isMetadataKey(providerId)) {
continue;
}
// Only include actual provider objects that are enabled
if (providerConfig &&
typeof providerConfig === 'object' &&
Object.prototype.hasOwnProperty.call(providerConfig, 'enabled') &&
providerConfig.enabled) {
enabledProviders.push(providerId);
}
}
return enabledProviders;
}
/**
* Count configured AI providers
*/
async getConfiguredProviderCount() {
const config = await this.loadAiConfig();
let count = 0;
for (const [providerId, providerConfig] of Object.entries(config)) {
// Skip metadata and configuration entries
if (this.isMetadataKey(providerId)) {
continue;
}
// Only count actual provider objects
if (providerConfig &&
typeof providerConfig === 'object' &&
Object.prototype.hasOwnProperty.call(providerConfig, 'enabled')) {
count++;
}
}
return count;
}
/**
* Create an empty .env file with header
*/
async createEmptyEnvFile() {
const header = `# WOARU API Keys - Managed by ConfigManager
# This file contains sensitive API keys - never commit to version control
# Generated on ${new Date().toISOString()}
`;
await fs.writeFile(this.envFile, header);
}
/**
* Create an empty AI config file
*/
async createEmptyAiConfigFile() {
const defaultConfig = {
_metadata: {
created: new Date().toISOString(),
description: 'WOARU Global AI Configuration - Managed by ConfigManager',
},
multi_ai_review_enabled: false,
primary_review_provider_id: null,
};
await fs.writeFile(this.aiConfigFile, JSON.stringify(defaultConfig, null, 2));
}
/**
* Set secure file permissions (600 - owner read/write only)
*/
async setSecurePermissions() {
try {
if (process.platform !== 'win32') {
await fs.chmod(this.envFile, 0o600);
if (await fs.pathExists(this.aiConfigFile)) {
await fs.chmod(this.aiConfigFile, 0o600);
}
}
}
catch (error) {
console.warn(chalk.yellow(`⚠️ Warning: Could not set secure permissions: ${error instanceof Error ? error.message : error}`));
}
}
/**
* Setup git ignore protection to prevent accidental commits
*/
async setupGitIgnoreProtection() {
const gitIgnorePaths = [
path.join(os.homedir(), '.config', 'git', 'ignore'),
path.join(os.homedir(), '.gitignore_global'),
path.join(os.homedir(), '.gitignore'),
];
let foundGitIgnore = false;
for (const gitIgnorePath of gitIgnorePaths) {
if (await fs.pathExists(gitIgnorePath)) {
await this.addToGitIgnore(gitIgnorePath);
foundGitIgnore = true;
break;
}
}
if (!foundGitIgnore) {
console.log(chalk.yellow('⚠️ Warning: No global .gitignore found. Consider creating one to prevent accidental commits:'));
console.log(chalk.gray(' echo "~/.woaru/.env" >> ~/.gitignore_global'));
console.log(chalk.gray(' git config --global core.excludesfile ~/.gitignore_global'));
}
}
/**
* Add .env protection to gitignore file
*/
async addToGitIgnore(gitIgnorePath) {
try {
const ignoreEntries = ['~/.woaru/.env', '~/.woaru/config/'];
const content = await fs.readFile(gitIgnorePath, 'utf-8').catch(() => '');
let newContent = content;
let hasChanges = false;
for (const entry of ignoreEntries) {
if (!content.includes(entry)) {
newContent +=
(newContent.endsWith('\n') ? '' : '\n') +
`\n# WOARU configuration protection\n${entry}\n`;
hasChanges = true;
}
}
if (hasChanges) {
await fs.writeFile(gitIgnorePath, newContent);
console.log(chalk.green(`✅ Added WOARU config protection to ${gitIgnorePath}`));
}
}
catch (error) {
console.warn(chalk.yellow(`⚠️ Warning: Could not update gitignore: ${error instanceof Error ? error.message : error}`));
}
}
/**
* Migration function: Automatically migrate legacy llm_config.json to ai_config.json
* This ensures existing user configurations are not lost during refactoring
*/
async migrateLegacyConfiguration() {
try {
const legacyConfigFile = path.join(this.configDir, 'llm_config.json');
// Check conditions for migration:
// a. Legacy llm_config.json exists
// b. New ai_config.json does NOT exist
const legacyExists = await fs.pathExists(legacyConfigFile);
const newExists = await fs.pathExists(this.aiConfigFile);
if (legacyExists && !newExists) {
// Perform automatic migration
await fs.move(legacyConfigFile, this.aiConfigFile);
// Inform user about the migration
console.log(chalk.cyan("ℹ️ WOARU-Konfiguration wurde automatisch auf das neue 'ai'-Format migriert."));
console.log(chalk.gray(` ${legacyConfigFile} → ${this.aiConfigFile}`));
console.log(chalk.gray(' Alle bestehenden Einstellungen bleiben erhalten.'));
}
}
catch (error) {
console.warn(chalk.yellow(`⚠️ Warning: Could not migrate legacy configuration: ${error instanceof Error ? error.message : error}`));
}
}
/**
* Load user configuration from global user config file with schema validation
* 🛡️ REGEL: Alle User-Konfigurationen MÜSSEN validiert werden
*/
async loadUserConfig() {
try {
// 🪝 HOOK: beforeConfigLoad - KI-freundliche Regelwelt
try {
await triggerHook('onConfigLoad', {
configType: 'user',
configPath: this.userConfigFile,
configData: null,
timestamp: new Date(),
});
}
catch (hookError) {
console.debug(`Hook error (onConfigLoad user): ${hookError}`);
}
if (!(await fs.pathExists(this.userConfigFile))) {
return {};
}
const content = await fs.readFile(this.userConfigFile, 'utf-8');
const rawData = JSON.parse(content);
// 🛡️ SCHEMA-VALIDIERUNG: User Config - KI-freundliche Regelwelt
const validation = SchemaValidator.validateUserConfig(rawData);
if (validation.success && validation.data) {
console.log(chalk.green('✅ User-Konfiguration erfolgreich validiert'));
// 🪝 HOOK: afterConfigLoad - KI-freundliche Regelwelt
try {
await triggerHook('onConfigLoad', {
configType: 'user',
configPath: this.userConfigFile,
configData: validation.data,
timestamp: new Date(),
});
}
catch (hookError) {
console.debug(`Hook error (onConfigLoad user after): ${hookError}`);
}
return validation.data;
}
else {
console.warn(chalk.yellow('⚠️ User-Config Schema-Validierung fehlgeschlagen:'));
validation.errors?.forEach(error => {
console.warn(chalk.yellow(` • ${error}`));
});
console.warn(chalk.yellow('💡 Verwende Fallback für Kompatibilität'));
return rawData; // Fallback to raw data for compatibility
}
}
catch (error) {
console.warn(chalk.yellow(`⚠️ Warning: Could not load user config: ${error instanceof Error ? error.message : error}`));
return {};
}
}
/**
* Store user configuration in global user config file with schema validation
* 🛡️ REGEL: Alle User-Konfigurationen MÜSSEN vor dem Speichern validiert werden
*/
async storeUserConfig(config) {
try {
await this.initialize();
// 🛡️ SCHEMA-VALIDIERUNG vor dem Speichern - KI-freundliche Regelwelt
const validation = SchemaValidator.validateUserConfig(config);
if (!validation.success) {
console.error(chalk.red('❌ Cannot store invalid User config:'));
validation.errors?.forEach(error => {
console.error(chalk.red(` • ${error}`));
});
throw new Error('User configuration validation failed');
}
await fs.writeFile(this.userConfigFile, JSON.stringify(validation.data, null, 2));
await this.setSecurePermissions();
console.log(chalk.green(`✅ User-Konfiguration validiert und gespeichert in ${this.userConfigFile}`));
}
catch (error) {
if (error instanceof Error &&
error.message.includes('validation failed')) {
throw error; // Re-throw validation errors
}
throw new Error(`Failed to store user config: ${error instanceof Error ? error.message : error}`);
}
}
/**
* Set user's preferred language
*/
async setUserLanguage(language) {
const config = await this.loadUserConfig();
config.language = language;
config.lastModified = new Date().toISOString();
await this.storeUserConfig(config);
}
/**
* Get user's preferred language
*/
async getUserLanguage() {
const config = await this.loadUserConfig();
return config.language || null;
}
/**
* Get user config file path
*/
getUserConfigFilePath() {
return this.userConfigFile;
}
}
//# sourceMappingURL=ConfigManager.js.map