#
tokens: 49512/50000 34/195 files (page 2/5)
lines: on (toggle) GitHub
raw markdown copy reset
This is page 2 of 5. Use http://codebase.md/stefan-nitu/mcp-xcode-server?lines=true&page={x} to view the full context.

# Directory Structure

```
├── .claude
│   └── settings.local.json
├── .github
│   └── workflows
│       └── ci.yml
├── .gitignore
├── .vscode
│   └── settings.json
├── CLAUDE.md
├── CONTRIBUTING.md
├── docs
│   ├── ARCHITECTURE.md
│   ├── ERROR-HANDLING.md
│   └── TESTING-PHILOSOPHY.md
├── examples
│   └── screenshot-demo.js
├── jest.config.cjs
├── jest.e2e.config.cjs
├── LICENSE
├── package-lock.json
├── package.json
├── README.md
├── scripts
│   └── xcode-sync.swift
├── src
│   ├── application
│   │   └── ports
│   │       ├── ArtifactPorts.ts
│   │       ├── BuildPorts.ts
│   │       ├── CommandPorts.ts
│   │       ├── ConfigPorts.ts
│   │       ├── LoggingPorts.ts
│   │       ├── MappingPorts.ts
│   │       ├── OutputFormatterPorts.ts
│   │       ├── OutputParserPorts.ts
│   │       └── SimulatorPorts.ts
│   ├── cli.ts
│   ├── config.ts
│   ├── domain
│   │   ├── errors
│   │   │   └── DomainError.ts
│   │   ├── services
│   │   │   └── PlatformDetector.ts
│   │   ├── shared
│   │   │   └── Result.ts
│   │   └── tests
│   │       └── unit
│   │           └── PlatformDetector.unit.test.ts
│   ├── features
│   │   ├── app-management
│   │   │   ├── controllers
│   │   │   │   └── InstallAppController.ts
│   │   │   ├── domain
│   │   │   │   ├── InstallRequest.ts
│   │   │   │   └── InstallResult.ts
│   │   │   ├── factories
│   │   │   │   └── InstallAppControllerFactory.ts
│   │   │   ├── index.ts
│   │   │   ├── infrastructure
│   │   │   │   └── AppInstallerAdapter.ts
│   │   │   ├── tests
│   │   │   │   ├── e2e
│   │   │   │   │   ├── InstallAppController.e2e.test.ts
│   │   │   │   │   └── InstallAppMCP.e2e.test.ts
│   │   │   │   ├── integration
│   │   │   │   │   └── InstallAppController.integration.test.ts
│   │   │   │   └── unit
│   │   │   │       ├── AppInstallerAdapter.unit.test.ts
│   │   │   │       ├── InstallAppController.unit.test.ts
│   │   │   │       ├── InstallAppUseCase.unit.test.ts
│   │   │   │       ├── InstallRequest.unit.test.ts
│   │   │   │       └── InstallResult.unit.test.ts
│   │   │   └── use-cases
│   │   │       └── InstallAppUseCase.ts
│   │   ├── build
│   │   │   ├── controllers
│   │   │   │   └── BuildXcodeController.ts
│   │   │   ├── domain
│   │   │   │   ├── BuildDestination.ts
│   │   │   │   ├── BuildIssue.ts
│   │   │   │   ├── BuildRequest.ts
│   │   │   │   ├── BuildResult.ts
│   │   │   │   └── PlatformInfo.ts
│   │   │   ├── factories
│   │   │   │   └── BuildXcodeControllerFactory.ts
│   │   │   ├── index.ts
│   │   │   ├── infrastructure
│   │   │   │   ├── BuildArtifactLocatorAdapter.ts
│   │   │   │   ├── BuildDestinationMapperAdapter.ts
│   │   │   │   ├── XcbeautifyFormatterAdapter.ts
│   │   │   │   ├── XcbeautifyOutputParserAdapter.ts
│   │   │   │   └── XcodeBuildCommandAdapter.ts
│   │   │   ├── tests
│   │   │   │   ├── e2e
│   │   │   │   │   ├── BuildXcodeController.e2e.test.ts
│   │   │   │   │   └── BuildXcodeMCP.e2e.test.ts
│   │   │   │   ├── integration
│   │   │   │   │   └── BuildXcodeController.integration.test.ts
│   │   │   │   └── unit
│   │   │   │       ├── BuildArtifactLocatorAdapter.unit.test.ts
│   │   │   │       ├── BuildDestinationMapperAdapter.unit.test.ts
│   │   │   │       ├── BuildIssue.unit.test.ts
│   │   │   │       ├── BuildProjectUseCase.unit.test.ts
│   │   │   │       ├── BuildRequest.unit.test.ts
│   │   │   │       ├── BuildResult.unit.test.ts
│   │   │   │       ├── BuildXcodeController.unit.test.ts
│   │   │   │       ├── BuildXcodePresenter.unit.test.ts
│   │   │   │       ├── PlatformInfo.unit.test.ts
│   │   │   │       ├── XcbeautifyFormatterAdapter.unit.test.ts
│   │   │   │       ├── XcbeautifyOutputParserAdapter.unit.test.ts
│   │   │   │       └── XcodeBuildCommandAdapter.unit.test.ts
│   │   │   └── use-cases
│   │   │       └── BuildProjectUseCase.ts
│   │   └── simulator
│   │       ├── controllers
│   │       │   ├── BootSimulatorController.ts
│   │       │   ├── ListSimulatorsController.ts
│   │       │   └── ShutdownSimulatorController.ts
│   │       ├── domain
│   │       │   ├── BootRequest.ts
│   │       │   ├── BootResult.ts
│   │       │   ├── ListSimulatorsRequest.ts
│   │       │   ├── ListSimulatorsResult.ts
│   │       │   ├── ShutdownRequest.ts
│   │       │   ├── ShutdownResult.ts
│   │       │   └── SimulatorState.ts
│   │       ├── factories
│   │       │   ├── BootSimulatorControllerFactory.ts
│   │       │   ├── ListSimulatorsControllerFactory.ts
│   │       │   └── ShutdownSimulatorControllerFactory.ts
│   │       ├── index.ts
│   │       ├── infrastructure
│   │       │   ├── SimulatorControlAdapter.ts
│   │       │   └── SimulatorLocatorAdapter.ts
│   │       ├── tests
│   │       │   ├── e2e
│   │       │   │   ├── BootSimulatorController.e2e.test.ts
│   │       │   │   ├── BootSimulatorMCP.e2e.test.ts
│   │       │   │   ├── ListSimulatorsController.e2e.test.ts
│   │       │   │   ├── ListSimulatorsMCP.e2e.test.ts
│   │       │   │   ├── ShutdownSimulatorController.e2e.test.ts
│   │       │   │   └── ShutdownSimulatorMCP.e2e.test.ts
│   │       │   ├── integration
│   │       │   │   ├── BootSimulatorController.integration.test.ts
│   │       │   │   ├── ListSimulatorsController.integration.test.ts
│   │       │   │   └── ShutdownSimulatorController.integration.test.ts
│   │       │   └── unit
│   │       │       ├── BootRequest.unit.test.ts
│   │       │       ├── BootResult.unit.test.ts
│   │       │       ├── BootSimulatorController.unit.test.ts
│   │       │       ├── BootSimulatorUseCase.unit.test.ts
│   │       │       ├── ListSimulatorsController.unit.test.ts
│   │       │       ├── ListSimulatorsUseCase.unit.test.ts
│   │       │       ├── ShutdownRequest.unit.test.ts
│   │       │       ├── ShutdownResult.unit.test.ts
│   │       │       ├── ShutdownSimulatorUseCase.unit.test.ts
│   │       │       ├── SimulatorControlAdapter.unit.test.ts
│   │       │       └── SimulatorLocatorAdapter.unit.test.ts
│   │       └── use-cases
│   │           ├── BootSimulatorUseCase.ts
│   │           ├── ListSimulatorsUseCase.ts
│   │           └── ShutdownSimulatorUseCase.ts
│   ├── index.ts
│   ├── infrastructure
│   │   ├── repositories
│   │   │   └── DeviceRepository.ts
│   │   ├── services
│   │   │   └── DependencyChecker.ts
│   │   └── tests
│   │       └── unit
│   │           ├── DependencyChecker.unit.test.ts
│   │           └── DeviceRepository.unit.test.ts
│   ├── logger.ts
│   ├── presentation
│   │   ├── decorators
│   │   │   └── DependencyCheckingDecorator.ts
│   │   ├── formatters
│   │   │   ├── ErrorFormatter.ts
│   │   │   └── strategies
│   │   │       ├── BuildIssuesStrategy.ts
│   │   │       ├── DefaultErrorStrategy.ts
│   │   │       ├── ErrorFormattingStrategy.ts
│   │   │       └── OutputFormatterErrorStrategy.ts
│   │   ├── interfaces
│   │   │   ├── IDependencyChecker.ts
│   │   │   ├── MCPController.ts
│   │   │   └── MCPResponse.ts
│   │   ├── presenters
│   │   │   └── BuildXcodePresenter.ts
│   │   └── tests
│   │       └── unit
│   │           ├── BuildIssuesStrategy.unit.test.ts
│   │           ├── DefaultErrorStrategy.unit.test.ts
│   │           ├── DependencyCheckingDecorator.unit.test.ts
│   │           └── ErrorFormatter.unit.test.ts
│   ├── shared
│   │   ├── domain
│   │   │   ├── AppPath.ts
│   │   │   ├── DeviceId.ts
│   │   │   ├── Platform.ts
│   │   │   └── ProjectPath.ts
│   │   ├── index.ts
│   │   ├── infrastructure
│   │   │   ├── ConfigProviderAdapter.ts
│   │   │   └── ShellCommandExecutorAdapter.ts
│   │   └── tests
│   │       ├── mocks
│   │       │   ├── promisifyExec.ts
│   │       │   ├── selectiveExecMock.ts
│   │       │   └── xcodebuildHelpers.ts
│   │       ├── skipped
│   │       │   ├── cli.e2e.test.skip
│   │       │   ├── hook-e2e.test.skip
│   │       │   ├── hook-path.e2e.test.skip
│   │       │   └── hook.test.skip
│   │       ├── types
│   │       │   └── execTypes.ts
│   │       ├── unit
│   │       │   ├── AppPath.unit.test.ts
│   │       │   ├── ConfigProviderAdapter.unit.test.ts
│   │       │   ├── ProjectPath.unit.test.ts
│   │       │   └── ShellCommandExecutorAdapter.unit.test.ts
│   │       └── utils
│   │           ├── gitResetTestArtifacts.ts
│   │           ├── mockHelpers.ts
│   │           ├── TestEnvironmentCleaner.ts
│   │           ├── TestErrorInjector.ts
│   │           ├── testHelpers.ts
│   │           ├── TestProjectManager.ts
│   │           └── TestSimulatorManager.ts
│   ├── types.ts
│   ├── utils
│   │   ├── devices
│   │   │   ├── Devices.ts
│   │   │   ├── SimulatorApps.ts
│   │   │   ├── SimulatorBoot.ts
│   │   │   ├── SimulatorDevice.ts
│   │   │   ├── SimulatorInfo.ts
│   │   │   ├── SimulatorReset.ts
│   │   │   └── SimulatorUI.ts
│   │   ├── errors
│   │   │   ├── index.ts
│   │   │   └── xcbeautify-parser.ts
│   │   ├── index.ts
│   │   ├── LogManager.ts
│   │   ├── LogManagerInstance.ts
│   │   └── projects
│   │       ├── SwiftBuild.ts
│   │       ├── SwiftPackage.ts
│   │       ├── SwiftPackageInfo.ts
│   │       ├── Xcode.ts
│   │       ├── XcodeArchive.ts
│   │       ├── XcodeBuild.ts
│   │       ├── XcodeErrors.ts
│   │       ├── XcodeInfo.ts
│   │       └── XcodeProject.ts
│   └── utils.ts
├── test_artifacts
│   ├── Test.xcworkspace
│   │   ├── contents.xcworkspacedata
│   │   └── xcuserdata
│   │       └── stefan.xcuserdatad
│   │           └── UserInterfaceState.xcuserstate
│   ├── TestProjectSwiftTesting
│   │   ├── TestProjectSwiftTesting
│   │   │   ├── Assets.xcassets
│   │   │   │   ├── AccentColor.colorset
│   │   │   │   │   └── Contents.json
│   │   │   │   ├── AppIcon.appiconset
│   │   │   │   │   └── Contents.json
│   │   │   │   └── Contents.json
│   │   │   ├── ContentView.swift
│   │   │   ├── Item.swift
│   │   │   ├── TestProjectSwiftTesting.entitlements
│   │   │   └── TestProjectSwiftTestingApp.swift
│   │   ├── TestProjectSwiftTesting.xcodeproj
│   │   │   ├── project.pbxproj
│   │   │   ├── project.xcworkspace
│   │   │   │   ├── contents.xcworkspacedata
│   │   │   │   └── xcuserdata
│   │   │   │       └── stefan.xcuserdatad
│   │   │   │           └── UserInterfaceState.xcuserstate
│   │   │   └── xcuserdata
│   │   │       └── stefan.xcuserdatad
│   │   │           └── xcschemes
│   │   │               └── xcschememanagement.plist
│   │   ├── TestProjectSwiftTestingTests
│   │   │   └── TestProjectSwiftTestingTests.swift
│   │   └── TestProjectSwiftTestingUITests
│   │       ├── TestProjectSwiftTestingUITests.swift
│   │       └── TestProjectSwiftTestingUITestsLaunchTests.swift
│   ├── TestProjectWatchOS
│   │   ├── TestProjectWatchOS
│   │   │   ├── Assets.xcassets
│   │   │   │   ├── AccentColor.colorset
│   │   │   │   │   └── Contents.json
│   │   │   │   ├── AppIcon.appiconset
│   │   │   │   │   └── Contents.json
│   │   │   │   └── Contents.json
│   │   │   ├── ContentView.swift
│   │   │   └── TestProjectWatchOSApp.swift
│   │   ├── TestProjectWatchOS Watch App
│   │   │   ├── Assets.xcassets
│   │   │   │   ├── AccentColor.colorset
│   │   │   │   │   └── Contents.json
│   │   │   │   ├── AppIcon.appiconset
│   │   │   │   │   └── Contents.json
│   │   │   │   └── Contents.json
│   │   │   ├── ContentView.swift
│   │   │   └── TestProjectWatchOSApp.swift
│   │   ├── TestProjectWatchOS Watch AppTests
│   │   │   └── TestProjectWatchOS_Watch_AppTests.swift
│   │   ├── TestProjectWatchOS Watch AppUITests
│   │   │   ├── TestProjectWatchOS_Watch_AppUITests.swift
│   │   │   └── TestProjectWatchOS_Watch_AppUITestsLaunchTests.swift
│   │   ├── TestProjectWatchOS.xcodeproj
│   │   │   ├── project.pbxproj
│   │   │   └── project.xcworkspace
│   │   │       └── contents.xcworkspacedata
│   │   ├── TestProjectWatchOSTests
│   │   │   └── TestProjectWatchOSTests.swift
│   │   └── TestProjectWatchOSUITests
│   │       ├── TestProjectWatchOSUITests.swift
│   │       └── TestProjectWatchOSUITestsLaunchTests.swift
│   ├── TestProjectXCTest
│   │   ├── TestProjectXCTest
│   │   │   ├── Assets.xcassets
│   │   │   │   ├── AccentColor.colorset
│   │   │   │   │   └── Contents.json
│   │   │   │   ├── AppIcon.appiconset
│   │   │   │   │   └── Contents.json
│   │   │   │   └── Contents.json
│   │   │   ├── ContentView.swift
│   │   │   ├── Item.swift
│   │   │   ├── TestProjectXCTest.entitlements
│   │   │   └── TestProjectXCTestApp.swift
│   │   ├── TestProjectXCTest.xcodeproj
│   │   │   ├── project.pbxproj
│   │   │   ├── project.xcworkspace
│   │   │   │   ├── contents.xcworkspacedata
│   │   │   │   └── xcuserdata
│   │   │   │       └── stefan.xcuserdatad
│   │   │   │           └── UserInterfaceState.xcuserstate
│   │   │   └── xcuserdata
│   │   │       └── stefan.xcuserdatad
│   │   │           └── xcschemes
│   │   │               └── xcschememanagement.plist
│   │   ├── TestProjectXCTestTests
│   │   │   └── TestProjectXCTestTests.swift
│   │   └── TestProjectXCTestUITests
│   │       ├── TestProjectXCTestUITests.swift
│   │       └── TestProjectXCTestUITestsLaunchTests.swift
│   ├── TestSwiftPackageSwiftTesting
│   │   ├── .gitignore
│   │   ├── Package.swift
│   │   ├── Sources
│   │   │   ├── TestSwiftPackageSwiftTesting
│   │   │   │   └── TestSwiftPackageSwiftTesting.swift
│   │   │   └── TestSwiftPackageSwiftTestingExecutable
│   │   │       └── main.swift
│   │   └── Tests
│   │       └── TestSwiftPackageSwiftTestingTests
│   │           └── TestSwiftPackageSwiftTestingTests.swift
│   └── TestSwiftPackageXCTest
│       ├── .gitignore
│       ├── Package.swift
│       ├── Sources
│       │   ├── TestSwiftPackageXCTest
│       │   │   └── TestSwiftPackageXCTest.swift
│       │   └── TestSwiftPackageXCTestExecutable
│       │       └── main.swift
│       └── Tests
│           └── TestSwiftPackageXCTestTests
│               └── TestSwiftPackageXCTestTests.swift
├── tsconfig.json
└── XcodeProjectModifier
    ├── Package.resolved
    ├── Package.swift
    └── Sources
        └── XcodeProjectModifier
            └── main.swift
```

# Files

--------------------------------------------------------------------------------
/src/utils/devices/SimulatorUI.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { execAsync } from '../../utils.js';
  2 | import { createModuleLogger } from '../../logger.js';
  3 | import { readFileSync, existsSync, unlinkSync } from 'fs';
  4 | import { join } from 'path';
  5 | import { tmpdir } from 'os';
  6 | 
  7 | const logger = createModuleLogger('SimulatorUI');
  8 | 
  9 | /**
 10 |  * Handles simulator UI operations
 11 |  * Single responsibility: GUI operations and screenshots
 12 |  */
 13 | export class SimulatorUI {
 14 |   /**
 15 |    * Opens the Simulator app GUI (skipped during tests)
 16 |    */
 17 |   async open(): Promise<void> {
 18 |     // Skip opening GUI during tests
 19 |     if (process.env.NODE_ENV === 'test' || process.env.JEST_WORKER_ID) {
 20 |       logger.debug('Skipping Simulator GUI in test environment');
 21 |       return;
 22 |     }
 23 |     
 24 |     try {
 25 |       await execAsync('open -g -a Simulator');
 26 |       logger.debug('Opened Simulator app');
 27 |     } catch (error: any) {
 28 |       logger.warn({ error: error.message }, 'Failed to open Simulator app');
 29 |     }
 30 |   }
 31 | 
 32 |   /**
 33 |    * Capture a screenshot from the simulator
 34 |    */
 35 |   async screenshot(outputPath: string, deviceId?: string): Promise<void> {
 36 |     let command = `xcrun simctl io `;
 37 |     if (deviceId) {
 38 |       command += `"${deviceId}" `;
 39 |     } else {
 40 |       command += 'booted ';
 41 |     }
 42 |     command += `screenshot "${outputPath}"`;
 43 | 
 44 |     try {
 45 |       await execAsync(command);
 46 |       logger.debug({ outputPath, deviceId }, 'Screenshot captured successfully');
 47 |     } catch (error: any) {
 48 |       logger.error({ error: error.message, outputPath, deviceId }, 'Failed to capture screenshot');
 49 |       throw new Error(`Failed to capture screenshot: ${error.message}`);
 50 |     }
 51 |   }
 52 | 
 53 |   /**
 54 |    * Capture a screenshot and return as base64
 55 |    */
 56 |   async screenshotData(deviceId?: string): Promise<{ base64: string; mimeType: string }> {
 57 |     // Create a temporary file path
 58 |     const tempPath = join(tmpdir(), `simulator-screenshot-${Date.now()}.png`);
 59 |     
 60 |     try {
 61 |       // Capture the screenshot to temp file
 62 |       await this.screenshot(tempPath, deviceId);
 63 |       
 64 |       // Read the file and convert to base64
 65 |       const imageData = readFileSync(tempPath);
 66 |       const base64 = imageData.toString('base64');
 67 |       
 68 |       return {
 69 |         base64,
 70 |         mimeType: 'image/png'
 71 |       };
 72 |     } finally {
 73 |       // Clean up temp file
 74 |       if (existsSync(tempPath)) {
 75 |         unlinkSync(tempPath);
 76 |       }
 77 |     }
 78 |   }
 79 | 
 80 |   /**
 81 |    * Set simulator appearance (light/dark mode)
 82 |    * May fail on older Xcode versions
 83 |    */
 84 |   async setAppearance(appearance: 'light' | 'dark', deviceId?: string): Promise<void> {
 85 |     let command = `xcrun simctl ui `;
 86 |     if (deviceId) {
 87 |       command += `"${deviceId}" `;
 88 |     } else {
 89 |       command += 'booted ';
 90 |     }
 91 |     command += appearance;
 92 | 
 93 |     try {
 94 |       await execAsync(command);
 95 |       logger.debug({ appearance, deviceId }, 'Appearance set successfully');
 96 |     } catch (error: any) {
 97 |       // This command may not be available on older Xcode versions
 98 |       logger.debug({ error: error.message }, 'Could not set appearance (may not be supported)');
 99 |     }
100 |   }
101 | }
```

--------------------------------------------------------------------------------
/src/shared/domain/AppPath.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { DomainEmptyError, DomainInvalidTypeError, DomainInvalidFormatError, DomainRequiredError } from '../../domain/errors/DomainError.js';
  2 | 
  3 | /**
  4 |  * Value object for an app bundle path
  5 |  * Ensures the path ends with .app extension
  6 |  */
  7 | export class AppPath {
  8 |   private constructor(private readonly value: string) {}
  9 | 
 10 |   static create(path: unknown): AppPath {
 11 |     // Required check (for undefined/null)
 12 |     if (path === undefined || path === null) {
 13 |       throw new AppPath.RequiredError();
 14 |     }
 15 | 
 16 |     // Type checking
 17 |     if (typeof path !== 'string') {
 18 |       throw new AppPath.InvalidTypeError(path);
 19 |     }
 20 | 
 21 |     // Empty check
 22 |     if (path.trim() === '') {
 23 |       throw new AppPath.EmptyError(path);
 24 |     }
 25 | 
 26 |     const trimmed = path.trim();
 27 | 
 28 |     // Security checks first (before format validation)
 29 |     if (trimmed.includes('..')) {
 30 |       throw new AppPath.TraversalError(trimmed);
 31 |     }
 32 | 
 33 |     if (trimmed.includes('\0')) {
 34 |       throw new AppPath.NullCharacterError(trimmed);
 35 |     }
 36 | 
 37 |     // Format validation
 38 |     if (!trimmed.endsWith('.app') && !trimmed.endsWith('.app/')) {
 39 |       throw new AppPath.InvalidFormatError(trimmed);
 40 |     }
 41 | 
 42 |     return new AppPath(trimmed);
 43 |   }
 44 | 
 45 |   toString(): string {
 46 |     return this.value;
 47 |   }
 48 | 
 49 |   get name(): string {
 50 |     // Handle both forward slash and backslash for cross-platform support
 51 |     const separatorPattern = /[/\\]/;
 52 |     const parts = this.value.split(separatorPattern);
 53 |     const lastPart = parts[parts.length - 1];
 54 | 
 55 |     // If path ends with /, the last part will be empty, so take the second to last
 56 |     return lastPart || parts[parts.length - 2];
 57 |   }
 58 | }
 59 | 
 60 | // Nested error classes under AppPath namespace
 61 | export namespace AppPath {
 62 |   // All AppPath errors extend DomainError for consistency
 63 | 
 64 |   export class RequiredError extends DomainRequiredError {
 65 |     constructor() {
 66 |       super('App path');
 67 |       this.name = 'AppPath.RequiredError';
 68 |     }
 69 |   }
 70 | 
 71 |   export class InvalidTypeError extends DomainInvalidTypeError {
 72 |     constructor(public readonly providedValue: unknown) {
 73 |       super('App path', 'string');
 74 |       this.name = 'AppPath.InvalidTypeError';
 75 |     }
 76 |   }
 77 | 
 78 |   export class EmptyError extends DomainEmptyError {
 79 |     constructor(public readonly providedValue: unknown) {
 80 |       super('App path');
 81 |       this.name = 'AppPath.EmptyError';
 82 |     }
 83 |   }
 84 | 
 85 |   export class InvalidFormatError extends DomainInvalidFormatError {
 86 |     constructor(public readonly path: string) {
 87 |       super('App path must end with .app');
 88 |       this.name = 'AppPath.InvalidFormatError';
 89 |     }
 90 |   }
 91 | 
 92 |   export class TraversalError extends DomainInvalidFormatError {
 93 |     constructor(public readonly path: string) {
 94 |       super('App path cannot contain directory traversal');
 95 |       this.name = 'AppPath.TraversalError';
 96 |     }
 97 |   }
 98 | 
 99 |   export class NullCharacterError extends DomainInvalidFormatError {
100 |     constructor(public readonly path: string) {
101 |       super('App path cannot contain null characters');
102 |       this.name = 'AppPath.NullCharacterError';
103 |     }
104 |   }
105 | }
```

