mcp-xcode
Version:
MCP server that wraps Xcode command-line tools for iOS/macOS development workflows
206 lines • 8.12 kB
JavaScript
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