UNPKG

mcp-xcode

Version:

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

206 lines 8.12 kB
import { executeCommand, buildSimctlCommand } from '../utils/command.js'; export class SimulatorCache { cache = null; cacheMaxAge = 60 * 60 * 1000; // 1 hour default bootStates = new Map(); preferredByProject = new Map(); lastUsed = new Map(); // Cache management methods setCacheMaxAge(milliseconds) { this.cacheMaxAge = milliseconds; } getCacheMaxAge() { return this.cacheMaxAge; } clearCache() { this.cache = null; this.bootStates.clear(); this.preferredByProject.clear(); this.lastUsed.clear(); } async getSimulatorList(force = false) { if (!force && this.cache && this.isCacheValid()) { return this.cache; } // Fetch fresh data const command = buildSimctlCommand('list', { json: true }); const result = await executeCommand(command); if (result.code !== 0) { throw new Error(`Failed to list simulators: ${result.stderr}`); } const simulatorList = JSON.parse(result.stdout); // Transform to cached format with enhanced info const cachedList = { devices: {}, runtimes: simulatorList.runtimes, devicetypes: simulatorList.devicetypes, lastUpdated: new Date(), preferredByProject: this.preferredByProject, }; // Enhance device info with historical data for (const [runtime, devices] of Object.entries(simulatorList.devices)) { cachedList.devices[runtime] = devices.map(device => { const existingInfo = this.findExistingDevice(device.udid); return { ...device, lastUsed: existingInfo?.lastUsed || this.lastUsed.get(device.udid), bootHistory: existingInfo?.bootHistory || [], performanceMetrics: existingInfo?.performanceMetrics, }; }); } this.cache = cachedList; return cachedList; } async getAvailableSimulators(deviceType, runtime) { const list = await this.getSimulatorList(); const devices = []; for (const [runtimeKey, runtimeDevices] of Object.entries(list.devices)) { if (runtime && !runtimeKey.toLowerCase().includes(runtime.toLowerCase())) { continue; } const filteredDevices = runtimeDevices.filter(device => { if (!device.isAvailable) return false; if (deviceType && !device.name.toLowerCase().includes(deviceType.toLowerCase())) { return false; } return true; }); devices.push(...filteredDevices); } // Sort by preference: recently used, then by name return devices.sort((a, b) => { const aLastUsed = a.lastUsed?.getTime() || 0; const bLastUsed = b.lastUsed?.getTime() || 0; if (aLastUsed !== bLastUsed) { return bLastUsed - aLastUsed; // Most recent first } return a.name.localeCompare(b.name); }); } async getPreferredSimulator(projectPath, deviceType) { // Check project-specific preference first if (projectPath) { const preferredUdid = this.preferredByProject.get(projectPath); if (preferredUdid) { const preferred = await this.findSimulatorByUdid(preferredUdid); if (preferred && preferred.isAvailable) { return preferred; } } } // Fallback to most recently used available simulator const available = await this.getAvailableSimulators(deviceType); return available[0] || null; } async findSimulatorByUdid(udid) { const list = await this.getSimulatorList(); for (const devices of Object.values(list.devices)) { const found = devices.find(device => device.udid === udid); if (found) return found; } return null; } recordSimulatorUsage(udid, projectPath) { const now = new Date(); this.lastUsed.set(udid, now); if (projectPath) { this.preferredByProject.set(projectPath, udid); } // Update cache if exists if (this.cache) { for (const devices of Object.values(this.cache.devices)) { const device = devices.find(d => d.udid === udid); if (device) { device.lastUsed = now; break; } } } } recordBootEvent(udid, success, duration) { this.bootStates.set(udid, success ? 'booted' : 'shutdown'); if (this.cache && success) { for (const devices of Object.values(this.cache.devices)) { const device = devices.find(d => d.udid === udid); if (device) { device.bootHistory.push(new Date()); // Update performance metrics if (duration) { const metrics = device.performanceMetrics || { avgBootTime: 0, reliability: 1.0 }; const bootTimes = device.bootHistory.slice(-10); // Last 10 boots const currentAvg = metrics.avgBootTime || 0; metrics.avgBootTime = bootTimes.length > 1 ? (currentAvg + duration) / 2 : duration; metrics.reliability = Math.min(1.0, bootTimes.length / 10); device.performanceMetrics = metrics; } break; } } } } getBootState(udid) { return this.bootStates.get(udid) || 'unknown'; } getCacheStats() { const cacheMaxAgeHuman = this.formatDuration(this.cacheMaxAge); if (!this.cache) { return { isCached: false, cacheMaxAgeMs: this.cacheMaxAge, cacheMaxAgeHuman, deviceCount: 0, recentlyUsedCount: 0, isExpired: false, }; } const deviceCount = Object.values(this.cache.devices).reduce((sum, devices) => sum + devices.length, 0); const recentlyUsedCount = Array.from(this.lastUsed.values()).filter(date => Date.now() - date.getTime() < 24 * 60 * 60 * 1000).length; const ageMs = Date.now() - this.cache.lastUpdated.getTime(); const isExpired = ageMs >= this.cacheMaxAge; const timeUntilExpiry = isExpired ? undefined : this.formatDuration(this.cacheMaxAge - ageMs); return { isCached: true, lastUpdated: this.cache.lastUpdated, cacheMaxAgeMs: this.cacheMaxAge, cacheMaxAgeHuman, deviceCount, recentlyUsedCount, isExpired, timeUntilExpiry, }; } isCacheValid() { if (!this.cache) return false; return Date.now() - this.cache.lastUpdated.getTime() < this.cacheMaxAge; } findExistingDevice(udid) { if (!this.cache) return undefined; for (const devices of Object.values(this.cache.devices)) { const found = devices.find(device => device.udid === udid); if (found) return found; } return undefined; } 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 simulator cache instance export const simulatorCache = new SimulatorCache(); //# sourceMappingURL=simulator-cache.js.map