--------------------------------------------------------------------------------
/src/features/simulator/use-cases/ListSimulatorsUseCase.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import { ListSimulatorsRequest } from '../domain/ListSimulatorsRequest.js';
 2 | import { ListSimulatorsResult, SimulatorInfo, SimulatorListParseError } from '../domain/ListSimulatorsResult.js';
 3 | import { DeviceRepository } from '../../../infrastructure/repositories/DeviceRepository.js';
 4 | import { Platform } from '../../../shared/domain/Platform.js';
 5 | import { SimulatorState } from '../domain/SimulatorState.js';
 6 | 
 7 | /**
 8 |  * Use case for listing available simulators
 9 |  */
10 | export class ListSimulatorsUseCase {
11 |   constructor(
12 |     private readonly deviceRepository: DeviceRepository
13 |   ) {}
14 | 
15 |   async execute(request: ListSimulatorsRequest): Promise<ListSimulatorsResult> {
16 |     try {
17 |       let allDevices;
18 |       try {
19 |         allDevices = await this.deviceRepository.getAllDevices();
20 |       } catch (error) {
21 |         // JSON parsing errors from the repository
22 |         return ListSimulatorsResult.failed(new SimulatorListParseError());
23 |       }
24 | 
25 |       const simulatorInfos: SimulatorInfo[] = [];
26 | 
27 |       for (const [runtime, devices] of Object.entries(allDevices)) {
28 |         const platform = this.extractPlatformFromRuntime(runtime);
29 |         const runtimeVersion = this.extractVersionFromRuntime(runtime);
30 | 
31 |         for (const device of devices) {
32 |           if (!device.isAvailable) continue;
33 | 
34 |           const simulatorInfo: SimulatorInfo = {
35 |             udid: device.udid,
36 |             name: device.name,
37 |             state: SimulatorState.parse(device.state),
38 |             platform: platform,
39 |             runtime: `${platform} ${runtimeVersion}`
40 |           };
41 | 
42 |           if (this.matchesFilter(simulatorInfo, request)) {
43 |             simulatorInfos.push(simulatorInfo);
44 |           }
45 |         }
46 |       }
47 | 
48 |       return ListSimulatorsResult.success(simulatorInfos);
49 |     } catch (error) {
50 |       return ListSimulatorsResult.failed(
51 |         error instanceof Error ? error : new Error(String(error))
52 |       );
53 |     }
54 |   }
55 | 
56 |   private matchesFilter(simulator: SimulatorInfo, request: ListSimulatorsRequest): boolean {
57 |     if (request.platform) {
58 |       const platformString = Platform[request.platform];
59 |       if (simulator.platform !== platformString) {
60 |         return false;
61 |       }
62 |     }
63 | 
64 |     if (request.state && simulator.state !== request.state) {
65 |       return false;
66 |     }
67 | 
68 |     if (request.name) {
69 |       const nameLower = simulator.name.toLowerCase();
70 |       const filterLower = request.name.toLowerCase();
71 |       if (!nameLower.includes(filterLower)) {
72 |         return false;
73 |       }
74 |     }
75 | 
76 |     return true;
77 |   }
78 | 
79 |   private extractPlatformFromRuntime(runtime: string): string {
80 |     if (runtime.includes('iOS')) return 'iOS';
81 |     if (runtime.includes('tvOS')) return 'tvOS';
82 |     if (runtime.includes('watchOS')) return 'watchOS';
83 |     if (runtime.includes('xrOS') || runtime.includes('visionOS')) return 'visionOS';
84 |     if (runtime.includes('macOS')) return 'macOS';
85 |     return 'Unknown';
86 |   }
87 | 
88 |   private extractVersionFromRuntime(runtime: string): string {
89 |     const match = runtime.match(/(\d+[-.]?\d*(?:[-.]?\d+)?)/);
90 |     return match ? match[1].replace(/-/g, '.') : 'Unknown';
91 |   }
92 | 
93 | }
```

--------------------------------------------------------------------------------
/src/logger.ts:
--------------------------------------------------------------------------------

```typescript
  1 | /**
  2 |  * Structured logging configuration for production
  3 |  * Uses Pino for high-performance JSON logging
  4 |  */
  5 | 
  6 | import pino from 'pino';
  7 | 
  8 | // Environment-based configuration
  9 | const isDevelopment = process.env.NODE_ENV !== 'production';
 10 | const isTest = process.env.NODE_ENV === 'test' || process.env.JEST_WORKER_ID;
 11 | const logLevel = process.env.LOG_LEVEL || (isDevelopment ? 'debug' : 'info');
 12 | 
 13 | // Create logger instance
 14 | export const logger = pino({
 15 |   level: logLevel,
 16 |   // Use pretty printing in development, JSON in production
 17 |   // Disable transport in test environment to avoid thread-stream issues
 18 |   transport: (isDevelopment) ? {
 19 |     target: 'pino-pretty',
 20 |     options: {
 21 |       colorize: true,
 22 |       ignore: 'pid,hostname',
 23 |       translateTime: 'SYS:standard',
 24 |       singleLine: false,
 25 |       sync: true  // Make pino-pretty synchronous to avoid race conditions
 26 |     }
 27 |   } : undefined,
 28 |   // Add metadata to all logs
 29 |   base: {
 30 |     service: 'mcp-xcode',
 31 |     version: '2.2.0'
 32 |   },
 33 |   // Redact sensitive information
 34 |   redact: {
 35 |     paths: ['deviceId', 'udid', '*.password', '*.secret', '*.token'],
 36 |     censor: '[REDACTED]'
 37 |   },
 38 |   // Add timestamp
 39 |   timestamp: pino.stdTimeFunctions.isoTime,
 40 |   // Serializers for common objects
 41 |   serializers: {
 42 |     error: pino.stdSerializers.err,
 43 |     request: (req: any) => ({
 44 |       tool: req.tool,
 45 |       platform: req.platform,
 46 |       projectPath: req.projectPath?.replace(/\/Users\/[^/]+/, '/Users/[USER]')
 47 |     })
 48 |   }
 49 | });
 50 | 
 51 | // Create child loggers for different modules
 52 | export const createModuleLogger = (module: string) => {
 53 |   const moduleLogger = logger.child({ module });
 54 |   
 55 |   // In test environment, wrap methods to add test name dynamically
 56 |   if (isTest) {
 57 |     const methods = ['info', 'error', 'warn', 'debug', 'trace', 'fatal'] as const;
 58 |     
 59 |     methods.forEach(method => {
 60 |       const originalMethod = moduleLogger[method].bind(moduleLogger);
 61 |       (moduleLogger as any)[method] = function(obj: any, ...rest: any[]) {
 62 |         try {
 63 |           // @ts-ignore - expect is only available in test environment
 64 |           const testName = global.expect?.getState?.()?.currentTestName;
 65 |           if (testName && obj && typeof obj === 'object') {
 66 |             // Add test name to the context object
 67 |             obj = { ...obj, testName };
 68 |           }
 69 |         } catch {
 70 |           // Ignore if expect is not available
 71 |         }
 72 |         return originalMethod(obj, ...rest);
 73 |       };
 74 |     });
 75 |   }
 76 |   
 77 |   return moduleLogger;
 78 | };
 79 | 
 80 | // Export log levels for use in code
 81 | export const LogLevel = {
 82 |   FATAL: 'fatal',
 83 |   ERROR: 'error',
 84 |   WARN: 'warn',
 85 |   INFO: 'info',
 86 |   DEBUG: 'debug',
 87 |   TRACE: 'trace'
 88 | } as const;
 89 | 
 90 | // Helper for logging tool executions
 91 | export const logToolExecution = (toolName: string, args: any, duration?: number) => {
 92 |   logger.info({
 93 |     event: 'tool_execution',
 94 |     tool: toolName,
 95 |     args: args,
 96 |     duration_ms: duration
 97 |   }, `Executed tool: ${toolName}`);
 98 | };
 99 | 
100 | // Helper for logging errors with context
101 | export const logError = (error: Error, context: Record<string, any>) => {
102 |   logger.error({
103 |     error,
104 |     ...context
105 |   }, error.message);
106 | };
```

--------------------------------------------------------------------------------
/XcodeProjectModifier/Sources/XcodeProjectModifier/main.swift:
--------------------------------------------------------------------------------

```swift
 1 | import Foundation
 2 | import XcodeProj
 3 | import PathKit
 4 | import ArgumentParser
 5 | 
 6 | struct XcodeProjectModifier: ParsableCommand {
 7 |     @Argument(help: "Path to the .xcodeproj file")
 8 |     var projectPath: String
 9 |     
10 |     @Argument(help: "Action to perform: add or remove")
11 |     var action: String
12 |     
13 |     @Argument(help: "Path to the file to add/remove")
14 |     var filePath: String
15 |     
16 |     @Argument(help: "Target name")
17 |     var targetName: String
18 |     
19 |     @Option(name: .long, help: "Group path for the file")
20 |     var groupPath: String = ""
21 |     
22 |     func run() throws {
23 |         let project = try XcodeProj(pathString: projectPath)
24 |         let pbxproj = project.pbxproj
25 |         
26 |         guard let target = pbxproj.nativeTargets.first(where: { $0.name == targetName }) else {
27 |             print("Error: Target '\(targetName)' not found")
28 |             throw ExitCode.failure
29 |         }
30 |         
31 |         let fileName = URL(fileURLWithPath: filePath).lastPathComponent
32 |         
33 |         if action == "remove" {
34 |             // Remove file reference
35 |             if let fileRef = pbxproj.fileReferences.first(where: { $0.path == fileName || $0.path == filePath }) {
36 |                 pbxproj.delete(object: fileRef)
37 |                 print("Removed \(fileName) from project")
38 |             }
39 |         } else if action == "add" {
40 |             // Remove existing reference if it exists
41 |             if let existingRef = pbxproj.fileReferences.first(where: { $0.path == fileName || $0.path == filePath }) {
42 |                 pbxproj.delete(object: existingRef)
43 |             }
44 |             
45 |             // Add new file reference
46 |             let fileRef = PBXFileReference(
47 |                 sourceTree: .group,
48 |                 name: fileName,
49 |                 path: filePath
50 |             )
51 |             pbxproj.add(object: fileRef)
52 |             
53 |             // Add to appropriate build phase based on file type
54 |             let fileExtension = URL(fileURLWithPath: filePath).pathExtension.lowercased()
55 |             
56 |             if ["swift", "m", "mm", "c", "cpp", "cc", "cxx"].contains(fileExtension) {
57 |                 // Add to sources build phase
58 |                 if let sourcesBuildPhase = target.buildPhases.compactMap({ $0 as? PBXSourcesBuildPhase }).first {
59 |                     let buildFile = PBXBuildFile(file: fileRef)
60 |                     pbxproj.add(object: buildFile)
61 |                     sourcesBuildPhase.files?.append(buildFile)
62 |                 }
63 |             } else if ["png", "jpg", "jpeg", "gif", "pdf", "json", "plist", "xib", "storyboard", "xcassets"].contains(fileExtension) {
64 |                 // Add to resources build phase
65 |                 if let resourcesBuildPhase = target.buildPhases.compactMap({ $0 as? PBXResourcesBuildPhase }).first {
66 |                     let buildFile = PBXBuildFile(file: fileRef)
67 |                     pbxproj.add(object: buildFile)
68 |                     resourcesBuildPhase.files?.append(buildFile)
69 |                 }
70 |             }
71 |             
72 |             // Add to group
73 |             if let mainGroup = try? pbxproj.rootProject()?.mainGroup {
74 |                 mainGroup.children.append(fileRef)
75 |             }
76 |             
77 |             print("Added \(fileName) to project")
78 |         }
79 |         
80 |         try project.write(path: Path(projectPath))
81 |     }
82 | }
83 | 
84 | XcodeProjectModifier.main()
```

--------------------------------------------------------------------------------
/src/utils/devices/SimulatorApps.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { execAsync } from '../../utils.js';
  2 | import { createModuleLogger } from '../../logger.js';
  3 | import path from 'path';
  4 | 
  5 | const logger = createModuleLogger('SimulatorApps');
  6 | 
  7 | /**
  8 |  * Handles app management on simulators
  9 |  * Single responsibility: Install, uninstall, and launch apps
 10 |  */
 11 | export class SimulatorApps {
 12 |   /**
 13 |    * Install an app on the simulator
 14 |    */
 15 |   async install(appPath: string, deviceId?: string): Promise<void> {
 16 |     let command = `xcrun simctl install `;
 17 |     if (deviceId) {
 18 |       command += `"${deviceId}" `;
 19 |     } else {
 20 |       command += 'booted ';
 21 |     }
 22 |     command += `"${appPath}"`;
 23 | 
 24 |     try {
 25 |       await execAsync(command);
 26 |       logger.debug({ appPath, deviceId }, 'App installed successfully');
 27 |     } catch (error: any) {
 28 |       logger.error({ error: error.message, appPath, deviceId }, 'Failed to install app');
 29 |       throw new Error(`Failed to install app: ${error.message}`);
 30 |     }
 31 |   }
 32 | 
 33 |   /**
 34 |    * Uninstall an app from the simulator
 35 |    */
 36 |   async uninstall(bundleId: string, deviceId?: string): Promise<void> {
 37 |     // First check if the app exists
 38 |     let listCommand = `xcrun simctl listapps `;
 39 |     if (deviceId) {
 40 |       listCommand += `"${deviceId}"`;
 41 |     } else {
 42 |       listCommand += 'booted';
 43 |     }
 44 |     
 45 |     try {
 46 |       const { stdout: listOutput } = await execAsync(listCommand);
 47 |       
 48 |       // Check if the bundle ID exists in the output
 49 |       if (!listOutput.includes(bundleId)) {
 50 |         throw new Error(`App with bundle ID '${bundleId}' is not installed`);
 51 |       }
 52 |       
 53 |       // Now uninstall the app
 54 |       let command = `xcrun simctl uninstall `;
 55 |       if (deviceId) {
 56 |         command += `"${deviceId}" `;
 57 |       } else {
 58 |         command += 'booted ';
 59 |       }
 60 |       command += `"${bundleId}"`;
 61 |       
 62 |       await execAsync(command);
 63 |       logger.debug({ bundleId, deviceId }, 'App uninstalled successfully');
 64 |     } catch (error: any) {
 65 |       logger.error({ error: error.message, bundleId, deviceId }, 'Failed to uninstall app');
 66 |       throw new Error(`Failed to uninstall app: ${error.message}`);
 67 |     }
 68 |   }
 69 | 
 70 |   /**
 71 |    * Launch an app on the simulator
 72 |    * Returns the process ID of the launched app
 73 |    */
 74 |   async launch(bundleId: string, deviceId?: string): Promise<string> {
 75 |     let command = `xcrun simctl launch --terminate-running-process `;
 76 |     if (deviceId) {
 77 |       command += `"${deviceId}" `;
 78 |     } else {
 79 |       command += 'booted ';
 80 |     }
 81 |     command += `"${bundleId}"`;
 82 | 
 83 |     try {
 84 |       const { stdout } = await execAsync(command);
 85 |       const pid = stdout.trim();
 86 |       logger.debug({ bundleId, deviceId, pid }, 'App launched successfully');
 87 |       return pid;
 88 |     } catch (error: any) {
 89 |       logger.error({ error: error.message, bundleId, deviceId }, 'Failed to launch app');
 90 |       throw new Error(`Failed to launch app: ${error.message}`);
 91 |     }
 92 |   }
 93 | 
 94 |   /**
 95 |    * Get bundle ID from an app bundle
 96 |    */
 97 |   async getBundleId(appPath: string): Promise<string> {
 98 |     try {
 99 |       const plistPath = path.join(appPath, 'Info.plist');
100 |       const { stdout } = await execAsync(
101 |         `/usr/libexec/PlistBuddy -c "Print :CFBundleIdentifier" "${plistPath}"`
102 |       );
103 |       return stdout.trim();
104 |     } catch (error: any) {
105 |       logger.error({ error: error.message, appPath }, 'Failed to get bundle ID');
106 |       throw new Error(`Failed to get bundle ID: ${error.message}`);
107 |     }
108 |   }
109 | }
```

--------------------------------------------------------------------------------
/src/features/simulator/controllers/ShutdownSimulatorController.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { ShutdownSimulatorUseCase } from '../use-cases/ShutdownSimulatorUseCase.js';
  2 | import { DeviceId } from '../../../shared/domain/DeviceId.js';
  3 | import { ShutdownRequest } from '../domain/ShutdownRequest.js';
  4 | import { ShutdownResult, ShutdownOutcome, SimulatorNotFoundError, ShutdownCommandFailedError } from '../domain/ShutdownResult.js';
  5 | import { ErrorFormatter } from '../../../presentation/formatters/ErrorFormatter.js';
  6 | import { MCPController } from '../../../presentation/interfaces/MCPController.js';
  7 | 
  8 | /**
  9 |  * Controller for the shutdown_simulator MCP tool
 10 |  * 
 11 |  * Handles input validation and orchestrates the shutdown simulator use case
 12 |  */
 13 | export class ShutdownSimulatorController implements MCPController {
 14 |   readonly name = 'shutdown_simulator';
 15 |   readonly description = 'Shutdown a simulator';
 16 |   
 17 |   constructor(
 18 |     private useCase: ShutdownSimulatorUseCase
 19 |   ) {}
 20 |   
 21 |   get inputSchema() {
 22 |     return {
 23 |       type: 'object' as const,
 24 |       properties: {
 25 |         deviceId: {
 26 |           type: 'string' as const,
 27 |           description: 'Device UDID or name of the simulator to shutdown'
 28 |         }
 29 |       },
 30 |       required: ['deviceId'] as const
 31 |     };
 32 |   }
 33 |   
 34 |   getToolDefinition() {
 35 |     return {
 36 |       name: this.name,
 37 |       description: this.description,
 38 |       inputSchema: this.inputSchema
 39 |     };
 40 |   }
 41 |   
 42 |   async execute(args: unknown): Promise<{ content: Array<{ type: string; text: string }> }> {
 43 |     try {
 44 |       // Cast to expected shape
 45 |       const input = args as { deviceId: unknown };
 46 | 
 47 |       // Create domain value object - will validate
 48 |       const deviceId = DeviceId.create(input.deviceId);
 49 | 
 50 |       // Create domain request
 51 |       const request = ShutdownRequest.create(deviceId);
 52 | 
 53 |       // Execute use case
 54 |       const result = await this.useCase.execute(request);
 55 |       
 56 |       // Format response
 57 |       return {
 58 |         content: [{
 59 |           type: 'text',
 60 |           text: this.formatResult(result)
 61 |         }]
 62 |       };
 63 |     } catch (error: any) {
 64 |       // Handle validation and other errors consistently
 65 |       const message = ErrorFormatter.format(error);
 66 |       return {
 67 |         content: [{
 68 |           type: 'text',
 69 |           text: `❌ ${message}`
 70 |         }]
 71 |       };
 72 |     }
 73 |   }
 74 |   
 75 |   private formatResult(result: ShutdownResult): string {
 76 |     const { outcome, diagnostics } = result;
 77 |     
 78 |     switch (outcome) {
 79 |       case ShutdownOutcome.Shutdown:
 80 |         return `✅ Successfully shutdown simulator: ${diagnostics.simulatorName} (${diagnostics.simulatorId})`;
 81 |       
 82 |       case ShutdownOutcome.AlreadyShutdown:
 83 |         return `✅ Simulator already shutdown: ${diagnostics.simulatorName} (${diagnostics.simulatorId})`;
 84 |       
 85 |       case ShutdownOutcome.Failed:
 86 |         const { error } = diagnostics;
 87 |         
 88 |         if (error instanceof SimulatorNotFoundError) {
 89 |           // Use consistent error formatting with ❌ emoji
 90 |           return `❌ Simulator not found: ${error.deviceId}`;
 91 |         }
 92 |         
 93 |         if (error instanceof ShutdownCommandFailedError) {
 94 |           const message = ErrorFormatter.format(error);
 95 |           // Include simulator context if available
 96 |           if (diagnostics.simulatorName && diagnostics.simulatorId) {
 97 |             return `❌ ${diagnostics.simulatorName} (${diagnostics.simulatorId}) - ${message}`;
 98 |           }
 99 |           return `❌ ${message}`;
100 |         }
101 |         
102 |         // Shouldn't happen but handle gracefully
103 |         const fallbackMessage = error ? ErrorFormatter.format(error) : 'Shutdown operation failed';
104 |         return `❌ ${fallbackMessage}`;
105 |     }
106 |   }
107 | }
```

--------------------------------------------------------------------------------
/src/features/simulator/tests/unit/SimulatorControlAdapter.unit.test.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { describe, it, expect, jest, beforeEach } from '@jest/globals';
  2 | import { SimulatorControlAdapter } from '../../infrastructure/SimulatorControlAdapter.js';
  3 | import { ICommandExecutor } from '../../../../application/ports/CommandPorts.js';
  4 | 
  5 | describe('SimulatorControlAdapter', () => {
  6 |   beforeEach(() => {
  7 |     jest.clearAllMocks();
  8 |   });
  9 | 
 10 |   function createSUT() {
 11 |     const mockExecute = jest.fn<ICommandExecutor['execute']>();
 12 |     const mockExecutor: ICommandExecutor = {
 13 |       execute: mockExecute
 14 |     };
 15 |     const sut = new SimulatorControlAdapter(mockExecutor);
 16 |     return { sut, mockExecute };
 17 |   }
 18 | 
 19 |   describe('boot', () => {
 20 |     it('should boot simulator successfully', async () => {
 21 |       // Arrange
 22 |       const { sut, mockExecute } = createSUT();
 23 |       mockExecute.mockResolvedValue({
 24 |         stdout: '',
 25 |         stderr: '',
 26 |         exitCode: 0
 27 |       });
 28 | 
 29 |       // Act
 30 |       await sut.boot('ABC-123');
 31 | 
 32 |       // Assert
 33 |       expect(mockExecute).toHaveBeenCalledWith('xcrun simctl boot "ABC-123"');
 34 |     });
 35 | 
 36 |     it('should handle already booted simulator gracefully', async () => {
 37 |       // Arrange
 38 |       const { sut, mockExecute } = createSUT();
 39 |       mockExecute.mockResolvedValue({
 40 |         stdout: '',
 41 |         stderr: 'Unable to boot device in current state: Booted',
 42 |         exitCode: 149
 43 |       });
 44 | 
 45 |       // Act & Assert - should not throw
 46 |       await expect(sut.boot('ABC-123')).resolves.toBeUndefined();
 47 |     });
 48 | 
 49 |     it('should throw error for device not found', async () => {
 50 |       // Arrange
 51 |       const { sut, mockExecute } = createSUT();
 52 |       mockExecute.mockResolvedValue({
 53 |         stdout: '',
 54 |         stderr: 'Invalid device: ABC-123',
 55 |         exitCode: 164
 56 |       });
 57 | 
 58 |       // Act & Assert
 59 |       await expect(sut.boot('ABC-123'))
 60 |         .rejects.toThrow('Invalid device: ABC-123');
 61 |     });
 62 | 
 63 |     it('should throw error when simulator runtime is not installed on system', async () => {
 64 |       // Arrange
 65 |       const { sut, mockExecute } = createSUT();
 66 |       mockExecute.mockResolvedValue({
 67 |         stdout: '',
 68 |         stderr: 'The device runtime is not available',
 69 |         exitCode: 1
 70 |       });
 71 | 
 72 |       // Act & Assert
 73 |       await expect(sut.boot('ABC-123'))
 74 |         .rejects.toThrow('The device runtime is not available');
 75 |     });
 76 |   });
 77 | 
 78 |   describe('shutdown', () => {
 79 |     it('should shutdown simulator successfully', async () => {
 80 |       // Arrange
 81 |       const { sut, mockExecute } = createSUT();
 82 |       mockExecute.mockResolvedValue({
 83 |         stdout: '',
 84 |         stderr: '',
 85 |         exitCode: 0
 86 |       });
 87 | 
 88 |       // Act
 89 |       await sut.shutdown('ABC-123');
 90 | 
 91 |       // Assert
 92 |       expect(mockExecute).toHaveBeenCalledWith('xcrun simctl shutdown "ABC-123"');
 93 |     });
 94 | 
 95 |     it('should handle already shutdown simulator gracefully', async () => {
 96 |       // Arrange
 97 |       const { sut, mockExecute } = createSUT();
 98 |       mockExecute.mockResolvedValue({
 99 |         stdout: '',
100 |         stderr: 'Unable to shutdown device in current state: Shutdown',
101 |         exitCode: 149
102 |       });
103 | 
104 |       // Act & Assert - should not throw
105 |       await expect(sut.shutdown('ABC-123')).resolves.toBeUndefined();
106 |     });
107 | 
108 |     it('should throw error for device not found', async () => {
109 |       // Arrange
110 |       const { sut, mockExecute } = createSUT();
111 |       mockExecute.mockResolvedValue({
112 |         stdout: '',
113 |         stderr: 'Invalid device: ABC-123',
114 |         exitCode: 164
115 |       });
116 | 
117 |       // Act & Assert
118 |       await expect(sut.shutdown('ABC-123'))
119 |         .rejects.toThrow('Invalid device: ABC-123');
120 |     });
121 |   });
122 | });
```

