UNPKG

mcp-xcode

Version:

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

214 lines 8.56 kB
import { executeCommand, buildXcodebuildCommand } from '../utils/command.js'; import { promises as fs } from 'fs'; import { join, dirname } from 'path'; export class ProjectCache { projectConfigs = new Map(); buildHistory = new Map(); dependencyCache = new Map(); cacheMaxAge = 60 * 60 * 1000; // 1 hour default // Cache management methods setCacheMaxAge(milliseconds) { this.cacheMaxAge = milliseconds; } getCacheMaxAge() { return this.cacheMaxAge; } clearCache() { this.projectConfigs.clear(); this.buildHistory.clear(); this.dependencyCache.clear(); } async getProjectInfo(projectPath, force = false) { const normalizedPath = this.normalizePath(projectPath); // Check if we need to refresh const existing = this.projectConfigs.get(normalizedPath); if (!force && existing && (await this.isProjectCacheValid(existing))) { return existing; } // Get file modification time const stats = await fs.stat(projectPath); const lastModified = stats.mtime; // Fetch project data const command = projectPath.endsWith('.xcworkspace') ? buildXcodebuildCommand('-list', projectPath, { workspace: true, json: true }) : buildXcodebuildCommand('-list', projectPath, { json: true }); const result = await executeCommand(command); if (result.code !== 0) { throw new Error(`Failed to get project info: ${result.stderr}`); } const projectData = JSON.parse(result.stdout); const projectInfo = { path: normalizedPath, lastModified, projectData, preferredScheme: existing?.preferredScheme, lastSuccessfulConfig: existing?.lastSuccessfulConfig, }; this.projectConfigs.set(normalizedPath, projectInfo); return projectInfo; } async getPreferredBuildConfig(projectPath) { const projectInfo = await this.getProjectInfo(projectPath); // Return last successful config if available if (projectInfo.lastSuccessfulConfig) { return projectInfo.lastSuccessfulConfig; } // Generate smart defaults const schemes = projectInfo.projectData.project?.schemes || projectInfo.projectData.workspace?.schemes || []; if (schemes.length === 0) { return null; } // Prefer scheme that matches project name or first scheme const projectName = projectInfo.projectData.project?.name || projectInfo.projectData.workspace?.name; const preferredScheme = projectName && schemes.includes(projectName) ? projectName : schemes[0]; return { scheme: preferredScheme, configuration: 'Debug', // Safe default }; } recordBuildResult(projectPath, config, metrics) { const normalizedPath = this.normalizePath(projectPath); const buildMetric = { ...metrics, config, }; // Update build history const history = this.buildHistory.get(normalizedPath) || []; history.push(buildMetric); // Keep only last 20 builds if (history.length > 20) { history.splice(0, history.length - 20); } this.buildHistory.set(normalizedPath, history); // Update last successful config if build succeeded if (metrics.success) { const projectInfo = this.projectConfigs.get(normalizedPath); if (projectInfo) { projectInfo.lastSuccessfulConfig = config; projectInfo.preferredScheme = config.scheme; } } } getBuildHistory(projectPath, limit = 10) { const normalizedPath = this.normalizePath(projectPath); const history = this.buildHistory.get(normalizedPath) || []; return history.slice(-limit).reverse(); // Most recent first } getPerformanceTrends(projectPath) { const history = this.getBuildHistory(projectPath, 20); if (history.length === 0) { return { successRate: 0, recentErrorCount: 0 }; } const successful = history.filter(h => h.success); const successRate = successful.length / history.length; const durations = successful.map(h => h.duration).filter((d) => d !== undefined); const avgBuildTime = durations.length > 0 ? durations.reduce((sum, d) => sum + d, 0) / durations.length : undefined; const recentErrorCount = history .slice(0, 5) // Last 5 builds .reduce((sum, h) => sum + h.errorCount, 0); // Calculate build time trend (recent vs older builds) let buildTimeImprovement; if (durations.length >= 6) { const recent = durations.slice(0, 3); const older = durations.slice(3, 6); const recentAvg = recent.reduce((s, d) => s + d, 0) / recent.length; const olderAvg = older.reduce((s, d) => s + d, 0) / older.length; buildTimeImprovement = ((olderAvg - recentAvg) / olderAvg) * 100; } return { avgBuildTime, successRate, recentErrorCount, buildTimeImprovement, }; } async getDependencyInfo(projectPath) { const normalizedPath = this.normalizePath(projectPath); const projectDir = dirname(projectPath); // Check cache first const existing = this.dependencyCache.get(normalizedPath); if (existing && this.isDependencyCacheValid(existing)) { return existing; } const depInfo = { lastChecked: new Date(), }; try { // Check for Package.resolved (SPM) const packageResolvedPath = join(projectDir, 'Package.resolved'); try { const packageResolved = JSON.parse(await fs.readFile(packageResolvedPath, 'utf8')); depInfo.packageResolved = packageResolved; } catch { // File doesn't exist or invalid JSON } // Check for Podfile.lock (CocoaPods) const podfileLockPath = join(projectDir, 'Podfile.lock'); try { const podfileLock = await fs.readFile(podfileLockPath, 'utf8'); depInfo.podfileLock = podfileLock; } catch { // File doesn't exist } // Check for Cartfile.resolved (Carthage) const carthagePath = join(projectDir, 'Cartfile.resolved'); try { const carthageResolved = await fs.readFile(carthagePath, 'utf8'); depInfo.carthageResolved = carthageResolved; } catch { // File doesn't exist } this.dependencyCache.set(normalizedPath, depInfo); return depInfo; } catch { return null; } } getCacheStats() { return { projectCount: this.projectConfigs.size, buildHistoryCount: Array.from(this.buildHistory.values()).reduce((sum, history) => sum + history.length, 0), dependencyCount: this.dependencyCache.size, cacheMaxAgeMs: this.cacheMaxAge, cacheMaxAgeHuman: this.formatDuration(this.cacheMaxAge), }; } async isProjectCacheValid(projectInfo) { try { const stats = await fs.stat(projectInfo.path); return stats.mtime.getTime() === projectInfo.lastModified.getTime(); } catch { return false; } } isDependencyCacheValid(depInfo) { const age = Date.now() - depInfo.lastChecked.getTime(); return age < this.cacheMaxAge; } normalizePath(path) { return path.replace(/\/$/, ''); // Remove trailing slash } formatDuration(ms) { const seconds = Math.floor(ms / 1000); const minutes = Math.floor(seconds / 60); const hours = Math.floor(minutes / 60); const days = Math.floor(hours / 24); if (days > 0) return `${days}d ${hours % 24}h`; if (hours > 0) return `${hours}h ${minutes % 60}m`; if (minutes > 0) return `${minutes}m ${seconds % 60}s`; return `${seconds}s`; } } // Global project cache instance export const projectCache = new ProjectCache(); //# sourceMappingURL=project-cache.js.map