Skip to main content
Glama
prefs-reader.ts7.45 kB
/** * iOS UserDefaults/Preferences Reader * Reads app preferences from simulator via simctl */ import { ShellExecutor, defaultShellExecutor } from '../../utils/shell-executor.js'; import { PreferencesFile, parsePlistXml, } from '../../models/app-state.js'; /** * Options for reading preferences */ export interface ReadPreferencesOptions { /** Device ID (default: booted) */ deviceId?: string; /** Specific preferences file name */ fileName?: string; /** Timeout in milliseconds */ timeoutMs?: number; /** Shell executor for dependency injection */ shell?: ShellExecutor; } /** * Read UserDefaults for an iOS app */ export async function readUserDefaults( bundleId: string, options: ReadPreferencesOptions = {} ): Promise<PreferencesFile[]> { const { deviceId = 'booted', fileName, timeoutMs = 10000, shell = defaultShellExecutor } = options; // Get app container path const containerPath = await getAppContainerPath(bundleId, deviceId, timeoutMs, shell); if (!containerPath) { return []; } // UserDefaults are stored in Library/Preferences const prefsDir = `${containerPath}/Library/Preferences`; // List plist files const files = await listPlistFiles(prefsDir, deviceId, timeoutMs, shell); if (files.length === 0) { // Try the bundle ID plist directly const defaultPlist = `${bundleId}.plist`; const content = await readPlistFile(`${prefsDir}/${defaultPlist}`, deviceId, timeoutMs, shell); if (content) { const entries = parsePlistXml(content); return [{ name: bundleId, path: `${prefsDir}/${defaultPlist}`, entries, }]; } return []; } // Filter to specific file if requested const targetFiles = fileName ? files.filter((f) => f === fileName || f === `${fileName}.plist`) : files; // Read each plist file const results: PreferencesFile[] = []; for (const file of targetFiles) { const filePath = `${prefsDir}/${file}`; const content = await readPlistFile(filePath, deviceId, timeoutMs, shell); if (content) { const entries = parsePlistXml(content); results.push({ name: file.replace('.plist', ''), path: filePath, entries, }); } } return results; } /** * Get app container path on simulator */ export async function getAppContainerPath( bundleId: string, deviceId: string = 'booted', timeoutMs: number = 5000, shell: ShellExecutor = defaultShellExecutor ): Promise<string | null> { const args = [ 'simctl', 'get_app_container', deviceId, bundleId, 'data', ]; try { const result = await shell.execute('xcrun', args, { timeoutMs }); if (result.exitCode !== 0) { return null; } return result.stdout.trim(); } catch { return null; } } /** * List plist files in a directory */ async function listPlistFiles( directory: string, deviceId: string, timeoutMs: number, shell: ShellExecutor ): Promise<string[]> { const args = [ 'simctl', 'spawn', deviceId, 'ls', directory, ]; try { const result = await shell.execute('xcrun', args, { timeoutMs }); if (result.exitCode !== 0) { return []; } return result.stdout .split('\n') .map((f) => f.trim()) .filter((f) => f.endsWith('.plist')); } catch { return []; } } /** * Read a plist file from simulator */ async function readPlistFile( filePath: string, deviceId: string, timeoutMs: number, shell: ShellExecutor ): Promise<string | null> { // First convert binary plist to XML if needed const convertArgs = [ 'simctl', 'spawn', deviceId, 'plutil', '-convert', 'xml1', '-o', '-', filePath, ]; try { const result = await shell.execute('xcrun', convertArgs, { timeoutMs }); if (result.exitCode === 0) { return result.stdout; } // Try reading directly (might already be XML) const catArgs = ['simctl', 'spawn', deviceId, 'cat', filePath]; const catResult = await shell.execute('xcrun', catArgs, { timeoutMs }); return catResult.exitCode === 0 ? catResult.stdout : null; } catch { return null; } } /** * Read NSUserDefaults using defaults command * This reads the standard UserDefaults domain */ export async function readDefaultsDomain( bundleId: string, deviceId: string = 'booted', timeoutMs: number = 10000, shell: ShellExecutor = defaultShellExecutor ): Promise<PreferencesFile | null> { const args = [ 'simctl', 'spawn', deviceId, 'defaults', 'read', bundleId, ]; try { const result = await shell.execute('xcrun', args, { timeoutMs }); if (result.exitCode !== 0) { return null; } // Parse defaults output (it's not XML, but a property list text format) const entries = parseDefaultsOutput(result.stdout); return { name: bundleId, entries, }; } catch { return null; } } /** * Parse output from defaults read command */ function parseDefaultsOutput(output: string): Array<{ key: string; value: string | number | boolean; type: 'string' | 'int' | 'float' | 'boolean' }> { const entries: Array<{ key: string; value: string | number | boolean; type: 'string' | 'int' | 'float' | 'boolean' }> = []; // Output format: "key = value;" const lines = output.split('\n'); for (const line of lines) { const match = line.match(/^\s*"?([^"=]+)"?\s*=\s*(.+?);?\s*$/); if (match) { const key = match[1].trim(); let valueStr = match[2].trim(); // Remove quotes if (valueStr.startsWith('"') && valueStr.endsWith('"')) { entries.push({ key, value: valueStr.slice(1, -1), type: 'string' }); } else if (valueStr === '1' || valueStr === '0') { // Could be boolean or int - assume boolean for common patterns if (key.toLowerCase().includes('enable') || key.toLowerCase().includes('is')) { entries.push({ key, value: valueStr === '1', type: 'boolean' }); } else { entries.push({ key, value: parseInt(valueStr, 10), type: 'int' }); } } else if (/^-?\d+$/.test(valueStr)) { entries.push({ key, value: parseInt(valueStr, 10), type: 'int' }); } else if (/^-?\d+\.\d+$/.test(valueStr)) { entries.push({ key, value: parseFloat(valueStr), type: 'float' }); } else { entries.push({ key, value: valueStr, type: 'string' }); } } } return entries; } /** * Get all installed apps on simulator */ export async function listInstalledApps( deviceId: string = 'booted', timeoutMs: number = 10000, shell: ShellExecutor = defaultShellExecutor ): Promise<string[]> { const args = ['simctl', 'listapps', deviceId]; try { const result = await shell.execute('xcrun', args, { timeoutMs }); if (result.exitCode !== 0) { return []; } // Parse the output to extract bundle IDs const bundleIds: string[] = []; const matches = result.stdout.matchAll(/CFBundleIdentifier\s*=\s*"([^"]+)"/g); for (const match of matches) { bundleIds.push(match[1]); } return bundleIds; } catch { return []; } } /** * Check if app is installed on simulator */ export async function isAppInstalled( bundleId: string, deviceId: string = 'booted', shell: ShellExecutor = defaultShellExecutor ): Promise<boolean> { const containerPath = await getAppContainerPath(bundleId, deviceId, 5000, shell); return containerPath !== null; }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/abd3lraouf/specter-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server