--------------------------------------------------------------------------------
/src/utils/devices/SimulatorInfo.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { execAsync } from '../../utils.js';
  2 | import { createModuleLogger } from '../../logger.js';
  3 | import { SimulatorDevice, Platform } from '../../types.js';
  4 | 
  5 | const logger = createModuleLogger('SimulatorInfo');
  6 | 
  7 | /**
  8 |  * Provides information about simulators
  9 |  * Single responsibility: Query simulator state and logs
 10 |  */
 11 | export class SimulatorInfo {
 12 |   /**
 13 |    * List all available simulators, optionally filtered by platform
 14 |    */
 15 |   async list(platform?: Platform, showAll = false): Promise<SimulatorDevice[]> {
 16 |     try {
 17 |       const { stdout } = await execAsync('xcrun simctl list devices --json');
 18 |       const data = JSON.parse(stdout);
 19 |       
 20 |       const devices: SimulatorDevice[] = [];
 21 |       for (const [runtime, deviceList] of Object.entries(data.devices)) {
 22 |         // Filter by platform if specified
 23 |         if (platform) {
 24 |           const runtimeLower = runtime.toLowerCase();
 25 |           const platformLower = platform.toLowerCase();
 26 |           
 27 |           // Handle visionOS which is internally called xrOS
 28 |           const isVisionOS = platformLower === 'visionos' && runtimeLower.includes('xros');
 29 |           const isOtherPlatform = platformLower !== 'visionos' && runtimeLower.includes(platformLower);
 30 |           
 31 |           if (!isVisionOS && !isOtherPlatform) {
 32 |             continue;
 33 |           }
 34 |         }
 35 | 
 36 |         for (const device of deviceList as any[]) {
 37 |           if (!showAll && !device.isAvailable) {
 38 |             continue;
 39 |           }
 40 |           devices.push({
 41 |             udid: device.udid,
 42 |             name: device.name,
 43 |             state: device.state,
 44 |             deviceTypeIdentifier: device.deviceTypeIdentifier,
 45 |             runtime: runtime.replace('com.apple.CoreSimulator.SimRuntime.', ''),
 46 |             isAvailable: device.isAvailable
 47 |           });
 48 |         }
 49 |       }
 50 | 
 51 |       return devices;
 52 |     } catch (error: any) {
 53 |       logger.error({ error: error.message }, 'Failed to list simulators');
 54 |       throw new Error(`Failed to list simulators: ${error.message}`);
 55 |     }
 56 |   }
 57 | 
 58 |   /**
 59 |    * Get device logs from the simulator
 60 |    */
 61 |   async logs(deviceId?: string, predicate?: string, last: string = '5m'): Promise<string> {
 62 |     let command = `xcrun simctl spawn `;
 63 |     if (deviceId) {
 64 |       command += `"${deviceId}" `;
 65 |     } else {
 66 |       command += 'booted ';
 67 |     }
 68 |     command += `log show --style syslog --last ${last}`;
 69 |     
 70 |     if (predicate) {
 71 |       command += ` --predicate '${predicate}'`;
 72 |     }
 73 | 
 74 |     try {
 75 |       const { stdout } = await execAsync(command, { maxBuffer: 10 * 1024 * 1024 });
 76 |       // Return last 100 lines to keep it manageable
 77 |       const lines = stdout.split('\n').slice(-100);
 78 |       return lines.join('\n');
 79 |     } catch (error: any) {
 80 |       logger.error({ error: error.message, deviceId, predicate }, 'Failed to get device logs');
 81 |       throw new Error(`Failed to get device logs: ${error.message}`);
 82 |     }
 83 |   }
 84 | 
 85 |   /**
 86 |    * Get the state of a specific device
 87 |    */
 88 |   async getDeviceState(deviceId: string): Promise<string> {
 89 |     const devices = await this.list(undefined, true);
 90 |     const device = devices.find(d => d.udid === deviceId || d.name === deviceId);
 91 |     
 92 |     if (!device) {
 93 |       throw new Error(`Device '${deviceId}' not found`);
 94 |     }
 95 |     
 96 |     return device.state;
 97 |   }
 98 | 
 99 |   /**
100 |    * Check if a device is available
101 |    */
102 |   async isAvailable(deviceId: string): Promise<boolean> {
103 |     const devices = await this.list(undefined, true);
104 |     const device = devices.find(d => d.udid === deviceId || d.name === deviceId);
105 |     
106 |     return device?.isAvailable || false;
107 |   }
108 | }
```

--------------------------------------------------------------------------------
/src/features/simulator/controllers/BootSimulatorController.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { BootSimulatorUseCase } from '../use-cases/BootSimulatorUseCase.js';
  2 | import { BootRequest } from '../domain/BootRequest.js';
  3 | import { DeviceId } from '../../../shared/domain/DeviceId.js';
  4 | import { BootResult, BootOutcome, SimulatorNotFoundError, BootCommandFailedError, SimulatorBusyError } from '../domain/BootResult.js';
  5 | import { ErrorFormatter } from '../../../presentation/formatters/ErrorFormatter.js';
  6 | import { MCPController } from '../../../presentation/interfaces/MCPController.js';
  7 | 
  8 | /**
  9 |  * Controller for the boot_simulator MCP tool
 10 |  *
 11 |  * Handles input validation and orchestrates the boot simulator use case
 12 |  */
 13 | export class BootSimulatorController implements MCPController {
 14 |   readonly name = 'boot_simulator';
 15 |   readonly description = 'Boot a simulator';
 16 |   
 17 |   constructor(
 18 |     private useCase: BootSimulatorUseCase
 19 |   ) {}
 20 |   
 21 |   get inputSchema() {
 22 |     return {
 23 |       type: 'object' as const,
 24 |       properties: {
 25 |         deviceId: {
 26 |           type: 'string' as const,
 27 |           description: 'Device UDID or name of the simulator to boot'
 28 |         }
 29 |       },
 30 |       required: ['deviceId'] as const
 31 |     };
 32 |   }
 33 |   
 34 |   getToolDefinition() {
 35 |     return {
 36 |       name: this.name,
 37 |       description: this.description,
 38 |       inputSchema: this.inputSchema
 39 |     };
 40 |   }
 41 |   
 42 |   async execute(args: unknown): Promise<{ content: Array<{ type: string; text: string }> }> {
 43 |     try {
 44 |       // Cast to expected shape
 45 |       const input = args as { deviceId: unknown };
 46 | 
 47 |       // Create domain value object - will validate
 48 |       const deviceId = DeviceId.create(input.deviceId);
 49 | 
 50 |       // Create domain request
 51 |       const request = BootRequest.create(deviceId);
 52 | 
 53 |       // Execute use case
 54 |       const result = await this.useCase.execute(request);
 55 |       
 56 |       // Format response
 57 |       return {
 58 |         content: [{
 59 |           type: 'text',
 60 |           text: this.formatResult(result)
 61 |         }]
 62 |       };
 63 |     } catch (error: any) {
 64 |       // Handle validation and other errors consistently
 65 |       const message = ErrorFormatter.format(error);
 66 |       return {
 67 |         content: [{
 68 |           type: 'text',
 69 |           text: `❌ ${message}`
 70 |         }]
 71 |       };
 72 |     }
 73 |   }
 74 |   
 75 |   private formatResult(result: BootResult): string {
 76 |     const { outcome, diagnostics } = result;
 77 |     
 78 |     switch (outcome) {
 79 |       case BootOutcome.Booted:
 80 |         return `✅ Successfully booted simulator: ${diagnostics.simulatorName} (${diagnostics.simulatorId})`;
 81 |       
 82 |       case BootOutcome.AlreadyBooted:
 83 |         return `✅ Simulator already booted: ${diagnostics.simulatorName} (${diagnostics.simulatorId})`;
 84 |       
 85 |       case BootOutcome.Failed:
 86 |         const { error } = diagnostics;
 87 |         
 88 |         if (error instanceof SimulatorNotFoundError) {
 89 |           // Use consistent error formatting with ❌ emoji
 90 |           return `❌ Simulator not found: ${error.deviceId}`;
 91 |         }
 92 |         
 93 |         if (error instanceof SimulatorBusyError) {
 94 |           // Handle simulator busy scenarios
 95 |           return `❌ Cannot boot simulator: currently ${error.currentState.toLowerCase()}`;
 96 |         }
 97 |         
 98 |         if (error instanceof BootCommandFailedError) {
 99 |           const message = ErrorFormatter.format(error);
100 |           // Include simulator context if available
101 |           if (diagnostics.simulatorName && diagnostics.simulatorId) {
102 |             return `❌ ${diagnostics.simulatorName} (${diagnostics.simulatorId}) - ${message}`;
103 |           }
104 |           return `❌ ${message}`;
105 |         }
106 |         
107 |         // Shouldn't happen but handle gracefully
108 |         const fallbackMessage = error ? ErrorFormatter.format(error) : 'Boot operation failed';
109 |         return `❌ ${fallbackMessage}`;
110 |     }
111 |   }
112 | }
```

--------------------------------------------------------------------------------
/src/features/simulator/infrastructure/SimulatorLocatorAdapter.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { ISimulatorLocator, SimulatorInfo } from '../../../application/ports/SimulatorPorts.js';
  2 | import { ICommandExecutor } from '../../../application/ports/CommandPorts.js';
  3 | import { SimulatorState } from '../domain/SimulatorState.js';
  4 | 
  5 | /**
  6 |  * Locates simulators using xcrun simctl
  7 |  */
  8 | export class SimulatorLocatorAdapter implements ISimulatorLocator {
  9 |   constructor(private executor: ICommandExecutor) {}
 10 | 
 11 |   async findSimulator(idOrName: string): Promise<SimulatorInfo | null> {
 12 |     const result = await this.executor.execute('xcrun simctl list devices --json');
 13 |     const data = JSON.parse(result.stdout);
 14 |     
 15 |     const allMatches: Array<{
 16 |       device: any;
 17 |       runtime: string;
 18 |     }> = [];
 19 |     
 20 |     for (const [runtime, deviceList] of Object.entries(data.devices)) {
 21 |       for (const device of deviceList as any[]) {
 22 |         if ((device.udid === idOrName || device.name === idOrName) && device.isAvailable) {
 23 |           allMatches.push({ device, runtime });
 24 |         }
 25 |       }
 26 |     }
 27 |     
 28 |     if (allMatches.length === 0) {
 29 |       return null;
 30 |     }
 31 |     
 32 |     // Sort by: booted first, then newer runtime
 33 |     allMatches.sort((a, b) => {
 34 |       // Booted devices first
 35 |       if (a.device.state === SimulatorState.Booted && b.device.state !== SimulatorState.Booted) return -1;
 36 |       if (b.device.state === SimulatorState.Booted && a.device.state !== SimulatorState.Booted) return 1;
 37 |       
 38 |       // Then by runtime version (newer first)
 39 |       return this.compareRuntimeVersions(b.runtime, a.runtime);
 40 |     });
 41 |     
 42 |     const selected = allMatches[0];
 43 |     return {
 44 |       id: selected.device.udid,
 45 |       name: selected.device.name,
 46 |       state: SimulatorState.parse(selected.device.state),
 47 |       platform: this.extractPlatform(selected.runtime),
 48 |       runtime: selected.runtime
 49 |     };
 50 |   }
 51 | 
 52 |   async findBootedSimulator(): Promise<SimulatorInfo | null> {
 53 |     const result = await this.executor.execute('xcrun simctl list devices --json');
 54 |     const data = JSON.parse(result.stdout);
 55 |     
 56 |     const bootedDevices: SimulatorInfo[] = [];
 57 |     
 58 |     for (const [runtime, deviceList] of Object.entries(data.devices)) {
 59 |       for (const device of deviceList as any[]) {
 60 |         if (device.state === SimulatorState.Booted && device.isAvailable) {
 61 |           bootedDevices.push({
 62 |             id: device.udid,
 63 |             name: device.name,
 64 |             state: SimulatorState.Booted,
 65 |             platform: this.extractPlatform(runtime),
 66 |             runtime: runtime
 67 |           });
 68 |         }
 69 |       }
 70 |     }
 71 |     
 72 |     if (bootedDevices.length === 0) {
 73 |       return null;
 74 |     }
 75 |     
 76 |     if (bootedDevices.length > 1) {
 77 |       throw new Error(`Multiple booted simulators found (${bootedDevices.length}). Please specify a simulator ID.`);
 78 |     }
 79 |     
 80 |     return bootedDevices[0];
 81 |   }
 82 | 
 83 |   private extractPlatform(runtime: string): string {
 84 |     const runtimeLower = runtime.toLowerCase();
 85 |     
 86 |     if (runtimeLower.includes('ios')) return 'iOS';
 87 |     if (runtimeLower.includes('tvos')) return 'tvOS';
 88 |     if (runtimeLower.includes('watchos')) return 'watchOS';
 89 |     if (runtimeLower.includes('xros') || runtimeLower.includes('visionos')) return 'visionOS';
 90 |     
 91 |     return 'iOS';
 92 |   }
 93 | 
 94 |   private compareRuntimeVersions(runtimeA: string, runtimeB: string): number {
 95 |     const extractVersion = (runtime: string): number[] => {
 96 |       const match = runtime.match(/(\d+)-(\d+)/);
 97 |       if (!match) return [0, 0];
 98 |       return [parseInt(match[1]), parseInt(match[2])];
 99 |     };
100 |     
101 |     const [majorA, minorA] = extractVersion(runtimeA);
102 |     const [majorB, minorB] = extractVersion(runtimeB);
103 |     
104 |     if (majorA !== majorB) return majorA - majorB;
105 |     return minorA - minorB;
106 |   }
107 | }
```

--------------------------------------------------------------------------------
/src/features/app-management/controllers/InstallAppController.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { InstallAppUseCase } from '../use-cases/InstallAppUseCase.js';
  2 | import { InstallRequest } from '../domain/InstallRequest.js';
  3 | import {
  4 |   InstallResult,
  5 |   InstallOutcome,
  6 |   InstallCommandFailedError,
  7 |   SimulatorNotFoundError,
  8 |   NoBootedSimulatorError
  9 | } from '../domain/InstallResult.js';
 10 | import { ErrorFormatter } from '../../../presentation/formatters/ErrorFormatter.js';
 11 | import { MCPController } from '../../../presentation/interfaces/MCPController.js';
 12 | 
 13 | /**
 14 |  * MCP Controller for installing apps on simulators
 15 |  *
 16 |  * Handles input validation and orchestrates the install app use case
 17 |  */
 18 | 
 19 | interface InstallAppArgs {
 20 |   appPath: unknown;
 21 |   simulatorId?: unknown;
 22 | }
 23 | 
 24 | export class InstallAppController implements MCPController {
 25 |   // MCP Tool metadata
 26 |   readonly name = 'install_app';
 27 |   readonly description = 'Install an app on the simulator';
 28 |   
 29 |   constructor(
 30 |     private useCase: InstallAppUseCase
 31 |   ) {}
 32 |   
 33 |   get inputSchema() {
 34 |     return {
 35 |       type: 'object' as const,
 36 |       properties: {
 37 |         appPath: {
 38 |           type: 'string',
 39 |           description: 'Path to the .app bundle'
 40 |         },
 41 |         simulatorId: {
 42 |           type: 'string',
 43 |           description: 'Device UDID or name of the simulator (optional, uses booted device if not specified)'
 44 |         }
 45 |       },
 46 |       required: ['appPath']
 47 |     };
 48 |   }
 49 |   
 50 |   getToolDefinition() {
 51 |     return {
 52 |       name: this.name,
 53 |       description: this.description,
 54 |       inputSchema: this.inputSchema
 55 |     };
 56 |   }
 57 |   
 58 |   async execute(args: unknown): Promise<{ content: Array<{ type: string; text: string }> }> {
 59 |     try {
 60 |       // Type guard for input
 61 |       if (!args || typeof args !== 'object') {
 62 |         throw new Error('Invalid input: expected an object');
 63 |       }
 64 | 
 65 |       const input = args as InstallAppArgs;
 66 | 
 67 |       // Create domain request (validation happens here)
 68 |       const request = InstallRequest.create(input.appPath, input.simulatorId);
 69 | 
 70 |       // Execute use case
 71 |       const result = await this.useCase.execute(request);
 72 | 
 73 |       // Format response
 74 |       return {
 75 |         content: [{
 76 |           type: 'text',
 77 |           text: this.formatResult(result)
 78 |         }]
 79 |       };
 80 |     } catch (error: any) {
 81 |       // Handle validation and use case errors consistently
 82 |       const message = ErrorFormatter.format(error);
 83 |       return {
 84 |         content: [{
 85 |           type: 'text',
 86 |           text: `❌ ${message}`
 87 |         }]
 88 |       };
 89 |     }
 90 |   }
 91 |   
 92 |   private formatResult(result: InstallResult): string {
 93 |     const { outcome, diagnostics } = result;
 94 |     
 95 |     switch (outcome) {
 96 |       case InstallOutcome.Succeeded:
 97 |         return `✅ Successfully installed ${diagnostics.bundleId} on ${diagnostics.simulatorName} (${diagnostics.simulatorId?.toString()})`;
 98 |       
 99 |       case InstallOutcome.Failed:
100 |         const { error } = diagnostics;
101 |         
102 |         if (error instanceof NoBootedSimulatorError) {
103 |           return `❌ No booted simulator found. Please boot a simulator first or specify a simulator ID.`;
104 |         }
105 | 
106 |         if (error instanceof SimulatorNotFoundError) {
107 |           return `❌ Simulator not found: ${error.simulatorId}`;
108 |         }
109 |         
110 |         if (error instanceof InstallCommandFailedError) {
111 |           const message = ErrorFormatter.format(error);
112 |           // Include simulator context if available
113 |           if (diagnostics.simulatorName && diagnostics.simulatorId) {
114 |             return `❌ ${diagnostics.simulatorName} (${diagnostics.simulatorId.toString()}) - ${message}`;
115 |           }
116 |           return `❌ ${message}`;
117 |         }
118 |         
119 |         // Shouldn't happen but handle gracefully
120 |         const fallbackMessage = error ? ErrorFormatter.format(error) : 'Install operation failed';
121 |         return `❌ ${fallbackMessage}`;
122 |     }
123 |   }
124 | }
```

