UNPKG

xc-mcp

Version:

MCP server that wraps Xcode command-line tools for iOS/macOS development workflows

195 lines 6.82 kB
import { promises as fs } from 'fs'; import { join } from 'path'; import { homedir } from 'os'; import { randomUUID } from 'crypto'; /** * ConfigManager handles project-local configuration with auto-learning capabilities. * Stores `.xc-mcp/config.json` in project directories for persistent preferences. * * Features: * - Project-specific simulator preferences * - Build history tracking * - Atomic writes (temp file + rename) * - Graceful degradation if config unavailable */ export class ConfigManager { configDir; projectConfigs = new Map(); schemaVersion = '1.0.0'; configFileName = 'config.json'; constructor(projectPath) { // Determine config directory - prefer .xc-mcp in project root if (projectPath) { this.configDir = join(projectPath, '.xc-mcp'); } else { // Fallback to user home for global config this.configDir = join(homedir(), '.xc-mcp'); } } /** * Get the full path to the config file */ getConfigPath() { return join(this.configDir, this.configFileName); } /** * Get project-specific configuration */ async getProjectConfig(projectPath) { // Load from cache if available const cached = this.projectConfigs.get(projectPath); if (cached) { return cached; } // Try to load from disk const diskConfig = await this.loadConfigFromDisk(); if (diskConfig && diskConfig.projectConfigs) { const configMap = new Map(diskConfig.projectConfigs); this.projectConfigs = configMap; const diskConfigValue = configMap.get(projectPath); if (diskConfigValue) { return diskConfigValue; } } // Return empty config if not found const emptyConfig = {}; this.projectConfigs.set(projectPath, emptyConfig); return emptyConfig; } /** * Update project configuration */ async updateProjectConfig(projectPath, updates) { // Update in-memory config const currentConfig = await this.getProjectConfig(projectPath); const updatedConfig = { ...currentConfig, ...updates }; this.projectConfigs.set(projectPath, updatedConfig); // Save to disk atomically await this.saveConfigToDisk(); } /** * Record a successful build with simulator preference */ async recordSuccessfulBuild(projectPath, simulatorUDID, simulatorName) { const config = await this.getProjectConfig(projectPath); const updatedConfig = { ...config, lastBuildTime: Date.now(), buildCount: (config.buildCount || 0) + 1, successfulBuilds: (config.successfulBuilds || 0) + 1, }; // Update simulator preference if provided if (simulatorUDID) { updatedConfig.lastUsedSimulator = simulatorUDID; updatedConfig.lastUsedSimulatorName = simulatorName; } this.projectConfigs.set(projectPath, updatedConfig); await this.saveConfigToDisk(); } /** * Get last used simulator for project */ async getLastUsedSimulator(projectPath) { const config = await this.getProjectConfig(projectPath); return config.lastUsedSimulator; } /** * Get build success rate for project */ async getBuildSuccessRate(projectPath) { const config = await this.getProjectConfig(projectPath); if (!config.buildCount || config.buildCount === 0) { return 0; } return ((config.successfulBuilds || 0) / config.buildCount) * 100; } /** * Load entire config from disk */ async loadConfigFromDisk() { try { const configPath = this.getConfigPath(); const content = await fs.readFile(configPath, 'utf8'); const parsed = JSON.parse(content); // Validate schema version if (parsed.version !== this.schemaVersion) { console.warn(`Config schema version mismatch: ${parsed.version} vs ${this.schemaVersion}`); return null; } return parsed; } catch (error) { // File doesn't exist or is corrupted - return null for graceful degradation if (error instanceof Error && 'code' in error && error.code === 'ENOENT') { return null; // File doesn't exist yet } // JSON parse errors or other corruption - gracefully degrade return null; } } /** * Save entire config to disk with atomic writes */ async saveConfigToDisk() { try { // Ensure directory exists await fs.mkdir(this.configDir, { recursive: true }); // Convert Map to array for serialization const configData = { version: this.schemaVersion, projectConfigs: Array.from(this.projectConfigs.entries()), lastUpdated: new Date().toISOString(), }; const content = JSON.stringify(configData, null, 2); const configPath = this.getConfigPath(); // Atomic write: write to temp file, then rename const tempFile = `${configPath}.tmp.${randomUUID()}`; await fs.writeFile(tempFile, content, 'utf8'); await fs.rename(tempFile, configPath); } catch (error) { console.warn('Failed to save config to disk:', error); // Gracefully continue - config is still in memory } } /** * Clear all configurations */ async clear() { this.projectConfigs.clear(); try { const configPath = this.getConfigPath(); await fs.unlink(configPath); } catch (error) { // File doesn't exist, that's fine - only log other errors if (error instanceof Error && 'code' in error && error.code !== 'ENOENT') { console.warn('Failed to delete config file:', error); } } } /** * Get all project configurations */ getAllProjectConfigs() { return new Map(this.projectConfigs); } } // Global config manager instance (project-aware) export function createConfigManager(projectPath) { return new ConfigManager(projectPath); } // Singleton pattern for backward compatibility let globalConfigManager = null; export function getConfigManager(projectPath) { if (!globalConfigManager) { globalConfigManager = new ConfigManager(projectPath); } return globalConfigManager; } //# sourceMappingURL=config.js.map