--------------------------------------------------------------------------------
/src/features/app-management/use-cases/InstallAppUseCase.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { SimulatorState } from '../../simulator/domain/SimulatorState.js';
  2 | import { InstallRequest } from '../domain/InstallRequest.js';
  3 | import { DeviceId } from '../../../shared/domain/DeviceId.js';
  4 | import {
  5 |   InstallResult,
  6 |   InstallCommandFailedError,
  7 |   SimulatorNotFoundError,
  8 |   NoBootedSimulatorError
  9 | } from '../domain/InstallResult.js';
 10 | import { 
 11 |   ISimulatorLocator,
 12 |   ISimulatorControl,
 13 |   IAppInstaller 
 14 | } from '../../../application/ports/SimulatorPorts.js';
 15 | import { ILogManager } from '../../../application/ports/LoggingPorts.js';
 16 | 
 17 | /**
 18 |  * Use Case: Install an app on a simulator
 19 |  * Orchestrates finding the target simulator, booting if needed, and installing the app
 20 |  */
 21 | export class InstallAppUseCase {
 22 |   constructor(
 23 |     private simulatorLocator: ISimulatorLocator,
 24 |     private simulatorControl: ISimulatorControl,
 25 |     private appInstaller: IAppInstaller,
 26 |     private logManager: ILogManager
 27 |   ) {}
 28 | 
 29 |   async execute(request: InstallRequest): Promise<InstallResult> {
 30 |     // Get app name from the AppPath value object
 31 |     const appName = request.appPath.name;
 32 | 
 33 |     // Find target simulator
 34 |     const simulator = request.simulatorId
 35 |       ? await this.simulatorLocator.findSimulator(request.simulatorId.toString())
 36 |       : await this.simulatorLocator.findBootedSimulator();
 37 |     
 38 |     if (!simulator) {
 39 |       this.logManager.saveDebugData('install-app-failed', {
 40 |         reason: 'simulator_not_found',
 41 |         requestedId: request.simulatorId?.toString()
 42 |       }, appName);
 43 | 
 44 |       const error = request.simulatorId
 45 |         ? new SimulatorNotFoundError(request.simulatorId)
 46 |         : new NoBootedSimulatorError();
 47 |       return InstallResult.failed(error, request.appPath, request.simulatorId);
 48 |     }
 49 |     
 50 |     // Boot simulator if needed (only when specific ID provided)
 51 |     if (request.simulatorId) {
 52 |       if (simulator.state === SimulatorState.Shutdown) {
 53 |         try {
 54 |           await this.simulatorControl.boot(simulator.id);
 55 |           this.logManager.saveDebugData('simulator-auto-booted', {
 56 |             simulatorId: simulator.id,
 57 |             simulatorName: simulator.name
 58 |           }, appName);
 59 |         } catch (error: any) {
 60 |           this.logManager.saveDebugData('simulator-boot-failed', {
 61 |             simulatorId: simulator.id,
 62 |             error: error.message
 63 |           }, appName);
 64 |           const installError = new InstallCommandFailedError(error.message || error.toString());
 65 |           return InstallResult.failed(
 66 |             installError,
 67 |             request.appPath,
 68 |             DeviceId.create(simulator.id),
 69 |             simulator.name
 70 |           );
 71 |         }
 72 |       }
 73 |     }
 74 |     
 75 |     // Install the app
 76 |     try {
 77 |       await this.appInstaller.installApp(
 78 |         request.appPath.toString(),
 79 |         simulator.id
 80 |       );
 81 |       
 82 |       this.logManager.saveDebugData('install-app-success', {
 83 |         simulator: simulator.name,
 84 |         simulatorId: simulator.id,
 85 |         app: appName
 86 |       }, appName);
 87 |       
 88 |       // Try to get bundle ID from app (could be enhanced later)
 89 |       const bundleId = appName; // For now, use app name as bundle ID
 90 |       
 91 |       return InstallResult.succeeded(
 92 |         bundleId,
 93 |         DeviceId.create(simulator.id),
 94 |         simulator.name,
 95 |         request.appPath
 96 |       );
 97 |     } catch (error: any) {
 98 |       this.logManager.saveDebugData('install-app-error', {
 99 |         simulator: simulator.name,
100 |         simulatorId: simulator.id,
101 |         app: appName,
102 |         error: error.message
103 |       }, appName);
104 |       
105 |       const installError = new InstallCommandFailedError(error.message || error.toString());
106 |       return InstallResult.failed(
107 |         installError,
108 |         request.appPath,
109 |         DeviceId.create(simulator.id),
110 |         simulator.name
111 |       );
112 |     }
113 |   }
114 | }
```

--------------------------------------------------------------------------------
/src/features/simulator/tests/unit/BootResult.unit.test.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { describe, it, expect } from '@jest/globals';
  2 | import { 
  3 |   BootResult, 
  4 |   BootOutcome, 
  5 |   SimulatorNotFoundError, 
  6 |   BootCommandFailedError 
  7 | } from '../../domain/BootResult.js';
  8 | 
  9 | describe('BootResult', () => {
 10 |   describe('booted', () => {
 11 |     it('should create a result for newly booted simulator', () => {
 12 |       // Arrange
 13 |       const simulatorId = 'ABC123';
 14 |       const simulatorName = 'iPhone 15';
 15 |       
 16 |       // Act
 17 |       const result = BootResult.booted(simulatorId, simulatorName);
 18 |       
 19 |       // Assert - Test behavior: result indicates success
 20 |       expect(result.outcome).toBe(BootOutcome.Booted);
 21 |       expect(result.diagnostics.simulatorId).toBe(simulatorId);
 22 |       expect(result.diagnostics.simulatorName).toBe(simulatorName);
 23 |     });
 24 | 
 25 |     it('should include optional diagnostics', () => {
 26 |       // Arrange
 27 |       const diagnostics = { platform: 'iOS', runtime: 'iOS-17.0' };
 28 |       
 29 |       // Act
 30 |       const result = BootResult.booted('ABC123', 'iPhone 15', diagnostics);
 31 |       
 32 |       // Assert - Test behavior: diagnostics are preserved
 33 |       expect(result.diagnostics.platform).toBe('iOS');
 34 |       expect(result.diagnostics.runtime).toBe('iOS-17.0');
 35 |     });
 36 |   });
 37 | 
 38 |   describe('alreadyBooted', () => {
 39 |     it('should create a result for already running simulator', () => {
 40 |       // Arrange & Act
 41 |       const result = BootResult.alreadyBooted('ABC123', 'iPhone 15');
 42 |       
 43 |       // Assert - Test behavior: result indicates already running
 44 |       expect(result.outcome).toBe(BootOutcome.AlreadyBooted);
 45 |       expect(result.diagnostics.simulatorId).toBe('ABC123');
 46 |       expect(result.diagnostics.simulatorName).toBe('iPhone 15');
 47 |     });
 48 |   });
 49 | 
 50 |   describe('failed', () => {
 51 |     it('should create a failure result with error', () => {
 52 |       // Arrange
 53 |       const error = new BootCommandFailedError('Device is locked');
 54 |       
 55 |       // Act
 56 |       const result = BootResult.failed('ABC123', 'iPhone 15', error);
 57 |       
 58 |       // Assert - Test behavior: result indicates failure with error
 59 |       expect(result.outcome).toBe(BootOutcome.Failed);
 60 |       expect(result.diagnostics.simulatorId).toBe('ABC123');
 61 |       expect(result.diagnostics.simulatorName).toBe('iPhone 15');
 62 |       expect(result.diagnostics.error).toBe(error);
 63 |     });
 64 | 
 65 |     it('should handle simulator not found error', () => {
 66 |       // Arrange
 67 |       const error = new SimulatorNotFoundError('iPhone-16');
 68 |       
 69 |       // Act
 70 |       const result = BootResult.failed('iPhone-16', '', error);
 71 |       
 72 |       // Assert - Test behavior: error type is preserved
 73 |       expect(result.outcome).toBe(BootOutcome.Failed);
 74 |       expect(result.diagnostics.error).toBeInstanceOf(SimulatorNotFoundError);
 75 |       expect((result.diagnostics.error as SimulatorNotFoundError).deviceId).toBe('iPhone-16');
 76 |     });
 77 | 
 78 |     it('should include optional diagnostics on failure', () => {
 79 |       // Arrange
 80 |       const error = new BootCommandFailedError('Boot failed');
 81 |       const diagnostics = { runtime: 'iOS-17.0' };
 82 |       
 83 |       // Act
 84 |       const result = BootResult.failed('ABC123', 'iPhone 15', error, diagnostics);
 85 |       
 86 |       // Assert - Test behavior: diagnostics preserved even on failure
 87 |       expect(result.diagnostics.runtime).toBe('iOS-17.0');
 88 |       expect(result.diagnostics.error).toBe(error);
 89 |     });
 90 |   });
 91 | 
 92 |   describe('immutability', () => {
 93 |     it('should create immutable results', () => {
 94 |       // Arrange
 95 |       const result = BootResult.booted('ABC123', 'iPhone 15');
 96 |       
 97 |       // Act & Assert - Test behavior: results cannot be modified
 98 |       expect(() => {
 99 |         (result as any).outcome = BootOutcome.Failed;
100 |       }).toThrow();
101 |       
102 |       expect(() => {
103 |         (result.diagnostics as any).simulatorId = 'XYZ789';
104 |       }).toThrow();
105 |     });
106 |   });
107 | });
```

--------------------------------------------------------------------------------
/src/infrastructure/tests/unit/DeviceRepository.unit.test.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { describe, it, expect, jest } from '@jest/globals';
  2 | import { DeviceRepository, DeviceList } from '../../repositories/DeviceRepository.js';
  3 | import { ICommandExecutor } from '../../../application/ports/CommandPorts.js';
  4 | 
  5 | describe('DeviceRepository', () => {
  6 |   function createSUT() {
  7 |     const mockExecute = jest.fn<ICommandExecutor['execute']>();
  8 |     const mockExecutor: ICommandExecutor = { execute: mockExecute };
  9 |     const sut = new DeviceRepository(mockExecutor);
 10 |     return { sut, mockExecute };
 11 |   }
 12 | 
 13 |   describe('getAllDevices', () => {
 14 |     it('should return parsed device list from xcrun simctl', async () => {
 15 |       // Arrange
 16 |       const { sut, mockExecute } = createSUT();
 17 | 
 18 |       const mockDevices: DeviceList = {
 19 |         'com.apple.CoreSimulator.SimRuntime.iOS-17-0': [
 20 |           {
 21 |             udid: 'test-uuid-1',
 22 |             name: 'iPhone 15',
 23 |             state: 'Booted',
 24 |             isAvailable: true,
 25 |             deviceTypeIdentifier: 'com.apple.iPhone15'
 26 |           }
 27 |         ],
 28 |         'com.apple.CoreSimulator.SimRuntime.iOS-16-4': [
 29 |           {
 30 |             udid: 'test-uuid-2',
 31 |             name: 'iPhone 14',
 32 |             state: 'Shutdown',
 33 |             isAvailable: true
 34 |           }
 35 |         ]
 36 |       };
 37 | 
 38 |       mockExecute.mockResolvedValue({
 39 |         stdout: JSON.stringify({ devices: mockDevices }),
 40 |         stderr: '',
 41 |         exitCode: 0
 42 |       });
 43 | 
 44 |       // Act
 45 |       const result = await sut.getAllDevices();
 46 | 
 47 |       // Assert
 48 |       expect(mockExecute).toHaveBeenCalledWith('xcrun simctl list devices --json');
 49 |       expect(result).toEqual(mockDevices);
 50 |     });
 51 | 
 52 |     it('should handle empty device list', async () => {
 53 |       // Arrange
 54 |       const { sut, mockExecute } = createSUT();
 55 |       const emptyDevices: DeviceList = {};
 56 | 
 57 |       mockExecute.mockResolvedValue({
 58 |         stdout: JSON.stringify({ devices: emptyDevices }),
 59 |         stderr: '',
 60 |         exitCode: 0
 61 |       });
 62 | 
 63 |       // Act
 64 |       const result = await sut.getAllDevices();
 65 | 
 66 |       // Assert
 67 |       expect(result).toEqual(emptyDevices);
 68 |     });
 69 | 
 70 |     it('should propagate executor errors', async () => {
 71 |       // Arrange
 72 |       const { sut, mockExecute } = createSUT();
 73 |       const error = new Error('Command failed');
 74 |       mockExecute.mockRejectedValue(error);
 75 | 
 76 |       // Act & Assert
 77 |       await expect(sut.getAllDevices()).rejects.toThrow('Command failed');
 78 |     });
 79 | 
 80 |     it('should throw on invalid JSON response', async () => {
 81 |       // Arrange
 82 |       const { sut, mockExecute } = createSUT();
 83 |       mockExecute.mockResolvedValue({
 84 |         stdout: 'not valid json',
 85 |         stderr: '',
 86 |         exitCode: 0
 87 |       });
 88 | 
 89 |       // Act & Assert
 90 |       await expect(sut.getAllDevices()).rejects.toThrow();
 91 |     });
 92 | 
 93 |     it('should handle devices with all optional fields', async () => {
 94 |       // Arrange
 95 |       const { sut, mockExecute } = createSUT();
 96 | 
 97 |       const deviceWithAllFields: DeviceList = {
 98 |         'com.apple.CoreSimulator.SimRuntime.iOS-17-0': [
 99 |           {
100 |             udid: 'full-uuid',
101 |             name: 'iPhone 15 Pro',
102 |             state: 'Booted',
103 |             isAvailable: true,
104 |             deviceTypeIdentifier: 'com.apple.iPhone15Pro',
105 |             dataPath: '/path/to/data',
106 |             dataPathSize: 1024000,
107 |             logPath: '/path/to/logs'
108 |           }
109 |         ]
110 |       };
111 | 
112 |       mockExecute.mockResolvedValue({
113 |         stdout: JSON.stringify({ devices: deviceWithAllFields }),
114 |         stderr: '',
115 |         exitCode: 0
116 |       });
117 | 
118 |       // Act
119 |       const result = await sut.getAllDevices();
120 | 
121 |       // Assert
122 |       expect(result).toEqual(deviceWithAllFields);
123 |       const device = result['com.apple.CoreSimulator.SimRuntime.iOS-17-0'][0];
124 |       expect(device.dataPath).toBe('/path/to/data');
125 |       expect(device.dataPathSize).toBe(1024000);
126 |       expect(device.logPath).toBe('/path/to/logs');
127 |     });
128 |   });
129 | });
```

--------------------------------------------------------------------------------
/src/infrastructure/tests/unit/DependencyChecker.unit.test.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { describe, it, expect, jest } from '@jest/globals';
  2 | import { DependencyChecker } from '../../services/DependencyChecker.js';
  3 | import { ICommandExecutor } from '../../../application/ports/CommandPorts.js';
  4 | 
  5 | describe('DependencyChecker', () => {
  6 |   function createSUT() {
  7 |     const mockExecute = jest.fn<ICommandExecutor['execute']>();
  8 |     const mockExecutor: ICommandExecutor = { execute: mockExecute };
  9 |     const sut = new DependencyChecker(mockExecutor);
 10 |     return { sut, mockExecute };
 11 |   }
 12 | 
 13 |   describe('check', () => {
 14 |     it('should return empty array when all dependencies are installed', async () => {
 15 |       // Arrange
 16 |       const { sut, mockExecute } = createSUT();
 17 | 
 18 |       // All which commands succeed
 19 |       mockExecute.mockResolvedValue({
 20 |         stdout: '/usr/bin/xcodebuild',
 21 |         stderr: '',
 22 |         exitCode: 0
 23 |       });
 24 | 
 25 |       // Act
 26 |       const result = await sut.check(['xcodebuild', 'xcbeautify']);
 27 | 
 28 |       // Assert - behavior: no missing dependencies
 29 |       expect(result).toEqual([]);
 30 |     });
 31 | 
 32 |     it('should return missing dependencies with install commands', async () => {
 33 |       // Arrange
 34 |       const { sut, mockExecute } = createSUT();
 35 | 
 36 |       // xcbeautify not found
 37 |       mockExecute.mockResolvedValue({
 38 |         stdout: '',
 39 |         stderr: 'xcbeautify not found',
 40 |         exitCode: 1
 41 |       });
 42 | 
 43 |       // Act
 44 |       const result = await sut.check(['xcbeautify']);
 45 | 
 46 |       // Assert - behavior: returns missing dependency with install command
 47 |       expect(result).toEqual([
 48 |         {
 49 |           name: 'xcbeautify',
 50 |           installCommand: 'brew install xcbeautify'
 51 |         }
 52 |       ]);
 53 |     });
 54 | 
 55 |     it('should handle mix of installed and missing dependencies', async () => {
 56 |       // Arrange
 57 |       const { sut, mockExecute } = createSUT();
 58 | 
 59 |       // xcodebuild found, xcbeautify not found
 60 |       mockExecute
 61 |         .mockResolvedValueOnce({
 62 |           stdout: '/usr/bin/xcodebuild',
 63 |           stderr: '',
 64 |           exitCode: 0
 65 |         })
 66 |         .mockResolvedValueOnce({
 67 |           stdout: '',
 68 |           stderr: 'xcbeautify not found',
 69 |           exitCode: 1
 70 |         });
 71 | 
 72 |       // Act
 73 |       const result = await sut.check(['xcodebuild', 'xcbeautify']);
 74 | 
 75 |       // Assert - behavior: only missing dependencies returned
 76 |       expect(result).toEqual([
 77 |         {
 78 |           name: 'xcbeautify',
 79 |           installCommand: 'brew install xcbeautify'
 80 |         }
 81 |       ]);
 82 |     });
 83 | 
 84 |     it('should handle unknown dependencies', async () => {
 85 |       // Arrange
 86 |       const { sut, mockExecute } = createSUT();
 87 | 
 88 |       // Unknown tool not found
 89 |       mockExecute.mockResolvedValue({
 90 |         stdout: '',
 91 |         stderr: 'unknowntool not found',
 92 |         exitCode: 1
 93 |       });
 94 | 
 95 |       // Act
 96 |       const result = await sut.check(['unknowntool']);
 97 | 
 98 |       // Assert - behavior: returns missing dependency without install command
 99 |       expect(result).toEqual([
100 |         {
101 |           name: 'unknowntool'
102 |         }
103 |       ]);
104 |     });
105 | 
106 |     it('should provide appropriate install commands for known tools', async () => {
107 |       // Arrange
108 |       const { sut, mockExecute } = createSUT();
109 | 
110 |       // All tools missing
111 |       mockExecute.mockResolvedValue({
112 |         stdout: '',
113 |         stderr: 'not found',
114 |         exitCode: 1
115 |       });
116 | 
117 |       // Act
118 |       const result = await sut.check(['xcodebuild', 'xcrun', 'xcbeautify']);
119 | 
120 |       // Assert - behavior: each tool has appropriate install command
121 |       expect(result).toContainEqual({
122 |         name: 'xcodebuild',
123 |         installCommand: 'Install Xcode from the App Store'
124 |       });
125 |       expect(result).toContainEqual({
126 |         name: 'xcrun',
127 |         installCommand: 'Install Xcode Command Line Tools: xcode-select --install'
128 |       });
129 |       expect(result).toContainEqual({
130 |         name: 'xcbeautify',
131 |         installCommand: 'brew install xcbeautify'
132 |       });
133 |     });
134 |   });
135 | });
```

--------------------------------------------------------------------------------
/src/features/app-management/tests/e2e/InstallAppMCP.e2e.test.ts:
--------------------------------------------------------------------------------

```typescript
  1 | /**
  2 |  * E2E Test for Install App through MCP Protocol
  3 |  * 
  4 |  * Tests critical user journey: Installing an app on simulator through MCP
  5 |  * Following testing philosophy: E2E tests for critical paths only (10%)
  6 |  * 
  7 |  * Focus: MCP protocol interaction, not app installation logic
  8 |  * The controller tests already verify installation works with real simulators
  9 |  * This test verifies the MCP transport/serialization/protocol works
 10 |  * 
 11 |  * NO MOCKS - Uses real MCP server, real simulators, real apps
 12 |  */
 13 | 
 14 | import { describe, it, expect, beforeAll, afterAll, beforeEach, afterEach } from '@jest/globals';
 15 | import { Client } from '@modelcontextprotocol/sdk/client/index.js';
 16 | import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
 17 | import { CallToolResultSchema } from '@modelcontextprotocol/sdk/types.js';
 18 | import { createAndConnectClient, cleanupClientAndTransport, bootAndWaitForSimulator } from '../../../../shared/tests/utils/testHelpers.js';
 19 | import { TestProjectManager } from '../../../../shared/tests/utils/TestProjectManager.js';
 20 | import { exec } from 'child_process';
 21 | import { promisify } from 'util';
 22 | import * as fs from 'fs';
 23 | 
 24 | const execAsync = promisify(exec);
 25 | 
 26 | describe('Install App MCP E2E', () => {
 27 |   let client: Client;
 28 |   let transport: StdioClientTransport;
 29 |   let testManager: TestProjectManager;
 30 |   let testDeviceId: string;
 31 |   let testAppPath: string;
 32 |   
 33 |   beforeAll(async () => {
 34 |     // Prepare test projects
 35 |     testManager = new TestProjectManager();
 36 |     await testManager.setup();
 37 |     
 38 |     // Build the server
 39 |     const { execSync } = await import('child_process');
 40 |     execSync('npm run build', { stdio: 'inherit' });
 41 | 
 42 |     // Build the test app using TestProjectManager
 43 |     testAppPath = await testManager.buildApp('xcodeProject');
 44 |     
 45 |     // Get the latest iOS runtime
 46 |     const runtimesResult = await execAsync('xcrun simctl list runtimes --json');
 47 |     const runtimes = JSON.parse(runtimesResult.stdout);
 48 |     const iosRuntime = runtimes.runtimes.find((r: { platform: string }) => r.platform === 'iOS');
 49 |     
 50 |     if (!iosRuntime) {
 51 |       throw new Error('No iOS runtime found. Please install an iOS simulator runtime.');
 52 |     }
 53 |     
 54 |     // Create and boot a test simulator
 55 |     const createResult = await execAsync(
 56 |       `xcrun simctl create "TestSimulator-InstallAppMCP" "iPhone 15" "${iosRuntime.identifier}"`
 57 |     );
 58 |     testDeviceId = createResult.stdout.trim();
 59 |     
 60 |     // Boot the simulator and wait for it to be ready
 61 |     await bootAndWaitForSimulator(testDeviceId, 30);
 62 |   });
 63 |   
 64 |   afterAll(async () => {
 65 |     // Clean up simulator
 66 |     if (testDeviceId) {
 67 |       try {
 68 |         await execAsync(`xcrun simctl shutdown "${testDeviceId}"`);
 69 |         await execAsync(`xcrun simctl delete "${testDeviceId}"`);
 70 |       } catch (error) {
 71 |         // Ignore cleanup errors
 72 |       }
 73 |     }
 74 |     
 75 |     // Clean up test project
 76 |     await testManager.cleanup();
 77 |   });
 78 |   
 79 |   beforeEach(async () => {
 80 |     ({ client, transport } = await createAndConnectClient());
 81 |   });
 82 |   
 83 |   afterEach(async () => {
 84 |     await cleanupClientAndTransport(client, transport);
 85 |   });
 86 |   
 87 |   it('should complete install workflow through MCP', async () => {
 88 |     // This tests the critical user journey:
 89 |     // User connects via MCP → calls install_app → receives result
 90 |     
 91 |     const result = await client.request(
 92 |       {
 93 |         method: 'tools/call',
 94 |         params: {
 95 |           name: 'install_app',
 96 |           arguments: {
 97 |             appPath: testAppPath,
 98 |             simulatorId: testDeviceId
 99 |           }
100 |         }
101 |       },
102 |       CallToolResultSchema,
103 |       { timeout: 120000 }
104 |     );
105 |     
106 |     expect(result).toBeDefined();
107 |     expect(result.content).toBeInstanceOf(Array);
108 |     
109 |     const textContent = result.content.find((c: any) => c.type === 'text');
110 |     expect(textContent?.text).toContain('Successfully installed');
111 |   });
112 | });
```

--------------------------------------------------------------------------------
/src/features/simulator/tests/e2e/BootSimulatorMCP.e2e.test.ts:
--------------------------------------------------------------------------------

```typescript
  1 | /**
  2 |  * E2E Test for Boot Simulator through MCP Protocol
  3 |  * 
  4 |  * Tests critical user journey: Booting a simulator through MCP
  5 |  * Following testing philosophy: E2E tests for critical paths only (10%)
  6 |  * 
  7 |  * Focus: MCP protocol interaction, not simulator boot logic
  8 |  * The controller tests already verify boot works with real simulators
  9 |  * This test verifies the MCP transport/serialization/protocol works
 10 |  * 
 11 |  * NO MOCKS - Uses real MCP server, real simulators
 12 |  */
 13 | 
 14 | import { describe, it, expect, beforeAll, afterAll, beforeEach } from '@jest/globals';
 15 | import { Client } from '@modelcontextprotocol/sdk/client/index.js';
 16 | import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
 17 | import { CallToolResultSchema } from '@modelcontextprotocol/sdk/types.js';
 18 | import { createAndConnectClient, cleanupClientAndTransport } from '../../../../shared/tests/utils/testHelpers.js';
 19 | import { TestSimulatorManager } from '../../../../shared/tests/utils/TestSimulatorManager.js';
 20 | import { exec } from 'child_process';
 21 | import { promisify } from 'util';
 22 | 
 23 | const execAsync = promisify(exec);
 24 | 
 25 | describe('Boot Simulator MCP E2E', () => {
 26 |   let client: Client;
 27 |   let transport: StdioClientTransport;
 28 |   let testSimManager: TestSimulatorManager;
 29 |   
 30 |   beforeAll(async () => {
 31 |     // Build the server
 32 |     const { execSync } = await import('child_process');
 33 |     execSync('npm run build', { stdio: 'inherit' });
 34 | 
 35 |     // Set up test simulator
 36 |     testSimManager = new TestSimulatorManager();
 37 |     await testSimManager.getOrCreateSimulator('TestSimulator-BootMCP');
 38 | 
 39 |     // Connect to MCP server
 40 |     ({ client, transport } = await createAndConnectClient());
 41 |   });
 42 |   
 43 |   beforeEach(async () => {
 44 |     // Ensure simulator is shutdown before each test
 45 |     await testSimManager.shutdownAndWait(5);
 46 |   });
 47 |   
 48 |   afterAll(async () => {
 49 |     // Cleanup test simulator
 50 |     await testSimManager.cleanup();
 51 | 
 52 |     // Cleanup MCP connection
 53 |     await cleanupClientAndTransport(client, transport);
 54 |   });
 55 | 
 56 |   describe('boot simulator through MCP', () => {
 57 |     it('should boot simulator via MCP protocol', async () => {
 58 |       // Act - Call tool through MCP
 59 |       const result = await client.callTool({
 60 |         name: 'boot_simulator',
 61 |         arguments: {
 62 |           deviceId: testSimManager.getSimulatorName()
 63 |         }
 64 |       });
 65 |       
 66 |       // Assert - Verify MCP response
 67 |       const parsed = CallToolResultSchema.parse(result);
 68 |       expect(parsed.content[0].type).toBe('text');
 69 |       expect(parsed.content[0].text).toBe(`✅ Successfully booted simulator: ${testSimManager.getSimulatorName()} (${testSimManager.getSimulatorId()})`);
 70 |       
 71 |       // Verify simulator is actually booted
 72 |       const isBooted = await testSimManager.isBooted();
 73 |       expect(isBooted).toBe(true);
 74 |     });
 75 |     
 76 |     it('should handle already booted simulator via MCP', async () => {
 77 |       // Arrange - boot the simulator first
 78 |       await testSimManager.bootAndWait(5);
 79 |       await new Promise(resolve => setTimeout(resolve, 2000)); // Wait for boot
 80 |       
 81 |       // Act - Call tool through MCP
 82 |       const result = await client.callTool({
 83 |         name: 'boot_simulator',
 84 |         arguments: {
 85 |           deviceId: testSimManager.getSimulatorId()
 86 |         }
 87 |       });
 88 |       
 89 |       // Assert
 90 |       const parsed = CallToolResultSchema.parse(result);
 91 |       expect(parsed.content[0].text).toBe(`✅ Simulator already booted: ${testSimManager.getSimulatorName()} (${testSimManager.getSimulatorId()})`);
 92 |     });
 93 |   });
 94 | 
 95 |   describe('error handling through MCP', () => {
 96 |     it('should return error for non-existent simulator', async () => {
 97 |       // Act
 98 |       const result = await client.callTool({
 99 |         name: 'boot_simulator',
100 |         arguments: {
101 |           deviceId: 'NonExistentSimulator-MCP'
102 |         }
103 |       });
104 |       
105 |       // Assert
106 |       const parsed = CallToolResultSchema.parse(result);
107 |       expect(parsed.content[0].text).toBe('❌ Simulator not found: NonExistentSimulator-MCP');
108 |     });
109 |   });
110 | });
```

--------------------------------------------------------------------------------
/src/shared/tests/unit/AppPath.unit.test.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { describe, it, expect } from '@jest/globals';
  2 | import { AppPath } from '../../domain/AppPath.js';
  3 | 
  4 | describe('AppPath', () => {
  5 |   describe('create', () => {
  6 |     it('should create valid AppPath with .app extension', () => {
  7 |       // Arrange & Act
  8 |       const appPath = AppPath.create('/path/to/MyApp.app');
  9 |       
 10 |       // Assert
 11 |       expect(appPath.toString()).toBe('/path/to/MyApp.app');
 12 |       expect(appPath.name).toBe('MyApp.app');
 13 |     });
 14 | 
 15 |     it('should accept paths with spaces', () => {
 16 |       // Arrange & Act
 17 |       const appPath = AppPath.create('/path/to/My Cool App.app');
 18 |       
 19 |       // Assert
 20 |       expect(appPath.toString()).toBe('/path/to/My Cool App.app');
 21 |       expect(appPath.name).toBe('My Cool App.app');
 22 |     });
 23 | 
 24 |     it('should accept relative paths', () => {
 25 |       // Arrange & Act
 26 |       const appPath = AppPath.create('./build/Debug/TestApp.app');
 27 |       
 28 |       // Assert
 29 |       expect(appPath.toString()).toBe('./build/Debug/TestApp.app');
 30 |       expect(appPath.name).toBe('TestApp.app');
 31 |     });
 32 | 
 33 |     it('should throw error for empty path', () => {
 34 |       // Arrange, Act & Assert
 35 |       expect(() => AppPath.create('')).toThrow('App path cannot be empty');
 36 |     });
 37 | 
 38 |     it('should throw error for path without .app extension', () => {
 39 |       // Arrange, Act & Assert
 40 |       expect(() => AppPath.create('/path/to/MyApp')).toThrow('App path must end with .app');
 41 |       expect(() => AppPath.create('/path/to/MyApp.ipa')).toThrow('App path must end with .app');
 42 |       expect(() => AppPath.create('/path/to/binary')).toThrow('App path must end with .app');
 43 |     });
 44 | 
 45 |     it('should throw error for path with directory traversal', () => {
 46 |       // Arrange, Act & Assert
 47 |       expect(() => AppPath.create('../../../etc/passwd.app')).toThrow('App path cannot contain directory traversal');
 48 |       expect(() => AppPath.create('/path/../../../etc/evil.app')).toThrow('App path cannot contain directory traversal');
 49 |       expect(() => AppPath.create('/valid/path/../../sneaky.app')).toThrow('App path cannot contain directory traversal');
 50 |     });
 51 | 
 52 |     it('should throw error for path with null characters', () => {
 53 |       // Arrange, Act & Assert
 54 |       expect(() => AppPath.create('/path/to/MyApp.app\0')).toThrow('App path cannot contain null characters');
 55 |       expect(() => AppPath.create('/path\0/to/MyApp.app')).toThrow('App path cannot contain null characters');
 56 |     });
 57 | 
 58 |     it('should handle paths ending with slash after .app', () => {
 59 |       // Arrange & Act
 60 |       const appPath = AppPath.create('/path/to/MyApp.app/');
 61 |       
 62 |       // Assert
 63 |       expect(appPath.toString()).toBe('/path/to/MyApp.app/');
 64 |       expect(appPath.name).toBe('MyApp.app');
 65 |     });
 66 |   });
 67 | 
 68 |   describe('name property', () => {
 69 |     it('should extract app name from simple path', () => {
 70 |       // Arrange & Act
 71 |       const appPath = AppPath.create('/Users/dev/MyApp.app');
 72 |       
 73 |       // Assert
 74 |       expect(appPath.name).toBe('MyApp.app');
 75 |     });
 76 | 
 77 |     it('should extract app name from Windows-style path', () => {
 78 |       // Arrange & Act  
 79 |       const appPath = AppPath.create('C:\\Users\\dev\\MyApp.app');
 80 |       
 81 |       // Assert
 82 |       expect(appPath.name).toBe('MyApp.app');
 83 |     });
 84 | 
 85 |     it('should handle app name with special characters', () => {
 86 |       // Arrange & Act
 87 |       const appPath = AppPath.create('/path/to/My-App_v1.2.3.app');
 88 |       
 89 |       // Assert
 90 |       expect(appPath.name).toBe('My-App_v1.2.3.app');
 91 |     });
 92 | 
 93 |     it('should handle just the app name without path', () => {
 94 |       // Arrange & Act
 95 |       const appPath = AppPath.create('MyApp.app');
 96 |       
 97 |       // Assert
 98 |       expect(appPath.name).toBe('MyApp.app');
 99 |     });
100 |   });
101 | 
102 |   describe('toString', () => {
103 |     it('should return the original path', () => {
104 |       // Arrange
105 |       const originalPath = '/path/to/MyApp.app';
106 |       
107 |       // Act
108 |       const appPath = AppPath.create(originalPath);
109 |       
110 |       // Assert
111 |       expect(appPath.toString()).toBe(originalPath);
112 |     });
113 |   });
114 | });
```

--------------------------------------------------------------------------------
/src/features/app-management/tests/unit/AppInstallerAdapter.unit.test.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { describe, it, expect, jest, beforeEach } from '@jest/globals';
  2 | import { AppInstallerAdapter } from '../../infrastructure/AppInstallerAdapter.js';
  3 | import { ICommandExecutor } from '../../../../application/ports/CommandPorts.js';
  4 | 
  5 | describe('AppInstallerAdapter', () => {
  6 |   beforeEach(() => {
  7 |     jest.clearAllMocks();
  8 |   });
  9 | 
 10 |   function createSUT() {
 11 |     const mockExecute = jest.fn<ICommandExecutor['execute']>();
 12 |     const mockExecutor: ICommandExecutor = {
 13 |       execute: mockExecute
 14 |     };
 15 |     const sut = new AppInstallerAdapter(mockExecutor);
 16 |     return { sut, mockExecute };
 17 |   }
 18 | 
 19 |   describe('installApp', () => {
 20 |     it('should install app successfully', async () => {
 21 |       // Arrange
 22 |       const { sut, mockExecute } = createSUT();
 23 |       mockExecute.mockResolvedValue({
 24 |         stdout: '',
 25 |         stderr: '',
 26 |         exitCode: 0
 27 |       });
 28 | 
 29 |       // Act
 30 |       await sut.installApp('/path/to/MyApp.app', 'ABC-123');
 31 | 
 32 |       // Assert
 33 |       expect(mockExecute).toHaveBeenCalledWith(
 34 |         'xcrun simctl install "ABC-123" "/path/to/MyApp.app"'
 35 |       );
 36 |     });
 37 | 
 38 |     it('should handle paths with spaces', async () => {
 39 |       // Arrange
 40 |       const { sut, mockExecute } = createSUT();
 41 |       mockExecute.mockResolvedValue({
 42 |         stdout: '',
 43 |         stderr: '',
 44 |         exitCode: 0
 45 |       });
 46 | 
 47 |       // Act
 48 |       await sut.installApp('/path/to/My Cool App.app', 'ABC-123');
 49 | 
 50 |       // Assert
 51 |       expect(mockExecute).toHaveBeenCalledWith(
 52 |         'xcrun simctl install "ABC-123" "/path/to/My Cool App.app"'
 53 |       );
 54 |     });
 55 | 
 56 |     it('should throw error for invalid app bundle', async () => {
 57 |       // Arrange
 58 |       const { sut, mockExecute } = createSUT();
 59 |       mockExecute.mockResolvedValue({
 60 |         stdout: '',
 61 |         stderr: 'An error was encountered processing the command (domain=NSPOSIXErrorDomain, code=2):\nFailed to install "/path/to/NotAnApp.app"',
 62 |         exitCode: 1
 63 |       });
 64 | 
 65 |       // Act & Assert
 66 |       await expect(sut.installApp('/path/to/NotAnApp.app', 'ABC-123'))
 67 |         .rejects.toThrow('An error was encountered processing the command');
 68 |     });
 69 | 
 70 |     it('should throw error when device not found', async () => {
 71 |       // Arrange
 72 |       const { sut, mockExecute } = createSUT();
 73 |       mockExecute.mockResolvedValue({
 74 |         stdout: '',
 75 |         stderr: 'Invalid device: NON-EXISTENT',
 76 |         exitCode: 164
 77 |       });
 78 | 
 79 |       // Act & Assert
 80 |       await expect(sut.installApp('/path/to/MyApp.app', 'NON-EXISTENT'))
 81 |         .rejects.toThrow('Invalid device: NON-EXISTENT');
 82 |     });
 83 | 
 84 |     it('should throw error when simulator not booted', async () => {
 85 |       // Arrange
 86 |       const { sut, mockExecute } = createSUT();
 87 |       mockExecute.mockResolvedValue({
 88 |         stdout: '',
 89 |         stderr: 'Unable to install "/path/to/MyApp.app"\nAn error was encountered processing the command (domain=com.apple.CoreSimulator.SimError, code=405):\nUnable to install applications when the device is not booted.',
 90 |         exitCode: 149
 91 |       });
 92 | 
 93 |       // Act & Assert
 94 |       await expect(sut.installApp('/path/to/MyApp.app', 'ABC-123'))
 95 |         .rejects.toThrow('Unable to install applications when the device is not booted');
 96 |     });
 97 | 
 98 |     it('should throw generic error when stderr is empty', async () => {
 99 |       // Arrange
100 |       const { sut, mockExecute } = createSUT();
101 |       mockExecute.mockResolvedValue({
102 |         stdout: '',
103 |         stderr: '',
104 |         exitCode: 1
105 |       });
106 | 
107 |       // Act & Assert
108 |       await expect(sut.installApp('/path/to/MyApp.app', 'ABC-123'))
109 |         .rejects.toThrow('Failed to install app');
110 |     });
111 | 
112 |     it('should throw error for app with invalid signature', async () => {
113 |       // Arrange
114 |       const { sut, mockExecute } = createSUT();
115 |       mockExecute.mockResolvedValue({
116 |         stdout: '',
117 |         stderr: 'The code signature version is no longer supported',
118 |         exitCode: 1
119 |       });
120 | 
121 |       // Act & Assert
122 |       await expect(sut.installApp('/path/to/MyApp.app', 'ABC-123'))
123 |         .rejects.toThrow('The code signature version is no longer supported');
124 |     });
125 |   });
126 | });
```

--------------------------------------------------------------------------------
/src/features/simulator/tests/unit/ShutdownResult.unit.test.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { describe, it, expect } from '@jest/globals';
  2 | import { 
  3 |   ShutdownResult, 
  4 |   ShutdownOutcome, 
  5 |   SimulatorNotFoundError, 
  6 |   ShutdownCommandFailedError 
  7 | } from '../../domain/ShutdownResult.js';
  8 | 
  9 | describe('ShutdownResult', () => {
 10 |   describe('shutdown', () => {
 11 |     it('should create a successful shutdown result', () => {
 12 |       // Arrange
 13 |       const simulatorId = 'ABC123';
 14 |       const simulatorName = 'iPhone 15';
 15 |       
 16 |       // Act
 17 |       const result = ShutdownResult.shutdown(simulatorId, simulatorName);
 18 |       
 19 |       // Assert
 20 |       expect(result.outcome).toBe(ShutdownOutcome.Shutdown);
 21 |       expect(result.diagnostics.simulatorId).toBe('ABC123');
 22 |       expect(result.diagnostics.simulatorName).toBe('iPhone 15');
 23 |       expect(result.diagnostics.error).toBeUndefined();
 24 |     });
 25 |   });
 26 | 
 27 |   describe('alreadyShutdown', () => {
 28 |     it('should create an already shutdown result', () => {
 29 |       // Arrange
 30 |       const simulatorId = 'ABC123';
 31 |       const simulatorName = 'iPhone 15';
 32 |       
 33 |       // Act
 34 |       const result = ShutdownResult.alreadyShutdown(simulatorId, simulatorName);
 35 |       
 36 |       // Assert
 37 |       expect(result.outcome).toBe(ShutdownOutcome.AlreadyShutdown);
 38 |       expect(result.diagnostics.simulatorId).toBe('ABC123');
 39 |       expect(result.diagnostics.simulatorName).toBe('iPhone 15');
 40 |       expect(result.diagnostics.error).toBeUndefined();
 41 |     });
 42 |   });
 43 | 
 44 |   describe('failed', () => {
 45 |     it('should create a failed result with SimulatorNotFoundError', () => {
 46 |       // Arrange
 47 |       const error = new SimulatorNotFoundError('non-existent');
 48 |       
 49 |       // Act
 50 |       const result = ShutdownResult.failed(undefined, undefined, error);
 51 |       
 52 |       // Assert
 53 |       expect(result.outcome).toBe(ShutdownOutcome.Failed);
 54 |       expect(result.diagnostics.simulatorId).toBeUndefined();
 55 |       expect(result.diagnostics.simulatorName).toBeUndefined();
 56 |       expect(result.diagnostics.error).toBe(error);
 57 |       expect(result.diagnostics.error).toBeInstanceOf(SimulatorNotFoundError);
 58 |     });
 59 | 
 60 |     it('should create a failed result with ShutdownCommandFailedError', () => {
 61 |       // Arrange
 62 |       const error = new ShutdownCommandFailedError('Device is busy');
 63 |       const simulatorId = 'ABC123';
 64 |       const simulatorName = 'iPhone 15';
 65 |       
 66 |       // Act
 67 |       const result = ShutdownResult.failed(simulatorId, simulatorName, error);
 68 |       
 69 |       // Assert
 70 |       expect(result.outcome).toBe(ShutdownOutcome.Failed);
 71 |       expect(result.diagnostics.simulatorId).toBe('ABC123');
 72 |       expect(result.diagnostics.simulatorName).toBe('iPhone 15');
 73 |       expect(result.diagnostics.error).toBe(error);
 74 |       expect(result.diagnostics.error).toBeInstanceOf(ShutdownCommandFailedError);
 75 |     });
 76 | 
 77 |     it('should handle generic errors', () => {
 78 |       // Arrange
 79 |       const error = new Error('Unknown error');
 80 |       
 81 |       // Act
 82 |       const result = ShutdownResult.failed('123', 'Test Device', error);
 83 |       
 84 |       // Assert
 85 |       expect(result.outcome).toBe(ShutdownOutcome.Failed);
 86 |       expect(result.diagnostics.error).toBe(error);
 87 |     });
 88 |   });
 89 | });
 90 | 
 91 | describe('SimulatorNotFoundError', () => {
 92 |   it('should store device ID', () => {
 93 |     // Arrange & Act
 94 |     const error = new SimulatorNotFoundError('iPhone-16');
 95 |     
 96 |     // Assert
 97 |     expect(error.deviceId).toBe('iPhone-16');
 98 |     expect(error.name).toBe('SimulatorNotFoundError');
 99 |     expect(error.message).toBe('iPhone-16');
100 |   });
101 | 
102 |   it('should be an instance of Error', () => {
103 |     // Arrange & Act
104 |     const error = new SimulatorNotFoundError('test');
105 |     
106 |     // Assert
107 |     expect(error).toBeInstanceOf(Error);
108 |   });
109 | });
110 | 
111 | describe('ShutdownCommandFailedError', () => {
112 |   it('should store stderr output', () => {
113 |     // Arrange & Act
114 |     const error = new ShutdownCommandFailedError('Device is locked');
115 |     
116 |     // Assert
117 |     expect(error.stderr).toBe('Device is locked');
118 |     expect(error.name).toBe('ShutdownCommandFailedError');
119 |     expect(error.message).toBe('Device is locked');
120 |   });
121 | 
122 |   it('should handle empty stderr', () => {
123 |     // Arrange & Act
124 |     const error = new ShutdownCommandFailedError('');
125 |     
126 |     // Assert
127 |     expect(error.stderr).toBe('');
128 |     expect(error.message).toBe('');
129 |   });
130 | 
131 |   it('should be an instance of Error', () => {
132 |     // Arrange & Act
133 |     const error = new ShutdownCommandFailedError('test');
134 |     
135 |     // Assert
136 |     expect(error).toBeInstanceOf(Error);
137 |   });
138 | });
```

--------------------------------------------------------------------------------
/src/utils/projects/XcodeArchive.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { execAsync } from '../../utils.js';
  2 | import { createModuleLogger } from '../../logger.js';
  3 | import { Platform } from '../../types.js';
  4 | import { PlatformInfo } from '../../features/build/domain/PlatformInfo.js';
  5 | import path from 'path';
  6 | 
  7 | const logger = createModuleLogger('XcodeArchive');
  8 | 
  9 | export interface ArchiveOptions {
 10 |   scheme: string;
 11 |   configuration?: string;
 12 |   platform?: Platform;
 13 |   archivePath?: string;
 14 | }
 15 | 
 16 | export interface ExportOptions {
 17 |   exportMethod?: 'app-store' | 'ad-hoc' | 'enterprise' | 'development';
 18 |   exportPath?: string;
 19 | }
 20 | 
 21 | /**
 22 |  * Handles archiving and exporting for Xcode projects
 23 |  */
 24 | export class XcodeArchive {
 25 |   /**
 26 |    * Archive an Xcode project
 27 |    */
 28 |   async archive(
 29 |     projectPath: string,
 30 |     isWorkspace: boolean,
 31 |     options: ArchiveOptions
 32 |   ): Promise<{ success: boolean; archivePath: string }> {
 33 |     const {
 34 |       scheme,
 35 |       configuration = 'Release',
 36 |       platform = Platform.iOS,
 37 |       archivePath
 38 |     } = options;
 39 |     
 40 |     // Generate archive path if not provided
 41 |     const finalArchivePath = archivePath || 
 42 |       `./build/${scheme}-${new Date().toISOString().split('T')[0]}.xcarchive`;
 43 |     
 44 |     const projectFlag = isWorkspace ? '-workspace' : '-project';
 45 |     let command = `xcodebuild archive ${projectFlag} "${projectPath}"`;
 46 |     command += ` -scheme "${scheme}"`;
 47 |     command += ` -configuration "${configuration}"`;
 48 |     command += ` -archivePath "${finalArchivePath}"`;
 49 |     
 50 |     // Add platform-specific destination
 51 |     const platformInfo = PlatformInfo.fromPlatform(platform);
 52 |     const destination = platformInfo.generateGenericDestination();
 53 |     command += ` -destination "${destination}"`;
 54 |     
 55 |     logger.debug({ command }, 'Archive command');
 56 |     
 57 |     try {
 58 |       const { stdout } = await execAsync(command, { 
 59 |         maxBuffer: 50 * 1024 * 1024 
 60 |       });
 61 |       
 62 |       logger.info({ 
 63 |         projectPath, 
 64 |         scheme, 
 65 |         archivePath: finalArchivePath 
 66 |       }, 'Archive succeeded');
 67 |       
 68 |       return {
 69 |         success: true,
 70 |         archivePath: finalArchivePath
 71 |       };
 72 |     } catch (error: any) {
 73 |       logger.error({ error: error.message, projectPath }, 'Archive failed');
 74 |       throw new Error(`Archive failed: ${error.message}`);
 75 |     }
 76 |   }
 77 |   
 78 |   /**
 79 |    * Export an IPA from an archive
 80 |    */
 81 |   async exportIPA(
 82 |     archivePath: string,
 83 |     options: ExportOptions = {}
 84 |   ): Promise<{ success: boolean; ipaPath: string }> {
 85 |     const {
 86 |       exportMethod = 'development',
 87 |       exportPath = './build'
 88 |     } = options;
 89 |     
 90 |     // Create export options plist
 91 |     const exportPlist = `<?xml version="1.0" encoding="UTF-8"?>
 92 | <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
 93 | <plist version="1.0">
 94 | <dict>
 95 |     <key>method</key>
 96 |     <string>${exportMethod}</string>
 97 |     <key>compileBitcode</key>
 98 |     <false/>
 99 | </dict>
100 | </plist>`;
101 |     
102 |     // Write plist to temp file
103 |     const tempPlistPath = path.join(exportPath, 'ExportOptions.plist');
104 |     const { writeFile, mkdir } = await import('fs/promises');
105 |     await mkdir(exportPath, { recursive: true });
106 |     await writeFile(tempPlistPath, exportPlist);
107 |     
108 |     const command = `xcodebuild -exportArchive -archivePath "${archivePath}" -exportPath "${exportPath}" -exportOptionsPlist "${tempPlistPath}"`;
109 |     
110 |     logger.debug({ command }, 'Export command');
111 |     
112 |     try {
113 |       const { stdout } = await execAsync(command, { 
114 |         maxBuffer: 10 * 1024 * 1024 
115 |       });
116 |       
117 |       // Find the IPA file in the export directory
118 |       const { readdir } = await import('fs/promises');
119 |       const files = await readdir(exportPath);
120 |       const ipaFile = files.find(f => f.endsWith('.ipa'));
121 |       
122 |       if (!ipaFile) {
123 |         throw new Error('IPA file not found in export directory');
124 |       }
125 |       
126 |       const ipaPath = path.join(exportPath, ipaFile);
127 |       
128 |       // Clean up temp plist
129 |       const { unlink } = await import('fs/promises');
130 |       await unlink(tempPlistPath).catch(() => {});
131 |       
132 |       logger.info({ 
133 |         archivePath, 
134 |         ipaPath,
135 |         exportMethod 
136 |       }, 'IPA export succeeded');
137 |       
138 |       return {
139 |         success: true,
140 |         ipaPath
141 |       };
142 |     } catch (error: any) {
143 |       logger.error({ error: error.message, archivePath }, 'Export failed');
144 |       
145 |       // Clean up temp plist
146 |       const { unlink } = await import('fs/promises');
147 |       await unlink(tempPlistPath).catch(() => {});
148 |       
149 |       throw new Error(`Export failed: ${error.message}`);
150 |     }
151 |   }
152 | }
```

--------------------------------------------------------------------------------
/src/features/app-management/tests/unit/InstallAppController.unit.test.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { describe, it, expect, beforeEach, jest } from '@jest/globals';
  2 | import { InstallAppController } from '../../controllers/InstallAppController.js';
  3 | import { InstallAppUseCase } from '../../use-cases/InstallAppUseCase.js';
  4 | import { InstallRequest } from '../../domain/InstallRequest.js';
  5 | import { InstallResult } from '../../domain/InstallResult.js';
  6 | import { AppPath } from '../../../../shared/domain/AppPath.js';
  7 | import { DeviceId } from '../../../../shared/domain/DeviceId.js';
  8 | 
  9 | describe('InstallAppController', () => {
 10 |   function createSUT() {
 11 |     const mockExecute = jest.fn<(request: InstallRequest) => Promise<InstallResult>>();
 12 |     const mockUseCase: Partial<InstallAppUseCase> = {
 13 |       execute: mockExecute
 14 |     };
 15 |     const sut = new InstallAppController(mockUseCase as InstallAppUseCase);
 16 |     return { sut, mockExecute };
 17 |   }
 18 | 
 19 |   describe('MCP tool interface', () => {
 20 |     it('should define correct tool metadata', () => {
 21 |       const { sut } = createSUT();
 22 |       
 23 |       const definition = sut.getToolDefinition();
 24 |       
 25 |       expect(definition.name).toBe('install_app');
 26 |       expect(definition.description).toBe('Install an app on the simulator');
 27 |       expect(definition.inputSchema).toBeDefined();
 28 |     });
 29 | 
 30 |     it('should define correct input schema', () => {
 31 |       const { sut } = createSUT();
 32 |       
 33 |       const schema = sut.inputSchema;
 34 |       
 35 |       expect(schema.type).toBe('object');
 36 |       expect(schema.properties.appPath).toBeDefined();
 37 |       expect(schema.properties.simulatorId).toBeDefined();
 38 |       expect(schema.required).toEqual(['appPath']);
 39 |     });
 40 |   });
 41 | 
 42 |   describe('execute', () => {
 43 |     it('should install app on specified simulator', async () => {
 44 |       // Arrange
 45 |       const { sut, mockExecute } = createSUT();
 46 |       const mockResult = InstallResult.succeeded(
 47 |         'com.example.app',
 48 |         DeviceId.create('iPhone-15-Simulator'),
 49 |         'iPhone 15',
 50 |         AppPath.create('/path/to/app.app')
 51 |       );
 52 |       mockExecute.mockResolvedValue(mockResult);
 53 |       
 54 |       // Act
 55 |       const result = await sut.execute({
 56 |         appPath: '/path/to/app.app',
 57 |         simulatorId: 'iPhone-15-Simulator'
 58 |       });
 59 | 
 60 |       // Assert
 61 |       expect(result.content[0].text).toBe('✅ Successfully installed com.example.app on iPhone 15 (iPhone-15-Simulator)');
 62 |     });
 63 | 
 64 |     it('should install app on booted simulator when no ID specified', async () => {
 65 |       // Arrange
 66 |       const { sut, mockExecute } = createSUT();
 67 |       const mockResult = InstallResult.succeeded(
 68 |         'com.example.app',
 69 |         DeviceId.create('Booted-iPhone-15'),
 70 |         'iPhone 15',
 71 |         AppPath.create('/path/to/app.app')
 72 |       );
 73 |       mockExecute.mockResolvedValue(mockResult);
 74 |       
 75 |       // Act
 76 |       const result = await sut.execute({
 77 |         appPath: '/path/to/app.app'
 78 |       });
 79 | 
 80 |       // Assert
 81 |       expect(result.content[0].text).toBe('✅ Successfully installed com.example.app on iPhone 15 (Booted-iPhone-15)');
 82 |     });
 83 | 
 84 |     it('should handle validation errors', async () => {
 85 |       // Arrange
 86 |       const { sut } = createSUT();
 87 |       
 88 |       // Act
 89 |       const result = await sut.execute({
 90 |         // Missing required appPath
 91 |         simulatorId: 'test-sim'
 92 |       });
 93 |       
 94 |       // Assert
 95 |       expect(result.content[0].text).toBe('❌ App path is required');
 96 |     });
 97 | 
 98 |     it('should handle use case errors', async () => {
 99 |       // Arrange
100 |       const { sut, mockExecute } = createSUT();
101 |       mockExecute.mockRejectedValue(new Error('Simulator not found'));
102 |       
103 |       // Act
104 |       const result = await sut.execute({
105 |         appPath: '/path/to/app.app',
106 |         simulatorId: 'non-existent'
107 |       });
108 |       
109 |       // Assert
110 |       expect(result.content[0].text).toBe('❌ Simulator not found');
111 |     });
112 | 
113 |     it('should validate app path format', async () => {
114 |       // Arrange
115 |       const { sut } = createSUT();
116 | 
117 |       // Act
118 |       const result = await sut.execute({
119 |         appPath: '../../../etc/passwd' // Path traversal attempt
120 |       });
121 | 
122 |       // Assert
123 |       expect(result.content[0].text).toBe('❌ App path cannot contain directory traversal');
124 |     });
125 | 
126 |     it('should handle app not found errors', async () => {
127 |       // Arrange
128 |       const { sut, mockExecute } = createSUT();
129 |       mockExecute.mockRejectedValue(new Error('App bundle not found at path'));
130 |       
131 |       // Act
132 |       const result = await sut.execute({
133 |         appPath: '/non/existent/app.app'
134 |       });
135 |       
136 |       // Assert
137 |       expect(result.content[0].text).toBe('❌ App bundle not found at path');
138 |     });
139 |   });
140 | 
141 | });
```

--------------------------------------------------------------------------------
/src/features/simulator/tests/unit/BootSimulatorController.unit.test.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { describe, it, expect, jest } from '@jest/globals';
  2 | import { BootSimulatorController } from '../../controllers/BootSimulatorController.js';
  3 | import { BootSimulatorUseCase } from '../../use-cases/BootSimulatorUseCase.js';
  4 | import { BootRequest } from '../../domain/BootRequest.js';
  5 | import { BootResult, BootOutcome, BootCommandFailedError } from '../../domain/BootResult.js';
  6 | 
  7 | describe('BootSimulatorController', () => {
  8 |   function createSUT() {
  9 |     const mockExecute = jest.fn<(request: BootRequest) => Promise<BootResult>>();
 10 |     const mockUseCase: Partial<BootSimulatorUseCase> = {
 11 |       execute: mockExecute
 12 |     };
 13 |     const sut = new BootSimulatorController(mockUseCase as BootSimulatorUseCase);
 14 |     return { sut, mockExecute };
 15 |   }
 16 | 
 17 |   describe('MCP tool interface', () => {
 18 |     it('should define correct tool metadata', () => {
 19 |       // Arrange
 20 |       const { sut } = createSUT();
 21 |       
 22 |       // Act
 23 |       const definition = sut.getToolDefinition();
 24 |       
 25 |       // Assert
 26 |       expect(definition.name).toBe('boot_simulator');
 27 |       expect(definition.description).toBe('Boot a simulator');
 28 |       expect(definition.inputSchema).toBeDefined();
 29 |     });
 30 | 
 31 |     it('should define correct input schema', () => {
 32 |       // Arrange
 33 |       const { sut } = createSUT();
 34 |       
 35 |       // Act
 36 |       const schema = sut.inputSchema;
 37 |       
 38 |       // Assert
 39 |       expect(schema.type).toBe('object');
 40 |       expect(schema.properties.deviceId).toBeDefined();
 41 |       expect(schema.properties.deviceId.type).toBe('string');
 42 |       expect(schema.required).toEqual(['deviceId']);
 43 |     });
 44 |   });
 45 | 
 46 |   describe('execute', () => {
 47 |     it('should boot simulator successfully', async () => {
 48 |       // Arrange
 49 |       const { sut, mockExecute } = createSUT();
 50 |       const mockResult = BootResult.booted('ABC123', 'iPhone 15', {
 51 |         platform: 'iOS',
 52 |         runtime: 'iOS-17.0'
 53 |       });
 54 |       mockExecute.mockResolvedValue(mockResult);
 55 |       
 56 |       // Act
 57 |       const result = await sut.execute({
 58 |         deviceId: 'iPhone-15'
 59 |       });
 60 | 
 61 |       // Assert
 62 |       expect(result.content[0].text).toBe('✅ Successfully booted simulator: iPhone 15 (ABC123)');
 63 |     });
 64 | 
 65 |     it('should handle already booted simulator', async () => {
 66 |       // Arrange
 67 |       const { sut, mockExecute } = createSUT();
 68 |       const mockResult = BootResult.alreadyBooted('ABC123', 'iPhone 15');
 69 |       mockExecute.mockResolvedValue(mockResult);
 70 |       
 71 |       // Act
 72 |       const result = await sut.execute({
 73 |         deviceId: 'iPhone-15'
 74 |       });
 75 |       
 76 |       // Assert
 77 |       expect(result.content[0].text).toBe('✅ Simulator already booted: iPhone 15 (ABC123)');
 78 |     });
 79 | 
 80 |     it('should handle boot failure', async () => {
 81 |       // Arrange
 82 |       const { sut, mockExecute } = createSUT();
 83 |       const mockResult = BootResult.failed(
 84 |         'ABC123', 
 85 |         'iPhone 15', 
 86 |         new BootCommandFailedError('Device is locked')
 87 |       );
 88 |       mockExecute.mockResolvedValue(mockResult);
 89 |       
 90 |       // Act
 91 |       const result = await sut.execute({
 92 |         deviceId: 'iPhone-15'
 93 |       });
 94 |       
 95 |       // Assert - Error with ❌ emoji prefix and simulator context
 96 |       expect(result.content[0].text).toBe('❌ iPhone 15 (ABC123) - Device is locked');
 97 |     });
 98 | 
 99 |     it('should validate required deviceId', async () => {
100 |       // Arrange
101 |       const { sut } = createSUT();
102 |       
103 |       // Act
104 |       const result = await sut.execute({} as any);
105 |       
106 |       // Assert
107 |       expect(result.content[0].text).toBe('❌ Device ID is required');
108 |     });
109 | 
110 |     it('should validate empty deviceId', async () => {
111 |       // Arrange
112 |       const { sut } = createSUT();
113 |       
114 |       // Act
115 |       const result = await sut.execute({ deviceId: '' });
116 |       
117 |       // Assert
118 |       expect(result.content[0].text).toBe('❌ Device ID cannot be empty');
119 |     });
120 | 
121 |     it('should validate whitespace-only deviceId', async () => {
122 |       // Arrange
123 |       const { sut } = createSUT();
124 |       
125 |       // Act
126 |       const result = await sut.execute({ deviceId: '   ' });
127 |       
128 |       // Assert
129 |       expect(result.content[0].text).toBe('❌ Device ID cannot be whitespace only');
130 |     });
131 | 
132 |     it('should pass UUID directly to use case', async () => {
133 |       // Arrange
134 |       const { sut, mockExecute } = createSUT();
135 |       const uuid = '838C707D-5703-4AEE-AF43-4798E0BA1B05';
136 |       const mockResult = BootResult.booted(uuid, 'iPhone 15');
137 |       mockExecute.mockResolvedValue(mockResult);
138 |       
139 |       // Act
140 |       await sut.execute({ deviceId: uuid });
141 |       
142 |       // Assert
143 |       const calledWith = mockExecute.mock.calls[0][0];
144 |       expect(calledWith.deviceId).toBe(uuid);
145 |     });
146 |   });
147 | });
```

--------------------------------------------------------------------------------
/src/shared/tests/utils/TestEnvironmentCleaner.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { execSync } from 'child_process';
  2 | import { createModuleLogger } from '../../../logger';
  3 | 
  4 | const logger = createModuleLogger('TestEnvironmentCleaner');
  5 | 
  6 | /**
  7 |  * Utility class for cleaning up test environment (simulators, processes, etc.)
  8 |  * Separate from TestProjectManager to maintain single responsibility
  9 |  */
 10 | export class TestEnvironmentCleaner {
 11 |   /**
 12 |    * Shutdown all running simulators
 13 |    * Faster than erasing simulators, just powers them off
 14 |    */
 15 |   static shutdownAllSimulators(): void {
 16 |     try {
 17 |       execSync('xcrun simctl shutdown all', { stdio: 'ignore' });
 18 |     } catch (error) {
 19 |       logger.debug('No simulators to shutdown or shutdown failed (normal)');
 20 |     }
 21 |   }
 22 | 
 23 |   /**
 24 |    * Kill a running macOS app by name
 25 |    * @param appName Name of the app process to kill
 26 |    */
 27 |   static killMacOSApp(appName: string): void {
 28 |     try {
 29 |       execSync(`pkill -f ${appName}`, { stdio: 'ignore' });
 30 |     } catch (error) {
 31 |       // Ignore errors - app might not be running
 32 |     }
 33 |   }
 34 | 
 35 |   /**
 36 |    * Kill the test project app if it's running on macOS
 37 |    */
 38 |   static killTestProjectApp(): void {
 39 |     this.killMacOSApp('TestProjectXCTest');
 40 |   }
 41 | 
 42 |   /**
 43 |    * Clean DerivedData and SPM build artifacts for test projects
 44 |    */
 45 |   static cleanDerivedData(): void {
 46 |     try {
 47 |       // Clean MCP-Xcode DerivedData location (where our tests actually write)
 48 |       // This includes Logs/Test/*.xcresult files
 49 |       execSync('rm -rf ~/Library/Developer/Xcode/DerivedData/MCP-Xcode/TestProjectSwiftTesting', { 
 50 |         shell: '/bin/bash',
 51 |         stdio: 'ignore' 
 52 |       });
 53 |       
 54 |       execSync('rm -rf ~/Library/Developer/Xcode/DerivedData/MCP-Xcode/TestProjectXCTest', { 
 55 |         shell: '/bin/bash',
 56 |         stdio: 'ignore' 
 57 |       });
 58 |       
 59 |       execSync('rm -rf ~/Library/Developer/Xcode/DerivedData/MCP-Xcode/TestSwiftPackage*', { 
 60 |         shell: '/bin/bash',
 61 |         stdio: 'ignore' 
 62 |       });
 63 |       
 64 |       // Also clean standard Xcode DerivedData locations (in case xcodebuild uses them directly)
 65 |       execSync('rm -rf ~/Library/Developer/Xcode/DerivedData/TestProjectSwiftTesting-*', { 
 66 |         shell: '/bin/bash',
 67 |         stdio: 'ignore' 
 68 |       });
 69 |       
 70 |       execSync('rm -rf ~/Library/Developer/Xcode/DerivedData/TestProjectXCTest-*', { 
 71 |         shell: '/bin/bash',
 72 |         stdio: 'ignore' 
 73 |       });
 74 |       
 75 |       execSync('rm -rf ~/Library/Developer/Xcode/DerivedData/TestSwiftPackage-*', { 
 76 |         shell: '/bin/bash',
 77 |         stdio: 'ignore' 
 78 |       });
 79 |       
 80 |       // Clean SPM .build directories in test artifacts
 81 |       const testArtifactsDir = process.cwd() + '/test_artifacts';
 82 |       execSync(`find ${testArtifactsDir} -name .build -type d -exec rm -rf {} + 2>/dev/null || true`, {
 83 |         shell: '/bin/bash',
 84 |         stdio: 'ignore'
 85 |       });
 86 |       
 87 |       // Clean .swiftpm directories
 88 |       execSync(`find ${testArtifactsDir} -name .swiftpm -type d -exec rm -rf {} + 2>/dev/null || true`, {
 89 |         shell: '/bin/bash',
 90 |         stdio: 'ignore'
 91 |       });
 92 |       
 93 |     } catch (error) {
 94 |       logger.debug('DerivedData cleanup failed or nothing to clean (normal)');
 95 |     }
 96 |   }
 97 | 
 98 |   /**
 99 |    * Full cleanup of test environment
100 |    * Shuts down simulators, kills test apps, and cleans DerivedData
101 |    */
102 |   static cleanupTestEnvironment(): void {
103 |     // Shutdown all simulators
104 |     this.shutdownAllSimulators();
105 |     
106 |     // Kill any running test apps
107 |     this.killTestProjectApp();
108 |     
109 |     // Clean DerivedData for test projects
110 |     this.cleanDerivedData();
111 |   }
112 | 
113 |   /**
114 |    * Reset a specific simulator by erasing its contents
115 |    * @param deviceId The simulator device ID to reset
116 |    */
117 |   static resetSimulator(deviceId: string): void {
118 |     try {
119 |       execSync(`xcrun simctl erase "${deviceId}"`);
120 |     } catch (error: any) {
121 |       // Log the actual error message for debugging
122 |       logger.warn({ deviceId, error: error.message, stderr: error.stderr?.toString() }, 'Failed to erase simulator');
123 |     }
124 |   }
125 | 
126 |   /**
127 |    * Boot a specific simulator
128 |    * @param deviceId The simulator device ID to boot
129 |    */
130 |   static bootSimulator(deviceId: string): void {
131 |     try {
132 |       execSync(`xcrun simctl boot "${deviceId}"`, { stdio: 'ignore' });
133 |     } catch (error) {
134 |       logger.warn({ deviceId }, 'Simulator already booted or boot failed');
135 |     }
136 |   }
137 | 
138 |   /**
139 |    * Shutdown a specific simulator
140 |    * @param deviceId The simulator device ID to boot
141 |    */
142 |   static shutdownSimulator(deviceId: string): void {
143 |     try {
144 |       execSync(`xcrun simctl shutdown "${deviceId}"`, { stdio: 'ignore' });
145 |     } catch (error) {
146 |       logger.warn({ deviceId }, 'Simulator already booted or boot failed');
147 |     }
148 |   }
149 | }
```

--------------------------------------------------------------------------------
/src/utils/projects/XcodeProject.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { XcodeBuild, BuildOptions, TestOptions } from './XcodeBuild.js';
  2 | import { XcodeArchive, ArchiveOptions, ExportOptions } from './XcodeArchive.js';
  3 | import { XcodeInfo } from './XcodeInfo.js';
  4 | import { Issue } from '../errors/index.js';
  5 | import { createModuleLogger } from '../../logger.js';
  6 | import * as pathModule from 'path';
  7 | import { Platform } from '../../types.js';
  8 | 
  9 | const logger = createModuleLogger('XcodeProject');
 10 | 
 11 | /**
 12 |  * Represents an Xcode project (.xcodeproj) or workspace (.xcworkspace)
 13 |  */
 14 | export class XcodeProject {
 15 |   public readonly name: string;
 16 |   private build: XcodeBuild;
 17 |   private archive: XcodeArchive;
 18 |   private info: XcodeInfo;
 19 |   
 20 |   constructor(
 21 |     public readonly path: string,
 22 |     public readonly type: 'project' | 'workspace',
 23 |     components?: {
 24 |       build?: XcodeBuild;
 25 |       archive?: XcodeArchive;
 26 |       info?: XcodeInfo;
 27 |     }
 28 |   ) {
 29 |     // Extract name from path
 30 |     const ext = type === 'workspace' ? '.xcworkspace' : '.xcodeproj';
 31 |     this.name = pathModule.basename(this.path, ext);
 32 |     
 33 |     // Initialize components
 34 |     this.build = components?.build || new XcodeBuild();
 35 |     this.archive = components?.archive || new XcodeArchive();
 36 |     this.info = components?.info || new XcodeInfo();
 37 |     
 38 |     logger.debug({ path: this.path, type, name: this.name }, 'XcodeProject created');
 39 |   }
 40 |   
 41 |   /**
 42 |    * Build the project
 43 |    */
 44 |   async buildProject(options: BuildOptions = {}): Promise<{
 45 |     success: boolean;
 46 |     output: string;
 47 |     appPath?: string;
 48 |     logPath?: string;
 49 |     errors?: Issue[];
 50 |   }> {
 51 |     logger.info({ path: this.path, options }, 'Building Xcode project');
 52 |     
 53 |     const isWorkspace = this.type === 'workspace';
 54 |     return await this.build.build(this.path, isWorkspace, options);
 55 |   }
 56 |   
 57 |   /**
 58 |    * Run tests for the project
 59 |    */
 60 |   async test(options: TestOptions = {}): Promise<{
 61 |     success: boolean;
 62 |     output: string;
 63 |     passed: number;
 64 |     failed: number;
 65 |     failingTests?: Array<{ identifier: string; reason: string }>;
 66 |     compileErrors?: Issue[];
 67 |     compileWarnings?: Issue[];
 68 |     buildErrors?: Issue[];
 69 |     logPath: string;
 70 |   }> {
 71 |     logger.info({ path: this.path, options }, 'Testing Xcode project');
 72 |     
 73 |     const isWorkspace = this.type === 'workspace';
 74 |     return await this.build.test(this.path, isWorkspace, options);
 75 |   }
 76 |   
 77 |   /**
 78 |    * Archive the project for distribution
 79 |    */
 80 |   async archiveProject(options: ArchiveOptions): Promise<{
 81 |     success: boolean;
 82 |     archivePath: string;
 83 |   }> {
 84 |     logger.info({ path: this.path, options }, 'Archiving Xcode project');
 85 |     
 86 |     const isWorkspace = this.type === 'workspace';
 87 |     return await this.archive.archive(this.path, isWorkspace, options);
 88 |   }
 89 |   
 90 |   /**
 91 |    * Export an IPA from an archive
 92 |    */
 93 |   async exportIPA(
 94 |     archivePath: string,
 95 |     options: ExportOptions = {}
 96 |   ): Promise<{
 97 |     success: boolean;
 98 |     ipaPath: string;
 99 |   }> {
100 |     logger.info({ archivePath, options }, 'Exporting IPA');
101 |     
102 |     return await this.archive.exportIPA(archivePath, options);
103 |   }
104 |   
105 |   /**
106 |    * Clean build artifacts
107 |    */
108 |   async clean(options: {
109 |     scheme?: string;
110 |     configuration?: string;
111 |   } = {}): Promise<void> {
112 |     logger.info({ path: this.path, options }, 'Cleaning Xcode project');
113 |     
114 |     const isWorkspace = this.type === 'workspace';
115 |     await this.build.clean(this.path, isWorkspace, options);
116 |   }
117 |   
118 |   /**
119 |    * Get list of schemes
120 |    */
121 |   async getSchemes(): Promise<string[]> {
122 |     const isWorkspace = this.type === 'workspace';
123 |     return await this.info.getSchemes(this.path, isWorkspace);
124 |   }
125 |   
126 |   /**
127 |    * Get list of targets
128 |    */
129 |   async getTargets(): Promise<string[]> {
130 |     const isWorkspace = this.type === 'workspace';
131 |     return await this.info.getTargets(this.path, isWorkspace);
132 |   }
133 |   
134 |   /**
135 |    * Get build settings for a scheme
136 |    */
137 |   async getBuildSettings(
138 |     scheme: string,
139 |     configuration?: string,
140 |     platform?: Platform
141 |   ): Promise<any> {
142 |     const isWorkspace = this.type === 'workspace';
143 |     return await this.info.getBuildSettings(
144 |       this.path,
145 |       isWorkspace,
146 |       scheme,
147 |       configuration,
148 |       platform
149 |     );
150 |   }
151 |   
152 |   /**
153 |    * Get comprehensive project information
154 |    */
155 |   async getProjectInfo(): Promise<{
156 |     name: string;
157 |     schemes: string[];
158 |     targets: string[];
159 |     configurations: string[];
160 |   }> {
161 |     const isWorkspace = this.type === 'workspace';
162 |     return await this.info.getProjectInfo(this.path, isWorkspace);
163 |   }
164 |   
165 |   /**
166 |    * Check if this is a workspace
167 |    */
168 |   isWorkspace(): boolean {
169 |     return this.type === 'workspace';
170 |   }
171 |   
172 |   /**
173 |    * Get the project directory
174 |    */
175 |   getDirectory(): string {
176 |     return pathModule.dirname(this.path);
177 |   }
178 | }
```

--------------------------------------------------------------------------------
/src/utils/projects/SwiftPackage.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { SwiftBuild, SwiftBuildOptions, SwiftRunOptions, SwiftTestOptions } from './SwiftBuild.js';
  2 | import { SwiftPackageInfo, Dependency, Product } from './SwiftPackageInfo.js';
  3 | import { Issue } from '../errors/index.js';
  4 | import { createModuleLogger } from '../../logger.js';
  5 | import * as pathModule from 'path';
  6 | import { existsSync } from 'fs';
  7 | 
  8 | const logger = createModuleLogger('SwiftPackage');
  9 | 
 10 | /**
 11 |  * Represents a Swift package (Package.swift)
 12 |  */
 13 | export class SwiftPackage {
 14 |   public readonly name: string;
 15 |   private build: SwiftBuild;
 16 |   private info: SwiftPackageInfo;
 17 |   
 18 |   constructor(
 19 |     public readonly path: string,
 20 |     components?: {
 21 |       build?: SwiftBuild;
 22 |       info?: SwiftPackageInfo;
 23 |     }
 24 |   ) {
 25 |     // Validate that Package.swift exists
 26 |     const packageSwiftPath = pathModule.join(this.path, 'Package.swift');
 27 |     if (!existsSync(packageSwiftPath)) {
 28 |       throw new Error(`No Package.swift found at: ${this.path}`);
 29 |     }
 30 |     
 31 |     // Extract name from directory
 32 |     this.name = pathModule.basename(this.path);
 33 |     
 34 |     // Initialize components
 35 |     this.build = components?.build || new SwiftBuild();
 36 |     this.info = components?.info || new SwiftPackageInfo();
 37 |     
 38 |     logger.debug({ path: this.path, name: this.name }, 'SwiftPackage created');
 39 |   }
 40 |   
 41 |   /**
 42 |    * Build the package
 43 |    */
 44 |   async buildPackage(options: SwiftBuildOptions = {}): Promise<{
 45 |     success: boolean;
 46 |     output: string;
 47 |     logPath?: string;
 48 |     compileErrors?: Issue[];
 49 |     buildErrors?: Issue[];
 50 |   }> {
 51 |     logger.info({ path: this.path, options }, 'Building Swift package');
 52 |     
 53 |     return await this.build.build(this.path, options);
 54 |   }
 55 |   
 56 |   /**
 57 |    * Run an executable from the package
 58 |    */
 59 |   async run(options: SwiftRunOptions = {}): Promise<{
 60 |     success: boolean;
 61 |     output: string;
 62 |     logPath?: string;
 63 |     compileErrors?: Issue[];
 64 |     buildErrors?: Issue[];
 65 |   }> {
 66 |     logger.info({ path: this.path, options }, 'Running Swift package');
 67 |     
 68 |     return await this.build.run(this.path, options);
 69 |   }
 70 |   
 71 |   /**
 72 |    * Test the package
 73 |    */
 74 |   async test(options: SwiftTestOptions = {}): Promise<{
 75 |     success: boolean;
 76 |     output: string;
 77 |     passed: number;
 78 |     failed: number;
 79 |     failingTests?: Array<{ identifier: string; reason: string }>;
 80 |     compileErrors?: Issue[];
 81 |     buildErrors?: Issue[];
 82 |     logPath: string;
 83 |   }> {
 84 |     logger.info({ path: this.path, options }, 'Testing Swift package');
 85 |     
 86 |     return await this.build.test(this.path, options);
 87 |   }
 88 |   
 89 |   /**
 90 |    * Clean build artifacts
 91 |    */
 92 |   async clean(): Promise<void> {
 93 |     logger.info({ path: this.path }, 'Cleaning Swift package');
 94 |     
 95 |     await this.build.clean(this.path);
 96 |   }
 97 |   
 98 |   /**
 99 |    * Get list of products (executables and libraries)
100 |    */
101 |   async getProducts(): Promise<Product[]> {
102 |     return await this.info.getProducts(this.path);
103 |   }
104 |   
105 |   /**
106 |    * Get list of targets
107 |    */
108 |   async getTargets(): Promise<string[]> {
109 |     return await this.info.getTargets(this.path);
110 |   }
111 |   
112 |   /**
113 |    * Get list of dependencies
114 |    */
115 |   async getDependencies(): Promise<Dependency[]> {
116 |     return await this.info.getDependencies(this.path);
117 |   }
118 |   
119 |   /**
120 |    * Add a dependency
121 |    */
122 |   async addDependency(
123 |     url: string,
124 |     options: {
125 |       version?: string;
126 |       branch?: string;
127 |       exact?: boolean;
128 |       from?: string;
129 |       upToNextMajor?: string;
130 |     } = {}
131 |   ): Promise<void> {
132 |     logger.info({ path: this.path, url, options }, 'Adding dependency');
133 |     
134 |     await this.info.addDependency(this.path, url, options);
135 |   }
136 |   
137 |   /**
138 |    * Remove a dependency
139 |    */
140 |   async removeDependency(name: string): Promise<void> {
141 |     logger.info({ path: this.path, name }, 'Removing dependency');
142 |     
143 |     await this.info.removeDependency(this.path, name);
144 |   }
145 |   
146 |   /**
147 |    * Update all dependencies
148 |    */
149 |   async updateDependencies(): Promise<void> {
150 |     logger.info({ path: this.path }, 'Updating dependencies');
151 |     
152 |     await this.info.updateDependencies(this.path);
153 |   }
154 |   
155 |   /**
156 |    * Resolve dependencies
157 |    */
158 |   async resolveDependencies(): Promise<void> {
159 |     logger.info({ path: this.path }, 'Resolving dependencies');
160 |     
161 |     await this.info.resolveDependencies(this.path);
162 |   }
163 |   
164 |   /**
165 |    * Get the package directory
166 |    */
167 |   getDirectory(): string {
168 |     return this.path;
169 |   }
170 |   
171 |   /**
172 |    * Check if this is an executable package
173 |    */
174 |   async isExecutable(): Promise<boolean> {
175 |     const products = await this.getProducts();
176 |     return products.some(p => p.type === 'executable');
177 |   }
178 |   
179 |   /**
180 |    * Get executable products
181 |    */
182 |   async getExecutables(): Promise<string[]> {
183 |     const products = await this.getProducts();
184 |     return products
185 |       .filter(p => p.type === 'executable')
186 |       .map(p => p.name);
187 |   }
188 | }
```

--------------------------------------------------------------------------------
/src/utils/LogManager.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import * as fs from 'fs';
  2 | import * as path from 'path';
  3 | import * as os from 'os';
  4 | 
  5 | /**
  6 |  * Manages persistent logs for MCP server debugging
  7 |  * Stores logs in ~/.mcp-xcode-server/logs/ with daily rotation
  8 |  */
  9 | export class LogManager {
 10 |   private static readonly LOG_DIR = path.join(os.homedir(), '.mcp-xcode-server', 'logs');
 11 |   private static readonly MAX_AGE_DAYS = 7;
 12 |   
 13 |   /**
 14 |    * Initialize log directory structure
 15 |    */
 16 |   private init(): void {
 17 |     if (!fs.existsSync(LogManager.LOG_DIR)) {
 18 |       fs.mkdirSync(LogManager.LOG_DIR, { recursive: true });
 19 |     }
 20 |     
 21 |     // Clean up old logs on startup
 22 |     this.cleanupOldLogs();
 23 |   }
 24 |   
 25 |   /**
 26 |    * Get the log directory for today
 27 |    */
 28 |   private getTodayLogDir(): string {
 29 |     const today = new Date().toISOString().split('T')[0]; // YYYY-MM-DD
 30 |     const dir = path.join(LogManager.LOG_DIR, today);
 31 |     
 32 |     if (!fs.existsSync(dir)) {
 33 |       fs.mkdirSync(dir, { recursive: true });
 34 |     }
 35 |     
 36 |     return dir;
 37 |   }
 38 |   
 39 |   /**
 40 |    * Generate a log filename with timestamp
 41 |    */
 42 |   private getLogFilename(operation: string, projectName?: string): string {
 43 |     const timestamp = new Date().toISOString()
 44 |       .replace(/:/g, '-')
 45 |       .replace(/\./g, '-')
 46 |       .split('T')[1]
 47 |       .slice(0, 8); // HH-MM-SS
 48 |     
 49 |     const name = projectName ? `${operation}-${projectName}` : operation;
 50 |     return `${timestamp}-${name}.log`;
 51 |   }
 52 |   
 53 |   /**
 54 |    * Save log content to a file
 55 |    * Returns the full path to the log file
 56 |    */
 57 |   saveLog(
 58 |     operation: 'build' | 'test' | 'run' | 'archive' | 'clean',
 59 |     content: string,
 60 |     projectName?: string,
 61 |     metadata?: Record<string, any>
 62 |   ): string {
 63 |     const dir = this.getTodayLogDir();
 64 |     const filename = this.getLogFilename(operation, projectName);
 65 |     const filepath = path.join(dir, filename);
 66 |     
 67 |     // Add metadata header if provided
 68 |     let fullContent = '';
 69 |     if (metadata) {
 70 |       fullContent += '=== Log Metadata ===\n';
 71 |       fullContent += JSON.stringify(metadata, null, 2) + '\n';
 72 |       fullContent += '=== End Metadata ===\n\n';
 73 |     }
 74 |     fullContent += content;
 75 |     
 76 |     fs.writeFileSync(filepath, fullContent, 'utf8');
 77 |     
 78 |     // Also create/update a symlink to latest log
 79 |     const latestLink = path.join(LogManager.LOG_DIR, `latest-${operation}.log`);
 80 |     if (fs.existsSync(latestLink)) {
 81 |       fs.unlinkSync(latestLink);
 82 |     }
 83 |     
 84 |     // Create relative symlink for portability
 85 |     const relativePath = `./${new Date().toISOString().split('T')[0]}/${filename}`;
 86 |     try {
 87 |       // Use execSync to create symlink as fs.symlinkSync has issues on some systems
 88 |       const { execSync } = require('child_process');
 89 |       execSync(`ln -sf "${relativePath}" "${latestLink}"`, { cwd: LogManager.LOG_DIR });
 90 |     } catch {
 91 |       // Symlink creation failed, not critical
 92 |     }
 93 |     
 94 |     return filepath;
 95 |   }
 96 |   
 97 |   /**
 98 |    * Save debug data (like parsed xcresult) for analysis
 99 |    */
100 |   saveDebugData(
101 |     operation: string,
102 |     data: any,
103 |     projectName?: string
104 |   ): string {
105 |     const dir = this.getTodayLogDir();
106 |     const timestamp = new Date().toISOString()
107 |       .replace(/:/g, '-')
108 |       .replace(/\./g, '-')
109 |       .split('T')[1]
110 |       .slice(0, 8);
111 |     
112 |     const name = projectName ? `${operation}-${projectName}` : operation;
113 |     const filename = `${timestamp}-${name}-debug.json`;
114 |     const filepath = path.join(dir, filename);
115 |     
116 |     fs.writeFileSync(filepath, JSON.stringify(data, null, 2), 'utf8');
117 |     
118 |     return filepath;
119 |   }
120 |   
121 |   /**
122 |    * Clean up logs older than MAX_AGE_DAYS
123 |    */
124 |   cleanupOldLogs(): void {
125 |     if (!fs.existsSync(LogManager.LOG_DIR)) {
126 |       return;
127 |     }
128 |     
129 |     const now = Date.now();
130 |     const maxAge = LogManager.MAX_AGE_DAYS * 24 * 60 * 60 * 1000;
131 |     
132 |     try {
133 |       const entries = fs.readdirSync(LogManager.LOG_DIR);
134 |       
135 |       for (const entry of entries) {
136 |         const fullPath = path.join(LogManager.LOG_DIR, entry);
137 |         
138 |         // Skip symlinks
139 |         const stat = fs.statSync(fullPath);
140 |         if (stat.isSymbolicLink()) {
141 |           continue;
142 |         }
143 |         
144 |         // Check if it's a date directory (YYYY-MM-DD format)
145 |         if (stat.isDirectory() && /^\d{4}-\d{2}-\d{2}$/.test(entry)) {
146 |           const dirDate = new Date(entry).getTime();
147 |           
148 |           if (now - dirDate > maxAge) {
149 |             fs.rmSync(fullPath, { recursive: true, force: true });
150 |           }
151 |         }
152 |       }
153 |     } catch (error) {
154 |       // Cleanup failed, not critical
155 |     }
156 |   }
157 |   
158 |   /**
159 |    * Get the user-friendly log path for display
160 |    */
161 |   getDisplayPath(fullPath: string): string {
162 |     // Replace home directory with ~
163 |     const home = os.homedir();
164 |     return fullPath.replace(home, '~');
165 |   }
166 |   
167 |   /**
168 |    * Get the log directory path
169 |    */
170 |   getLogDirectory(): string {
171 |     return LogManager.LOG_DIR;
172 |   }
173 | }
```

--------------------------------------------------------------------------------
/src/features/app-management/tests/unit/InstallResult.unit.test.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { describe, it, expect } from '@jest/globals';
  2 | import {
  3 |   InstallResult,
  4 |   InstallOutcome,
  5 |   InstallCommandFailedError,
  6 |   SimulatorNotFoundError
  7 | } from '../../domain/InstallResult.js';
  8 | import { DeviceId } from '../../../../shared/domain/DeviceId.js';
  9 | import { AppPath } from '../../../../shared/domain/AppPath.js';
 10 | 
 11 | describe('InstallResult', () => {
 12 |   describe('succeeded', () => {
 13 |     it('should create successful install result', () => {
 14 |       // Arrange & Act
 15 |       const simulatorId = DeviceId.create('iPhone-15-Simulator');
 16 |       const appPath = AppPath.create('/path/to/app.app');
 17 |       const result = InstallResult.succeeded(
 18 |         'com.example.app',
 19 |         simulatorId,
 20 |         'iPhone 15',
 21 |         appPath
 22 |       );
 23 |       
 24 |       // Assert
 25 |       expect(result.outcome).toBe(InstallOutcome.Succeeded);
 26 |       expect(result.diagnostics.bundleId).toBe('com.example.app');
 27 |       expect(result.diagnostics.simulatorId?.toString()).toBe('iPhone-15-Simulator');
 28 |       expect(result.diagnostics.simulatorName).toBe('iPhone 15');
 29 |       expect(result.diagnostics.appPath.toString()).toBe('/path/to/app.app');
 30 |       expect(result.diagnostics.error).toBeUndefined();
 31 |     });
 32 | 
 33 |     it('should include install timestamp', () => {
 34 |       // Arrange & Act
 35 |       const before = Date.now();
 36 |       const simulatorId = DeviceId.create('test-sim');
 37 |       const appPath = AppPath.create('/path/to/app.app');
 38 |       const result = InstallResult.succeeded(
 39 |         'com.example.app',
 40 |         simulatorId,
 41 |         'Test Simulator',
 42 |         appPath
 43 |       );
 44 |       const after = Date.now();
 45 |       
 46 |       // Assert
 47 |       expect(result.diagnostics.installedAt.getTime()).toBeGreaterThanOrEqual(before);
 48 |       expect(result.diagnostics.installedAt.getTime()).toBeLessThanOrEqual(after);
 49 |     });
 50 |   });
 51 | 
 52 |   describe('failed', () => {
 53 |     it('should create failed install result with SimulatorNotFoundError', () => {
 54 |       // Arrange
 55 |       const simulatorId = DeviceId.create('non-existent-sim');
 56 |       const error = new SimulatorNotFoundError(simulatorId);
 57 |       
 58 |       // Act
 59 |       const appPath = AppPath.create('/path/to/app.app');
 60 |       const result = InstallResult.failed(
 61 |         error,
 62 |         appPath,
 63 |         simulatorId,
 64 |         'Unknown Simulator'
 65 |       );
 66 |       
 67 |       // Assert
 68 |       expect(result.outcome).toBe(InstallOutcome.Failed);
 69 |       expect(result.diagnostics.error).toBe(error);
 70 |       expect(result.diagnostics.appPath.toString()).toBe('/path/to/app.app');
 71 |       expect(result.diagnostics.simulatorId?.toString()).toBe('non-existent-sim');
 72 |       expect(result.diagnostics.bundleId).toBeUndefined();
 73 |     });
 74 | 
 75 |     it('should handle failure without simulator ID', () => {
 76 |       // Arrange
 77 |       const simulatorId = DeviceId.create('booted');
 78 |       const error = new SimulatorNotFoundError(simulatorId);
 79 |       
 80 |       // Act
 81 |       const appPath = AppPath.create('/path/to/app.app');
 82 |       const result = InstallResult.failed(
 83 |         error,
 84 |         appPath
 85 |       );
 86 |       
 87 |       // Assert
 88 |       expect(result.outcome).toBe(InstallOutcome.Failed);
 89 |       expect(result.diagnostics.error).toBe(error);
 90 |       expect(result.diagnostics.appPath.toString()).toBe('/path/to/app.app');
 91 |       expect(result.diagnostics.simulatorId).toBeUndefined();
 92 |     });
 93 | 
 94 |     it('should create failed install result with InstallCommandFailedError', () => {
 95 |       // Arrange
 96 |       const error = new InstallCommandFailedError('App bundle not found');
 97 |       
 98 |       // Act
 99 |       const appPath = AppPath.create('/path/to/app.app');
100 |       const simulatorId = DeviceId.create('test-sim');
101 |       const result = InstallResult.failed(
102 |         error,
103 |         appPath,
104 |         simulatorId,
105 |         'Test Simulator'
106 |       );
107 |       
108 |       // Assert
109 |       expect(result.outcome).toBe(InstallOutcome.Failed);
110 |       expect(result.diagnostics.error).toBe(error);
111 |       expect((result.diagnostics.error as InstallCommandFailedError).stderr).toBe('App bundle not found');
112 |     });
113 |   });
114 | 
115 |   describe('outcome checking', () => {
116 |     it('should identify successful installation', () => {
117 |       // Arrange & Act
118 |       const simulatorId = DeviceId.create('sim-id');
119 |       const appPath = AppPath.create('/app.app');
120 |       const result = InstallResult.succeeded(
121 |         'com.example.app',
122 |         simulatorId,
123 |         'Simulator',
124 |         appPath
125 |       );
126 |       
127 |       // Assert
128 |       expect(result.outcome).toBe(InstallOutcome.Succeeded);
129 |     });
130 | 
131 |     it('should identify failed installation', () => {
132 |       // Arrange
133 |       const error = new InstallCommandFailedError('Installation failed');
134 |       
135 |       // Act
136 |       const appPath = AppPath.create('/app.app');
137 |       const result = InstallResult.failed(
138 |         error,
139 |         appPath
140 |       );
141 |       
142 |       // Assert
143 |       expect(result.outcome).toBe(InstallOutcome.Failed);
144 |     });
145 |   });
146 | });
```

--------------------------------------------------------------------------------
/src/features/simulator/tests/e2e/BootSimulatorController.e2e.test.ts:
--------------------------------------------------------------------------------

```typescript
  1 | /**
  2 |  * E2E Test for BootSimulatorController
  3 |  * 
  4 |  * Tests the controller with REAL simulators and REAL system commands
  5 |  * Following testing philosophy: E2E tests for critical paths only (10%)
  6 |  * 
  7 |  * NO MOCKS - Uses real xcrun simctl commands with actual simulators
  8 |  */
  9 | 
 10 | import { describe, it, expect, beforeAll, afterAll, beforeEach } from '@jest/globals';
 11 | import { MCPController } from '../../../../presentation/interfaces/MCPController.js';
 12 | import { BootSimulatorControllerFactory } from '../../factories/BootSimulatorControllerFactory.js';
 13 | import { exec } from 'child_process';
 14 | import { promisify } from 'util';
 15 | 
 16 | const execAsync = promisify(exec);
 17 | 
 18 | describe('BootSimulatorController E2E', () => {
 19 |   let controller: MCPController;
 20 |   let testDeviceId: string;
 21 |   let testSimulatorName: string;
 22 |   
 23 |   beforeAll(async () => {
 24 |     // Create controller with REAL components
 25 |     controller = BootSimulatorControllerFactory.create();
 26 |     
 27 |     // Find or create a test simulator
 28 |     const listResult = await execAsync('xcrun simctl list devices --json');
 29 |     const devices = JSON.parse(listResult.stdout);
 30 |     
 31 |     // Look for an existing test simulator
 32 |     for (const runtime of Object.values(devices.devices) as any[]) {
 33 |       const testSim = runtime.find((d: any) => d.name.includes('TestSimulator-Boot'));
 34 |       if (testSim) {
 35 |         testDeviceId = testSim.udid;
 36 |         testSimulatorName = testSim.name;
 37 |         break;
 38 |       }
 39 |     }
 40 |     
 41 |     // Create one if not found
 42 |     if (!testDeviceId) {
 43 |       // Get available runtime
 44 |       const runtimesResult = await execAsync('xcrun simctl list runtimes --json');
 45 |       const runtimes = JSON.parse(runtimesResult.stdout);
 46 |       const iosRuntime = runtimes.runtimes.find((r: any) => r.platform === 'iOS');
 47 |       
 48 |       if (!iosRuntime) {
 49 |         throw new Error('No iOS runtime available. Please install Xcode with iOS simulator support.');
 50 |       }
 51 |       
 52 |       const createResult = await execAsync(
 53 |         `xcrun simctl create "TestSimulator-Boot" "com.apple.CoreSimulator.SimDeviceType.iPhone-15" "${iosRuntime.identifier}"`
 54 |       );
 55 |       testDeviceId = createResult.stdout.trim();
 56 |       testSimulatorName = 'TestSimulator-Boot';
 57 |     }
 58 |   });
 59 |   
 60 |   beforeEach(async () => {
 61 |     // Ensure simulator is shutdown before each test
 62 |     try {
 63 |       await execAsync(`xcrun simctl shutdown "${testDeviceId}"`);
 64 |     } catch {
 65 |       // Ignore if already shutdown
 66 |     }
 67 |     // Wait for shutdown to complete
 68 |     await new Promise(resolve => setTimeout(resolve, 1000));
 69 |   });
 70 |   
 71 |   afterAll(async () => {
 72 |     // Shutdown the test simulator
 73 |     try {
 74 |       await execAsync(`xcrun simctl shutdown "${testDeviceId}"`);
 75 |     } catch {
 76 |       // Ignore if already shutdown
 77 |     }
 78 |   });
 79 | 
 80 |   describe('boot real simulators', () => {
 81 |     it('should boot a shutdown simulator', async () => {
 82 |       // Act
 83 |       const result = await controller.execute({
 84 |         deviceId: testSimulatorName
 85 |       });
 86 |       
 87 |       // Assert
 88 |       expect(result.content[0].text).toBe(`✅ Successfully booted simulator: ${testSimulatorName} (${testDeviceId})`);
 89 |       
 90 |       // Verify simulator is actually booted
 91 |       const listResult = await execAsync('xcrun simctl list devices --json');
 92 |       const devices = JSON.parse(listResult.stdout);
 93 |       let found = false;
 94 |       for (const runtime of Object.values(devices.devices) as any[]) {
 95 |         const device = runtime.find((d: any) => d.udid === testDeviceId);
 96 |         if (device) {
 97 |           expect(device.state).toBe('Booted');
 98 |           found = true;
 99 |           break;
100 |         }
101 |       }
102 |       expect(found).toBe(true);
103 |     });
104 |     
105 |     it('should handle already booted simulator', async () => {
106 |       // Arrange - boot the simulator first
107 |       await execAsync(`xcrun simctl boot "${testDeviceId}"`);
108 |       await new Promise(resolve => setTimeout(resolve, 2000)); // Wait for boot
109 |       
110 |       // Act
111 |       const result = await controller.execute({
112 |         deviceId: testDeviceId
113 |       });
114 |       
115 |       // Assert
116 |       expect(result.content[0].text).toBe(`✅ Simulator already booted: ${testSimulatorName} (${testDeviceId})`);
117 |     });
118 |     
119 |     it('should boot simulator by UUID', async () => {
120 |       // Act - use UUID directly
121 |       const result = await controller.execute({
122 |         deviceId: testDeviceId
123 |       });
124 |       
125 |       // Assert
126 |       expect(result.content[0].text).toBe(`✅ Successfully booted simulator: ${testSimulatorName} (${testDeviceId})`);
127 |     });
128 |   });
129 | 
130 |   describe('error handling with real simulators', () => {
131 |     it('should fail when simulator does not exist', async () => {
132 |       // Act
133 |       const result = await controller.execute({
134 |         deviceId: 'NonExistentSimulator-12345'
135 |       });
136 |       
137 |       // Assert
138 |       expect(result.content[0].text).toBe('❌ Simulator not found: NonExistentSimulator-12345');
139 |     });
140 |   });
141 | });
```

--------------------------------------------------------------------------------
/src/presentation/tests/unit/DependencyCheckingDecorator.unit.test.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { describe, it, expect, jest } from '@jest/globals';
  2 | import { DependencyCheckingDecorator } from '../../decorators/DependencyCheckingDecorator.js';
  3 | import { MCPController } from '../../interfaces/MCPController.js';
  4 | import { MCPResponse } from '../../interfaces/MCPResponse.js';
  5 | import { IDependencyChecker, MissingDependency } from '../../interfaces/IDependencyChecker.js';
  6 | 
  7 | describe('DependencyCheckingDecorator', () => {
  8 |   function createSUT(missingDeps: MissingDependency[] = []) {
  9 |     // Create mock controller
 10 |     const mockExecute = jest.fn<(args: unknown) => Promise<MCPResponse>>();
 11 |     const mockController: MCPController = {
 12 |       name: 'test_tool',
 13 |       description: 'Test tool',
 14 |       inputSchema: {},
 15 |       execute: mockExecute,
 16 |       getToolDefinition: () => ({
 17 |         name: 'test_tool',
 18 |         description: 'Test tool',
 19 |         inputSchema: {}
 20 |       })
 21 |     };
 22 | 
 23 |     // Create mock dependency checker
 24 |     const mockCheck = jest.fn<IDependencyChecker['check']>();
 25 |     mockCheck.mockResolvedValue(missingDeps);
 26 |     const mockChecker: IDependencyChecker = {
 27 |       check: mockCheck
 28 |     };
 29 | 
 30 |     // Create decorator
 31 |     const sut = new DependencyCheckingDecorator(
 32 |       mockController,
 33 |       ['xcodebuild', 'xcbeautify'],
 34 |       mockChecker
 35 |     );
 36 | 
 37 |     return { sut, mockExecute, mockCheck };
 38 |   }
 39 | 
 40 |   describe('execute', () => {
 41 |     it('should execute controller when all dependencies are available', async () => {
 42 |       // Arrange
 43 |       const { sut, mockExecute } = createSUT([]); // No missing dependencies
 44 |       const args = { someArg: 'value' };
 45 |       const expectedResponse = {
 46 |         content: [{ type: 'text', text: 'Success' }]
 47 |       };
 48 |       mockExecute.mockResolvedValue(expectedResponse);
 49 | 
 50 |       // Act
 51 |       const result = await sut.execute(args);
 52 | 
 53 |       // Assert - behavior: delegates to controller
 54 |       expect(result).toBe(expectedResponse);
 55 |       expect(mockExecute).toHaveBeenCalledWith(args);
 56 |     });
 57 | 
 58 |     it('should return error when dependencies are missing', async () => {
 59 |       // Arrange
 60 |       const missingDeps: MissingDependency[] = [
 61 |         { name: 'xcbeautify', installCommand: 'brew install xcbeautify' }
 62 |       ];
 63 |       const { sut, mockExecute } = createSUT(missingDeps);
 64 | 
 65 |       // Act
 66 |       const result = await sut.execute({});
 67 | 
 68 |       // Assert - behavior: returns error, doesn't execute controller
 69 |       expect(result.content[0].text).toContain('Missing required dependencies');
 70 |       expect(result.content[0].text).toContain('xcbeautify');
 71 |       expect(result.content[0].text).toContain('brew install xcbeautify');
 72 |       expect(mockExecute).not.toHaveBeenCalled();
 73 |     });
 74 | 
 75 |     it('should format multiple missing dependencies clearly', async () => {
 76 |       // Arrange
 77 |       const missingDeps: MissingDependency[] = [
 78 |         { name: 'xcodebuild', installCommand: 'Install Xcode from the App Store' },
 79 |         { name: 'xcbeautify', installCommand: 'brew install xcbeautify' }
 80 |       ];
 81 |       const { sut, mockExecute } = createSUT(missingDeps);
 82 | 
 83 |       // Act
 84 |       const result = await sut.execute({});
 85 | 
 86 |       // Assert - behavior: shows all missing dependencies
 87 |       expect(result.content[0].text).toContain('xcodebuild');
 88 |       expect(result.content[0].text).toContain('Install Xcode from the App Store');
 89 |       expect(result.content[0].text).toContain('xcbeautify');
 90 |       expect(result.content[0].text).toContain('brew install xcbeautify');
 91 |       expect(mockExecute).not.toHaveBeenCalled();
 92 |     });
 93 | 
 94 |     it('should handle dependencies without install commands', async () => {
 95 |       // Arrange
 96 |       const missingDeps: MissingDependency[] = [
 97 |         { name: 'customtool' } // No install command
 98 |       ];
 99 |       const { sut, mockExecute } = createSUT(missingDeps);
100 | 
101 |       // Act
102 |       const result = await sut.execute({});
103 | 
104 |       // Assert - behavior: shows tool name without install command
105 |       expect(result.content[0].text).toContain('customtool');
106 |       expect(result.content[0].text).not.toContain('undefined');
107 |       expect(mockExecute).not.toHaveBeenCalled();
108 |     });
109 |   });
110 | 
111 |   describe('getToolDefinition', () => {
112 |     it('should delegate to decoratee', () => {
113 |       // Arrange
114 |       const { sut } = createSUT();
115 | 
116 |       // Act
117 |       const definition = sut.getToolDefinition();
118 | 
119 |       // Assert - behavior: returns controller's definition
120 |       expect(definition).toEqual({
121 |         name: 'test_tool',
122 |         description: 'Test tool',
123 |         inputSchema: {}
124 |       });
125 |     });
126 |   });
127 | 
128 |   describe('properties', () => {
129 |     it('should delegate properties to decoratee', () => {
130 |       // Arrange
131 |       const { sut } = createSUT();
132 | 
133 |       // Act & Assert - behavior: properties match controller
134 |       expect(sut.name).toBe('test_tool');
135 |       expect(sut.description).toBe('Test tool');
136 |       expect(sut.inputSchema).toEqual({});
137 |     });
138 |   });
139 | });
```

--------------------------------------------------------------------------------
/src/utils/LogManagerInstance.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import * as fs from 'fs';
  2 | import * as path from 'path';
  3 | import * as os from 'os';
  4 | import { ILogManager } from '../application/ports/LoggingPorts.js';
  5 | 
  6 | /**
  7 |  * Instance-based log manager for dependency injection
  8 |  * Manages persistent logs for MCP server debugging
  9 |  */
 10 | export class LogManagerInstance implements ILogManager {
 11 |   private readonly LOG_DIR: string;
 12 |   private readonly MAX_AGE_DAYS = 7;
 13 |   
 14 |   constructor(logDir?: string) {
 15 |     this.LOG_DIR = logDir || path.join(os.homedir(), '.mcp-xcode-server', 'logs');
 16 |     this.init();
 17 |   }
 18 |   
 19 |   /**
 20 |    * Initialize log directory structure
 21 |    */
 22 |   private init(): void {
 23 |     if (!fs.existsSync(this.LOG_DIR)) {
 24 |       fs.mkdirSync(this.LOG_DIR, { recursive: true });
 25 |     }
 26 |     
 27 |     // Clean up old logs on startup
 28 |     this.cleanupOldLogs();
 29 |   }
 30 |   
 31 |   /**
 32 |    * Get the log directory for today
 33 |    */
 34 |   private getTodayLogDir(): string {
 35 |     const today = new Date().toISOString().split('T')[0]; // YYYY-MM-DD
 36 |     const dir = path.join(this.LOG_DIR, today);
 37 |     
 38 |     if (!fs.existsSync(dir)) {
 39 |       fs.mkdirSync(dir, { recursive: true });
 40 |     }
 41 |     
 42 |     return dir;
 43 |   }
 44 |   
 45 |   /**
 46 |    * Generate a log filename with timestamp
 47 |    */
 48 |   private getLogFilename(operation: string, projectName?: string): string {
 49 |     const timestamp = new Date().toISOString()
 50 |       .replace(/:/g, '-')
 51 |       .replace(/\./g, '-')
 52 |       .split('T')[1]
 53 |       .slice(0, 8); // HH-MM-SS
 54 |     
 55 |     const name = projectName ? `${operation}-${projectName}` : operation;
 56 |     return `${timestamp}-${name}.log`;
 57 |   }
 58 |   
 59 |   /**
 60 |    * Save log content to a file
 61 |    * Returns the full path to the log file
 62 |    */
 63 |   saveLog(
 64 |     operation: 'build' | 'test' | 'run' | 'archive' | 'clean',
 65 |     content: string,
 66 |     projectName?: string,
 67 |     metadata?: Record<string, any>
 68 |   ): string {
 69 |     const dir = this.getTodayLogDir();
 70 |     const filename = this.getLogFilename(operation, projectName);
 71 |     const filepath = path.join(dir, filename);
 72 |     
 73 |     // Add metadata header if provided
 74 |     let fullContent = '';
 75 |     if (metadata) {
 76 |       fullContent += '=== Log Metadata ===\n';
 77 |       fullContent += JSON.stringify(metadata, null, 2) + '\n';
 78 |       fullContent += '=== End Metadata ===\n\n';
 79 |     }
 80 |     fullContent += content;
 81 |     
 82 |     fs.writeFileSync(filepath, fullContent, 'utf8');
 83 |     
 84 |     // Also create/update a symlink to latest log
 85 |     const latestLink = path.join(this.LOG_DIR, `latest-${operation}.log`);
 86 |     if (fs.existsSync(latestLink)) {
 87 |       fs.unlinkSync(latestLink);
 88 |     }
 89 |     
 90 |     // Create relative symlink for portability
 91 |     const relativePath = `./${new Date().toISOString().split('T')[0]}/${filename}`;
 92 |     try {
 93 |       // Use execSync to create symlink as fs.symlinkSync has issues on some systems
 94 |       const { execSync } = require('child_process');
 95 |       execSync(`ln -sf "${relativePath}" "${latestLink}"`, { cwd: this.LOG_DIR });
 96 |     } catch {
 97 |       // Symlink creation failed, not critical
 98 |     }
 99 |     
100 |     return filepath;
101 |   }
102 |   
103 |   /**
104 |    * Save debug data (like parsed xcresult) for analysis
105 |    */
106 |   saveDebugData(
107 |     operation: string,
108 |     data: any,
109 |     projectName?: string
110 |   ): string {
111 |     const dir = this.getTodayLogDir();
112 |     const timestamp = new Date().toISOString()
113 |       .replace(/:/g, '-')
114 |       .replace(/\./g, '-')
115 |       .split('T')[1]
116 |       .slice(0, 8);
117 |     
118 |     const name = projectName ? `${operation}-${projectName}` : operation;
119 |     const filename = `${timestamp}-${name}-debug.json`;
120 |     const filepath = path.join(dir, filename);
121 |     
122 |     fs.writeFileSync(filepath, JSON.stringify(data, null, 2), 'utf8');
123 |     
124 |     return filepath;
125 |   }
126 |   
127 |   /**
128 |    * Clean up logs older than MAX_AGE_DAYS
129 |    */
130 |   cleanupOldLogs(): void {
131 |     if (!fs.existsSync(this.LOG_DIR)) {
132 |       return;
133 |     }
134 |     
135 |     const now = Date.now();
136 |     const maxAge = this.MAX_AGE_DAYS * 24 * 60 * 60 * 1000;
137 |     
138 |     try {
139 |       const entries = fs.readdirSync(this.LOG_DIR);
140 |       
141 |       for (const entry of entries) {
142 |         const fullPath = path.join(this.LOG_DIR, entry);
143 |         
144 |         // Skip symlinks
145 |         const stat = fs.statSync(fullPath);
146 |         if (stat.isSymbolicLink()) {
147 |           continue;
148 |         }
149 |         
150 |         // Check if it's a date directory (YYYY-MM-DD format)
151 |         if (stat.isDirectory() && /^\d{4}-\d{2}-\d{2}$/.test(entry)) {
152 |           const dirDate = new Date(entry).getTime();
153 |           
154 |           if (now - dirDate > maxAge) {
155 |             fs.rmSync(fullPath, { recursive: true, force: true });
156 |           }
157 |         }
158 |       }
159 |     } catch (error) {
160 |       // Cleanup failed, not critical
161 |     }
162 |   }
163 |   
164 |   /**
165 |    * Get the user-friendly log path for display
166 |    */
167 |   getDisplayPath(fullPath: string): string {
168 |     // Replace home directory with ~
169 |     const home = os.homedir();
170 |     return fullPath.replace(home, '~');
171 |   }
172 |   
173 |   /**
174 |    * Get the log directory path
175 |    */
176 |   getLogDirectory(): string {
177 |     return this.LOG_DIR;
178 |   }
179 | }
```

--------------------------------------------------------------------------------
/src/presentation/presenters/BuildXcodePresenter.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { BuildResult, BuildOutcome, OutputFormatterError } from '../../features/build/domain/BuildResult.js';
  2 | import { Platform } from '../../shared/domain/Platform.js';
  3 | import { ErrorFormatter } from '../formatters/ErrorFormatter.js';
  4 | import { MCPResponse } from '../interfaces/MCPResponse.js';
  5 | 
  6 | /**
  7 |  * Presenter for build results
  8 |  * 
  9 |  * Single Responsibility: Format BuildResult for MCP display
 10 |  * - Success formatting
 11 |  * - Failure formatting with errors/warnings
 12 |  * - Log path information
 13 |  */
 14 | 
 15 | export class BuildXcodePresenter {
 16 |   private readonly maxErrorsToShow = 50;
 17 |   private readonly maxWarningsToShow = 20;
 18 |   
 19 |   present(result: BuildResult, metadata: {
 20 |     scheme: string;
 21 |     platform: Platform;
 22 |     configuration: string;
 23 |     showWarningDetails?: boolean;
 24 |   }): MCPResponse {
 25 |     if (result.outcome === BuildOutcome.Succeeded) {
 26 |       return this.presentSuccess(result, metadata);
 27 |     }
 28 |     return this.presentFailure(result, metadata);
 29 |   }
 30 |   
 31 |   private presentSuccess(
 32 |     result: BuildResult,
 33 |     metadata: { scheme: string; platform: Platform; configuration: string; showWarningDetails?: boolean }
 34 |   ): MCPResponse {
 35 |     const warnings = BuildResult.getWarnings(result);
 36 |     
 37 |     let text = `✅ Build succeeded: ${metadata.scheme}
 38 | 
 39 | Platform: ${metadata.platform}
 40 | Configuration: ${metadata.configuration}`;
 41 | 
 42 |     // Show warning count if there are any
 43 |     if (warnings.length > 0) {
 44 |       text += `\nWarnings: ${warnings.length}`;
 45 |       
 46 |       // Show warning details if requested
 47 |       if (metadata.showWarningDetails) {
 48 |         text += '\n\n⚠️  Warnings:';
 49 |         const warningsToShow = Math.min(warnings.length, this.maxWarningsToShow);
 50 |         warnings.slice(0, warningsToShow).forEach(warning => {
 51 |           text += `\n  • ${this.formatIssue(warning)}`;
 52 |         });
 53 |         if (warnings.length > this.maxWarningsToShow) {
 54 |           text += `\n  ... and ${warnings.length - this.maxWarningsToShow} more warnings`;
 55 |         }
 56 |       }
 57 |     }
 58 | 
 59 |     text += `\nApp path: ${result.diagnostics.appPath || 'N/A'}${result.diagnostics.logPath ? `
 60 | 
 61 | 📁 Full logs saved to: ${result.diagnostics.logPath}` : ''}`;
 62 |     
 63 |     return {
 64 |       content: [{ type: 'text', text }]
 65 |     };
 66 |   }
 67 |   
 68 |   private presentFailure(
 69 |     result: BuildResult,
 70 |     metadata: { scheme: string; platform: Platform; configuration: string; showWarningDetails?: boolean }
 71 |   ): MCPResponse {
 72 |     // Check if this is a dependency/tool error (not an actual build failure)
 73 |     if (result.diagnostics.error && result.diagnostics.error instanceof OutputFormatterError) {
 74 |       // Tool dependency missing - show only that error
 75 |       const text = `❌ ${ErrorFormatter.format(result.diagnostics.error)}`;
 76 |       return {
 77 |         content: [{ type: 'text', text }]
 78 |       };
 79 |     }
 80 |     
 81 |     const errors = BuildResult.getErrors(result);
 82 |     const warnings = BuildResult.getWarnings(result);
 83 |     
 84 |     let text = `❌ Build failed: ${metadata.scheme}\n`;
 85 |     text += `Platform: ${metadata.platform}\n`;
 86 |     text += `Configuration: ${metadata.configuration}\n`;
 87 |     
 88 |     // Check for other errors in diagnostics
 89 |     if (result.diagnostics.error) {
 90 |       text += `\n❌ ${ErrorFormatter.format(result.diagnostics.error)}\n`;
 91 |     }
 92 |     
 93 |     if (errors.length > 0) {
 94 |       text += `\n❌ Errors (${errors.length}):\n`;
 95 |       // Show up to maxErrorsToShow errors
 96 |       const errorsToShow = Math.min(errors.length, this.maxErrorsToShow);
 97 |       errors.slice(0, errorsToShow).forEach(error => {
 98 |         text += `  • ${this.formatIssue(error)}\n`;
 99 |       });
100 |       if (errors.length > this.maxErrorsToShow) {
101 |         text += `  ... and ${errors.length - this.maxErrorsToShow} more errors\n`;
102 |       }
103 |     }
104 |     
105 |     // Always show warning count if there are warnings
106 |     if (warnings.length > 0) {
107 |       if (metadata.showWarningDetails) {
108 |         // Show detailed warnings
109 |         text += `\n⚠️ Warnings (${warnings.length}):\n`;
110 |         const warningsToShow = Math.min(warnings.length, this.maxWarningsToShow);
111 |         warnings.slice(0, warningsToShow).forEach(warning => {
112 |           text += `  • ${this.formatIssue(warning)}\n`;
113 |         });
114 |         if (warnings.length > this.maxWarningsToShow) {
115 |           text += `  ... and ${warnings.length - this.maxWarningsToShow} more warnings\n`;
116 |         }
117 |       } else {
118 |         // Just show count
119 |         text += `\n⚠️ Warnings: ${warnings.length}\n`;
120 |       }
121 |     }
122 |     
123 |     if (result.diagnostics.logPath) {
124 |       text += `\n📁 Full logs saved to: ${result.diagnostics.logPath}\n`;
125 |     }
126 |     
127 |     return {
128 |       content: [{ type: 'text', text }]
129 |     };
130 |   }
131 |   
132 |   private formatIssue(issue: any): string {
133 |     if (issue.file && issue.line) {
134 |       if (issue.column) {
135 |         return `${issue.file}:${issue.line}:${issue.column}: ${issue.message}`;
136 |       }
137 |       return `${issue.file}:${issue.line}: ${issue.message}`;
138 |     }
139 |     return issue.message;
140 |   }
141 |   
142 |   presentError(error: Error): MCPResponse {
143 |     const message = ErrorFormatter.format(error);
144 |     return {
145 |       content: [{ 
146 |         type: 'text', 
147 |         text: `❌ ${message}` 
148 |       }]
149 |     };
150 |   }
151 | }
```

--------------------------------------------------------------------------------
/src/utils/projects/XcodeInfo.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { execAsync } from '../../utils.js';
  2 | import { createModuleLogger } from '../../logger.js';
  3 | import { Platform } from '../../types.js';
  4 | import path from 'path';
  5 | 
  6 | const logger = createModuleLogger('XcodeInfo');
  7 | 
  8 | /**
  9 |  * Queries information about Xcode projects
 10 |  */
 11 | export class XcodeInfo {
 12 |   /**
 13 |    * Get list of schemes in a project
 14 |    */
 15 |   async getSchemes(
 16 |     projectPath: string,
 17 |     isWorkspace: boolean
 18 |   ): Promise<string[]> {
 19 |     const projectFlag = isWorkspace ? '-workspace' : '-project';
 20 |     const command = `xcodebuild -list -json ${projectFlag} "${projectPath}"`;
 21 |     
 22 |     logger.debug({ command }, 'List schemes command');
 23 |     
 24 |     try {
 25 |       const { stdout } = await execAsync(command);
 26 |       const data = JSON.parse(stdout);
 27 |       
 28 |       // Get schemes from the appropriate property
 29 |       let schemes: string[] = [];
 30 |       if (isWorkspace && data.workspace?.schemes) {
 31 |         schemes = data.workspace.schemes;
 32 |       } else if (!isWorkspace && data.project?.schemes) {
 33 |         schemes = data.project.schemes;
 34 |       }
 35 |       
 36 |       logger.debug({ projectPath, schemes }, 'Found schemes');
 37 |       return schemes;
 38 |     } catch (error: any) {
 39 |       logger.error({ error: error.message, projectPath }, 'Failed to get schemes');
 40 |       throw new Error(`Failed to get schemes: ${error.message}`);
 41 |     }
 42 |   }
 43 |   
 44 |   /**
 45 |    * Get list of targets in a project
 46 |    */
 47 |   async getTargets(
 48 |     projectPath: string,
 49 |     isWorkspace: boolean
 50 |   ): Promise<string[]> {
 51 |     const projectFlag = isWorkspace ? '-workspace' : '-project';
 52 |     const command = `xcodebuild -list -json ${projectFlag} "${projectPath}"`;
 53 |     
 54 |     logger.debug({ command }, 'List targets command');
 55 |     
 56 |     try {
 57 |       const { stdout } = await execAsync(command);
 58 |       const data = JSON.parse(stdout);
 59 |       
 60 |       // Get targets from the project (even for workspaces, targets come from projects)
 61 |       const targets = data.project?.targets || [];
 62 |       
 63 |       logger.debug({ projectPath, targets }, 'Found targets');
 64 |       return targets;
 65 |     } catch (error: any) {
 66 |       logger.error({ error: error.message, projectPath }, 'Failed to get targets');
 67 |       throw new Error(`Failed to get targets: ${error.message}`);
 68 |     }
 69 |   }
 70 |   
 71 |   /**
 72 |    * Get build settings for a scheme
 73 |    */
 74 |   async getBuildSettings(
 75 |     projectPath: string,
 76 |     isWorkspace: boolean,
 77 |     scheme: string,
 78 |     configuration?: string,
 79 |     platform?: Platform
 80 |   ): Promise<any> {
 81 |     const projectFlag = isWorkspace ? '-workspace' : '-project';
 82 |     let command = `xcodebuild -showBuildSettings ${projectFlag} "${projectPath}"`;
 83 |     command += ` -scheme "${scheme}"`;
 84 |     
 85 |     if (configuration) {
 86 |       command += ` -configuration "${configuration}"`;
 87 |     }
 88 |     
 89 |     if (platform) {
 90 |       // Add a generic destination for the platform to get appropriate settings
 91 |       const { PlatformInfo } = await import('../../features/build/domain/PlatformInfo.js');
 92 |       const platformInfo = PlatformInfo.fromPlatform(platform);
 93 |       const destination = platformInfo.generateGenericDestination();
 94 |       command += ` -destination '${destination}'`;
 95 |     }
 96 |     
 97 |     command += ' -json';
 98 |     
 99 |     logger.debug({ command }, 'Get build settings command');
100 |     
101 |     try {
102 |       const { stdout } = await execAsync(command, {
103 |         maxBuffer: 10 * 1024 * 1024
104 |       });
105 |       
106 |       const settings = JSON.parse(stdout);
107 |       logger.debug({ projectPath, scheme }, 'Got build settings');
108 |       
109 |       return settings;
110 |     } catch (error: any) {
111 |       logger.error({ error: error.message, projectPath, scheme }, 'Failed to get build settings');
112 |       throw new Error(`Failed to get build settings: ${error.message}`);
113 |     }
114 |   }
115 |   
116 |   /**
117 |    * Get comprehensive project information
118 |    */
119 |   async getProjectInfo(
120 |     projectPath: string,
121 |     isWorkspace: boolean
122 |   ): Promise<{
123 |     name: string;
124 |     schemes: string[];
125 |     targets: string[];
126 |     configurations: string[];
127 |   }> {
128 |     const projectFlag = isWorkspace ? '-workspace' : '-project';
129 |     const command = `xcodebuild -list -json ${projectFlag} "${projectPath}"`;
130 |     
131 |     logger.debug({ command }, 'Get project info command');
132 |     
133 |     try {
134 |       const { stdout } = await execAsync(command);
135 |       const data = JSON.parse(stdout);
136 |       
137 |       // Extract info based on project type
138 |       let info;
139 |       if (isWorkspace) {
140 |         info = {
141 |           name: data.workspace?.name || path.basename(projectPath, '.xcworkspace'),
142 |           schemes: data.workspace?.schemes || [],
143 |           targets: data.project?.targets || [],
144 |           configurations: data.project?.configurations || []
145 |         };
146 |       } else {
147 |         info = {
148 |           name: data.project?.name || path.basename(projectPath, '.xcodeproj'),
149 |           schemes: data.project?.schemes || [],
150 |           targets: data.project?.targets || [],
151 |           configurations: data.project?.configurations || []
152 |         };
153 |       }
154 |       
155 |       logger.debug({ projectPath, info }, 'Got project info');
156 |       return info;
157 |     } catch (error: any) {
158 |       logger.error({ error: error.message, projectPath }, 'Failed to get project info');
159 |       throw new Error(`Failed to get project info: ${error.message}`);
160 |     }
161 |   }
162 | }
```
Page 2/5FirstPrevNextLast