#
tokens: 48643/50000 32/337 files (page 3/11)
lines: off (toggle) GitHub
raw markdown copy
This is page 3 of 11. Use http://codebase.md/cameroncooke/xcodebuildmcp?page={x} to view the full context.

# Directory Structure

```
├── .axe-version
├── .claude
│   └── agents
│       └── xcodebuild-mcp-qa-tester.md
├── .cursor
│   ├── BUGBOT.md
│   └── environment.json
├── .cursorrules
├── .github
│   ├── FUNDING.yml
│   ├── ISSUE_TEMPLATE
│   │   ├── bug_report.yml
│   │   ├── config.yml
│   │   └── feature_request.yml
│   └── workflows
│       ├── ci.yml
│       ├── claude-code-review.yml
│       ├── claude-dispatch.yml
│       ├── claude.yml
│       ├── droid-code-review.yml
│       ├── README.md
│       ├── release.yml
│       └── sentry.yml
├── .gitignore
├── .prettierignore
├── .prettierrc.js
├── .vscode
│   ├── extensions.json
│   ├── launch.json
│   ├── mcp.json
│   ├── settings.json
│   └── tasks.json
├── AGENTS.md
├── banner.png
├── build-plugins
│   ├── plugin-discovery.js
│   ├── plugin-discovery.ts
│   └── tsconfig.json
├── CHANGELOG.md
├── CLAUDE.md
├── CODE_OF_CONDUCT.md
├── Dockerfile
├── docs
│   ├── ARCHITECTURE.md
│   ├── CODE_QUALITY.md
│   ├── CONTRIBUTING.md
│   ├── ESLINT_TYPE_SAFETY.md
│   ├── MANUAL_TESTING.md
│   ├── NODEJS_2025.md
│   ├── PLUGIN_DEVELOPMENT.md
│   ├── RELEASE_PROCESS.md
│   ├── RELOADEROO_FOR_XCODEBUILDMCP.md
│   ├── RELOADEROO_XCODEBUILDMCP_PRIMER.md
│   ├── RELOADEROO.md
│   ├── session_management_plan.md
│   ├── session-aware-migration-todo.md
│   ├── TEST_RUNNER_ENV_IMPLEMENTATION_PLAN.md
│   ├── TESTING.md
│   └── TOOLS.md
├── eslint.config.js
├── example_projects
│   ├── .vscode
│   │   └── launch.json
│   ├── iOS
│   │   ├── .cursor
│   │   │   └── rules
│   │   │       └── errors.mdc
│   │   ├── .vscode
│   │   │   └── settings.json
│   │   ├── Makefile
│   │   ├── MCPTest
│   │   │   ├── Assets.xcassets
│   │   │   │   ├── AccentColor.colorset
│   │   │   │   │   └── Contents.json
│   │   │   │   ├── AppIcon.appiconset
│   │   │   │   │   └── Contents.json
│   │   │   │   └── Contents.json
│   │   │   ├── ContentView.swift
│   │   │   ├── MCPTestApp.swift
│   │   │   └── Preview Content
│   │   │       └── Preview Assets.xcassets
│   │   │           └── Contents.json
│   │   ├── MCPTest.xcodeproj
│   │   │   ├── project.pbxproj
│   │   │   └── xcshareddata
│   │   │       └── xcschemes
│   │   │           └── MCPTest.xcscheme
│   │   └── MCPTestUITests
│   │       └── MCPTestUITests.swift
│   ├── iOS_Calculator
│   │   ├── CalculatorApp
│   │   │   ├── Assets.xcassets
│   │   │   │   ├── AccentColor.colorset
│   │   │   │   │   └── Contents.json
│   │   │   │   ├── AppIcon.appiconset
│   │   │   │   │   └── Contents.json
│   │   │   │   └── Contents.json
│   │   │   ├── CalculatorApp.swift
│   │   │   └── CalculatorApp.xctestplan
│   │   ├── CalculatorApp.xcodeproj
│   │   │   ├── project.pbxproj
│   │   │   └── xcshareddata
│   │   │       └── xcschemes
│   │   │           └── CalculatorApp.xcscheme
│   │   ├── CalculatorApp.xcworkspace
│   │   │   └── contents.xcworkspacedata
│   │   ├── CalculatorAppPackage
│   │   │   ├── .gitignore
│   │   │   ├── Package.swift
│   │   │   ├── Sources
│   │   │   │   └── CalculatorAppFeature
│   │   │   │       ├── BackgroundEffect.swift
│   │   │   │       ├── CalculatorButton.swift
│   │   │   │       ├── CalculatorDisplay.swift
│   │   │   │       ├── CalculatorInputHandler.swift
│   │   │   │       ├── CalculatorService.swift
│   │   │   │       └── ContentView.swift
│   │   │   └── Tests
│   │   │       └── CalculatorAppFeatureTests
│   │   │           └── CalculatorServiceTests.swift
│   │   ├── CalculatorAppTests
│   │   │   └── CalculatorAppTests.swift
│   │   └── Config
│   │       ├── Debug.xcconfig
│   │       ├── Release.xcconfig
│   │       ├── Shared.xcconfig
│   │       └── Tests.xcconfig
│   ├── macOS
│   │   ├── MCPTest
│   │   │   ├── Assets.xcassets
│   │   │   │   ├── AccentColor.colorset
│   │   │   │   │   └── Contents.json
│   │   │   │   ├── AppIcon.appiconset
│   │   │   │   │   └── Contents.json
│   │   │   │   └── Contents.json
│   │   │   ├── ContentView.swift
│   │   │   ├── MCPTest.entitlements
│   │   │   ├── MCPTestApp.swift
│   │   │   └── Preview Content
│   │   │       └── Preview Assets.xcassets
│   │   │           └── Contents.json
│   │   └── MCPTest.xcodeproj
│   │       ├── project.pbxproj
│   │       └── xcshareddata
│   │           └── xcschemes
│   │               └── MCPTest.xcscheme
│   └── spm
│       ├── .gitignore
│       ├── Package.resolved
│       ├── Package.swift
│       ├── Sources
│       │   ├── long-server
│       │   │   └── main.swift
│       │   ├── quick-task
│       │   │   └── main.swift
│       │   ├── spm
│       │   │   └── main.swift
│       │   └── TestLib
│       │       └── TaskManager.swift
│       └── Tests
│           └── TestLibTests
│               └── SimpleTests.swift
├── LICENSE
├── mcp-install-dark.png
├── package-lock.json
├── package.json
├── README.md
├── scripts
│   ├── analysis
│   │   └── tools-analysis.ts
│   ├── bundle-axe.sh
│   ├── check-code-patterns.js
│   ├── release.sh
│   ├── tools-cli.ts
│   └── update-tools-docs.ts
├── server.json
├── smithery.yaml
├── src
│   ├── core
│   │   ├── __tests__
│   │   │   └── resources.test.ts
│   │   ├── dynamic-tools.ts
│   │   ├── plugin-registry.ts
│   │   ├── plugin-types.ts
│   │   └── resources.ts
│   ├── doctor-cli.ts
│   ├── index.ts
│   ├── mcp
│   │   ├── resources
│   │   │   ├── __tests__
│   │   │   │   ├── devices.test.ts
│   │   │   │   ├── doctor.test.ts
│   │   │   │   └── simulators.test.ts
│   │   │   ├── devices.ts
│   │   │   ├── doctor.ts
│   │   │   └── simulators.ts
│   │   └── tools
│   │       ├── device
│   │       │   ├── __tests__
│   │       │   │   ├── build_device.test.ts
│   │       │   │   ├── get_device_app_path.test.ts
│   │       │   │   ├── index.test.ts
│   │       │   │   ├── install_app_device.test.ts
│   │       │   │   ├── launch_app_device.test.ts
│   │       │   │   ├── list_devices.test.ts
│   │       │   │   ├── re-exports.test.ts
│   │       │   │   ├── stop_app_device.test.ts
│   │       │   │   └── test_device.test.ts
│   │       │   ├── build_device.ts
│   │       │   ├── clean.ts
│   │       │   ├── discover_projs.ts
│   │       │   ├── get_app_bundle_id.ts
│   │       │   ├── get_device_app_path.ts
│   │       │   ├── index.ts
│   │       │   ├── install_app_device.ts
│   │       │   ├── launch_app_device.ts
│   │       │   ├── list_devices.ts
│   │       │   ├── list_schemes.ts
│   │       │   ├── show_build_settings.ts
│   │       │   ├── start_device_log_cap.ts
│   │       │   ├── stop_app_device.ts
│   │       │   ├── stop_device_log_cap.ts
│   │       │   └── test_device.ts
│   │       ├── discovery
│   │       │   ├── __tests__
│   │       │   │   └── discover_tools.test.ts
│   │       │   ├── discover_tools.ts
│   │       │   └── index.ts
│   │       ├── doctor
│   │       │   ├── __tests__
│   │       │   │   ├── doctor.test.ts
│   │       │   │   └── index.test.ts
│   │       │   ├── doctor.ts
│   │       │   ├── index.ts
│   │       │   └── lib
│   │       │       └── doctor.deps.ts
│   │       ├── logging
│   │       │   ├── __tests__
│   │       │   │   ├── index.test.ts
│   │       │   │   ├── start_device_log_cap.test.ts
│   │       │   │   ├── start_sim_log_cap.test.ts
│   │       │   │   ├── stop_device_log_cap.test.ts
│   │       │   │   └── stop_sim_log_cap.test.ts
│   │       │   ├── index.ts
│   │       │   ├── start_device_log_cap.ts
│   │       │   ├── start_sim_log_cap.ts
│   │       │   ├── stop_device_log_cap.ts
│   │       │   └── stop_sim_log_cap.ts
│   │       ├── macos
│   │       │   ├── __tests__
│   │       │   │   ├── build_macos.test.ts
│   │       │   │   ├── build_run_macos.test.ts
│   │       │   │   ├── get_mac_app_path.test.ts
│   │       │   │   ├── index.test.ts
│   │       │   │   ├── launch_mac_app.test.ts
│   │       │   │   ├── re-exports.test.ts
│   │       │   │   ├── stop_mac_app.test.ts
│   │       │   │   └── test_macos.test.ts
│   │       │   ├── build_macos.ts
│   │       │   ├── build_run_macos.ts
│   │       │   ├── clean.ts
│   │       │   ├── discover_projs.ts
│   │       │   ├── get_mac_app_path.ts
│   │       │   ├── get_mac_bundle_id.ts
│   │       │   ├── index.ts
│   │       │   ├── launch_mac_app.ts
│   │       │   ├── list_schemes.ts
│   │       │   ├── show_build_settings.ts
│   │       │   ├── stop_mac_app.ts
│   │       │   └── test_macos.ts
│   │       ├── project-discovery
│   │       │   ├── __tests__
│   │       │   │   ├── discover_projs.test.ts
│   │       │   │   ├── get_app_bundle_id.test.ts
│   │       │   │   ├── get_mac_bundle_id.test.ts
│   │       │   │   ├── index.test.ts
│   │       │   │   ├── list_schemes.test.ts
│   │       │   │   └── show_build_settings.test.ts
│   │       │   ├── discover_projs.ts
│   │       │   ├── get_app_bundle_id.ts
│   │       │   ├── get_mac_bundle_id.ts
│   │       │   ├── index.ts
│   │       │   ├── list_schemes.ts
│   │       │   └── show_build_settings.ts
│   │       ├── project-scaffolding
│   │       │   ├── __tests__
│   │       │   │   ├── index.test.ts
│   │       │   │   ├── scaffold_ios_project.test.ts
│   │       │   │   └── scaffold_macos_project.test.ts
│   │       │   ├── index.ts
│   │       │   ├── scaffold_ios_project.ts
│   │       │   └── scaffold_macos_project.ts
│   │       ├── session-management
│   │       │   ├── __tests__
│   │       │   │   ├── index.test.ts
│   │       │   │   ├── session_clear_defaults.test.ts
│   │       │   │   ├── session_set_defaults.test.ts
│   │       │   │   └── session_show_defaults.test.ts
│   │       │   ├── index.ts
│   │       │   ├── session_clear_defaults.ts
│   │       │   ├── session_set_defaults.ts
│   │       │   └── session_show_defaults.ts
│   │       ├── simulator
│   │       │   ├── __tests__
│   │       │   │   ├── boot_sim.test.ts
│   │       │   │   ├── build_run_sim.test.ts
│   │       │   │   ├── build_sim.test.ts
│   │       │   │   ├── get_sim_app_path.test.ts
│   │       │   │   ├── index.test.ts
│   │       │   │   ├── install_app_sim.test.ts
│   │       │   │   ├── launch_app_logs_sim.test.ts
│   │       │   │   ├── launch_app_sim.test.ts
│   │       │   │   ├── list_sims.test.ts
│   │       │   │   ├── open_sim.test.ts
│   │       │   │   ├── record_sim_video.test.ts
│   │       │   │   ├── screenshot.test.ts
│   │       │   │   ├── stop_app_sim.test.ts
│   │       │   │   └── test_sim.test.ts
│   │       │   ├── boot_sim.ts
│   │       │   ├── build_run_sim.ts
│   │       │   ├── build_sim.ts
│   │       │   ├── clean.ts
│   │       │   ├── describe_ui.ts
│   │       │   ├── discover_projs.ts
│   │       │   ├── get_app_bundle_id.ts
│   │       │   ├── get_sim_app_path.ts
│   │       │   ├── index.ts
│   │       │   ├── install_app_sim.ts
│   │       │   ├── launch_app_logs_sim.ts
│   │       │   ├── launch_app_sim.ts
│   │       │   ├── list_schemes.ts
│   │       │   ├── list_sims.ts
│   │       │   ├── open_sim.ts
│   │       │   ├── record_sim_video.ts
│   │       │   ├── screenshot.ts
│   │       │   ├── show_build_settings.ts
│   │       │   ├── stop_app_sim.ts
│   │       │   └── test_sim.ts
│   │       ├── simulator-management
│   │       │   ├── __tests__
│   │       │   │   ├── erase_sims.test.ts
│   │       │   │   ├── index.test.ts
│   │       │   │   ├── reset_sim_location.test.ts
│   │       │   │   ├── set_sim_appearance.test.ts
│   │       │   │   ├── set_sim_location.test.ts
│   │       │   │   └── sim_statusbar.test.ts
│   │       │   ├── boot_sim.ts
│   │       │   ├── erase_sims.ts
│   │       │   ├── index.ts
│   │       │   ├── list_sims.ts
│   │       │   ├── open_sim.ts
│   │       │   ├── reset_sim_location.ts
│   │       │   ├── set_sim_appearance.ts
│   │       │   ├── set_sim_location.ts
│   │       │   └── sim_statusbar.ts
│   │       ├── swift-package
│   │       │   ├── __tests__
│   │       │   │   ├── active-processes.test.ts
│   │       │   │   ├── index.test.ts
│   │       │   │   ├── swift_package_build.test.ts
│   │       │   │   ├── swift_package_clean.test.ts
│   │       │   │   ├── swift_package_list.test.ts
│   │       │   │   ├── swift_package_run.test.ts
│   │       │   │   ├── swift_package_stop.test.ts
│   │       │   │   └── swift_package_test.test.ts
│   │       │   ├── active-processes.ts
│   │       │   ├── index.ts
│   │       │   ├── swift_package_build.ts
│   │       │   ├── swift_package_clean.ts
│   │       │   ├── swift_package_list.ts
│   │       │   ├── swift_package_run.ts
│   │       │   ├── swift_package_stop.ts
│   │       │   └── swift_package_test.ts
│   │       ├── ui-testing
│   │       │   ├── __tests__
│   │       │   │   ├── button.test.ts
│   │       │   │   ├── describe_ui.test.ts
│   │       │   │   ├── gesture.test.ts
│   │       │   │   ├── index.test.ts
│   │       │   │   ├── key_press.test.ts
│   │       │   │   ├── key_sequence.test.ts
│   │       │   │   ├── long_press.test.ts
│   │       │   │   ├── screenshot.test.ts
│   │       │   │   ├── swipe.test.ts
│   │       │   │   ├── tap.test.ts
│   │       │   │   ├── touch.test.ts
│   │       │   │   └── type_text.test.ts
│   │       │   ├── button.ts
│   │       │   ├── describe_ui.ts
│   │       │   ├── gesture.ts
│   │       │   ├── index.ts
│   │       │   ├── key_press.ts
│   │       │   ├── key_sequence.ts
│   │       │   ├── long_press.ts
│   │       │   ├── screenshot.ts
│   │       │   ├── swipe.ts
│   │       │   ├── tap.ts
│   │       │   ├── touch.ts
│   │       │   └── type_text.ts
│   │       └── utilities
│   │           ├── __tests__
│   │           │   ├── clean.test.ts
│   │           │   └── index.test.ts
│   │           ├── clean.ts
│   │           └── index.ts
│   ├── server
│   │   └── server.ts
│   ├── test-utils
│   │   └── mock-executors.ts
│   ├── types
│   │   └── common.ts
│   └── utils
│       ├── __tests__
│       │   ├── build-utils.test.ts
│       │   ├── environment.test.ts
│       │   ├── session-aware-tool-factory.test.ts
│       │   ├── session-store.test.ts
│       │   ├── simulator-utils.test.ts
│       │   ├── test-runner-env-integration.test.ts
│       │   └── typed-tool-factory.test.ts
│       ├── axe
│       │   └── index.ts
│       ├── axe-helpers.ts
│       ├── build
│       │   └── index.ts
│       ├── build-utils.ts
│       ├── capabilities.ts
│       ├── command.ts
│       ├── CommandExecutor.ts
│       ├── environment.ts
│       ├── errors.ts
│       ├── execution
│       │   └── index.ts
│       ├── FileSystemExecutor.ts
│       ├── log_capture.ts
│       ├── log-capture
│       │   └── index.ts
│       ├── logger.ts
│       ├── logging
│       │   └── index.ts
│       ├── plugin-registry
│       │   └── index.ts
│       ├── responses
│       │   └── index.ts
│       ├── schema-helpers.ts
│       ├── sentry.ts
│       ├── session-store.ts
│       ├── simulator-utils.ts
│       ├── template
│       │   └── index.ts
│       ├── template-manager.ts
│       ├── test
│       │   └── index.ts
│       ├── test-common.ts
│       ├── tool-registry.ts
│       ├── typed-tool-factory.ts
│       ├── validation
│       │   └── index.ts
│       ├── validation.ts
│       ├── version
│       │   └── index.ts
│       ├── video_capture.ts
│       ├── video-capture
│       │   └── index.ts
│       ├── xcode.ts
│       ├── xcodemake
│       │   └── index.ts
│       └── xcodemake.ts
├── tsconfig.json
├── tsconfig.test.json
├── tsup.config.ts
└── vitest.config.ts
```

# Files

--------------------------------------------------------------------------------
/eslint.config.js:
--------------------------------------------------------------------------------

```javascript
import eslint from '@eslint/js';
import tseslint from 'typescript-eslint';
import prettierPlugin from 'eslint-plugin-prettier';

export default [
  eslint.configs.recommended,
  ...tseslint.configs.recommended,
  {
    ignores: ['node_modules/**', 'build/**', 'dist/**', 'coverage/**', 'src/core/generated-plugins.ts', 'src/core/generated-resources.ts'],
  },
  {
    // TypeScript files in src/ directory (covered by tsconfig.json)
    files: ['src/**/*.ts'],
    languageOptions: {
      ecmaVersion: 2020,
      sourceType: 'module',
      parser: tseslint.parser,
      parserOptions: {
        project: ['./tsconfig.json'],
      },
    },
    plugins: {
      '@typescript-eslint': tseslint.plugin,
      'prettier': prettierPlugin,
    },
    rules: {
      'prettier/prettier': 'error',
      '@typescript-eslint/explicit-function-return-type': 'warn',
      '@typescript-eslint/no-explicit-any': 'error',
      '@typescript-eslint/no-unused-vars': ['error', { 
        argsIgnorePattern: 'never',
        varsIgnorePattern: 'never' 
      }],
      'no-console': ['warn', { allow: ['error'] }],
      
      // Prevent dangerous type casting anti-patterns (errors)
      '@typescript-eslint/consistent-type-assertions': ['error', {
        assertionStyle: 'as',
        objectLiteralTypeAssertions: 'never'
      }],
      '@typescript-eslint/no-unsafe-argument': 'error',
      '@typescript-eslint/no-unsafe-assignment': 'error',
      '@typescript-eslint/no-unsafe-call': 'error',
      '@typescript-eslint/no-unsafe-member-access': 'error',
      '@typescript-eslint/no-unsafe-return': 'error',
      
      // Prevent specific anti-patterns we found
      '@typescript-eslint/ban-ts-comment': ['error', {
        'ts-expect-error': 'allow-with-description',
        'ts-ignore': true,
        'ts-nocheck': true,
        'ts-check': false,
      }],
      
      // Encourage best practices (warnings - can be gradually fixed)
      '@typescript-eslint/prefer-as-const': 'warn',
      '@typescript-eslint/prefer-nullish-coalescing': 'warn',
      '@typescript-eslint/prefer-optional-chain': 'warn',
      
      // Prevent barrel imports to maintain architectural improvements
      'no-restricted-imports': ['error', {
        patterns: [
          {
            group: ['**/utils/index.js', '../utils/index.js', '../../utils/index.js', '../../../utils/index.js', '**/utils/index.ts', '../utils/index.ts', '../../utils/index.ts', '../../../utils/index.ts'],
            message: 'Barrel imports from utils/index are prohibited. Use focused facade imports instead (e.g., utils/logging/index.ts, utils/execution/index.ts).'
          },
          {
            group: ['./**/*.js', '../**/*.js'],
            message: 'Import TypeScript files with .ts extension, not .js. This ensures compatibility with native TypeScript runtimes like Bun and Deno. Change .js to .ts in your import path.'
          }
        ]
      }],
    },
  },
  {
    // JavaScript and TypeScript files outside the main project (scripts/, etc.)
    files: ['**/*.{js,ts}'],
    ignores: ['src/**/*', '**/*.test.ts'],
    languageOptions: {
      ecmaVersion: 2020,
      sourceType: 'module',
      parser: tseslint.parser,
      // No project reference for scripts - use standalone parsing
    },
    plugins: {
      '@typescript-eslint': tseslint.plugin,
      'prettier': prettierPlugin,
    },
    rules: {
      'prettier/prettier': 'error',
      // Relaxed TypeScript rules for scripts since they're not in the main project
      '@typescript-eslint/explicit-function-return-type': 'off',
      '@typescript-eslint/no-explicit-any': 'warn',
      '@typescript-eslint/no-unused-vars': ['warn', { 
        argsIgnorePattern: 'never',
        varsIgnorePattern: 'never' 
      }],
      'no-console': 'off', // Scripts are allowed to use console
      
      // Disable project-dependent rules for scripts
      '@typescript-eslint/no-unsafe-argument': 'off',
      '@typescript-eslint/no-unsafe-assignment': 'off',
      '@typescript-eslint/no-unsafe-call': 'off',
      '@typescript-eslint/no-unsafe-member-access': 'off',
      '@typescript-eslint/no-unsafe-return': 'off',
      '@typescript-eslint/prefer-nullish-coalescing': 'off',
      '@typescript-eslint/prefer-optional-chain': 'off',
    },
  },
  {
    files: ['**/*.test.ts'],
    languageOptions: {
      parser: tseslint.parser,
      parserOptions: {
        project: './tsconfig.test.json',
      },
    },
    rules: {
      '@typescript-eslint/no-explicit-any': 'off',
      '@typescript-eslint/no-unused-vars': 'off',
      '@typescript-eslint/explicit-function-return-type': 'off',
      'prefer-const': 'off',
      
      // Relax unsafe rules for tests - tests often need more flexibility
      '@typescript-eslint/no-unsafe-argument': 'off',
      '@typescript-eslint/no-unsafe-assignment': 'off',
      '@typescript-eslint/no-unsafe-call': 'off',
      '@typescript-eslint/no-unsafe-member-access': 'off',
      '@typescript-eslint/no-unsafe-return': 'off',
    },
  },
];

```

--------------------------------------------------------------------------------
/src/mcp/tools/simulator/stop_app_sim.ts:
--------------------------------------------------------------------------------

```typescript
import { z } from 'zod';
import { ToolResponse } from '../../../types/common.ts';
import { log } from '../../../utils/logging/index.ts';
import type { CommandExecutor } from '../../../utils/execution/index.ts';
import { getDefaultCommandExecutor } from '../../../utils/execution/index.ts';
import { nullifyEmptyStrings } from '../../../utils/schema-helpers.ts';
import { createSessionAwareTool } from '../../../utils/typed-tool-factory.ts';

const baseSchemaObject = z.object({
  simulatorId: z
    .string()
    .optional()
    .describe(
      'UUID of the simulator to use (obtained from list_sims). Provide EITHER this OR simulatorName, not both',
    ),
  simulatorName: z
    .string()
    .optional()
    .describe(
      "Name of the simulator (e.g., 'iPhone 16'). Provide EITHER this OR simulatorId, not both",
    ),
  bundleId: z.string().describe("Bundle identifier of the app to stop (e.g., 'com.example.MyApp')"),
});

const baseSchema = z.preprocess(nullifyEmptyStrings, baseSchemaObject);

const stopAppSimSchema = baseSchema
  .refine((val) => val.simulatorId !== undefined || val.simulatorName !== undefined, {
    message: 'Either simulatorId or simulatorName is required.',
  })
  .refine((val) => !(val.simulatorId !== undefined && val.simulatorName !== undefined), {
    message: 'simulatorId and simulatorName are mutually exclusive. Provide only one.',
  });

export type StopAppSimParams = z.infer<typeof stopAppSimSchema>;

export async function stop_app_simLogic(
  params: StopAppSimParams,
  executor: CommandExecutor,
): Promise<ToolResponse> {
  let simulatorId = params.simulatorId;
  let simulatorDisplayName = simulatorId ?? '';

  if (params.simulatorName && !simulatorId) {
    log('info', `Looking up simulator by name: ${params.simulatorName}`);

    const simulatorListResult = await executor(
      ['xcrun', 'simctl', 'list', 'devices', 'available', '--json'],
      'List Simulators',
      true,
    );
    if (!simulatorListResult.success) {
      return {
        content: [
          {
            type: 'text',
            text: `Failed to list simulators: ${simulatorListResult.error}`,
          },
        ],
        isError: true,
      };
    }

    const simulatorsData = JSON.parse(simulatorListResult.output) as {
      devices: Record<string, Array<{ udid: string; name: string }>>;
    };

    let foundSimulator: { udid: string; name: string } | null = null;
    for (const runtime in simulatorsData.devices) {
      const devices = simulatorsData.devices[runtime];
      const simulator = devices.find((device) => device.name === params.simulatorName);
      if (simulator) {
        foundSimulator = simulator;
        break;
      }
    }

    if (!foundSimulator) {
      return {
        content: [
          {
            type: 'text',
            text: `Simulator named "${params.simulatorName}" not found. Use list_sims to see available simulators.`,
          },
        ],
        isError: true,
      };
    }

    simulatorId = foundSimulator.udid;
    simulatorDisplayName = `"${params.simulatorName}" (${foundSimulator.udid})`;
  }

  if (!simulatorId) {
    return {
      content: [
        {
          type: 'text',
          text: 'No simulator identifier provided',
        },
      ],
      isError: true,
    };
  }

  log('info', `Stopping app ${params.bundleId} in simulator ${simulatorId}`);

  try {
    const command = ['xcrun', 'simctl', 'terminate', simulatorId, params.bundleId];
    const result = await executor(command, 'Stop App in Simulator', true, undefined);

    if (!result.success) {
      return {
        content: [
          {
            type: 'text',
            text: `Stop app in simulator operation failed: ${result.error}`,
          },
        ],
        isError: true,
      };
    }

    return {
      content: [
        {
          type: 'text',
          text: `✅ App ${params.bundleId} stopped successfully in simulator ${
            simulatorDisplayName || simulatorId
          }`,
        },
      ],
    };
  } catch (error) {
    const errorMessage = error instanceof Error ? error.message : String(error);
    log('error', `Error stopping app in simulator: ${errorMessage}`);
    return {
      content: [
        {
          type: 'text',
          text: `Stop app in simulator operation failed: ${errorMessage}`,
        },
      ],
      isError: true,
    };
  }
}

const publicSchemaObject = baseSchemaObject.omit({
  simulatorId: true,
  simulatorName: true,
} as const);

export default {
  name: 'stop_app_sim',
  description: 'Stops an app running in an iOS simulator.',
  schema: publicSchemaObject.shape,
  handler: createSessionAwareTool<StopAppSimParams>({
    internalSchema: stopAppSimSchema as unknown as z.ZodType<StopAppSimParams>,
    logicFunction: stop_app_simLogic,
    getExecutor: getDefaultCommandExecutor,
    requirements: [
      { oneOf: ['simulatorId', 'simulatorName'], message: 'Provide simulatorId or simulatorName' },
    ],
    exclusivePairs: [['simulatorId', 'simulatorName']],
  }),
};

```

--------------------------------------------------------------------------------
/src/mcp/tools/ui-testing/button.ts:
--------------------------------------------------------------------------------

```typescript
import { z } from 'zod';
import type { ToolResponse } from '../../../types/common.ts';
import { log } from '../../../utils/logging/index.ts';
import { createTextResponse, createErrorResponse } from '../../../utils/responses/index.ts';
import type { CommandExecutor } from '../../../utils/execution/index.ts';
import { getDefaultCommandExecutor } from '../../../utils/execution/index.ts';
import {
  createAxeNotAvailableResponse,
  getAxePath,
  getBundledAxeEnvironment,
} from '../../../utils/axe-helpers.ts';
import { DependencyError, AxeError, SystemError } from '../../../utils/errors.ts';
import { createTypedTool } from '../../../utils/typed-tool-factory.ts';

// Define schema as ZodObject
const buttonSchema = z.object({
  simulatorUuid: z.string().uuid('Invalid Simulator UUID format'),
  buttonType: z.enum(['apple-pay', 'home', 'lock', 'side-button', 'siri']),
  duration: z.number().min(0, 'Duration must be non-negative').optional(),
});

// Use z.infer for type safety
type ButtonParams = z.infer<typeof buttonSchema>;

export interface AxeHelpers {
  getAxePath: () => string | null;
  getBundledAxeEnvironment: () => Record<string, string>;
  createAxeNotAvailableResponse: () => ToolResponse;
}

const LOG_PREFIX = '[AXe]';

export async function buttonLogic(
  params: ButtonParams,
  executor: CommandExecutor,
  axeHelpers: AxeHelpers = {
    getAxePath,
    getBundledAxeEnvironment,
    createAxeNotAvailableResponse,
  },
): Promise<ToolResponse> {
  const toolName = 'button';
  const { simulatorUuid, buttonType, duration } = params;
  const commandArgs = ['button', buttonType];
  if (duration !== undefined) {
    commandArgs.push('--duration', String(duration));
  }

  log('info', `${LOG_PREFIX}/${toolName}: Starting ${buttonType} button press on ${simulatorUuid}`);

  try {
    await executeAxeCommand(commandArgs, simulatorUuid, 'button', executor, axeHelpers);
    log('info', `${LOG_PREFIX}/${toolName}: Success for ${simulatorUuid}`);
    return createTextResponse(`Hardware button '${buttonType}' pressed successfully.`);
  } catch (error) {
    log('error', `${LOG_PREFIX}/${toolName}: Failed - ${error}`);
    if (error instanceof DependencyError) {
      return axeHelpers.createAxeNotAvailableResponse();
    } else if (error instanceof AxeError) {
      return createErrorResponse(
        `Failed to press button '${buttonType}': ${error.message}`,
        error.axeOutput,
      );
    } else if (error instanceof SystemError) {
      return createErrorResponse(
        `System error executing axe: ${error.message}`,
        error.originalError?.stack,
      );
    }
    return createErrorResponse(
      `An unexpected error occurred: ${error instanceof Error ? error.message : String(error)}`,
    );
  }
}

export default {
  name: 'button',
  description:
    'Press hardware button on iOS simulator. Supported buttons: apple-pay, home, lock, side-button, siri',
  schema: buttonSchema.shape, // MCP SDK compatibility
  handler: createTypedTool(
    buttonSchema,
    (params: ButtonParams, executor: CommandExecutor) => {
      return buttonLogic(params, executor, {
        getAxePath,
        getBundledAxeEnvironment,
        createAxeNotAvailableResponse,
      });
    },
    getDefaultCommandExecutor,
  ),
};

// Helper function for executing axe commands (inlined from src/tools/axe/index.ts)
async function executeAxeCommand(
  commandArgs: string[],
  simulatorUuid: string,
  commandName: string,
  executor: CommandExecutor = getDefaultCommandExecutor(),
  axeHelpers: AxeHelpers = { getAxePath, getBundledAxeEnvironment, createAxeNotAvailableResponse },
): Promise<void> {
  // Get the appropriate axe binary path
  const axeBinary = axeHelpers.getAxePath();
  if (!axeBinary) {
    throw new DependencyError('AXe binary not found');
  }

  // Add --udid parameter to all commands
  const fullArgs = [...commandArgs, '--udid', simulatorUuid];

  // Construct the full command array with the axe binary as the first element
  const fullCommand = [axeBinary, ...fullArgs];

  try {
    // Determine environment variables for bundled AXe
    const axeEnv = axeBinary !== 'axe' ? axeHelpers.getBundledAxeEnvironment() : undefined;

    const result = await executor(fullCommand, `${LOG_PREFIX}: ${commandName}`, false, axeEnv);

    if (!result.success) {
      throw new AxeError(
        `axe command '${commandName}' failed.`,
        commandName,
        result.error ?? result.output,
        simulatorUuid,
      );
    }

    // Check for stderr output in successful commands
    if (result.error) {
      log(
        'warn',
        `${LOG_PREFIX}: Command '${commandName}' produced stderr output but exited successfully. Output: ${result.error}`,
      );
    }

    // Function now returns void - the calling code creates its own response
  } catch (error) {
    if (error instanceof Error) {
      if (error instanceof AxeError) {
        throw error;
      }

      // Otherwise wrap it in a SystemError
      throw new SystemError(`Failed to execute axe command: ${error.message}`, error);
    }

    // For any other type of error
    throw new SystemError(`Failed to execute axe command: ${String(error)}`);
  }
}

```

--------------------------------------------------------------------------------
/src/mcp/tools/ui-testing/key_press.ts:
--------------------------------------------------------------------------------

```typescript
import { z } from 'zod';
import { ToolResponse } from '../../../types/common.ts';
import { log } from '../../../utils/logging/index.ts';
import {
  createTextResponse,
  createErrorResponse,
  DependencyError,
  AxeError,
  SystemError,
} from '../../../utils/responses/index.ts';
import type { CommandExecutor } from '../../../utils/execution/index.ts';
import { getDefaultCommandExecutor } from '../../../utils/execution/index.ts';
import {
  createAxeNotAvailableResponse,
  getAxePath,
  getBundledAxeEnvironment,
} from '../../../utils/axe/index.ts';
import { createTypedTool } from '../../../utils/typed-tool-factory.ts';

// Define schema as ZodObject
const keyPressSchema = z.object({
  simulatorUuid: z.string().uuid('Invalid Simulator UUID format'),
  keyCode: z.number().int('HID keycode to press (0-255)').min(0).max(255),
  duration: z.number().min(0, 'Duration must be non-negative').optional(),
});

// Use z.infer for type safety
type KeyPressParams = z.infer<typeof keyPressSchema>;

export interface AxeHelpers {
  getAxePath: () => string | null;
  getBundledAxeEnvironment: () => Record<string, string>;
  createAxeNotAvailableResponse: () => ToolResponse;
}

const LOG_PREFIX = '[AXe]';

export async function key_pressLogic(
  params: KeyPressParams,
  executor: CommandExecutor,
  axeHelpers: AxeHelpers = {
    getAxePath,
    getBundledAxeEnvironment,
    createAxeNotAvailableResponse,
  },
): Promise<ToolResponse> {
  const toolName = 'key_press';
  const { simulatorUuid, keyCode, duration } = params;
  const commandArgs = ['key', String(keyCode)];
  if (duration !== undefined) {
    commandArgs.push('--duration', String(duration));
  }

  log('info', `${LOG_PREFIX}/${toolName}: Starting key press ${keyCode} on ${simulatorUuid}`);

  try {
    await executeAxeCommand(commandArgs, simulatorUuid, 'key', executor, axeHelpers);
    log('info', `${LOG_PREFIX}/${toolName}: Success for ${simulatorUuid}`);
    return createTextResponse(`Key press (code: ${keyCode}) simulated successfully.`);
  } catch (error) {
    log('error', `${LOG_PREFIX}/${toolName}: Failed - ${error}`);
    if (error instanceof DependencyError) {
      return axeHelpers.createAxeNotAvailableResponse();
    } else if (error instanceof AxeError) {
      return createErrorResponse(
        `Failed to simulate key press (code: ${keyCode}): ${error.message}`,
        error.axeOutput,
      );
    } else if (error instanceof SystemError) {
      return createErrorResponse(
        `System error executing axe: ${error.message}`,
        error.originalError?.stack,
      );
    }
    return createErrorResponse(
      `An unexpected error occurred: ${error instanceof Error ? error.message : String(error)}`,
    );
  }
}

export default {
  name: 'key_press',
  description:
    'Press a single key by keycode on the simulator. Common keycodes: 40=Return, 42=Backspace, 43=Tab, 44=Space, 58-67=F1-F10.',
  schema: keyPressSchema.shape, // MCP SDK compatibility
  handler: createTypedTool(
    keyPressSchema,
    (params: KeyPressParams, executor: CommandExecutor) => {
      return key_pressLogic(params, executor, {
        getAxePath,
        getBundledAxeEnvironment,
        createAxeNotAvailableResponse,
      });
    },
    getDefaultCommandExecutor,
  ),
};

// Helper function for executing axe commands (inlined from src/tools/axe/index.ts)
async function executeAxeCommand(
  commandArgs: string[],
  simulatorUuid: string,
  commandName: string,
  executor: CommandExecutor = getDefaultCommandExecutor(),
  axeHelpers: AxeHelpers = { getAxePath, getBundledAxeEnvironment, createAxeNotAvailableResponse },
): Promise<void> {
  // Get the appropriate axe binary path
  const axeBinary = axeHelpers.getAxePath();
  if (!axeBinary) {
    throw new DependencyError('AXe binary not found');
  }

  // Add --udid parameter to all commands
  const fullArgs = [...commandArgs, '--udid', simulatorUuid];

  // Construct the full command array with the axe binary as the first element
  const fullCommand = [axeBinary, ...fullArgs];

  try {
    // Determine environment variables for bundled AXe
    const axeEnv = axeBinary !== 'axe' ? axeHelpers.getBundledAxeEnvironment() : undefined;

    const result = await executor(fullCommand, `${LOG_PREFIX}: ${commandName}`, false, axeEnv);

    if (!result.success) {
      throw new AxeError(
        `axe command '${commandName}' failed.`,
        commandName,
        result.error ?? result.output,
        simulatorUuid,
      );
    }

    // Check for stderr output in successful commands
    if (result.error) {
      log(
        'warn',
        `${LOG_PREFIX}: Command '${commandName}' produced stderr output but exited successfully. Output: ${result.error}`,
      );
    }

    // Function now returns void - the calling code creates its own response
  } catch (error) {
    if (error instanceof Error) {
      if (error instanceof AxeError) {
        throw error;
      }

      // Otherwise wrap it in a SystemError
      throw new SystemError(`Failed to execute axe command: ${error.message}`, error);
    }

    // For any other type of error
    throw new SystemError(`Failed to execute axe command: ${String(error)}`);
  }
}

```

--------------------------------------------------------------------------------
/src/utils/template-manager.ts:
--------------------------------------------------------------------------------

```typescript
import { join } from 'path';
import { tmpdir } from 'os';
import { randomUUID } from 'crypto';
import { log } from './logger.ts';
import { iOSTemplateVersion, macOSTemplateVersion } from '../version.ts';
import { CommandExecutor } from './command.ts';
import { FileSystemExecutor } from './FileSystemExecutor.ts';

/**
 * Template manager for downloading and managing project templates
 */
export class TemplateManager {
  private static readonly GITHUB_ORG = 'cameroncooke';
  private static readonly IOS_TEMPLATE_REPO = 'XcodeBuildMCP-iOS-Template';
  private static readonly MACOS_TEMPLATE_REPO = 'XcodeBuildMCP-macOS-Template';

  /**
   * Get the template path for a specific platform
   * Checks for local override via environment variable first
   */
  static async getTemplatePath(
    platform: 'iOS' | 'macOS',
    commandExecutor: CommandExecutor,
    fileSystemExecutor: FileSystemExecutor,
  ): Promise<string> {
    // Check for local override
    const envVar =
      platform === 'iOS' ? 'XCODEBUILDMCP_IOS_TEMPLATE_PATH' : 'XCODEBUILDMCP_MACOS_TEMPLATE_PATH';

    const localPath = process.env[envVar];
    log('debug', `[TemplateManager] Checking env var '${envVar}'. Value: '${localPath}'`);

    if (localPath) {
      const pathExists = fileSystemExecutor.existsSync(localPath);
      log('debug', `[TemplateManager] Env var set. Path '${localPath}' exists? ${pathExists}`);
      if (pathExists) {
        const templateSubdir = join(localPath, 'template');
        const subdirExists = fileSystemExecutor.existsSync(templateSubdir);
        log(
          'debug',
          `[TemplateManager] Checking for subdir '${templateSubdir}'. Exists? ${subdirExists}`,
        );
        if (subdirExists) {
          log('info', `Using local ${platform} template from: ${templateSubdir}`);
          return templateSubdir;
        } else {
          log('info', `Template directory not found in ${localPath}, using GitHub release`);
        }
      }
    }

    log('debug', '[TemplateManager] Env var not set or path invalid, proceeding to download.');
    // Download from GitHub release
    return await this.downloadTemplate(platform, commandExecutor, fileSystemExecutor);
  }

  /**
   * Download template from GitHub release
   */
  private static async downloadTemplate(
    platform: 'iOS' | 'macOS',
    commandExecutor: CommandExecutor,
    fileSystemExecutor: FileSystemExecutor,
  ): Promise<string> {
    const repo = platform === 'iOS' ? this.IOS_TEMPLATE_REPO : this.MACOS_TEMPLATE_REPO;
    const defaultVersion = platform === 'iOS' ? iOSTemplateVersion : macOSTemplateVersion;
    const envVarName =
      platform === 'iOS'
        ? 'XCODEBUILD_MCP_IOS_TEMPLATE_VERSION'
        : 'XCODEBUILD_MCP_MACOS_TEMPLATE_VERSION';
    const version = String(
      process.env[envVarName] ?? process.env.XCODEBUILD_MCP_TEMPLATE_VERSION ?? defaultVersion,
    );

    // Create temp directory for download
    const tempDir = join(tmpdir(), `xcodebuild-mcp-template-${randomUUID()}`);
    await fileSystemExecutor.mkdir(tempDir, { recursive: true });

    try {
      const downloadUrl = `https://github.com/${this.GITHUB_ORG}/${repo}/releases/download/${version}/${repo}-${version.substring(1)}.zip`;
      const zipPath = join(tempDir, 'template.zip');

      log('info', `Downloading ${platform} template ${version} from GitHub...`);
      log('info', `Download URL: ${downloadUrl}`);

      // Download the release artifact
      const curlResult = await commandExecutor(
        ['curl', '-L', '-f', '-o', zipPath, downloadUrl],
        'Download Template',
        true,
        undefined,
      );

      if (!curlResult.success) {
        throw new Error(`Failed to download template: ${curlResult.error}`);
      }

      // Extract the zip file
      // Temporarily change to temp directory for extraction
      const originalCwd = process.cwd();
      try {
        process.chdir(tempDir);
        const unzipResult = await commandExecutor(
          ['unzip', '-q', zipPath],
          'Extract Template',
          true,
          undefined,
        );

        if (!unzipResult.success) {
          throw new Error(`Failed to extract template: ${unzipResult.error}`);
        }
      } finally {
        process.chdir(originalCwd);
      }

      // Find the extracted directory and return the template subdirectory
      const extractedDir = join(tempDir, `${repo}-${version.substring(1)}`);
      if (!fileSystemExecutor.existsSync(extractedDir)) {
        throw new Error(`Expected template directory not found: ${extractedDir}`);
      }

      log('info', `Successfully downloaded ${platform} template ${version}`);
      return extractedDir;
    } catch (error) {
      // Clean up on error
      log('error', `Failed to download ${platform} template ${version}: ${error}`);
      await this.cleanup(tempDir, fileSystemExecutor);
      throw error;
    }
  }

  /**
   * Clean up downloaded template directory
   */
  static async cleanup(
    templatePath: string,
    fileSystemExecutor: FileSystemExecutor,
  ): Promise<void> {
    // Only clean up if it's in temp directory
    if (templatePath.startsWith(tmpdir())) {
      await fileSystemExecutor.rm(templatePath, { recursive: true, force: true });
    }
  }
}

```

--------------------------------------------------------------------------------
/docs/ESLINT_TYPE_SAFETY.md:
--------------------------------------------------------------------------------

```markdown
# ESLint Type Safety Rules

This document explains the ESLint rules added to prevent TypeScript anti-patterns and improve type safety.

## Rules Added

### Error-Level Rules (Block CI/Deployment)

These rules prevent dangerous type casting patterns that can lead to runtime errors:

#### `@typescript-eslint/consistent-type-assertions`
- **Purpose**: Prevents dangerous object literal type assertions
- **Example**: Prevents `{ foo: 'bar' } as ComplexType`
- **Rationale**: Object literal assertions can hide missing properties

#### `@typescript-eslint/no-unsafe-*` (5 rules)
- **no-unsafe-argument**: Prevents passing `any` to typed parameters
- **no-unsafe-assignment**: Prevents assigning `any` to typed variables  
- **no-unsafe-call**: Prevents calling `any` as a function
- **no-unsafe-member-access**: Prevents accessing properties on `any`
- **no-unsafe-return**: Prevents returning `any` from typed functions

**Example of prevented anti-pattern:**
```typescript
// ❌ BAD - This would now be an ESLint error
function handleParams(args: Record<string, unknown>) {
  const typedParams = args as MyToolParams; // Unsafe casting
  return typedParams.someProperty as string; // Unsafe member access
}

// ✅ GOOD - Proper validation approach
function handleParams(args: Record<string, unknown>) {
  const typedParams = MyToolParamsSchema.parse(args); // Runtime validation
  return typedParams.someProperty; // Type-safe access
}
```

#### `@typescript-eslint/ban-ts-comment`
- **Purpose**: Prevents unsafe TypeScript comments
- **Blocks**: `@ts-ignore`, `@ts-nocheck`
- **Allows**: `@ts-expect-error` (with description)

### Warning-Level Rules (Encourage Best Practices)

These rules encourage modern TypeScript patterns but don't block builds:

#### `@typescript-eslint/prefer-nullish-coalescing`
- **Purpose**: Prefer `??` over `||` for default values
- **Example**: `value ?? 'default'` instead of `value || 'default'`
- **Rationale**: More precise handling of falsy values (0, '', false)

#### `@typescript-eslint/prefer-optional-chain`
- **Purpose**: Prefer `?.` for safe property access
- **Example**: `obj?.prop` instead of `obj && obj.prop`
- **Rationale**: More concise and readable

#### `@typescript-eslint/prefer-as-const`
- **Purpose**: Prefer `as const` for literal types
- **Example**: `['a', 'b'] as const` instead of `['a', 'b'] as string[]`

## Test File Exceptions

Test files (`.test.ts`) have relaxed rules for flexibility:
- All `no-unsafe-*` rules are disabled
- `no-explicit-any` is disabled
- Tests often need to test error conditions and edge cases

## Impact on Codebase

### Current Status (Post-Implementation)
- **387 total issues detected**
  - **207 errors**: Require fixing for type safety
  - **180 warnings**: Can be gradually improved

### Gradual Migration Strategy

1. **Phase 1** (Immediate): Error-level rules prevent new anti-patterns
2. **Phase 2** (Ongoing): Gradually fix warning-level violations
3. **Phase 3** (Future): Consider promoting warnings to errors

### Benefits

1. **Prevents Regression**: New code can't introduce the anti-patterns we just fixed
2. **Runtime Safety**: Catches potential runtime errors at compile time  
3. **Code Quality**: Encourages modern TypeScript best practices
4. **Developer Experience**: Better IDE support and autocomplete

## Related Issues Fixed

These rules prevent the specific anti-patterns identified in PR review:

1. **✅ Type Casting in Parameters**: `args as SomeType` patterns now flagged
2. **✅ Unsafe Property Access**: `params.field as string` patterns prevented
3. **✅ Missing Validation**: Encourages schema validation over casting
4. **✅ Return Type Mismatches**: Function signature inconsistencies caught
5. **✅ Nullish Coalescing**: Promotes safer default value handling

## Agent Orchestration for ESLint Fixes

### Parallel Agent Strategy

When fixing ESLint issues across the codebase:

1. **Deploy Multiple Agents**: Run agents in parallel on different files
2. **Single File Focus**: Each agent works on ONE tool file at a time
3. **Individual Linting**: Agents run `npm run lint path/to/single/file.ts` only
4. **Immediate Commits**: Commit each agent's work as soon as they complete
5. **Never Wait**: Don't wait for all agents to finish before committing
6. **Avoid Full Linting**: Never run `npm run lint` without a file path (eats context)
7. **Progress Tracking**: Update todo list and periodically check overall status
8. **Loop Until Done**: Keep deploying agents until all issues are resolved

### Example Commands for Agents

```bash
# Single file linting (what agents should run)
npm run lint src/mcp/tools/device-project/test_device_proj.ts

# NOT this (too much context)
npm run lint
```

### Commit Strategy

- **Individual commits**: One commit per agent completion
- **Clear messages**: `fix: resolve ESLint errors in tool_name.ts`
- **Never batch**: Don't wait to commit multiple files together
- **Progress preservation**: Each fix is immediately saved

## Future Improvements

Consider adding these rules in future iterations:

- `@typescript-eslint/strict-boolean-expressions`: Stricter boolean logic
- `@typescript-eslint/prefer-reduce-type-parameter`: Better generic usage
- `@typescript-eslint/switch-exhaustiveness-check`: Complete switch statements
```

--------------------------------------------------------------------------------
/src/mcp/tools/ui-testing/key_sequence.ts:
--------------------------------------------------------------------------------

```typescript
/**
 * UI Testing Plugin: Key Sequence
 *
 * Press key sequence using HID keycodes on iOS simulator with configurable delay.
 */

import { z } from 'zod';
import { ToolResponse } from '../../../types/common.ts';
import { log } from '../../../utils/logging/index.ts';
import {
  createTextResponse,
  createErrorResponse,
  DependencyError,
  AxeError,
  SystemError,
} from '../../../utils/responses/index.ts';
import type { CommandExecutor } from '../../../utils/execution/index.ts';
import { getDefaultCommandExecutor } from '../../../utils/execution/index.ts';
import {
  createAxeNotAvailableResponse,
  getAxePath,
  getBundledAxeEnvironment,
} from '../../../utils/axe/index.ts';
import { createTypedTool } from '../../../utils/typed-tool-factory.ts';

// Define schema as ZodObject
const keySequenceSchema = z.object({
  simulatorUuid: z.string().uuid('Invalid Simulator UUID format'),
  keyCodes: z.array(z.number().int().min(0).max(255)).min(1, 'At least one key code required'),
  delay: z.number().min(0, 'Delay must be non-negative').optional(),
});

// Use z.infer for type safety
type KeySequenceParams = z.infer<typeof keySequenceSchema>;

export interface AxeHelpers {
  getAxePath: () => string | null;
  getBundledAxeEnvironment: () => Record<string, string>;
  createAxeNotAvailableResponse: () => ToolResponse;
}

const LOG_PREFIX = '[AXe]';

export async function key_sequenceLogic(
  params: KeySequenceParams,
  executor: CommandExecutor,
  axeHelpers: AxeHelpers = {
    getAxePath,
    getBundledAxeEnvironment,
    createAxeNotAvailableResponse,
  },
): Promise<ToolResponse> {
  const toolName = 'key_sequence';
  const { simulatorUuid, keyCodes, delay } = params;
  const commandArgs = ['key-sequence', '--keycodes', keyCodes.join(',')];
  if (delay !== undefined) {
    commandArgs.push('--delay', String(delay));
  }

  log(
    'info',
    `${LOG_PREFIX}/${toolName}: Starting key sequence [${keyCodes.join(',')}] on ${simulatorUuid}`,
  );

  try {
    await executeAxeCommand(commandArgs, simulatorUuid, 'key-sequence', executor, axeHelpers);
    log('info', `${LOG_PREFIX}/${toolName}: Success for ${simulatorUuid}`);
    return createTextResponse(`Key sequence [${keyCodes.join(',')}] executed successfully.`);
  } catch (error) {
    log('error', `${LOG_PREFIX}/${toolName}: Failed - ${error}`);
    if (error instanceof DependencyError) {
      return axeHelpers.createAxeNotAvailableResponse();
    } else if (error instanceof AxeError) {
      return createErrorResponse(
        `Failed to execute key sequence: ${error.message}`,
        error.axeOutput,
      );
    } else if (error instanceof SystemError) {
      return createErrorResponse(
        `System error executing axe: ${error.message}`,
        error.originalError?.stack,
      );
    }
    return createErrorResponse(
      `An unexpected error occurred: ${error instanceof Error ? error.message : String(error)}`,
    );
  }
}

export default {
  name: 'key_sequence',
  description: 'Press key sequence using HID keycodes on iOS simulator with configurable delay',
  schema: keySequenceSchema.shape, // MCP SDK compatibility
  handler: createTypedTool(
    keySequenceSchema,
    (params: KeySequenceParams, executor: CommandExecutor) => {
      return key_sequenceLogic(params, executor, {
        getAxePath,
        getBundledAxeEnvironment,
        createAxeNotAvailableResponse,
      });
    },
    getDefaultCommandExecutor,
  ),
};

// Helper function for executing axe commands (inlined from src/tools/axe/index.ts)
async function executeAxeCommand(
  commandArgs: string[],
  simulatorUuid: string,
  commandName: string,
  executor: CommandExecutor = getDefaultCommandExecutor(),
  axeHelpers: AxeHelpers = { getAxePath, getBundledAxeEnvironment, createAxeNotAvailableResponse },
): Promise<void> {
  // Get the appropriate axe binary path
  const axeBinary = axeHelpers.getAxePath();
  if (!axeBinary) {
    throw new DependencyError('AXe binary not found');
  }

  // Add --udid parameter to all commands
  const fullArgs = [...commandArgs, '--udid', simulatorUuid];

  // Construct the full command array with the axe binary as the first element
  const fullCommand = [axeBinary, ...fullArgs];

  try {
    // Determine environment variables for bundled AXe
    const axeEnv = axeBinary !== 'axe' ? axeHelpers.getBundledAxeEnvironment() : undefined;

    const result = await executor(fullCommand, `${LOG_PREFIX}: ${commandName}`, false, axeEnv);

    if (!result.success) {
      throw new AxeError(
        `axe command '${commandName}' failed.`,
        commandName,
        result.error ?? result.output,
        simulatorUuid,
      );
    }

    // Check for stderr output in successful commands
    if (result.error) {
      log(
        'warn',
        `${LOG_PREFIX}: Command '${commandName}' produced stderr output but exited successfully. Output: ${result.error}`,
      );
    }

    // Function now returns void - the calling code creates its own response
  } catch (error) {
    if (error instanceof Error) {
      if (error instanceof AxeError) {
        throw error;
      }

      // Otherwise wrap it in a SystemError
      throw new SystemError(`Failed to execute axe command: ${error.message}`, error);
    }

    // For any other type of error
    throw new SystemError(`Failed to execute axe command: ${String(error)}`);
  }
}

```

--------------------------------------------------------------------------------
/src/mcp/tools/simulator/__tests__/record_sim_video.test.ts:
--------------------------------------------------------------------------------

```typescript
import { describe, it, expect, vi, afterEach } from 'vitest';

// Import the tool and logic
import tool, { record_sim_videoLogic } from '../record_sim_video.ts';
import { createMockFileSystemExecutor } from '../../../../test-utils/mock-executors.ts';

const DUMMY_EXECUTOR: any = (async () => ({ success: true })) as any; // CommandExecutor stub
const VALID_SIM_ID = '00000000-0000-0000-0000-000000000000';

afterEach(() => {
  vi.restoreAllMocks();
});

describe('record_sim_video tool - validation', () => {
  it('errors when start and stop are both true (mutually exclusive)', async () => {
    const res = await tool.handler({
      simulatorId: VALID_SIM_ID,
      start: true,
      stop: true,
    } as any);

    expect(res.isError).toBe(true);
    const text = (res.content?.[0] as any)?.text ?? '';
    expect(text.toLowerCase()).toContain('mutually exclusive');
  });

  it('errors when stop=true but outputFile is missing', async () => {
    const res = await tool.handler({
      simulatorId: VALID_SIM_ID,
      stop: true,
    } as any);

    expect(res.isError).toBe(true);
    const text = (res.content?.[0] as any)?.text ?? '';
    expect(text.toLowerCase()).toContain('outputfile is required');
  });
});

describe('record_sim_video logic - start behavior', () => {
  it('starts with default fps (30) and warns when outputFile is provided on start (ignored)', async () => {
    const video: any = {
      startSimulatorVideoCapture: async () => ({
        started: true,
        sessionId: 'sess-123',
      }),
      stopSimulatorVideoCapture: async () => ({
        stopped: false,
      }),
    };

    // DI for AXe helpers: available and version OK
    const axe = {
      areAxeToolsAvailable: () => true,
      isAxeAtLeastVersion: async () => true,
      createAxeNotAvailableResponse: () => ({
        content: [{ type: 'text', text: 'AXe not available' }],
        isError: true,
      }),
    };

    const fs = createMockFileSystemExecutor();

    const res = await record_sim_videoLogic(
      {
        simulatorId: VALID_SIM_ID,
        start: true,
        // fps omitted to hit default 30
        outputFile: '/tmp/ignored.mp4', // should be ignored with a note
      } as any,
      DUMMY_EXECUTOR,
      axe,
      video,
      fs,
    );

    expect(res.isError).toBe(false);
    const texts = (res.content ?? []).map((c: any) => c.text).join('\n');

    expect(texts).toContain('🎥');
    expect(texts).toMatch(/30\s*fps/i);
    expect(texts.toLowerCase()).toContain('outputfile is ignored');
    expect(texts).toContain('Next Steps');
    expect(texts).toContain('stop: true');
    expect(texts).toContain('outputFile');
  });
});

describe('record_sim_video logic - end-to-end stop with rename', () => {
  it('stops, parses stdout path, and renames to outputFile', async () => {
    const video: any = {
      startSimulatorVideoCapture: async () => ({
        started: true,
        sessionId: 'sess-abc',
      }),
      stopSimulatorVideoCapture: async () => ({
        stopped: true,
        parsedPath: '/tmp/recorded.mp4',
        stdout: 'Saved to /tmp/recorded.mp4',
      }),
    };

    const fs = createMockFileSystemExecutor();

    const axe = {
      areAxeToolsAvailable: () => true,
      isAxeAtLeastVersion: async () => true,
      createAxeNotAvailableResponse: () => ({
        content: [{ type: 'text', text: 'AXe not available' }],
        isError: true,
      }),
    };

    // Start (not strictly required for stop path, but included to mimic flow)
    const startRes = await record_sim_videoLogic(
      {
        simulatorId: VALID_SIM_ID,
        start: true,
      } as any,
      DUMMY_EXECUTOR,
      axe,
      video,
      fs,
    );
    expect(startRes.isError).toBe(false);

    // Stop and rename
    const outputFile = '/var/videos/final.mp4';
    const stopRes = await record_sim_videoLogic(
      {
        simulatorId: VALID_SIM_ID,
        stop: true,
        outputFile,
      } as any,
      DUMMY_EXECUTOR,
      axe,
      video,
      fs,
    );

    expect(stopRes.isError).toBe(false);
    const texts = (stopRes.content ?? []).map((c: any) => c.text).join('\n');
    expect(texts).toContain('Original file: /tmp/recorded.mp4');
    expect(texts).toContain(`Saved to: ${outputFile}`);

    // _meta should include final saved path
    expect((stopRes as any)._meta?.outputFile).toBe(outputFile);
  });
});

describe('record_sim_video logic - version gate', () => {
  it('errors when AXe version is below 1.1.0', async () => {
    const axe = {
      areAxeToolsAvailable: () => true,
      isAxeAtLeastVersion: async () => false,
      createAxeNotAvailableResponse: () => ({
        content: [{ type: 'text', text: 'AXe not available' }],
        isError: true,
      }),
    };

    const video: any = {
      startSimulatorVideoCapture: async () => ({
        started: true,
        sessionId: 'sess-xyz',
      }),
      stopSimulatorVideoCapture: async () => ({
        stopped: true,
      }),
    };

    const fs = createMockFileSystemExecutor();

    const res = await record_sim_videoLogic(
      {
        simulatorId: VALID_SIM_ID,
        start: true,
      } as any,
      DUMMY_EXECUTOR,
      axe,
      video,
      fs,
    );

    expect(res.isError).toBe(true);
    const text = (res.content?.[0] as any)?.text ?? '';
    expect(text).toContain('AXe v1.1.0');
  });
});

```

--------------------------------------------------------------------------------
/src/mcp/tools/ui-testing/screenshot.ts:
--------------------------------------------------------------------------------

```typescript
/**
 * Screenshot tool plugin - Capture screenshots from iOS Simulator
 */
import * as path from 'path';
import { tmpdir } from 'os';
import { z } from 'zod';
import { v4 as uuidv4 } from 'uuid';
import { ToolResponse, createImageContent } from '../../../types/common.ts';
import { log } from '../../../utils/logging/index.ts';
import { createErrorResponse, SystemError } from '../../../utils/responses/index.ts';
import type { CommandExecutor, FileSystemExecutor } from '../../../utils/execution/index.ts';
import {
  getDefaultFileSystemExecutor,
  getDefaultCommandExecutor,
} from '../../../utils/execution/index.ts';
import { createTypedTool } from '../../../utils/typed-tool-factory.ts';

const LOG_PREFIX = '[Screenshot]';

// Define schema as ZodObject
const screenshotSchema = z.object({
  simulatorUuid: z.string().uuid('Invalid Simulator UUID format'),
});

// Use z.infer for type safety
type ScreenshotParams = z.infer<typeof screenshotSchema>;

export async function screenshotLogic(
  params: ScreenshotParams,
  executor: CommandExecutor,
  fileSystemExecutor: FileSystemExecutor = getDefaultFileSystemExecutor(),
  pathUtils: { tmpdir: () => string; join: (...paths: string[]) => string } = { ...path, tmpdir },
  uuidUtils: { v4: () => string } = { v4: uuidv4 },
): Promise<ToolResponse> {
  const { simulatorUuid } = params;
  const tempDir = pathUtils.tmpdir();
  const screenshotFilename = `screenshot_${uuidUtils.v4()}.png`;
  const screenshotPath = pathUtils.join(tempDir, screenshotFilename);
  const optimizedFilename = `screenshot_optimized_${uuidUtils.v4()}.jpg`;
  const optimizedPath = pathUtils.join(tempDir, optimizedFilename);
  // Use xcrun simctl to take screenshot
  const commandArgs: string[] = [
    'xcrun',
    'simctl',
    'io',
    simulatorUuid,
    'screenshot',
    screenshotPath,
  ];

  log(
    'info',
    `${LOG_PREFIX}/screenshot: Starting capture to ${screenshotPath} on ${simulatorUuid}`,
  );

  try {
    // Execute the screenshot command
    const result = await executor(commandArgs, `${LOG_PREFIX}: screenshot`, false);

    if (!result.success) {
      throw new SystemError(`Failed to capture screenshot: ${result.error ?? result.output}`);
    }

    log('info', `${LOG_PREFIX}/screenshot: Success for ${simulatorUuid}`);

    try {
      // Optimize the image for LLM consumption: resize to max 800px width and convert to JPEG
      const optimizeArgs = [
        'sips',
        '-Z',
        '800', // Resize to max 800px (maintains aspect ratio)
        '-s',
        'format',
        'jpeg', // Convert to JPEG
        '-s',
        'formatOptions',
        '75', // 75% quality compression
        screenshotPath,
        '--out',
        optimizedPath,
      ];

      const optimizeResult = await executor(optimizeArgs, `${LOG_PREFIX}: optimize image`, false);

      if (!optimizeResult.success) {
        log('warning', `${LOG_PREFIX}/screenshot: Image optimization failed, using original PNG`);
        // Fallback to original PNG if optimization fails
        const base64Image = await fileSystemExecutor.readFile(screenshotPath, 'base64');

        // Clean up
        try {
          await fileSystemExecutor.rm(screenshotPath);
        } catch (err) {
          log('warning', `${LOG_PREFIX}/screenshot: Failed to delete temp file: ${err}`);
        }

        return {
          content: [createImageContent(base64Image, 'image/png')],
          isError: false,
        };
      }

      log('info', `${LOG_PREFIX}/screenshot: Image optimized successfully`);

      // Read the optimized image file as base64
      const base64Image = await fileSystemExecutor.readFile(optimizedPath, 'base64');

      log('info', `${LOG_PREFIX}/screenshot: Successfully encoded image as Base64`);

      // Clean up both temporary files
      try {
        await fileSystemExecutor.rm(screenshotPath);
        await fileSystemExecutor.rm(optimizedPath);
      } catch (err) {
        log('warning', `${LOG_PREFIX}/screenshot: Failed to delete temporary files: ${err}`);
      }

      // Return the optimized image (JPEG format, smaller size)
      return {
        content: [createImageContent(base64Image, 'image/jpeg')],
        isError: false,
      };
    } catch (fileError) {
      log('error', `${LOG_PREFIX}/screenshot: Failed to process image file: ${fileError}`);
      return createErrorResponse(
        `Screenshot captured but failed to process image file: ${fileError instanceof Error ? fileError.message : String(fileError)}`,
      );
    }
  } catch (_error) {
    log('error', `${LOG_PREFIX}/screenshot: Failed - ${_error}`);
    if (_error instanceof SystemError) {
      return createErrorResponse(
        `System error executing screenshot: ${_error.message}`,
        _error.originalError?.stack,
      );
    }
    return createErrorResponse(
      `An unexpected error occurred: ${_error instanceof Error ? _error.message : String(_error)}`,
    );
  }
}

export default {
  name: 'screenshot',
  description:
    "Captures screenshot for visual verification. For UI coordinates, use describe_ui instead (don't determine coordinates from screenshots).",
  schema: screenshotSchema.shape, // MCP SDK compatibility
  handler: createTypedTool(
    screenshotSchema,
    (params: ScreenshotParams, executor: CommandExecutor) => {
      return screenshotLogic(params, executor);
    },
    getDefaultCommandExecutor,
  ),
};

```

--------------------------------------------------------------------------------
/src/mcp/tools/simulator/test_sim.ts:
--------------------------------------------------------------------------------

```typescript
/**
 * Simulator Test Plugin: Test Simulator (Unified)
 *
 * Runs tests for a project or workspace on a simulator by UUID or name.
 * Accepts mutually exclusive `projectPath` or `workspacePath`.
 * Accepts mutually exclusive `simulatorId` or `simulatorName`.
 */

import { z } from 'zod';
import { handleTestLogic } from '../../../utils/test/index.ts';
import { log } from '../../../utils/logging/index.ts';
import { XcodePlatform } from '../../../types/common.ts';
import { ToolResponse } from '../../../types/common.ts';
import type { CommandExecutor } from '../../../utils/execution/index.ts';
import { getDefaultCommandExecutor } from '../../../utils/execution/index.ts';
import { nullifyEmptyStrings } from '../../../utils/schema-helpers.ts';
import { createSessionAwareTool } from '../../../utils/typed-tool-factory.ts';

// Define base schema object with all fields
const baseSchemaObject = z.object({
  projectPath: z
    .string()
    .optional()
    .describe('Path to .xcodeproj file. Provide EITHER this OR workspacePath, not both'),
  workspacePath: z
    .string()
    .optional()
    .describe('Path to .xcworkspace file. Provide EITHER this OR projectPath, not both'),
  scheme: z.string().describe('The scheme to use (Required)'),
  simulatorId: z
    .string()
    .optional()
    .describe(
      'UUID of the simulator (from list_sims). Provide EITHER this OR simulatorName, not both',
    ),
  simulatorName: z
    .string()
    .optional()
    .describe(
      "Name of the simulator (e.g., 'iPhone 16'). Provide EITHER this OR simulatorId, not both",
    ),
  configuration: z.string().optional().describe('Build configuration (Debug, Release, etc.)'),
  derivedDataPath: z
    .string()
    .optional()
    .describe('Path where build products and other derived data will go'),
  extraArgs: z.array(z.string()).optional().describe('Additional xcodebuild arguments'),
  useLatestOS: z
    .boolean()
    .optional()
    .describe('Whether to use the latest OS version for the named simulator'),
  preferXcodebuild: z
    .boolean()
    .optional()
    .describe(
      'If true, prefers xcodebuild over the experimental incremental build system, useful for when incremental build system fails.',
    ),
  testRunnerEnv: z
    .record(z.string(), z.string())
    .optional()
    .describe(
      'Environment variables to pass to the test runner (TEST_RUNNER_ prefix added automatically)',
    ),
});

// Apply preprocessor to handle empty strings
const baseSchema = z.preprocess(nullifyEmptyStrings, baseSchemaObject);

// Apply XOR validation: exactly one of projectPath OR workspacePath, and exactly one of simulatorId OR simulatorName required
const testSimulatorSchema = baseSchema
  .refine((val) => val.projectPath !== undefined || val.workspacePath !== undefined, {
    message: 'Either projectPath or workspacePath is required.',
  })
  .refine((val) => !(val.projectPath !== undefined && val.workspacePath !== undefined), {
    message: 'projectPath and workspacePath are mutually exclusive. Provide only one.',
  })
  .refine((val) => val.simulatorId !== undefined || val.simulatorName !== undefined, {
    message: 'Either simulatorId or simulatorName is required.',
  })
  .refine((val) => !(val.simulatorId !== undefined && val.simulatorName !== undefined), {
    message: 'simulatorId and simulatorName are mutually exclusive. Provide only one.',
  });

// Use z.infer for type safety
type TestSimulatorParams = z.infer<typeof testSimulatorSchema>;

export async function test_simLogic(
  params: TestSimulatorParams,
  executor: CommandExecutor,
): Promise<ToolResponse> {
  // Log warning if useLatestOS is provided with simulatorId
  if (params.simulatorId && params.useLatestOS !== undefined) {
    log(
      'warning',
      `useLatestOS parameter is ignored when using simulatorId (UUID implies exact device/OS)`,
    );
  }

  return handleTestLogic(
    {
      projectPath: params.projectPath,
      workspacePath: params.workspacePath,
      scheme: params.scheme,
      simulatorId: params.simulatorId,
      simulatorName: params.simulatorName,
      configuration: params.configuration ?? 'Debug',
      derivedDataPath: params.derivedDataPath,
      extraArgs: params.extraArgs,
      useLatestOS: params.simulatorId ? false : (params.useLatestOS ?? false),
      preferXcodebuild: params.preferXcodebuild ?? false,
      platform: XcodePlatform.iOSSimulator,
      testRunnerEnv: params.testRunnerEnv,
    },
    executor,
  );
}

const publicSchemaObject = baseSchemaObject.omit({
  projectPath: true,
  workspacePath: true,
  scheme: true,
  simulatorId: true,
  simulatorName: true,
  configuration: true,
  useLatestOS: true,
} as const);

export default {
  name: 'test_sim',
  description: 'Runs tests on an iOS simulator.',
  schema: publicSchemaObject.shape,
  handler: createSessionAwareTool<TestSimulatorParams>({
    internalSchema: testSimulatorSchema as unknown as z.ZodType<TestSimulatorParams>,
    logicFunction: test_simLogic,
    getExecutor: getDefaultCommandExecutor,
    requirements: [
      { allOf: ['scheme'], message: 'scheme is required' },
      { oneOf: ['projectPath', 'workspacePath'], message: 'Provide a project or workspace' },
      { oneOf: ['simulatorId', 'simulatorName'], message: 'Provide simulatorId or simulatorName' },
    ],
    exclusivePairs: [
      ['projectPath', 'workspacePath'],
      ['simulatorId', 'simulatorName'],
    ],
  }),
};

```

--------------------------------------------------------------------------------
/src/mcp/tools/utilities/__tests__/clean.test.ts:
--------------------------------------------------------------------------------

```typescript
import { describe, it, expect, beforeEach } from 'vitest';
import { z } from 'zod';
import tool, { cleanLogic } from '../clean.ts';
import { createMockExecutor } from '../../../../test-utils/mock-executors.ts';
import { sessionStore } from '../../../../utils/session-store.ts';

describe('clean (unified) tool', () => {
  beforeEach(() => {
    sessionStore.clear();
  });

  it('exports correct name/description/schema/handler', () => {
    expect(tool.name).toBe('clean');
    expect(tool.description).toBe('Cleans build products with xcodebuild.');
    expect(typeof tool.handler).toBe('function');

    const schema = z.object(tool.schema).strict();
    expect(schema.safeParse({}).success).toBe(true);
    expect(
      schema.safeParse({
        derivedDataPath: '/tmp/Derived',
        extraArgs: ['--quiet'],
        preferXcodebuild: true,
        platform: 'iOS Simulator',
      }).success,
    ).toBe(true);
    expect(schema.safeParse({ configuration: 'Debug' }).success).toBe(false);

    const schemaKeys = Object.keys(tool.schema).sort();
    expect(schemaKeys).toEqual(
      ['derivedDataPath', 'extraArgs', 'platform', 'preferXcodebuild'].sort(),
    );
  });

  it('handler validation: error when neither projectPath nor workspacePath provided', async () => {
    const result = await (tool as any).handler({});
    expect(result.isError).toBe(true);
    const text = String(result.content?.[0]?.text ?? '');
    expect(text).toContain('Missing required session defaults');
    expect(text).toContain('Provide a project or workspace');
  });

  it('handler validation: error when both projectPath and workspacePath provided', async () => {
    const result = await (tool as any).handler({
      projectPath: '/p.xcodeproj',
      workspacePath: '/w.xcworkspace',
    });
    expect(result.isError).toBe(true);
    const text = String(result.content?.[0]?.text ?? '');
    expect(text).toContain('Mutually exclusive parameters provided');
  });

  it('runs project-path flow via logic', async () => {
    const mock = createMockExecutor({ success: true, output: 'ok' });
    const result = await cleanLogic({ projectPath: '/p.xcodeproj', scheme: 'App' } as any, mock);
    expect(result.isError).not.toBe(true);
  });

  it('runs workspace-path flow via logic', async () => {
    const mock = createMockExecutor({ success: true, output: 'ok' });
    const result = await cleanLogic(
      { workspacePath: '/w.xcworkspace', scheme: 'App' } as any,
      mock,
    );
    expect(result.isError).not.toBe(true);
  });

  it('handler validation: requires scheme when workspacePath is provided', async () => {
    const result = await (tool as any).handler({ workspacePath: '/w.xcworkspace' });
    expect(result.isError).toBe(true);
    const text = String(result.content?.[0]?.text ?? '');
    expect(text).toContain('Parameter validation failed');
    expect(text).toContain('scheme is required when workspacePath is provided');
  });

  it('uses iOS platform by default', async () => {
    let capturedCommand: string[] = [];
    const mockExecutor = async (command: string[]) => {
      capturedCommand = command;
      return { success: true, output: 'clean success' };
    };

    const result = await cleanLogic(
      { projectPath: '/p.xcodeproj', scheme: 'App' } as any,
      mockExecutor,
    );
    expect(result.isError).not.toBe(true);

    // Check that the command contains iOS platform destination
    const commandStr = capturedCommand.join(' ');
    expect(commandStr).toContain('-destination');
    expect(commandStr).toContain('platform=iOS');
  });

  it('accepts custom platform parameter', async () => {
    let capturedCommand: string[] = [];
    const mockExecutor = async (command: string[]) => {
      capturedCommand = command;
      return { success: true, output: 'clean success' };
    };

    const result = await cleanLogic(
      {
        projectPath: '/p.xcodeproj',
        scheme: 'App',
        platform: 'macOS',
      } as any,
      mockExecutor,
    );
    expect(result.isError).not.toBe(true);

    // Check that the command contains macOS platform destination
    const commandStr = capturedCommand.join(' ');
    expect(commandStr).toContain('-destination');
    expect(commandStr).toContain('platform=macOS');
  });

  it('accepts iOS Simulator platform parameter (maps to iOS for clean)', async () => {
    let capturedCommand: string[] = [];
    const mockExecutor = async (command: string[]) => {
      capturedCommand = command;
      return { success: true, output: 'clean success' };
    };

    const result = await cleanLogic(
      {
        projectPath: '/p.xcodeproj',
        scheme: 'App',
        platform: 'iOS Simulator',
      } as any,
      mockExecutor,
    );
    expect(result.isError).not.toBe(true);

    // For clean operations, iOS Simulator should be mapped to iOS platform
    const commandStr = capturedCommand.join(' ');
    expect(commandStr).toContain('-destination');
    expect(commandStr).toContain('platform=iOS');
  });

  it('handler validation: rejects invalid platform values', async () => {
    const result = await (tool as any).handler({
      projectPath: '/p.xcodeproj',
      scheme: 'App',
      platform: 'InvalidPlatform',
    });
    expect(result.isError).toBe(true);
    const text = String(result.content?.[0]?.text ?? '');
    expect(text).toContain('Parameter validation failed');
    expect(text).toContain('platform');
  });
});

```

--------------------------------------------------------------------------------
/src/mcp/tools/swift-package/__tests__/active-processes.test.ts:
--------------------------------------------------------------------------------

```typescript
/**
 * Tests for active-processes module
 * Following CLAUDE.md testing standards with literal validation
 */

import { describe, it, expect, beforeEach } from 'vitest';
import {
  activeProcesses,
  getProcess,
  addProcess,
  removeProcess,
  clearAllProcesses,
  type ProcessInfo,
} from '../active-processes.ts';

describe('active-processes module', () => {
  // Clear the map before each test
  beforeEach(() => {
    clearAllProcesses();
  });

  describe('activeProcesses Map', () => {
    it('should be a Map instance', () => {
      expect(activeProcesses instanceof Map).toBe(true);
    });

    it('should start empty after clearing', () => {
      expect(activeProcesses.size).toBe(0);
    });
  });

  describe('getProcess function', () => {
    it('should return undefined for non-existent process', () => {
      const result = getProcess(12345);
      expect(result).toBe(undefined);
    });

    it('should return process info for existing process', () => {
      const mockProcess = {
        kill: () => {},
        on: () => {},
        pid: 12345,
      };
      const startedAt = new Date('2023-01-01T10:00:00.000Z');
      const processInfo: ProcessInfo = {
        process: mockProcess,
        startedAt: startedAt,
      };

      activeProcesses.set(12345, processInfo);
      const result = getProcess(12345);

      expect(result).toEqual({
        process: mockProcess,
        startedAt: startedAt,
      });
    });
  });

  describe('addProcess function', () => {
    it('should add process to the map', () => {
      const mockProcess = {
        kill: () => {},
        on: () => {},
        pid: 67890,
      };
      const startedAt = new Date('2023-02-15T14:30:00.000Z');
      const processInfo: ProcessInfo = {
        process: mockProcess,
        startedAt: startedAt,
      };

      addProcess(67890, processInfo);

      expect(activeProcesses.size).toBe(1);
      expect(activeProcesses.get(67890)).toEqual(processInfo);
    });

    it('should overwrite existing process with same pid', () => {
      const mockProcess1 = {
        kill: () => {},
        on: () => {},
        pid: 11111,
      };
      const mockProcess2 = {
        kill: () => {},
        on: () => {},
        pid: 11111,
      };
      const startedAt1 = new Date('2023-01-01T10:00:00.000Z');
      const startedAt2 = new Date('2023-01-01T11:00:00.000Z');

      addProcess(11111, { process: mockProcess1, startedAt: startedAt1 });
      addProcess(11111, { process: mockProcess2, startedAt: startedAt2 });

      expect(activeProcesses.size).toBe(1);
      expect(activeProcesses.get(11111)).toEqual({
        process: mockProcess2,
        startedAt: startedAt2,
      });
    });
  });

  describe('removeProcess function', () => {
    it('should return false for non-existent process', () => {
      const result = removeProcess(99999);
      expect(result).toBe(false);
    });

    it('should return true and remove existing process', () => {
      const mockProcess = {
        kill: () => {},
        on: () => {},
        pid: 54321,
      };
      const processInfo: ProcessInfo = {
        process: mockProcess,
        startedAt: new Date('2023-03-20T09:15:00.000Z'),
      };

      addProcess(54321, processInfo);
      expect(activeProcesses.size).toBe(1);

      const result = removeProcess(54321);

      expect(result).toBe(true);
      expect(activeProcesses.size).toBe(0);
      expect(activeProcesses.get(54321)).toBe(undefined);
    });
  });

  describe('clearAllProcesses function', () => {
    it('should clear all processes from the map', () => {
      const mockProcess1 = {
        kill: () => {},
        on: () => {},
        pid: 1111,
      };
      const mockProcess2 = {
        kill: () => {},
        on: () => {},
        pid: 2222,
      };

      addProcess(1111, { process: mockProcess1, startedAt: new Date() });
      addProcess(2222, { process: mockProcess2, startedAt: new Date() });

      expect(activeProcesses.size).toBe(2);

      clearAllProcesses();

      expect(activeProcesses.size).toBe(0);
    });

    it('should work on already empty map', () => {
      expect(activeProcesses.size).toBe(0);
      clearAllProcesses();
      expect(activeProcesses.size).toBe(0);
    });
  });

  describe('ProcessInfo interface', () => {
    it('should work with complete process object', () => {
      const mockProcess = {
        kill: () => {},
        on: () => {},
        pid: 12345,
      };
      const startedAt = new Date('2023-01-01T10:00:00.000Z');
      const processInfo: ProcessInfo = {
        process: mockProcess,
        startedAt: startedAt,
      };

      addProcess(12345, processInfo);
      const retrieved = getProcess(12345);

      expect(retrieved).toEqual({
        process: {
          kill: expect.any(Function),
          on: expect.any(Function),
          pid: 12345,
        },
        startedAt: startedAt,
      });
    });

    it('should work with minimal process object', () => {
      const mockProcess = {
        kill: () => {},
        on: () => {},
      };
      const startedAt = new Date('2023-01-01T10:00:00.000Z');
      const processInfo: ProcessInfo = {
        process: mockProcess,
        startedAt: startedAt,
      };

      addProcess(98765, processInfo);
      const retrieved = getProcess(98765);

      expect(retrieved).toEqual({
        process: {
          kill: expect.any(Function),
          on: expect.any(Function),
        },
        startedAt: startedAt,
      });
    });
  });
});

```

--------------------------------------------------------------------------------
/src/mcp/tools/simulator/__tests__/launch_app_logs_sim.test.ts:
--------------------------------------------------------------------------------

```typescript
/**
 * Tests for launch_app_logs_sim plugin (session-aware version)
 * Follows CLAUDE.md guidance with literal validation and DI.
 */

import { describe, it, expect, beforeEach } from 'vitest';
import { z } from 'zod';
import launchAppLogsSim, {
  launch_app_logs_simLogic,
  LogCaptureFunction,
} from '../launch_app_logs_sim.ts';
import { createMockExecutor } from '../../../../test-utils/mock-executors.ts';
import { sessionStore } from '../../../../utils/session-store.ts';

describe('launch_app_logs_sim tool', () => {
  beforeEach(() => {
    sessionStore.clear();
  });

  describe('Export Field Validation (Literal)', () => {
    it('should expose correct metadata', () => {
      expect(launchAppLogsSim.name).toBe('launch_app_logs_sim');
      expect(launchAppLogsSim.description).toBe(
        'Launches an app in an iOS simulator and captures its logs.',
      );
    });

    it('should expose only non-session fields in public schema', () => {
      const schema = z.object(launchAppLogsSim.schema);

      expect(schema.safeParse({ bundleId: 'com.example.app' }).success).toBe(true);
      expect(schema.safeParse({ bundleId: 'com.example.app', args: ['--debug'] }).success).toBe(
        true,
      );
      expect(schema.safeParse({}).success).toBe(false);
      expect(schema.safeParse({ bundleId: 42 }).success).toBe(false);

      expect(Object.keys(launchAppLogsSim.schema).sort()).toEqual(['args', 'bundleId'].sort());
    });
  });

  describe('Handler Requirements', () => {
    it('should require simulatorId when not provided', async () => {
      const result = await launchAppLogsSim.handler({ bundleId: 'com.example.testapp' });

      expect(result.isError).toBe(true);
      expect(result.content[0].text).toContain('Missing required session defaults');
      expect(result.content[0].text).toContain('simulatorId is required');
      expect(result.content[0].text).toContain('session-set-defaults');
    });

    it('should validate bundleId when simulatorId default exists', async () => {
      sessionStore.setDefaults({ simulatorId: 'SIM-UUID' });

      const result = await launchAppLogsSim.handler({});

      expect(result.isError).toBe(true);
      expect(result.content[0].text).toContain('Parameter validation failed');
      expect(result.content[0].text).toContain('bundleId: Required');
      expect(result.content[0].text).toContain(
        'Tip: set session defaults via session-set-defaults',
      );
    });
  });

  describe('Logic Behavior (Literal Returns)', () => {
    it('should handle successful app launch with log capture', async () => {
      let capturedParams: unknown = null;
      const logCaptureStub: LogCaptureFunction = async (params) => {
        capturedParams = params;
        return {
          sessionId: 'test-session-123',
          logFilePath: '/tmp/xcodemcp_sim_log_test-session-123.log',
          processes: [],
          error: undefined,
        };
      };

      const mockExecutor = createMockExecutor({ success: true, output: '' });

      const result = await launch_app_logs_simLogic(
        {
          simulatorId: 'test-uuid-123',
          bundleId: 'com.example.testapp',
        },
        mockExecutor,
        logCaptureStub,
      );

      expect(result).toEqual({
        content: [
          {
            type: 'text',
            text: `App launched successfully in simulator test-uuid-123 with log capture enabled.\n\nLog capture session ID: test-session-123\n\nNext Steps:\n1. Interact with your app in the simulator.\n2. Use 'stop_and_get_simulator_log({ logSessionId: "test-session-123" })' to stop capture and retrieve logs.`,
          },
        ],
        isError: false,
      });

      expect(capturedParams).toEqual({
        simulatorUuid: 'test-uuid-123',
        bundleId: 'com.example.testapp',
        captureConsole: true,
      });
    });

    it('should ignore args for log capture setup', async () => {
      let capturedParams: unknown = null;
      const logCaptureStub: LogCaptureFunction = async (params) => {
        capturedParams = params;
        return {
          sessionId: 'test-session-456',
          logFilePath: '/tmp/xcodemcp_sim_log_test-session-456.log',
          processes: [],
          error: undefined,
        };
      };

      const mockExecutor = createMockExecutor({ success: true, output: '' });

      await launch_app_logs_simLogic(
        {
          simulatorId: 'test-uuid-123',
          bundleId: 'com.example.testapp',
          args: ['--debug'],
        },
        mockExecutor,
        logCaptureStub,
      );

      expect(capturedParams).toEqual({
        simulatorUuid: 'test-uuid-123',
        bundleId: 'com.example.testapp',
        captureConsole: true,
        args: ['--debug'],
      });
    });

    it('should surface log capture failure', async () => {
      const logCaptureStub: LogCaptureFunction = async () => ({
        sessionId: '',
        logFilePath: '',
        processes: [],
        error: 'Failed to start log capture',
      });

      const mockExecutor = createMockExecutor({ success: true, output: '' });

      const result = await launch_app_logs_simLogic(
        {
          simulatorId: 'test-uuid-123',
          bundleId: 'com.example.testapp',
        },
        mockExecutor,
        logCaptureStub,
      );

      expect(result).toEqual({
        content: [
          {
            type: 'text',
            text: 'App was launched but log capture failed: Failed to start log capture',
          },
        ],
        isError: true,
      });
    });
  });
});

```

--------------------------------------------------------------------------------
/src/utils/__tests__/test-runner-env-integration.test.ts:
--------------------------------------------------------------------------------

```typescript
/**
 * Integration tests for TEST_RUNNER_ environment variable passing
 *
 * These tests verify that testRunnerEnv parameters are correctly processed
 * and passed through the execution chain. We focus on testing the core
 * functionality that matters most: environment variable normalization.
 */

import { describe, it, expect } from 'vitest';
import { normalizeTestRunnerEnv } from '../environment.ts';

describe('TEST_RUNNER_ Environment Variable Integration', () => {
  describe('Core normalization functionality', () => {
    it('should normalize environment variables correctly for real scenarios', () => {
      // Test the GitHub issue scenario: USE_DEV_MODE -> TEST_RUNNER_USE_DEV_MODE
      const gitHubIssueScenario = { USE_DEV_MODE: 'YES' };
      const normalized = normalizeTestRunnerEnv(gitHubIssueScenario);

      expect(normalized).toEqual({ TEST_RUNNER_USE_DEV_MODE: 'YES' });
    });

    it('should handle mixed prefixed and unprefixed variables', () => {
      const mixedVars = {
        USE_DEV_MODE: 'YES', // Should be prefixed
        TEST_RUNNER_SKIP_ANIMATIONS: '1', // Already prefixed, preserve
        DEBUG_MODE: 'true', // Should be prefixed
      };

      const normalized = normalizeTestRunnerEnv(mixedVars);

      expect(normalized).toEqual({
        TEST_RUNNER_USE_DEV_MODE: 'YES',
        TEST_RUNNER_SKIP_ANIMATIONS: '1',
        TEST_RUNNER_DEBUG_MODE: 'true',
      });
    });

    it('should filter out null and undefined values', () => {
      const varsWithNulls = {
        VALID_VAR: 'value1',
        NULL_VAR: null as any,
        UNDEFINED_VAR: undefined as any,
        ANOTHER_VALID: 'value2',
      };

      const normalized = normalizeTestRunnerEnv(varsWithNulls);

      expect(normalized).toEqual({
        TEST_RUNNER_VALID_VAR: 'value1',
        TEST_RUNNER_ANOTHER_VALID: 'value2',
      });

      // Ensure null/undefined vars are not present
      expect(normalized).not.toHaveProperty('TEST_RUNNER_NULL_VAR');
      expect(normalized).not.toHaveProperty('TEST_RUNNER_UNDEFINED_VAR');
    });

    it('should handle special characters in keys and values', () => {
      const specialChars = {
        'VAR_WITH-DASH': 'value-with-dash',
        'VAR.WITH.DOTS': 'value/with/slashes',
        VAR_WITH_SPACES: 'value with spaces',
        TEST_RUNNER_PRE_EXISTING: 'already=prefixed=value',
      };

      const normalized = normalizeTestRunnerEnv(specialChars);

      expect(normalized).toEqual({
        'TEST_RUNNER_VAR_WITH-DASH': 'value-with-dash',
        'TEST_RUNNER_VAR.WITH.DOTS': 'value/with/slashes',
        TEST_RUNNER_VAR_WITH_SPACES: 'value with spaces',
        TEST_RUNNER_PRE_EXISTING: 'already=prefixed=value',
      });
    });

    it('should handle empty values correctly', () => {
      const emptyValues = {
        EMPTY_STRING: '',
        NORMAL_VAR: 'normal_value',
      };

      const normalized = normalizeTestRunnerEnv(emptyValues);

      expect(normalized).toEqual({
        TEST_RUNNER_EMPTY_STRING: '',
        TEST_RUNNER_NORMAL_VAR: 'normal_value',
      });
    });

    it('should handle edge case prefix variations', () => {
      const prefixEdgeCases = {
        TEST_RUN: 'not_quite_prefixed', // Should get prefixed
        TEST_RUNNER: 'no_underscore', // Should get prefixed
        TEST_RUNNER_CORRECT: 'already_good', // Should stay as-is
        test_runner_lowercase: 'lowercase', // Should get prefixed (case sensitive)
      };

      const normalized = normalizeTestRunnerEnv(prefixEdgeCases);

      expect(normalized).toEqual({
        TEST_RUNNER_TEST_RUN: 'not_quite_prefixed',
        TEST_RUNNER_TEST_RUNNER: 'no_underscore',
        TEST_RUNNER_CORRECT: 'already_good',
        TEST_RUNNER_test_runner_lowercase: 'lowercase',
      });
    });

    it('should preserve immutability of input object', () => {
      const originalInput = { FOO: 'bar', BAZ: 'qux' };
      const inputCopy = { ...originalInput };

      const normalized = normalizeTestRunnerEnv(originalInput);

      // Original should be unchanged
      expect(originalInput).toEqual(inputCopy);

      // Result should be different
      expect(normalized).not.toEqual(originalInput);
      expect(normalized).toEqual({
        TEST_RUNNER_FOO: 'bar',
        TEST_RUNNER_BAZ: 'qux',
      });
    });

    it('should handle the complete test environment workflow', () => {
      // Simulate a comprehensive test environment setup
      const fullTestEnv = {
        // Core testing flags
        USE_DEV_MODE: 'YES',
        SKIP_ANIMATIONS: '1',
        FAST_MODE: 'true',

        // Already prefixed variables (user might provide these)
        TEST_RUNNER_TIMEOUT: '30',
        TEST_RUNNER_RETRIES: '3',

        // UI testing specific
        UI_TESTING_MODE: 'enabled',
        SCREENSHOT_MODE: 'disabled',

        // Performance testing
        PERFORMANCE_TESTS: 'false',
        MEMORY_TESTING: 'true',

        // Special values
        EMPTY_VAR: '',
        PATH_VAR: '/usr/local/bin:/usr/bin',
      };

      const normalized = normalizeTestRunnerEnv(fullTestEnv);

      expect(normalized).toEqual({
        TEST_RUNNER_USE_DEV_MODE: 'YES',
        TEST_RUNNER_SKIP_ANIMATIONS: '1',
        TEST_RUNNER_FAST_MODE: 'true',
        TEST_RUNNER_TIMEOUT: '30',
        TEST_RUNNER_RETRIES: '3',
        TEST_RUNNER_UI_TESTING_MODE: 'enabled',
        TEST_RUNNER_SCREENSHOT_MODE: 'disabled',
        TEST_RUNNER_PERFORMANCE_TESTS: 'false',
        TEST_RUNNER_MEMORY_TESTING: 'true',
        TEST_RUNNER_EMPTY_VAR: '',
        TEST_RUNNER_PATH_VAR: '/usr/local/bin:/usr/bin',
      });
    });
  });
});

```

--------------------------------------------------------------------------------
/src/mcp/tools/swift-package/__tests__/swift_package_clean.test.ts:
--------------------------------------------------------------------------------

```typescript
/**
 * Tests for swift_package_clean plugin
 * Following CLAUDE.md testing standards with literal validation
 * Using dependency injection for deterministic testing
 */

import { describe, it, expect } from 'vitest';
import {
  createMockExecutor,
  createMockFileSystemExecutor,
  createNoopExecutor,
} from '../../../../test-utils/mock-executors.ts';
import swiftPackageClean, { swift_package_cleanLogic } from '../swift_package_clean.ts';

describe('swift_package_clean plugin', () => {
  describe('Export Field Validation (Literal)', () => {
    it('should have correct name', () => {
      expect(swiftPackageClean.name).toBe('swift_package_clean');
    });

    it('should have correct description', () => {
      expect(swiftPackageClean.description).toBe(
        'Cleans Swift Package build artifacts and derived data',
      );
    });

    it('should have handler function', () => {
      expect(typeof swiftPackageClean.handler).toBe('function');
    });

    it('should validate schema correctly', () => {
      // Test required fields
      expect(swiftPackageClean.schema.packagePath.safeParse('/test/package').success).toBe(true);
      expect(swiftPackageClean.schema.packagePath.safeParse('').success).toBe(true);

      // Test invalid inputs
      expect(swiftPackageClean.schema.packagePath.safeParse(null).success).toBe(false);
      expect(swiftPackageClean.schema.packagePath.safeParse(undefined).success).toBe(false);
    });
  });

  describe('Command Generation Testing', () => {
    it('should build correct command for clean', async () => {
      const calls: Array<{
        command: string[];
        description: string;
        showOutput: boolean;
        workingDirectory: string | undefined;
      }> = [];

      const mockExecutor = async (
        command: string[],
        description: string,
        showOutput: boolean,
        workingDirectory?: string,
      ) => {
        calls.push({ command, description, showOutput, workingDirectory });
        return {
          success: true,
          output: 'Clean succeeded',
          error: undefined,
          process: { pid: 12345 },
        };
      };

      await swift_package_cleanLogic(
        {
          packagePath: '/test/package',
        },
        mockExecutor,
      );

      expect(calls).toHaveLength(1);
      expect(calls[0]).toEqual({
        command: ['swift', 'package', '--package-path', '/test/package', 'clean'],
        description: 'Swift Package Clean',
        showOutput: true,
        workingDirectory: undefined,
      });
    });
  });

  describe('Response Logic Testing', () => {
    it('should handle valid params without validation errors in logic function', async () => {
      // Note: The logic function assumes valid params since createTypedTool handles validation
      const mockExecutor = createMockExecutor({
        success: true,
        output: 'Package cleaned successfully',
      });

      const result = await swift_package_cleanLogic(
        {
          packagePath: '/test/package',
        },
        mockExecutor,
      );

      expect(result.isError).toBe(false);
      expect(result.content[0].text).toBe('✅ Swift package cleaned successfully.');
    });

    it('should return successful clean response', async () => {
      const mockExecutor = createMockExecutor({
        success: true,
        output: 'Package cleaned successfully',
      });

      const result = await swift_package_cleanLogic(
        {
          packagePath: '/test/package',
        },
        mockExecutor,
      );

      expect(result).toEqual({
        content: [
          { type: 'text', text: '✅ Swift package cleaned successfully.' },
          {
            type: 'text',
            text: '💡 Build artifacts and derived data removed. Ready for fresh build.',
          },
          { type: 'text', text: 'Package cleaned successfully' },
        ],
        isError: false,
      });
    });

    it('should return successful clean response with no output', async () => {
      const mockExecutor = createMockExecutor({
        success: true,
        output: '',
      });

      const result = await swift_package_cleanLogic(
        {
          packagePath: '/test/package',
        },
        mockExecutor,
      );

      expect(result).toEqual({
        content: [
          { type: 'text', text: '✅ Swift package cleaned successfully.' },
          {
            type: 'text',
            text: '💡 Build artifacts and derived data removed. Ready for fresh build.',
          },
          { type: 'text', text: '(clean completed silently)' },
        ],
        isError: false,
      });
    });

    it('should return error response for clean failure', async () => {
      const mockExecutor = createMockExecutor({
        success: false,
        error: 'Permission denied',
      });

      const result = await swift_package_cleanLogic(
        {
          packagePath: '/test/package',
        },
        mockExecutor,
      );

      expect(result).toEqual({
        content: [
          {
            type: 'text',
            text: 'Error: Swift package clean failed\nDetails: Permission denied',
          },
        ],
        isError: true,
      });
    });

    it('should handle spawn error', async () => {
      const mockExecutor = async () => {
        throw new Error('spawn ENOENT');
      };

      const result = await swift_package_cleanLogic(
        {
          packagePath: '/test/package',
        },
        mockExecutor,
      );

      expect(result).toEqual({
        content: [
          {
            type: 'text',
            text: 'Error: Failed to execute swift package clean\nDetails: spawn ENOENT',
          },
        ],
        isError: true,
      });
    });
  });
});

```

--------------------------------------------------------------------------------
/scripts/bundle-axe.sh:
--------------------------------------------------------------------------------

```bash
#!/bin/bash

# Build script for AXe artifacts
# This script downloads pre-built AXe artifacts from GitHub releases and bundles them

set -e

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_ROOT="$(dirname "$SCRIPT_DIR")"
BUNDLED_DIR="$PROJECT_ROOT/bundled"
AXE_LOCAL_DIR="/Volumes/Developer/AXe"
AXE_TEMP_DIR="/tmp/axe-download-$$"

echo "🔨 Preparing AXe artifacts for bundling..."

# Single source of truth for AXe version (overridable)
# 1) Use $AXE_VERSION if provided in env
# 2) Else, use repo-level pin from .axe-version if present
# 3) Else, fall back to default below
DEFAULT_AXE_VERSION="1.1.1"
VERSION_FILE="$PROJECT_ROOT/.axe-version"
if [ -n "${AXE_VERSION}" ]; then
    PINNED_AXE_VERSION="${AXE_VERSION}"
elif [ -f "$VERSION_FILE" ]; then
    PINNED_AXE_VERSION="$(cat "$VERSION_FILE" | tr -d ' \n\r')"
else
    PINNED_AXE_VERSION="$DEFAULT_AXE_VERSION"
fi
echo "📌 Using AXe version: $PINNED_AXE_VERSION"

# Clean up any existing bundled directory
if [ -d "$BUNDLED_DIR" ]; then
    echo "🧹 Cleaning existing bundled directory..."
    rm -rf "$BUNDLED_DIR"
fi

# Create bundled directory
mkdir -p "$BUNDLED_DIR"

# Use local AXe build if available (unless AXE_FORCE_REMOTE=1), otherwise download from GitHub releases
if [ -z "${AXE_FORCE_REMOTE}" ] && [ -d "$AXE_LOCAL_DIR" ] && [ -f "$AXE_LOCAL_DIR/Package.swift" ]; then
    echo "🏠 Using local AXe source at $AXE_LOCAL_DIR"
    cd "$AXE_LOCAL_DIR"

    # Build AXe in release configuration
    echo "🔨 Building AXe in release configuration..."
    swift build --configuration release

    # Check if build succeeded
    if [ ! -f ".build/release/axe" ]; then
        echo "❌ AXe build failed - binary not found"
        exit 1
    fi

    echo "✅ AXe build completed successfully"

    # Copy binary to bundled directory
    echo "📦 Copying AXe binary..."
    cp ".build/release/axe" "$BUNDLED_DIR/"

    # Fix rpath to find frameworks in Frameworks/ subdirectory
    echo "🔧 Configuring AXe binary rpath for bundled frameworks..."
    install_name_tool -add_rpath "@executable_path/Frameworks" "$BUNDLED_DIR/axe"

    # Create Frameworks directory and copy frameworks
    echo "📦 Copying frameworks..."
    mkdir -p "$BUNDLED_DIR/Frameworks"

    # Copy frameworks with better error handling
    for framework in .build/release/*.framework; do
        if [ -d "$framework" ]; then
            echo "📦 Copying framework: $(basename "$framework")"
            cp -r "$framework" "$BUNDLED_DIR/Frameworks/"

            # Only copy nested frameworks if they exist
            if [ -d "$framework/Frameworks" ]; then
                echo "📦 Found nested frameworks in $(basename "$framework")"
                cp -r "$framework/Frameworks"/* "$BUNDLED_DIR/Frameworks/" 2>/dev/null || true
            fi
        fi
    done
else
    echo "📥 Downloading latest AXe release from GitHub..."

    # Construct release download URL from pinned version
    AXE_RELEASE_URL="https://github.com/cameroncooke/AXe/releases/download/v${PINNED_AXE_VERSION}/AXe-macOS-v${PINNED_AXE_VERSION}.tar.gz"

    # Create temp directory
    mkdir -p "$AXE_TEMP_DIR"
    cd "$AXE_TEMP_DIR"

    # Download and extract the release
    echo "📥 Downloading AXe release archive ($AXE_RELEASE_URL)..."
    curl -L -o "axe-release.tar.gz" "$AXE_RELEASE_URL"

    echo "📦 Extracting AXe release archive..."
    tar -xzf "axe-release.tar.gz"

    # Find the extracted directory (might be named differently)
    EXTRACTED_DIR=$(find . -type d -name "*AXe*" -o -name "*axe*" | head -1)
    if [ -z "$EXTRACTED_DIR" ]; then
        # If no AXe directory found, assume files are in current directory
        EXTRACTED_DIR="."
    fi

    cd "$EXTRACTED_DIR"

    # Copy binary
    if [ -f "axe" ]; then
        echo "📦 Copying AXe binary..."
        cp "axe" "$BUNDLED_DIR/"
        chmod +x "$BUNDLED_DIR/axe"
    elif [ -f "bin/axe" ]; then
        echo "📦 Copying AXe binary from bin/..."
        cp "bin/axe" "$BUNDLED_DIR/"
        chmod +x "$BUNDLED_DIR/axe"
    else
        echo "❌ AXe binary not found in release archive"
        ls -la
        exit 1
    fi

    # Copy frameworks if they exist
    echo "📦 Copying frameworks..."
    mkdir -p "$BUNDLED_DIR/Frameworks"

    if [ -d "Frameworks" ]; then
        cp -r Frameworks/* "$BUNDLED_DIR/Frameworks/"
    elif [ -d "lib" ]; then
        # Look for frameworks in lib directory
        find lib -name "*.framework" -exec cp -r {} "$BUNDLED_DIR/Frameworks/" \;
    else
        echo "⚠️  No frameworks directory found in release archive"
        echo "📂 Contents of release archive:"
        find . -type f -name "*.framework" -o -name "*.dylib" | head -10
    fi
fi

# Verify frameworks were copied
FRAMEWORK_COUNT=$(find "$BUNDLED_DIR/Frameworks" -name "*.framework" | wc -l)
echo "📦 Copied $FRAMEWORK_COUNT frameworks"

# List the frameworks for verification
echo "🔍 Bundled frameworks:"
ls -la "$BUNDLED_DIR/Frameworks/"

# Verify binary can run with bundled frameworks
echo "🧪 Testing bundled AXe binary..."
if DYLD_FRAMEWORK_PATH="$BUNDLED_DIR/Frameworks" "$BUNDLED_DIR/axe" --version > /dev/null 2>&1; then
    echo "✅ Bundled AXe binary test passed"
else
    echo "❌ Bundled AXe binary test failed"
    exit 1
fi

# Get AXe version for logging
AXE_VERSION=$(DYLD_FRAMEWORK_PATH="$BUNDLED_DIR/Frameworks" "$BUNDLED_DIR/axe" --version 2>/dev/null || echo "unknown")
echo "📋 AXe version: $AXE_VERSION"

# Clean up temp directory if it was used
if [ -d "$AXE_TEMP_DIR" ]; then
    echo "🧹 Cleaning up temporary files..."
    rm -rf "$AXE_TEMP_DIR"
fi

# Show final bundle size
BUNDLE_SIZE=$(du -sh "$BUNDLED_DIR" | cut -f1)
echo "📊 Final bundle size: $BUNDLE_SIZE"

echo "🎉 AXe bundling completed successfully!"
echo "📁 Bundled artifacts location: $BUNDLED_DIR"

```

--------------------------------------------------------------------------------
/build-plugins/plugin-discovery.ts:
--------------------------------------------------------------------------------

```typescript
import { Plugin } from 'esbuild';
import { readdirSync, readFileSync, existsSync } from 'fs';
import { join } from 'path';
import path from 'path';

export interface WorkflowMetadata {
  name: string;
  description: string;
  platforms?: string[];
  targets?: string[];
  projectTypes?: string[];
  capabilities?: string[];
}

export function createPluginDiscoveryPlugin(): Plugin {
  return {
    name: 'plugin-discovery',
    setup(build) {
      // Generate the workflow loaders file before build starts
      build.onStart(async () => {
        try {
          await generateWorkflowLoaders();
        } catch (error) {
          console.error('Failed to generate workflow loaders:', error);
          throw error;
        }
      });
    }
  };
}

async function generateWorkflowLoaders(): Promise<void> {
  const pluginsDir = path.resolve(process.cwd(), 'src/plugins');
  
  if (!existsSync(pluginsDir)) {
    throw new Error(`Plugins directory not found: ${pluginsDir}`);
  }

  // Scan for workflow directories
  const workflowDirs = readdirSync(pluginsDir, { withFileTypes: true })
    .filter(dirent => dirent.isDirectory())
    .map(dirent => dirent.name);

  const workflowLoaders: Record<string, string> = {};
  const workflowMetadata: Record<string, WorkflowMetadata> = {};

  for (const dirName of workflowDirs) {
    const indexPath = join(pluginsDir, dirName, 'index.ts');
    
    // Check if workflow has index.ts file
    if (!existsSync(indexPath)) {
      console.warn(`Skipping ${dirName}: no index.ts file found`);
      continue;
    }

    // Try to extract workflow metadata from index.ts
    try {
      const indexContent = readFileSync(indexPath, 'utf8');
      const metadata = extractWorkflowMetadata(indexContent);
      
      if (metadata) {
        // Generate dynamic import for this workflow
        workflowLoaders[dirName] = `() => import('../plugins/${dirName}/index.js')`;
        workflowMetadata[dirName] = metadata;
        
        console.log(`✅ Discovered workflow: ${dirName} - ${metadata.name}`);
      } else {
        console.warn(`⚠️  Skipping ${dirName}: invalid workflow metadata`);
      }
    } catch (error) {
      console.warn(`⚠️  Error processing ${dirName}:`, error);
    }
  }

  // Generate the content for generated-plugins.ts
  const generatedContent = generatePluginsFileContent(workflowLoaders, workflowMetadata);
  
  // Write to the generated file
  const outputPath = path.resolve(process.cwd(), 'src/core/generated-plugins.ts');
  
  const fs = await import('fs');
  await fs.promises.writeFile(outputPath, generatedContent, 'utf8');
  
  console.log(`🔧 Generated workflow loaders for ${Object.keys(workflowLoaders).length} workflows`);
}

function extractWorkflowMetadata(content: string): WorkflowMetadata | null {
  try {
    // Simple regex to extract workflow export object
    const workflowMatch = content.match(/export\s+const\s+workflow\s*=\s*({[\s\S]*?});/);
    
    if (!workflowMatch) {
      return null;
    }

    const workflowObj = workflowMatch[1];
    
    // Extract name
    const nameMatch = workflowObj.match(/name\s*:\s*['"`]([^'"`]+)['"`]/);
    if (!nameMatch) return null;
    
    // Extract description
    const descMatch = workflowObj.match(/description\s*:\s*['"`]([\s\S]*?)['"`]/);
    if (!descMatch) return null;

    // Extract platforms (optional)
    const platformsMatch = workflowObj.match(/platforms\s*:\s*\[([^\]]*)\]/);
    let platforms: string[] | undefined;
    if (platformsMatch) {
      platforms = platformsMatch[1]
        .split(',')
        .map(p => p.trim().replace(/['"]/g, ''))
        .filter(p => p.length > 0);
    }

    // Extract targets (optional)
    const targetsMatch = workflowObj.match(/targets\s*:\s*\[([^\]]*)\]/);
    let targets: string[] | undefined;
    if (targetsMatch) {
      targets = targetsMatch[1]
        .split(',')
        .map(t => t.trim().replace(/['"]/g, ''))
        .filter(t => t.length > 0);
    }

    // Extract projectTypes (optional)
    const projectTypesMatch = workflowObj.match(/projectTypes\s*:\s*\[([^\]]*)\]/);
    let projectTypes: string[] | undefined;
    if (projectTypesMatch) {
      projectTypes = projectTypesMatch[1]
        .split(',')
        .map(pt => pt.trim().replace(/['"]/g, ''))
        .filter(pt => pt.length > 0);
    }

    // Extract capabilities (optional)
    const capabilitiesMatch = workflowObj.match(/capabilities\s*:\s*\[([^\]]*)\]/);
    let capabilities: string[] | undefined;
    if (capabilitiesMatch) {
      capabilities = capabilitiesMatch[1]
        .split(',')
        .map(c => c.trim().replace(/['"]/g, ''))
        .filter(c => c.length > 0);
    }

    return {
      name: nameMatch[1],
      description: descMatch[1],
      platforms,
      targets,
      projectTypes,
      capabilities
    };
  } catch (error) {
    console.warn('Failed to extract workflow metadata:', error);
    return null;
  }
}

function generatePluginsFileContent(
  workflowLoaders: Record<string, string>,
  workflowMetadata: Record<string, WorkflowMetadata>
): string {
  const loaderEntries = Object.entries(workflowLoaders)
    .map(([key, loader]) => `  '${key}': ${loader}`)
    .join(',\n');

  const metadataEntries = Object.entries(workflowMetadata)
    .map(([key, metadata]) => {
      const metadataJson = JSON.stringify(metadata, null, 4)
        .split('\n')
        .map(line => `    ${line}`)
        .join('\n');
      return `  '${key}': ${metadataJson.trim()}`;
    })
    .join(',\n');

  return `// AUTO-GENERATED - DO NOT EDIT
// This file is generated by the plugin discovery esbuild plugin

// Generated based on filesystem scan
export const WORKFLOW_LOADERS = {
${loaderEntries}
};

export type WorkflowName = keyof typeof WORKFLOW_LOADERS;

// Optional: Export workflow metadata for quick access
export const WORKFLOW_METADATA = {
${metadataEntries}
};
`;
}
```

--------------------------------------------------------------------------------
/src/mcp/tools/ui-testing/describe_ui.ts:
--------------------------------------------------------------------------------

```typescript
import { z } from 'zod';
import { ToolResponse } from '../../../types/common.ts';
import { log } from '../../../utils/logging/index.ts';
import { createErrorResponse } from '../../../utils/responses/index.ts';
import { DependencyError, AxeError, SystemError } from '../../../utils/errors.ts';
import type { CommandExecutor } from '../../../utils/execution/index.ts';
import { getDefaultCommandExecutor } from '../../../utils/execution/index.ts';
import {
  createAxeNotAvailableResponse,
  getAxePath,
  getBundledAxeEnvironment,
} from '../../../utils/axe-helpers.ts';
import { createTypedTool } from '../../../utils/typed-tool-factory.ts';

// Define schema as ZodObject
const describeUiSchema = z.object({
  simulatorUuid: z.string().uuid('Invalid Simulator UUID format'),
});

// Use z.infer for type safety
type DescribeUiParams = z.infer<typeof describeUiSchema>;

export interface AxeHelpers {
  getAxePath: () => string | null;
  getBundledAxeEnvironment: () => Record<string, string>;
  createAxeNotAvailableResponse: () => ToolResponse;
}

const LOG_PREFIX = '[AXe]';

// Session tracking for describe_ui warnings (shared across UI tools)
const describeUITimestamps = new Map();

function recordDescribeUICall(simulatorUuid: string): void {
  describeUITimestamps.set(simulatorUuid, {
    timestamp: Date.now(),
    simulatorUuid,
  });
}

/**
 * Core business logic for describe_ui functionality
 */
export async function describe_uiLogic(
  params: DescribeUiParams,
  executor: CommandExecutor,
  axeHelpers: AxeHelpers = {
    getAxePath,
    getBundledAxeEnvironment,
    createAxeNotAvailableResponse,
  },
): Promise<ToolResponse> {
  const toolName = 'describe_ui';
  const { simulatorUuid } = params;
  const commandArgs = ['describe-ui'];

  log('info', `${LOG_PREFIX}/${toolName}: Starting for ${simulatorUuid}`);

  try {
    const responseText = await executeAxeCommand(
      commandArgs,
      simulatorUuid,
      'describe-ui',
      executor,
      axeHelpers,
    );

    // Record the describe_ui call for warning system
    recordDescribeUICall(simulatorUuid);

    log('info', `${LOG_PREFIX}/${toolName}: Success for ${simulatorUuid}`);
    return {
      content: [
        {
          type: 'text',
          text:
            'Accessibility hierarchy retrieved successfully:\n```json\n' + responseText + '\n```',
        },
        {
          type: 'text',
          text: `Next Steps:
- Use frame coordinates for tap/swipe (center: x+width/2, y+height/2)
- Re-run describe_ui after layout changes
- Screenshots are for visual verification only`,
        },
      ],
    };
  } catch (error) {
    log('error', `${LOG_PREFIX}/${toolName}: Failed - ${error}`);
    if (error instanceof DependencyError) {
      return axeHelpers.createAxeNotAvailableResponse();
    } else if (error instanceof AxeError) {
      return createErrorResponse(
        `Failed to get accessibility hierarchy: ${error.message}`,
        error.axeOutput,
      );
    } else if (error instanceof SystemError) {
      return createErrorResponse(
        `System error executing axe: ${error.message}`,
        error.originalError?.stack,
      );
    }
    return createErrorResponse(
      `An unexpected error occurred: ${error instanceof Error ? error.message : String(error)}`,
    );
  }
}

export default {
  name: 'describe_ui',
  description:
    'Gets entire view hierarchy with precise frame coordinates (x, y, width, height) for all visible elements. Use this before UI interactions or after layout changes - do NOT guess coordinates from screenshots. Returns JSON tree with frame data for accurate automation.',
  schema: describeUiSchema.shape, // MCP SDK compatibility
  handler: createTypedTool(
    describeUiSchema,
    (params: DescribeUiParams, executor: CommandExecutor) => {
      return describe_uiLogic(params, executor, {
        getAxePath,
        getBundledAxeEnvironment,
        createAxeNotAvailableResponse,
      });
    },
    getDefaultCommandExecutor,
  ),
};

// Helper function for executing axe commands (inlined from src/tools/axe/index.ts)
async function executeAxeCommand(
  commandArgs: string[],
  simulatorUuid: string,
  commandName: string,
  executor: CommandExecutor = getDefaultCommandExecutor(),
  axeHelpers: AxeHelpers = { getAxePath, getBundledAxeEnvironment, createAxeNotAvailableResponse },
): Promise<string> {
  // Get the appropriate axe binary path
  const axeBinary = axeHelpers.getAxePath();
  if (!axeBinary) {
    throw new DependencyError('AXe binary not found');
  }

  // Add --udid parameter to all commands
  const fullArgs = [...commandArgs, '--udid', simulatorUuid];

  // Construct the full command array with the axe binary as the first element
  const fullCommand = [axeBinary, ...fullArgs];

  try {
    // Determine environment variables for bundled AXe
    const axeEnv = axeBinary !== 'axe' ? axeHelpers.getBundledAxeEnvironment() : undefined;

    const result = await executor(fullCommand, `${LOG_PREFIX}: ${commandName}`, false, axeEnv);

    if (!result.success) {
      throw new AxeError(
        `axe command '${commandName}' failed.`,
        commandName,
        result.error ?? result.output,
        simulatorUuid,
      );
    }

    // Check for stderr output in successful commands
    if (result.error) {
      log(
        'warn',
        `${LOG_PREFIX}: Command '${commandName}' produced stderr output but exited successfully. Output: ${result.error}`,
      );
    }

    return result.output.trim();
  } catch (error) {
    if (error instanceof Error) {
      if (error instanceof AxeError) {
        throw error;
      }

      // Otherwise wrap it in a SystemError
      throw new SystemError(`Failed to execute axe command: ${error.message}`, error);
    }

    // For any other type of error
    throw new SystemError(`Failed to execute axe command: ${String(error)}`);
  }
}

```

--------------------------------------------------------------------------------
/src/mcp/tools/macos/__tests__/stop_mac_app.test.ts:
--------------------------------------------------------------------------------

```typescript
/**
 * Pure dependency injection test for stop_mac_app plugin
 *
 * Tests plugin structure and macOS app stopping functionality including parameter validation,
 * command generation, and response formatting.
 *
 * Uses manual call tracking instead of vitest mocking.
 * NO VITEST MOCKING ALLOWED - Only manual stubs
 */

import { describe, it, expect } from 'vitest';
import { z } from 'zod';

import stopMacApp, { stop_mac_appLogic } from '../stop_mac_app.ts';

describe('stop_mac_app plugin', () => {
  describe('Export Field Validation (Literal)', () => {
    it('should have correct name', () => {
      expect(stopMacApp.name).toBe('stop_mac_app');
    });

    it('should have correct description', () => {
      expect(stopMacApp.description).toBe(
        'Stops a running macOS application. Can stop by app name or process ID.',
      );
    });

    it('should have handler function', () => {
      expect(typeof stopMacApp.handler).toBe('function');
    });

    it('should validate schema correctly', () => {
      // Test optional fields
      expect(stopMacApp.schema.appName.safeParse('Calculator').success).toBe(true);
      expect(stopMacApp.schema.appName.safeParse(undefined).success).toBe(true);
      expect(stopMacApp.schema.processId.safeParse(1234).success).toBe(true);
      expect(stopMacApp.schema.processId.safeParse(undefined).success).toBe(true);

      // Test invalid inputs
      expect(stopMacApp.schema.appName.safeParse(null).success).toBe(false);
      expect(stopMacApp.schema.processId.safeParse('not-number').success).toBe(false);
      expect(stopMacApp.schema.processId.safeParse(null).success).toBe(false);
    });
  });

  describe('Input Validation', () => {
    it('should return exact validation error for missing parameters', async () => {
      const mockExecutor = async () => ({ success: true, output: '', process: {} as any });
      const result = await stop_mac_appLogic({}, mockExecutor);

      expect(result).toEqual({
        content: [
          {
            type: 'text',
            text: 'Either appName or processId must be provided.',
          },
        ],
        isError: true,
      });
    });
  });

  describe('Command Generation', () => {
    it('should generate correct command for process ID', async () => {
      const calls: any[] = [];
      const mockExecutor = async (command: string[]) => {
        calls.push({ command });
        return { success: true, output: '', process: {} as any };
      };

      await stop_mac_appLogic(
        {
          processId: 1234,
        },
        mockExecutor,
      );

      expect(calls).toHaveLength(1);
      expect(calls[0].command).toEqual(['kill', '1234']);
    });

    it('should generate correct command for app name', async () => {
      const calls: any[] = [];
      const mockExecutor = async (command: string[]) => {
        calls.push({ command });
        return { success: true, output: '', process: {} as any };
      };

      await stop_mac_appLogic(
        {
          appName: 'Calculator',
        },
        mockExecutor,
      );

      expect(calls).toHaveLength(1);
      expect(calls[0].command).toEqual([
        'sh',
        '-c',
        'pkill -f "Calculator" || osascript -e \'tell application "Calculator" to quit\'',
      ]);
    });

    it('should prioritize processId over appName', async () => {
      const calls: any[] = [];
      const mockExecutor = async (command: string[]) => {
        calls.push({ command });
        return { success: true, output: '', process: {} as any };
      };

      await stop_mac_appLogic(
        {
          appName: 'Calculator',
          processId: 1234,
        },
        mockExecutor,
      );

      expect(calls).toHaveLength(1);
      expect(calls[0].command).toEqual(['kill', '1234']);
    });
  });

  describe('Response Processing', () => {
    it('should return exact successful stop response by app name', async () => {
      const mockExecutor = async () => ({ success: true, output: '', process: {} as any });

      const result = await stop_mac_appLogic(
        {
          appName: 'Calculator',
        },
        mockExecutor,
      );

      expect(result).toEqual({
        content: [
          {
            type: 'text',
            text: '✅ macOS app stopped successfully: Calculator',
          },
        ],
      });
    });

    it('should return exact successful stop response by process ID', async () => {
      const mockExecutor = async () => ({ success: true, output: '', process: {} as any });

      const result = await stop_mac_appLogic(
        {
          processId: 1234,
        },
        mockExecutor,
      );

      expect(result).toEqual({
        content: [
          {
            type: 'text',
            text: '✅ macOS app stopped successfully: PID 1234',
          },
        ],
      });
    });

    it('should return exact successful stop response with both parameters (processId takes precedence)', async () => {
      const mockExecutor = async () => ({ success: true, output: '', process: {} as any });

      const result = await stop_mac_appLogic(
        {
          appName: 'Calculator',
          processId: 1234,
        },
        mockExecutor,
      );

      expect(result).toEqual({
        content: [
          {
            type: 'text',
            text: '✅ macOS app stopped successfully: PID 1234',
          },
        ],
      });
    });

    it('should handle execution errors', async () => {
      const mockExecutor = async () => {
        throw new Error('Process not found');
      };

      const result = await stop_mac_appLogic(
        {
          processId: 9999,
        },
        mockExecutor,
      );

      expect(result).toEqual({
        content: [
          {
            type: 'text',
            text: '❌ Stop macOS app operation failed: Process not found',
          },
        ],
        isError: true,
      });
    });
  });
});

```

--------------------------------------------------------------------------------
/src/utils/tool-registry.ts:
--------------------------------------------------------------------------------

```typescript
import { McpServer, RegisteredTool } from '@camsoft/mcp-sdk/server/mcp.js';
import { loadPlugins } from '../core/plugin-registry.ts';
import { ToolResponse } from '../types/common.ts';
import { log } from './logger.ts';

// Global registry to track registered tools for cleanup
const toolRegistry = new Map<string, RegisteredTool>();

/**
 * Register a tool and track it for potential removal
 */
export function registerAndTrackTool(
  server: McpServer,
  name: string,
  config: Parameters<McpServer['registerTool']>[1],
  callback: Parameters<McpServer['registerTool']>[2],
): RegisteredTool {
  const registeredTool = server.registerTool(name, config, callback);
  toolRegistry.set(name, registeredTool);
  return registeredTool;
}

/**
 * Register multiple tools and track them for potential removal
 */
export function registerAndTrackTools(
  server: McpServer,
  tools: Parameters<McpServer['registerTools']>[0],
): RegisteredTool[] {
  const registeredTools = server.registerTools(tools);

  // Track each registered tool
  tools.forEach((tool, index) => {
    if (registeredTools[index]) {
      toolRegistry.set(tool.name, registeredTools[index]);
    }
  });

  return registeredTools;
}

/**
 * Check if a tool is already registered
 */
export function isToolRegistered(name: string): boolean {
  return toolRegistry.has(name);
}

/**
 * Remove a specific tracked tool by name
 */
export function removeTrackedTool(name: string): boolean {
  const tool = toolRegistry.get(name);
  if (!tool) {
    return false;
  }

  try {
    tool.remove();
    toolRegistry.delete(name);
    log('debug', `✅ Removed tool: ${name}`);
    return true;
  } catch (error) {
    log('error', `❌ Failed to remove tool ${name}: ${error}`);
    return false;
  }
}

/**
 * Remove multiple tracked tools by names
 */
export function removeTrackedTools(names: string[]): string[] {
  const removedTools: string[] = [];

  for (const name of names) {
    if (removeTrackedTool(name)) {
      removedTools.push(name);
    }
  }

  return removedTools;
}

/**
 * Remove all currently tracked tools
 */
export function removeAllTrackedTools(): void {
  const toolNames = Array.from(toolRegistry.keys());

  if (toolNames.length === 0) {
    return;
  }

  log('info', `Removing ${toolNames.length} tracked tools...`);

  const removedTools = removeTrackedTools(toolNames);
  log('info', `✅ Removed ${removedTools.length} tracked tools`);
}

/**
 * Get the number of currently tracked tools
 */
export function getTrackedToolCount(): number {
  return toolRegistry.size;
}

/**
 * Get the names of currently tracked tools
 */
export function getTrackedToolNames(): string[] {
  return Array.from(toolRegistry.keys());
}

/**
 * Register only discovery tools (discover_tools, discover_projs) with tracking
 */
export async function registerDiscoveryTools(server: McpServer): Promise<void> {
  const plugins = await loadPlugins();
  let registeredCount = 0;

  // Only register discovery tools initially
  const discoveryTools = [];
  for (const plugin of plugins.values()) {
    // Only load discover_tools and discover_projs initially - other tools will be loaded via workflows
    if (plugin.name === 'discover_tools' || plugin.name === 'discover_projs') {
      discoveryTools.push({
        name: plugin.name,
        config: {
          description: plugin.description ?? '',
          inputSchema: plugin.schema,
        },
        // Adapt callback to match SDK's expected signature
        callback: (args: unknown): Promise<ToolResponse> =>
          plugin.handler(args as Record<string, unknown>),
      });
      registeredCount++;
    }
  }

  // Register discovery tools using bulk registration with tracking
  if (discoveryTools.length > 0) {
    registerAndTrackTools(server, discoveryTools);
  }

  log('info', `✅ Registered ${registeredCount} discovery tools in dynamic mode.`);
}

/**
 * Register selected workflows based on environment variable
 */
export async function registerSelectedWorkflows(
  server: McpServer,
  workflowNames: string[],
): Promise<void> {
  const { loadWorkflowGroups } = await import('../core/plugin-registry.js');
  const workflowGroups = await loadWorkflowGroups();
  const selectedTools = [];

  for (const workflowName of workflowNames) {
    const workflow = workflowGroups.get(workflowName.trim());
    if (workflow) {
      for (const tool of workflow.tools) {
        selectedTools.push({
          name: tool.name,
          config: {
            description: tool.description ?? '',
            inputSchema: tool.schema,
          },
          callback: (args: unknown): Promise<ToolResponse> =>
            tool.handler(args as Record<string, unknown>),
        });
      }
    }
  }

  if (selectedTools.length > 0) {
    server.registerTools(selectedTools);
  }

  log(
    'info',
    `✅ Registered ${selectedTools.length} tools from workflows: ${workflowNames.join(', ')}`,
  );
}

/**
 * Register all tools (static mode) - no tracking needed since these won't be removed
 */
export async function registerAllToolsStatic(server: McpServer): Promise<void> {
  const plugins = await loadPlugins();
  const allTools = [];

  for (const plugin of plugins.values()) {
    // Exclude discovery tools in static mode - they should only be available in dynamic mode
    if (plugin.name === 'discover_tools') {
      continue;
    }

    allTools.push({
      name: plugin.name,
      config: {
        description: plugin.description ?? '',
        inputSchema: plugin.schema,
      },
      // Adapt callback to match SDK's expected signature
      callback: (args: unknown): Promise<ToolResponse> =>
        plugin.handler(args as Record<string, unknown>),
    });
  }

  // Register all tools using bulk registration (no tracking since static tools aren't removed)
  if (allTools.length > 0) {
    server.registerTools(allTools);
  }

  log('info', `✅ Registered ${allTools.length} tools in static mode.`);
}

```

--------------------------------------------------------------------------------
/src/utils/__tests__/simulator-utils.test.ts:
--------------------------------------------------------------------------------

```typescript
import { describe, it, expect } from 'vitest';
import { determineSimulatorUuid } from '../simulator-utils.ts';
import { createMockExecutor } from '../../test-utils/mock-executors.ts';

describe('determineSimulatorUuid', () => {
  const mockSimulatorListOutput = JSON.stringify({
    devices: {
      'com.apple.CoreSimulator.SimRuntime.iOS-17-0': [
        {
          udid: 'ABC-123-UUID',
          name: 'iPhone 16',
          isAvailable: true,
        },
        {
          udid: 'DEF-456-UUID',
          name: 'iPhone 15',
          isAvailable: false,
        },
      ],
      'com.apple.CoreSimulator.SimRuntime.iOS-16-0': [
        {
          udid: 'GHI-789-UUID',
          name: 'iPhone 14',
          isAvailable: true,
        },
      ],
    },
  });

  describe('UUID provided directly', () => {
    it('should return UUID when simulatorUuid is provided', async () => {
      const mockExecutor = createMockExecutor(
        new Error('Should not call executor when UUID provided'),
      );

      const result = await determineSimulatorUuid(
        { simulatorUuid: 'DIRECT-UUID-123' },
        mockExecutor,
      );

      expect(result.uuid).toBe('DIRECT-UUID-123');
      expect(result.warning).toBeUndefined();
      expect(result.error).toBeUndefined();
    });

    it('should prefer simulatorUuid when both UUID and name are provided', async () => {
      const mockExecutor = createMockExecutor(
        new Error('Should not call executor when UUID provided'),
      );

      const result = await determineSimulatorUuid(
        { simulatorUuid: 'DIRECT-UUID', simulatorName: 'iPhone 16' },
        mockExecutor,
      );

      expect(result.uuid).toBe('DIRECT-UUID');
    });
  });

  describe('Name that looks like UUID', () => {
    it('should detect and use UUID-like name directly', async () => {
      const mockExecutor = createMockExecutor(
        new Error('Should not call executor for UUID-like name'),
      );
      const uuidLikeName = '12345678-1234-1234-1234-123456789abc';

      const result = await determineSimulatorUuid({ simulatorName: uuidLikeName }, mockExecutor);

      expect(result.uuid).toBe(uuidLikeName);
      expect(result.warning).toContain('appears to be a UUID');
      expect(result.error).toBeUndefined();
    });

    it('should detect uppercase UUID-like name', async () => {
      const mockExecutor = createMockExecutor(
        new Error('Should not call executor for UUID-like name'),
      );
      const uuidLikeName = '12345678-1234-1234-1234-123456789ABC';

      const result = await determineSimulatorUuid({ simulatorName: uuidLikeName }, mockExecutor);

      expect(result.uuid).toBe(uuidLikeName);
      expect(result.warning).toContain('appears to be a UUID');
    });
  });

  describe('Name resolution via simctl', () => {
    it('should resolve name to UUID for available simulator', async () => {
      const mockExecutor = createMockExecutor({
        success: true,
        output: mockSimulatorListOutput,
      });

      const result = await determineSimulatorUuid({ simulatorName: 'iPhone 16' }, mockExecutor);

      expect(result.uuid).toBe('ABC-123-UUID');
      expect(result.warning).toBeUndefined();
      expect(result.error).toBeUndefined();
    });

    it('should find simulator across different runtimes', async () => {
      const mockExecutor = createMockExecutor({
        success: true,
        output: mockSimulatorListOutput,
      });

      const result = await determineSimulatorUuid({ simulatorName: 'iPhone 14' }, mockExecutor);

      expect(result.uuid).toBe('GHI-789-UUID');
      expect(result.error).toBeUndefined();
    });

    it('should error for unavailable simulator', async () => {
      const mockExecutor = createMockExecutor({
        success: true,
        output: mockSimulatorListOutput,
      });

      const result = await determineSimulatorUuid({ simulatorName: 'iPhone 15' }, mockExecutor);

      expect(result.uuid).toBeUndefined();
      expect(result.error).toBeDefined();
      expect(result.error?.content[0].text).toContain('exists but is not available');
    });

    it('should error for non-existent simulator', async () => {
      const mockExecutor = createMockExecutor({
        success: true,
        output: mockSimulatorListOutput,
      });

      const result = await determineSimulatorUuid({ simulatorName: 'iPhone 99' }, mockExecutor);

      expect(result.uuid).toBeUndefined();
      expect(result.error).toBeDefined();
      expect(result.error?.content[0].text).toContain('not found');
    });

    it('should handle simctl list failure', async () => {
      const mockExecutor = createMockExecutor({
        success: false,
        error: 'simctl command failed',
      });

      const result = await determineSimulatorUuid({ simulatorName: 'iPhone 16' }, mockExecutor);

      expect(result.uuid).toBeUndefined();
      expect(result.error).toBeDefined();
      expect(result.error?.content[0].text).toContain('Failed to list simulators');
    });

    it('should handle invalid JSON from simctl', async () => {
      const mockExecutor = createMockExecutor({
        success: true,
        output: 'invalid json {',
      });

      const result = await determineSimulatorUuid({ simulatorName: 'iPhone 16' }, mockExecutor);

      expect(result.uuid).toBeUndefined();
      expect(result.error).toBeDefined();
      expect(result.error?.content[0].text).toContain('Failed to parse simulator list');
    });
  });

  describe('No identifier provided', () => {
    it('should error when neither UUID nor name is provided', async () => {
      const mockExecutor = createMockExecutor(
        new Error('Should not call executor when no identifier'),
      );

      const result = await determineSimulatorUuid({}, mockExecutor);

      expect(result.uuid).toBeUndefined();
      expect(result.error).toBeDefined();
      expect(result.error?.content[0].text).toContain('No simulator identifier provided');
    });
  });
});

```

--------------------------------------------------------------------------------
/src/mcp/tools/simulator/build_sim.ts:
--------------------------------------------------------------------------------

```typescript
/**
 * Simulator Build Plugin: Build Simulator (Unified)
 *
 * Builds an app from a project or workspace for a specific simulator by UUID or name.
 * Accepts mutually exclusive `projectPath` or `workspacePath`.
 * Accepts mutually exclusive `simulatorId` or `simulatorName`.
 */

import { z } from 'zod';
import { log } from '../../../utils/logging/index.ts';
import { executeXcodeBuildCommand } from '../../../utils/build/index.ts';
import { ToolResponse, XcodePlatform } from '../../../types/common.ts';
import type { CommandExecutor } from '../../../utils/execution/index.ts';
import { getDefaultCommandExecutor } from '../../../utils/execution/index.ts';
import { createSessionAwareTool } from '../../../utils/typed-tool-factory.ts';
import { nullifyEmptyStrings } from '../../../utils/schema-helpers.ts';

// Unified schema: XOR between projectPath and workspacePath, and XOR between simulatorId and simulatorName
const baseOptions = {
  scheme: z.string().describe('The scheme to use (Required)'),
  simulatorId: z
    .string()
    .optional()
    .describe(
      'UUID of the simulator (from list_sims). Provide EITHER this OR simulatorName, not both',
    ),
  simulatorName: z
    .string()
    .optional()
    .describe(
      "Name of the simulator (e.g., 'iPhone 16'). Provide EITHER this OR simulatorId, not both",
    ),
  configuration: z.string().optional().describe('Build configuration (Debug, Release, etc.)'),
  derivedDataPath: z
    .string()
    .optional()
    .describe('Path where build products and other derived data will go'),
  extraArgs: z.array(z.string()).optional().describe('Additional xcodebuild arguments'),
  useLatestOS: z
    .boolean()
    .optional()
    .describe('Whether to use the latest OS version for the named simulator'),
  preferXcodebuild: z
    .boolean()
    .optional()
    .describe(
      'If true, prefers xcodebuild over the experimental incremental build system, useful for when incremental build system fails.',
    ),
};

const baseSchemaObject = z.object({
  projectPath: z
    .string()
    .optional()
    .describe('Path to .xcodeproj file. Provide EITHER this OR workspacePath, not both'),
  workspacePath: z
    .string()
    .optional()
    .describe('Path to .xcworkspace file. Provide EITHER this OR projectPath, not both'),
  ...baseOptions,
});

const baseSchema = z.preprocess(nullifyEmptyStrings, baseSchemaObject);

const buildSimulatorSchema = baseSchema
  .refine((val) => val.projectPath !== undefined || val.workspacePath !== undefined, {
    message: 'Either projectPath or workspacePath is required.',
  })
  .refine((val) => !(val.projectPath !== undefined && val.workspacePath !== undefined), {
    message: 'projectPath and workspacePath are mutually exclusive. Provide only one.',
  })
  .refine((val) => val.simulatorId !== undefined || val.simulatorName !== undefined, {
    message: 'Either simulatorId or simulatorName is required.',
  })
  .refine((val) => !(val.simulatorId !== undefined && val.simulatorName !== undefined), {
    message: 'simulatorId and simulatorName are mutually exclusive. Provide only one.',
  });

export type BuildSimulatorParams = z.infer<typeof buildSimulatorSchema>;

// Internal logic for building Simulator apps.
async function _handleSimulatorBuildLogic(
  params: BuildSimulatorParams,
  executor: CommandExecutor = getDefaultCommandExecutor(),
): Promise<ToolResponse> {
  const projectType = params.projectPath ? 'project' : 'workspace';
  const filePath = params.projectPath ?? params.workspacePath;

  // Log warning if useLatestOS is provided with simulatorId
  if (params.simulatorId && params.useLatestOS !== undefined) {
    log(
      'warning',
      `useLatestOS parameter is ignored when using simulatorId (UUID implies exact device/OS)`,
    );
  }

  log(
    'info',
    `Starting iOS Simulator build for scheme ${params.scheme} from ${projectType}: ${filePath}`,
  );

  // Ensure configuration has a default value for SharedBuildParams compatibility
  const sharedBuildParams = {
    ...params,
    configuration: params.configuration ?? 'Debug',
  };

  // executeXcodeBuildCommand handles both simulatorId and simulatorName
  return executeXcodeBuildCommand(
    sharedBuildParams,
    {
      platform: XcodePlatform.iOSSimulator,
      simulatorName: params.simulatorName,
      simulatorId: params.simulatorId,
      useLatestOS: params.simulatorId ? false : params.useLatestOS, // Ignore useLatestOS with ID
      logPrefix: 'iOS Simulator Build',
    },
    params.preferXcodebuild ?? false,
    'build',
    executor,
  );
}

export async function build_simLogic(
  params: BuildSimulatorParams,
  executor: CommandExecutor,
): Promise<ToolResponse> {
  // Provide defaults
  const processedParams: BuildSimulatorParams = {
    ...params,
    configuration: params.configuration ?? 'Debug',
    useLatestOS: params.useLatestOS ?? true, // May be ignored if simulatorId is provided
    preferXcodebuild: params.preferXcodebuild ?? false,
  };

  return _handleSimulatorBuildLogic(processedParams, executor);
}

// Public schema = internal minus session-managed fields
const publicSchemaObject = baseSchemaObject.omit({
  projectPath: true,
  workspacePath: true,
  scheme: true,
  configuration: true,
  simulatorId: true,
  simulatorName: true,
  useLatestOS: true,
} as const);

export default {
  name: 'build_sim',
  description: 'Builds an app for an iOS simulator.',
  schema: publicSchemaObject.shape, // MCP SDK compatibility (public inputs only)
  handler: createSessionAwareTool<BuildSimulatorParams>({
    internalSchema: buildSimulatorSchema as unknown as z.ZodType<BuildSimulatorParams>,
    logicFunction: build_simLogic,
    getExecutor: getDefaultCommandExecutor,
    requirements: [
      { allOf: ['scheme'], message: 'scheme is required' },
      { oneOf: ['projectPath', 'workspacePath'], message: 'Provide a project or workspace' },
      { oneOf: ['simulatorId', 'simulatorName'], message: 'Provide simulatorId or simulatorName' },
    ],
    exclusivePairs: [
      ['projectPath', 'workspacePath'],
      ['simulatorId', 'simulatorName'],
    ],
  }),
};

```

--------------------------------------------------------------------------------
/src/mcp/tools/utilities/clean.ts:
--------------------------------------------------------------------------------

```typescript
/**
 * Utilities Plugin: Clean (Unified)
 *
 * Cleans build products for either a project or workspace using xcodebuild.
 * Accepts mutually exclusive `projectPath` or `workspacePath`.
 */

import { z } from 'zod';
import { createSessionAwareTool } from '../../../utils/typed-tool-factory.ts';
import type { CommandExecutor } from '../../../utils/execution/index.ts';
import { getDefaultCommandExecutor } from '../../../utils/execution/index.ts';
import { executeXcodeBuildCommand } from '../../../utils/build/index.ts';
import { ToolResponse, SharedBuildParams, XcodePlatform } from '../../../types/common.ts';
import { createErrorResponse } from '../../../utils/responses/index.ts';
import { nullifyEmptyStrings } from '../../../utils/schema-helpers.ts';

// Unified schema: XOR between projectPath and workspacePath, sharing common options
const baseOptions = {
  scheme: z.string().optional().describe('Optional: The scheme to clean'),
  configuration: z
    .string()
    .optional()
    .describe('Optional: Build configuration to clean (Debug, Release, etc.)'),
  derivedDataPath: z
    .string()
    .optional()
    .describe('Optional: Path where derived data might be located'),
  extraArgs: z.array(z.string()).optional().describe('Additional xcodebuild arguments'),
  preferXcodebuild: z
    .boolean()
    .optional()
    .describe(
      'If true, prefers xcodebuild over the experimental incremental build system, useful for when incremental build system fails.',
    ),
  platform: z
    .enum([
      'macOS',
      'iOS',
      'iOS Simulator',
      'watchOS',
      'watchOS Simulator',
      'tvOS',
      'tvOS Simulator',
      'visionOS',
      'visionOS Simulator',
    ])
    .optional()
    .describe(
      'Optional: Platform to clean for (defaults to iOS). Choose from macOS, iOS, iOS Simulator, watchOS, watchOS Simulator, tvOS, tvOS Simulator, visionOS, visionOS Simulator',
    ),
};

const baseSchemaObject = z.object({
  projectPath: z.string().optional().describe('Path to the .xcodeproj file'),
  workspacePath: z.string().optional().describe('Path to the .xcworkspace file'),
  ...baseOptions,
});

const baseSchema = z.preprocess(nullifyEmptyStrings, baseSchemaObject);

const cleanSchema = baseSchema
  .refine((val) => val.projectPath !== undefined || val.workspacePath !== undefined, {
    message: 'Either projectPath or workspacePath is required.',
  })
  .refine((val) => !(val.projectPath !== undefined && val.workspacePath !== undefined), {
    message: 'projectPath and workspacePath are mutually exclusive. Provide only one.',
  })
  .refine((val) => !(val.workspacePath && !val.scheme), {
    message: 'scheme is required when workspacePath is provided.',
    path: ['scheme'],
  });

export type CleanParams = z.infer<typeof cleanSchema>;

export async function cleanLogic(
  params: CleanParams,
  executor: CommandExecutor,
): Promise<ToolResponse> {
  // Extra safety: ensure workspace path has a scheme (xcodebuild requires it)
  if (params.workspacePath && !params.scheme) {
    return createErrorResponse(
      'Parameter validation failed',
      'Invalid parameters:\nscheme: scheme is required when workspacePath is provided.',
    );
  }

  // Use provided platform or default to iOS
  const targetPlatform = params.platform ?? 'iOS';

  // Map human-friendly platform names to XcodePlatform enum values
  // This is safer than direct key lookup and handles the space-containing simulator names
  const platformMap = {
    macOS: XcodePlatform.macOS,
    iOS: XcodePlatform.iOS,
    'iOS Simulator': XcodePlatform.iOSSimulator,
    watchOS: XcodePlatform.watchOS,
    'watchOS Simulator': XcodePlatform.watchOSSimulator,
    tvOS: XcodePlatform.tvOS,
    'tvOS Simulator': XcodePlatform.tvOSSimulator,
    visionOS: XcodePlatform.visionOS,
    'visionOS Simulator': XcodePlatform.visionOSSimulator,
  };

  const platformEnum = platformMap[targetPlatform];
  if (!platformEnum) {
    return createErrorResponse(
      'Parameter validation failed',
      `Invalid parameters:\nplatform: unsupported value "${targetPlatform}".`,
    );
  }

  const hasProjectPath = typeof params.projectPath === 'string';
  const typedParams: SharedBuildParams = {
    ...(hasProjectPath
      ? { projectPath: params.projectPath as string }
      : { workspacePath: params.workspacePath as string }),
    // scheme may be omitted for project; when omitted we do not pass -scheme
    // Provide empty string to satisfy type, executeXcodeBuildCommand only emits -scheme when non-empty
    scheme: params.scheme ?? '',
    configuration: params.configuration ?? 'Debug',
    derivedDataPath: params.derivedDataPath,
    extraArgs: params.extraArgs,
  };

  // For clean operations, simulator platforms should be mapped to their device equivalents
  // since clean works at the build product level, not runtime level, and build products
  // are shared between device and simulator platforms
  const cleanPlatformMap: Partial<Record<XcodePlatform, XcodePlatform>> = {
    [XcodePlatform.iOSSimulator]: XcodePlatform.iOS,
    [XcodePlatform.watchOSSimulator]: XcodePlatform.watchOS,
    [XcodePlatform.tvOSSimulator]: XcodePlatform.tvOS,
    [XcodePlatform.visionOSSimulator]: XcodePlatform.visionOS,
  };

  const cleanPlatform = cleanPlatformMap[platformEnum] ?? platformEnum;

  return executeXcodeBuildCommand(
    typedParams,
    {
      platform: cleanPlatform,
      logPrefix: 'Clean',
    },
    false,
    'clean',
    executor,
  );
}

const publicSchemaObject = baseSchemaObject.omit({
  projectPath: true,
  workspacePath: true,
  scheme: true,
  configuration: true,
} as const);

export default {
  name: 'clean',
  description: 'Cleans build products with xcodebuild.',
  schema: publicSchemaObject.shape,
  handler: createSessionAwareTool<CleanParams>({
    internalSchema: cleanSchema as unknown as z.ZodType<CleanParams>,
    logicFunction: cleanLogic,
    getExecutor: getDefaultCommandExecutor,
    requirements: [
      { oneOf: ['projectPath', 'workspacePath'], message: 'Provide a project or workspace' },
    ],
    exclusivePairs: [['projectPath', 'workspacePath']],
  }),
};

```

--------------------------------------------------------------------------------
/docs/RELEASE_PROCESS.md:
--------------------------------------------------------------------------------

```markdown
# Release Process

## Step-by-Step Development Workflow

### 1. Starting New Work

**Always start by syncing with main:**
```bash
git checkout main
git pull origin main
```

**Create feature branch using standardized naming convention:**
```bash
git checkout -b feature/issue-123-add-new-feature
git checkout -b bugfix/issue-456-fix-simulator-crash
```

### 2. Development & Commits

**Before committing, ALWAYS run quality checks:**
```bash
npm run build      # Ensure code compiles
npm run typecheck  # MANDATORY: Fix all TypeScript errors
npm run lint       # Fix linting issues
npm run test       # Ensure tests pass
```

**🚨 CRITICAL: TypeScript errors are BLOCKING:**
- **ZERO tolerance** for TypeScript errors in commits
- The `npm run typecheck` command must pass with no errors
- Fix all `ts(XXXX)` errors before committing
- Do not ignore or suppress TypeScript errors without explicit approval

**Make logical, atomic commits:**
- Each commit should represent a single logical change  
- Write short, descriptive commit summaries
- Commit frequently to your feature branch

```bash
# Always run quality checks first
npm run typecheck && npm run lint && npm run test

# Then commit your changes
git add .
git commit -m "feat: add simulator boot validation logic"
git commit -m "fix: handle null response in device list parser"
```

### 3. Pushing Changes

**🚨 CRITICAL: Always ask permission before pushing**
- **NEVER push without explicit user permission**
- **NEVER force push without explicit permission**
- Pushing without permission is a fatal error resulting in termination

```bash
# Only after getting permission:
git push origin feature/your-branch-name
```

### 4. Pull Request Creation

**Use GitHub CLI tool exclusively:**
```bash
gh pr create --title "feat: add simulator boot validation" --body "$(cat <<'EOF'
## Summary
Brief description of what this PR does and why.

## Background/Details
### For New Features:
- Detailed explanation of the new feature
- Context and requirements that led to this implementation
- Design decisions and approach taken

### For Bug Fixes:
- **Root Cause Analysis**: Detailed explanation of what caused the bug
- Specific conditions that trigger the issue
- Why the current code fails in these scenarios

## Solution
- How the root cause was addressed
- Technical approach and implementation details
- Key changes made to resolve the issue

## Testing
- **Reproduction Steps**: How to reproduce the original issue (for bugs)
- **Validation Method**: How you verified the fix works
- **Test Coverage**: What tests were added or modified
- **Manual Testing**: Steps taken to validate the solution
- **Edge Cases**: Additional scenarios tested

## Notes
- Any important considerations for reviewers
- Potential impacts or side effects
- Future improvements or technical debt
- Deployment considerations
EOF
)"
```

**After PR creation, add automated review trigger:**
```bash
gh pr comment --body "Cursor review"
```

### 5. Branch Management & Rebasing

**Keep branch up to date with main:**
```bash
git checkout main
git pull origin main
git checkout your-feature-branch
git rebase main
```

**If rebase creates conflicts:**
- Resolve conflicts manually
- `git add .` resolved files
- `git rebase --continue`
- **Ask permission before force pushing rebased branch**

### 6. Merge Process

**Only merge via Pull Requests:**
- No direct merges to `main`
- Maintain linear commit history through rebasing
- Use "Squash and merge" or "Rebase and merge" as appropriate
- Delete feature branch after successful merge

## Pull Request Template Structure

Every PR must include these sections in order:

1. **Summary**: Brief overview of changes and purpose
2. **Background/Details**: 
   - New Feature: Requirements, context, design decisions
   - Bug Fix: Detailed root cause analysis
3. **Solution**: Technical approach and implementation details  
4. **Testing**: Reproduction steps, validation methods, test coverage
5. **Notes**: Additional considerations, impacts, future work

## Critical Rules

### ❌ FATAL ERRORS (Result in Termination)
- **NEVER push to `main` directly**
- **NEVER push without explicit user permission**
- **NEVER force push without explicit permission**
- **NEVER commit code with TypeScript errors**

### ✅ Required Practices
- Always pull from `main` before creating branches
- **MANDATORY: Run `npm run typecheck` before every commit**
- **MANDATORY: Fix all TypeScript errors before committing**
- Use `gh` CLI tool for all PR operations
- Add "Cursor review" comment after PR creation
- Maintain linear commit history via rebasing
- Ask permission before any push operation
- Use standardized branch naming conventions

## Branch Naming Conventions

- `feature/issue-xxx-description` - New features
- `bugfix/issue-xxx-description` - Bug fixes  
- `hotfix/critical-issue-description` - Critical production fixes
- `docs/update-readme` - Documentation updates
- `refactor/improve-error-handling` - Code refactoring

## Automated Quality Gates

### CI/CD Pipeline
Our GitHub Actions CI pipeline automatically enforces these quality checks:
1. `npm run build` - Compilation check
2. `npm run lint` - ESLint validation  
3. `npm run format:check` - Prettier formatting check
4. `npm run typecheck` - **TypeScript error validation**
5. `npm run test` - Test suite execution

**All checks must pass before PR merge is allowed.**

### Optional: Pre-commit Hook Setup
To catch TypeScript errors before committing locally:

```bash
# Create pre-commit hook
cat > .git/hooks/pre-commit << 'EOF'
#!/bin/sh
echo "🔍 Running pre-commit checks..."

# Run TypeScript type checking
echo "📝 Checking TypeScript..."
npm run typecheck
if [ $? -ne 0 ]; then
  echo "❌ TypeScript errors found. Please fix before committing."
  exit 1
fi

# Run linting
echo "🧹 Running linter..."
npm run lint
if [ $? -ne 0 ]; then
  echo "❌ Linting errors found. Please fix before committing."
  exit 1
fi

echo "✅ Pre-commit checks passed!"
EOF

# Make it executable  
chmod +x .git/hooks/pre-commit
```

This hook will automatically run `typecheck` and `lint` before every commit, preventing TypeScript errors from being committed.
```

--------------------------------------------------------------------------------
/src/mcp/resources/__tests__/simulators.test.ts:
--------------------------------------------------------------------------------

```typescript
import { describe, it, expect, beforeEach } from 'vitest';
import { z } from 'zod';

import simulatorsResource, { simulatorsResourceLogic } from '../simulators.ts';
import { createMockExecutor } from '../../../test-utils/mock-executors.ts';

describe('simulators resource', () => {
  describe('Export Field Validation', () => {
    it('should export correct uri', () => {
      expect(simulatorsResource.uri).toBe('xcodebuildmcp://simulators');
    });

    it('should export correct description', () => {
      expect(simulatorsResource.description).toBe(
        'Available iOS simulators with their UUIDs and states',
      );
    });

    it('should export correct mimeType', () => {
      expect(simulatorsResource.mimeType).toBe('text/plain');
    });

    it('should export handler function', () => {
      expect(typeof simulatorsResource.handler).toBe('function');
    });
  });

  describe('Handler Functionality', () => {
    it('should handle successful simulator data retrieval', async () => {
      const mockExecutor = createMockExecutor({
        success: true,
        output: JSON.stringify({
          devices: {
            'iOS 17.0': [
              {
                name: 'iPhone 15 Pro',
                udid: 'ABC123-DEF456-GHI789',
                state: 'Shutdown',
                isAvailable: true,
              },
            ],
          },
        }),
      });

      const result = await simulatorsResourceLogic(mockExecutor);

      expect(result.contents).toHaveLength(1);
      expect(result.contents[0].text).toContain('Available iOS Simulators:');
      expect(result.contents[0].text).toContain('iPhone 15 Pro');
      expect(result.contents[0].text).toContain('ABC123-DEF456-GHI789');
    });

    it('should handle command execution failure', async () => {
      const mockExecutor = createMockExecutor({
        success: false,
        output: '',
        error: 'Command failed',
      });

      const result = await simulatorsResourceLogic(mockExecutor);

      expect(result.contents).toHaveLength(1);
      expect(result.contents[0].text).toContain('Failed to list simulators');
      expect(result.contents[0].text).toContain('Command failed');
    });

    it('should handle JSON parsing errors and fall back to text parsing', async () => {
      const mockTextOutput = `== Devices ==
-- iOS 17.0 --
    iPhone 15 (test-uuid-123) (Shutdown)`;

      const mockExecutor = async (command: string[]) => {
        // JSON command returns invalid JSON
        if (command.includes('--json')) {
          return {
            success: true,
            output: 'invalid json',
            error: undefined,
            process: { pid: 12345 },
          };
        }

        // Text command returns valid text output
        return {
          success: true,
          output: mockTextOutput,
          error: undefined,
          process: { pid: 12345 },
        };
      };

      const result = await simulatorsResourceLogic(mockExecutor);

      expect(result.contents).toHaveLength(1);
      expect(result.contents[0].text).toContain('iPhone 15 (test-uuid-123)');
      expect(result.contents[0].text).toContain('iOS 17.0');
    });

    it('should handle spawn errors', async () => {
      const mockExecutor = createMockExecutor(new Error('spawn xcrun ENOENT'));

      const result = await simulatorsResourceLogic(mockExecutor);

      expect(result.contents).toHaveLength(1);
      expect(result.contents[0].text).toContain('Failed to list simulators');
      expect(result.contents[0].text).toContain('spawn xcrun ENOENT');
    });

    it('should handle empty simulator data', async () => {
      const mockExecutor = createMockExecutor({
        success: true,
        output: JSON.stringify({ devices: {} }),
      });

      const result = await simulatorsResourceLogic(mockExecutor);

      expect(result.contents).toHaveLength(1);
      expect(result.contents[0].text).toContain('Available iOS Simulators:');
    });

    it('should handle booted simulators correctly', async () => {
      const mockExecutor = createMockExecutor({
        success: true,
        output: JSON.stringify({
          devices: {
            'iOS 17.0': [
              {
                name: 'iPhone 15 Pro',
                udid: 'ABC123-DEF456-GHI789',
                state: 'Booted',
                isAvailable: true,
              },
            ],
          },
        }),
      });

      const result = await simulatorsResourceLogic(mockExecutor);

      expect(result.contents[0].text).toContain('[Booted]');
    });

    it('should filter out unavailable simulators', async () => {
      const mockExecutor = createMockExecutor({
        success: true,
        output: JSON.stringify({
          devices: {
            'iOS 17.0': [
              {
                name: 'iPhone 15 Pro',
                udid: 'ABC123-DEF456-GHI789',
                state: 'Shutdown',
                isAvailable: true,
              },
              {
                name: 'iPhone 14',
                udid: 'XYZ789-UVW456-RST123',
                state: 'Shutdown',
                isAvailable: false,
              },
            ],
          },
        }),
      });

      const result = await simulatorsResourceLogic(mockExecutor);

      expect(result.contents[0].text).toContain('iPhone 15 Pro');
      expect(result.contents[0].text).not.toContain('iPhone 14');
    });

    it('should include next steps guidance', async () => {
      const mockExecutor = createMockExecutor({
        success: true,
        output: JSON.stringify({
          devices: {
            'iOS 17.0': [
              {
                name: 'iPhone 15 Pro',
                udid: 'ABC123-DEF456-GHI789',
                state: 'Shutdown',
                isAvailable: true,
              },
            ],
          },
        }),
      });

      const result = await simulatorsResourceLogic(mockExecutor);

      expect(result.contents[0].text).toContain('Next Steps:');
      expect(result.contents[0].text).toContain('boot_sim');
      expect(result.contents[0].text).toContain('open_sim');
      expect(result.contents[0].text).toContain('build_sim');
      expect(result.contents[0].text).toContain('get_sim_app_path');
    });
  });
});

```

--------------------------------------------------------------------------------
/example_projects/iOS_Calculator/CalculatorAppPackage/Sources/CalculatorAppFeature/CalculatorService.swift:
--------------------------------------------------------------------------------

```swift
import Foundation

// MARK: - Calculator Business Logic Service

/// Handles all calculator operations and state management
/// Separated from UI concerns for better testability and modularity
@Observable
public final class CalculatorService {
    // MARK: - Public Properties

    public private(set) var display: String = "0"
    public private(set) var expressionDisplay: String = ""
    public private(set) var hasError: Bool = false

    // MARK: - Private State

    private var currentNumber: Double = 0
    private var previousNumber: Double = 0
    private var operation: Operation?
    private var shouldResetDisplay = false
    private var isNewCalculation = true
    private var lastOperation: Operation?
    private var lastOperand: Double = 0

    // MARK: - Operations

    public enum Operation: String, CaseIterable, Sendable {
        case add = "+"
        case subtract = "-"
        case multiply = "×"
        case divide = "÷"

        public func calculate(_ a: Double, _ b: Double) -> Double {
            switch self {
            case .add: return a + b
            case .subtract: return a - b
            case .multiply: return a * b
            case .divide: return b != 0 ? a / b : 0
            }
        }
    }

    public init() {}

    // MARK: - Public Interface

    public func inputNumber(_ digit: String) {
        guard !hasError else { clear(); return }

        if shouldResetDisplay || isNewCalculation {
            display = digit
            shouldResetDisplay = false
            isNewCalculation = false
        } else if display.count < 12 {
            display = display == "0" ? digit : display + digit
        }

        currentNumber = Double(display) ?? 0
        updateExpressionDisplay()
    }

    /// Inputs a decimal point into the display
    public func inputDecimal() {
        guard !hasError else {
            clear(); return
        }

        if shouldResetDisplay || isNewCalculation {
            display = "0."
            shouldResetDisplay = false
            isNewCalculation = false
        } else if !display.contains("."), display.count < 11 {
            display += "."
        }
        updateExpressionDisplay()
    }

    public func setOperation(_ op: Operation) {
        guard !hasError else { return }

        if operation != nil, !shouldResetDisplay {
            calculate()
            if hasError { return }
        }

        previousNumber = currentNumber
        operation = op
        shouldResetDisplay = true
        isNewCalculation = false
        updateExpressionDisplay()
    }

    public func calculate() {
        guard let op = operation ?? lastOperation else { return }
        let operand = (operation != nil) ? currentNumber : lastOperand

        let result = op.calculate(previousNumber, operand)

        // Error handling
        if result.isNaN || result.isInfinite {
            setError("Cannot divide by zero")
            return
        }

        if abs(result) > 1e12 {
            setError("Number too large")
            return
        }

        // Success path
        let prevFormatted = formatNumber(previousNumber)
        let currFormatted = formatNumber(operand)
        display = formatNumber(result)
        expressionDisplay = "\(prevFormatted) \(op.rawValue) \(currFormatted) ="

        previousNumber = result
        if operation != nil {
            lastOperand = currentNumber
        }

        lastOperation = op
        operation = nil
        currentNumber = result
        shouldResetDisplay = true
        isNewCalculation = false
    }

    public func toggleSign() {
        guard !hasError, currentNumber != 0 else { return }
        currentNumber *= -1
        display = formatNumber(currentNumber)
        updateExpressionDisplay()
    }

    public func percentage() {
        guard !hasError else { return }
        currentNumber /= 100
        display = formatNumber(currentNumber)
        updateExpressionDisplay()
    }

    public func clear() {
        display = "0"
        expressionDisplay = ""
        currentNumber = 0
        previousNumber = 0
        operation = nil
        shouldResetDisplay = false
        hasError = false
        isNewCalculation = true
    }

    public func deleteLastDigit() {
        guard !hasError else { clear(); return }

        if shouldResetDisplay || isNewCalculation {
            display = "0"
            shouldResetDisplay = false
            isNewCalculation = false
        } else if display.count > 1 {
            display.removeLast()
            if display == "-" { display = "0" }
        } else {
            display = "0"
        }
        currentNumber = Double(display) ?? 0
        updateExpressionDisplay()
    }

    // MARK: - Private Helpers

    private func setError(_ message: String) {
        hasError = true
        display = "Error"
        expressionDisplay = message
    }

    private func updateExpressionDisplay() {
        guard !hasError else { return }

        if let op = operation {
            let prevFormatted = formatNumber(previousNumber)
            expressionDisplay = "\(prevFormatted) \(op.rawValue)"
        } else if isNewCalculation {
            expressionDisplay = ""
        }
    }

    private func formatNumber(_ number: Double) -> String {
        guard !number.isNaN && !number.isInfinite else { return "Error" }

        let formatter = NumberFormatter()
        formatter.numberStyle = .decimal
        formatter.maximumFractionDigits = 8
        formatter.minimumFractionDigits = 0

        // For integers, don't show decimal places
        if number == floor(number) && abs(number) < 1e10 {
            formatter.maximumFractionDigits = 0
        }

        // For very small decimals, use scientific notation
        if abs(number) < 0.000001 && number != 0 {
            formatter.numberStyle = .scientific
            formatter.maximumFractionDigits = 2
        }

        return formatter.string(from: NSNumber(value: number)) ?? "0"
    }
}

// MARK: - Testing Support

public extension CalculatorService {
    var currentValue: Double { currentNumber }
    var previousValue: Double { previousNumber }
    var currentOperation: Operation? { operation }
    var willResetDisplay: Bool { shouldResetDisplay }
}

```

--------------------------------------------------------------------------------
/src/mcp/tools/device/get_device_app_path.ts:
--------------------------------------------------------------------------------

```typescript
/**
 * Device Shared Plugin: Get Device App Path (Unified)
 *
 * Gets the app bundle path for a physical device application (iOS, watchOS, tvOS, visionOS) using either a project or workspace.
 * Accepts mutually exclusive `projectPath` or `workspacePath`.
 */

import { z } from 'zod';
import { ToolResponse, XcodePlatform } from '../../../types/common.ts';
import { log } from '../../../utils/logging/index.ts';
import { createTextResponse } from '../../../utils/responses/index.ts';
import type { CommandExecutor } from '../../../utils/execution/index.ts';
import { getDefaultCommandExecutor } from '../../../utils/execution/index.ts';
import { createSessionAwareTool } from '../../../utils/typed-tool-factory.ts';
import { nullifyEmptyStrings } from '../../../utils/schema-helpers.ts';

// Unified schema: XOR between projectPath and workspacePath, sharing common options
const baseOptions = {
  scheme: z.string().describe('The scheme to use'),
  configuration: z.string().optional().describe('Build configuration (Debug, Release, etc.)'),
  platform: z
    .enum(['iOS', 'watchOS', 'tvOS', 'visionOS'])
    .optional()
    .describe('Target platform (defaults to iOS)'),
};

const baseSchemaObject = z.object({
  projectPath: z.string().optional().describe('Path to the .xcodeproj file'),
  workspacePath: z.string().optional().describe('Path to the .xcworkspace file'),
  ...baseOptions,
});

const baseSchema = z.preprocess(nullifyEmptyStrings, baseSchemaObject);

const getDeviceAppPathSchema = baseSchema
  .refine((val) => val.projectPath !== undefined || val.workspacePath !== undefined, {
    message: 'Either projectPath or workspacePath is required.',
  })
  .refine((val) => !(val.projectPath !== undefined && val.workspacePath !== undefined), {
    message: 'projectPath and workspacePath are mutually exclusive. Provide only one.',
  });

// Use z.infer for type safety
type GetDeviceAppPathParams = z.infer<typeof getDeviceAppPathSchema>;

export async function get_device_app_pathLogic(
  params: GetDeviceAppPathParams,
  executor: CommandExecutor,
): Promise<ToolResponse> {
  const platformMap = {
    iOS: XcodePlatform.iOS,
    watchOS: XcodePlatform.watchOS,
    tvOS: XcodePlatform.tvOS,
    visionOS: XcodePlatform.visionOS,
  };

  const platform = platformMap[params.platform ?? 'iOS'];
  const configuration = params.configuration ?? 'Debug';

  log('info', `Getting app path for scheme ${params.scheme} on platform ${platform}`);

  try {
    // Create the command array for xcodebuild with -showBuildSettings option
    const command = ['xcodebuild', '-showBuildSettings'];

    // Add the project or workspace
    if (params.projectPath) {
      command.push('-project', params.projectPath);
    } else if (params.workspacePath) {
      command.push('-workspace', params.workspacePath);
    } else {
      // This should never happen due to schema validation
      throw new Error('Either projectPath or workspacePath is required.');
    }

    // Add the scheme and configuration
    command.push('-scheme', params.scheme);
    command.push('-configuration', configuration);

    // Handle destination based on platform
    let destinationString = '';

    if (platform === XcodePlatform.iOS) {
      destinationString = 'generic/platform=iOS';
    } else if (platform === XcodePlatform.watchOS) {
      destinationString = 'generic/platform=watchOS';
    } else if (platform === XcodePlatform.tvOS) {
      destinationString = 'generic/platform=tvOS';
    } else if (platform === XcodePlatform.visionOS) {
      destinationString = 'generic/platform=visionOS';
    } else {
      return createTextResponse(`Unsupported platform: ${platform}`, true);
    }

    command.push('-destination', destinationString);

    // Execute the command directly
    const result = await executor(command, 'Get App Path', true);

    if (!result.success) {
      return createTextResponse(`Failed to get app path: ${result.error}`, true);
    }

    if (!result.output) {
      return createTextResponse('Failed to extract build settings output from the result.', true);
    }

    const buildSettingsOutput = result.output;
    const builtProductsDirMatch = buildSettingsOutput.match(/^\s*BUILT_PRODUCTS_DIR\s*=\s*(.+)$/m);
    const fullProductNameMatch = buildSettingsOutput.match(/^\s*FULL_PRODUCT_NAME\s*=\s*(.+)$/m);

    if (!builtProductsDirMatch || !fullProductNameMatch) {
      return createTextResponse(
        'Failed to extract app path from build settings. Make sure the app has been built first.',
        true,
      );
    }

    const builtProductsDir = builtProductsDirMatch[1].trim();
    const fullProductName = fullProductNameMatch[1].trim();
    const appPath = `${builtProductsDir}/${fullProductName}`;

    const nextStepsText = `Next Steps:
1. Get bundle ID: get_app_bundle_id({ appPath: "${appPath}" })
2. Install app on device: install_app_device({ deviceId: "DEVICE_UDID", appPath: "${appPath}" })
3. Launch app on device: launch_app_device({ deviceId: "DEVICE_UDID", bundleId: "BUNDLE_ID" })`;

    return {
      content: [
        {
          type: 'text',
          text: `✅ App path retrieved successfully: ${appPath}`,
        },
        {
          type: 'text',
          text: nextStepsText,
        },
      ],
    };
  } catch (error) {
    const errorMessage = error instanceof Error ? error.message : String(error);
    log('error', `Error retrieving app path: ${errorMessage}`);
    return createTextResponse(`Error retrieving app path: ${errorMessage}`, true);
  }
}

export default {
  name: 'get_device_app_path',
  description: 'Retrieves the built app path for a connected device.',
  schema: baseSchemaObject.omit({
    projectPath: true,
    workspacePath: true,
    scheme: true,
    configuration: true,
  } as const).shape,
  handler: createSessionAwareTool<GetDeviceAppPathParams>({
    internalSchema: getDeviceAppPathSchema as unknown as z.ZodType<GetDeviceAppPathParams>,
    logicFunction: get_device_app_pathLogic,
    getExecutor: getDefaultCommandExecutor,
    requirements: [
      { allOf: ['scheme'], message: 'scheme is required' },
      { oneOf: ['projectPath', 'workspacePath'], message: 'Provide a project or workspace' },
    ],
    exclusivePairs: [['projectPath', 'workspacePath']],
  }),
};

```

--------------------------------------------------------------------------------
/src/mcp/tools/ui-testing/tap.ts:
--------------------------------------------------------------------------------

```typescript
import { z } from 'zod';
import type { ToolResponse } from '../../../types/common.ts';
import { log } from '../../../utils/logging/index.ts';
import { createTextResponse, createErrorResponse } from '../../../utils/responses/index.ts';
import type { CommandExecutor } from '../../../utils/execution/index.ts';
import { getDefaultCommandExecutor } from '../../../utils/execution/index.ts';
import {
  createAxeNotAvailableResponse,
  getAxePath,
  getBundledAxeEnvironment,
} from '../../../utils/axe-helpers.ts';
import { DependencyError, AxeError, SystemError } from '../../../utils/errors.ts';
import { createTypedTool } from '../../../utils/typed-tool-factory.ts';

export interface AxeHelpers {
  getAxePath: () => string | null;
  getBundledAxeEnvironment: () => Record<string, string>;
  createAxeNotAvailableResponse: () => ToolResponse;
}

// Define schema as ZodObject
const tapSchema = z.object({
  simulatorUuid: z.string().uuid('Invalid Simulator UUID format'),
  x: z.number().int('X coordinate must be an integer'),
  y: z.number().int('Y coordinate must be an integer'),
  preDelay: z.number().min(0, 'Pre-delay must be non-negative').optional(),
  postDelay: z.number().min(0, 'Post-delay must be non-negative').optional(),
});

// Use z.infer for type safety
type TapParams = z.infer<typeof tapSchema>;

const LOG_PREFIX = '[AXe]';

// Session tracking for describe_ui warnings (shared across UI tools)
const describeUITimestamps = new Map<string, { timestamp: number }>();
const DESCRIBE_UI_WARNING_TIMEOUT = 60000; // 60 seconds

function getCoordinateWarning(simulatorUuid: string): string | null {
  const session = describeUITimestamps.get(simulatorUuid);
  if (!session) {
    return 'Warning: describe_ui has not been called yet. Consider using describe_ui for precise coordinates instead of guessing from screenshots.';
  }

  const timeSinceDescribe = Date.now() - session.timestamp;
  if (timeSinceDescribe > DESCRIBE_UI_WARNING_TIMEOUT) {
    const secondsAgo = Math.round(timeSinceDescribe / 1000);
    return `Warning: describe_ui was last called ${secondsAgo} seconds ago. Consider refreshing UI coordinates with describe_ui instead of using potentially stale coordinates.`;
  }

  return null;
}

export async function tapLogic(
  params: TapParams,
  executor: CommandExecutor,
  axeHelpers: AxeHelpers = {
    getAxePath,
    getBundledAxeEnvironment,
    createAxeNotAvailableResponse,
  },
): Promise<ToolResponse> {
  const toolName = 'tap';
  const { simulatorUuid, x, y, preDelay, postDelay } = params;
  const commandArgs = ['tap', '-x', String(x), '-y', String(y)];
  if (preDelay !== undefined) {
    commandArgs.push('--pre-delay', String(preDelay));
  }
  if (postDelay !== undefined) {
    commandArgs.push('--post-delay', String(postDelay));
  }

  log('info', `${LOG_PREFIX}/${toolName}: Starting for (${x}, ${y}) on ${simulatorUuid}`);

  try {
    await executeAxeCommand(commandArgs, simulatorUuid, 'tap', executor, axeHelpers);
    log('info', `${LOG_PREFIX}/${toolName}: Success for ${simulatorUuid}`);

    const warning = getCoordinateWarning(simulatorUuid);
    const message = `Tap at (${x}, ${y}) simulated successfully.`;

    if (warning) {
      return createTextResponse(`${message}\n\n${warning}`);
    }

    return createTextResponse(message);
  } catch (error) {
    log('error', `${LOG_PREFIX}/${toolName}: Failed - ${error}`);
    if (error instanceof DependencyError) {
      return axeHelpers.createAxeNotAvailableResponse();
    } else if (error instanceof AxeError) {
      return createErrorResponse(
        `Failed to simulate tap at (${x}, ${y}): ${error.message}`,
        error.axeOutput,
      );
    } else if (error instanceof SystemError) {
      return createErrorResponse(
        `System error executing axe: ${error.message}`,
        error.originalError?.stack,
      );
    }
    return createErrorResponse(
      `An unexpected error occurred: ${error instanceof Error ? error.message : String(error)}`,
    );
  }
}

export default {
  name: 'tap',
  description:
    "Tap at specific coordinates. Use describe_ui to get precise element coordinates (don't guess from screenshots). Supports optional timing delays.",
  schema: tapSchema.shape, // MCP SDK compatibility
  handler: createTypedTool(
    tapSchema,
    (params: TapParams, executor: CommandExecutor) => {
      return tapLogic(params, executor, {
        getAxePath,
        getBundledAxeEnvironment,
        createAxeNotAvailableResponse,
      });
    },
    getDefaultCommandExecutor,
  ),
};

// Helper function for executing axe commands (inlined from src/tools/axe/index.ts)
async function executeAxeCommand(
  commandArgs: string[],
  simulatorUuid: string,
  commandName: string,
  executor: CommandExecutor = getDefaultCommandExecutor(),
  axeHelpers: AxeHelpers = { getAxePath, getBundledAxeEnvironment, createAxeNotAvailableResponse },
): Promise<void> {
  // Get the appropriate axe binary path
  const axeBinary = axeHelpers.getAxePath();
  if (!axeBinary) {
    throw new DependencyError('AXe binary not found');
  }

  // Add --udid parameter to all commands
  const fullArgs = [...commandArgs, '--udid', simulatorUuid];

  // Construct the full command array with the axe binary as the first element
  const fullCommand = [axeBinary, ...fullArgs];

  try {
    // Determine environment variables for bundled AXe
    const axeEnv = axeBinary !== 'axe' ? axeHelpers.getBundledAxeEnvironment() : undefined;

    const result = await executor(fullCommand, `${LOG_PREFIX}: ${commandName}`, false, axeEnv);

    if (!result.success) {
      throw new AxeError(
        `axe command '${commandName}' failed.`,
        commandName,
        result.error ?? result.output,
        simulatorUuid,
      );
    }

    // Check for stderr output in successful commands
    if (result.error) {
      log(
        'warn',
        `${LOG_PREFIX}: Command '${commandName}' produced stderr output but exited successfully. Output: ${result.error}`,
      );
    }

    // Function now returns void - the calling code creates its own response
  } catch (error) {
    if (error instanceof Error) {
      if (error instanceof AxeError) {
        throw error;
      }

      // Otherwise wrap it in a SystemError
      throw new SystemError(`Failed to execute axe command: ${error.message}`, error);
    }

    // For any other type of error
    throw new SystemError(`Failed to execute axe command: ${String(error)}`);
  }
}

```

--------------------------------------------------------------------------------
/src/mcp/tools/simulator/__tests__/get_sim_app_path.test.ts:
--------------------------------------------------------------------------------

```typescript
/**
 * Tests for get_sim_app_path plugin (session-aware version)
 * Mirrors patterns from other simulator session-aware migrations.
 */

import { describe, it, expect, beforeEach } from 'vitest';
import { ChildProcess } from 'child_process';
import { z } from 'zod';
import { createMockExecutor } from '../../../../test-utils/mock-executors.ts';
import { sessionStore } from '../../../../utils/session-store.ts';
import getSimAppPath, { get_sim_app_pathLogic } from '../get_sim_app_path.ts';
import type { CommandExecutor } from '../../../../utils/CommandExecutor.ts';

describe('get_sim_app_path tool', () => {
  beforeEach(() => {
    sessionStore.clear();
  });

  describe('Export Field Validation (Literal)', () => {
    it('should have correct name', () => {
      expect(getSimAppPath.name).toBe('get_sim_app_path');
    });

    it('should have concise description', () => {
      expect(getSimAppPath.description).toBe('Retrieves the built app path for an iOS simulator.');
    });

    it('should have handler function', () => {
      expect(typeof getSimAppPath.handler).toBe('function');
    });

    it('should expose only platform in public schema', () => {
      const schema = z.object(getSimAppPath.schema);

      expect(schema.safeParse({ platform: 'iOS Simulator' }).success).toBe(true);
      expect(schema.safeParse({}).success).toBe(false);
      expect(schema.safeParse({ platform: 'iOS' }).success).toBe(false);

      const schemaKeys = Object.keys(getSimAppPath.schema).sort();
      expect(schemaKeys).toEqual(['platform']);
    });
  });

  describe('Handler Requirements', () => {
    it('should require scheme when not provided', async () => {
      const result = await getSimAppPath.handler({
        platform: 'iOS Simulator',
      });

      expect(result.isError).toBe(true);
      expect(result.content[0].text).toContain('scheme is required');
    });

    it('should require project or workspace when scheme default exists', async () => {
      sessionStore.setDefaults({ scheme: 'MyScheme' });

      const result = await getSimAppPath.handler({
        platform: 'iOS Simulator',
      });

      expect(result.isError).toBe(true);
      expect(result.content[0].text).toContain('Provide a project or workspace');
    });

    it('should require simulator identifier when scheme and project defaults exist', async () => {
      sessionStore.setDefaults({
        scheme: 'MyScheme',
        projectPath: '/path/to/project.xcodeproj',
      });

      const result = await getSimAppPath.handler({
        platform: 'iOS Simulator',
      });

      expect(result.isError).toBe(true);
      expect(result.content[0].text).toContain('Provide simulatorId or simulatorName');
    });

    it('should error when both projectPath and workspacePath provided explicitly', async () => {
      sessionStore.setDefaults({ scheme: 'MyScheme' });

      const result = await getSimAppPath.handler({
        platform: 'iOS Simulator',
        projectPath: '/path/project.xcodeproj',
        workspacePath: '/path/workspace.xcworkspace',
      });

      expect(result.isError).toBe(true);
      expect(result.content[0].text).toContain('Mutually exclusive parameters provided');
      expect(result.content[0].text).toContain('projectPath');
      expect(result.content[0].text).toContain('workspacePath');
    });

    it('should error when both simulatorId and simulatorName provided explicitly', async () => {
      sessionStore.setDefaults({
        scheme: 'MyScheme',
        workspacePath: '/path/to/workspace.xcworkspace',
      });

      const result = await getSimAppPath.handler({
        platform: 'iOS Simulator',
        simulatorId: 'SIM-UUID',
        simulatorName: 'iPhone 16',
      });

      expect(result.isError).toBe(true);
      expect(result.content[0].text).toContain('Mutually exclusive parameters provided');
      expect(result.content[0].text).toContain('simulatorId');
      expect(result.content[0].text).toContain('simulatorName');
    });
  });

  describe('Logic Behavior', () => {
    it('should return app path with simulator name destination', async () => {
      const callHistory: Array<{
        command: string[];
        logPrefix?: string;
        useShell?: boolean;
        opts?: unknown;
      }> = [];

      const trackingExecutor: CommandExecutor = async (
        command,
        logPrefix,
        useShell,
        opts,
      ): Promise<{
        success: boolean;
        output: string;
        process: ChildProcess;
      }> => {
        callHistory.push({ command, logPrefix, useShell, opts });
        return {
          success: true,
          output:
            '    BUILT_PRODUCTS_DIR = /tmp/DerivedData/Build\n    FULL_PRODUCT_NAME = MyApp.app\n',
          process: { pid: 12345 } as unknown as ChildProcess,
        };
      };

      const result = await get_sim_app_pathLogic(
        {
          workspacePath: '/path/to/workspace.xcworkspace',
          scheme: 'MyScheme',
          platform: 'iOS Simulator',
          simulatorName: 'iPhone 16',
          useLatestOS: true,
        },
        trackingExecutor,
      );

      expect(callHistory).toHaveLength(1);
      expect(callHistory[0].logPrefix).toBe('Get App Path');
      expect(callHistory[0].useShell).toBe(true);
      expect(callHistory[0].command).toEqual([
        'xcodebuild',
        '-showBuildSettings',
        '-workspace',
        '/path/to/workspace.xcworkspace',
        '-scheme',
        'MyScheme',
        '-configuration',
        'Debug',
        '-destination',
        'platform=iOS Simulator,name=iPhone 16,OS=latest',
      ]);

      expect(result.isError).toBe(false);
      expect(result.content[0].text).toContain(
        '✅ App path retrieved successfully: /tmp/DerivedData/Build/MyApp.app',
      );
    });

    it('should surface executor failures when build settings cannot be retrieved', async () => {
      const mockExecutor = createMockExecutor({
        success: false,
        error: 'Failed to run xcodebuild',
      });

      const result = await get_sim_app_pathLogic(
        {
          projectPath: '/path/to/project.xcodeproj',
          scheme: 'MyScheme',
          platform: 'iOS Simulator',
          simulatorId: 'SIM-UUID',
        },
        mockExecutor,
      );

      expect(result.isError).toBe(true);
      expect(result.content[0].text).toContain('Failed to get app path');
      expect(result.content[0].text).toContain('Failed to run xcodebuild');
    });
  });
});

```

--------------------------------------------------------------------------------
/src/utils/video_capture.ts:
--------------------------------------------------------------------------------

```typescript
/**
 * Video capture utility for simulator recordings using AXe.
 *
 * Manages long-running AXe "record-video" processes keyed by simulator UUID.
 * It aggregates stdout/stderr to parse the generated MP4 path on stop.
 */

import type { ChildProcess } from 'child_process';
import { log } from './logging/index.ts';
import { getAxePath, getBundledAxeEnvironment } from './axe-helpers.ts';
import type { CommandExecutor } from './execution/index.ts';

type Session = {
  process: unknown;
  sessionId: string;
  startedAt: number;
  buffer: string;
  ended: boolean;
};

const sessions = new Map<string, Session>();
let signalHandlersAttached = false;

export interface AxeHelpers {
  getAxePath: () => string | null;
  getBundledAxeEnvironment: () => Record<string, string>;
}

function ensureSignalHandlersAttached(): void {
  if (signalHandlersAttached) return;
  signalHandlersAttached = true;

  const stopAll = (): void => {
    for (const [simulatorUuid, sess] of sessions) {
      try {
        const child = sess.process as ChildProcess | undefined;
        child?.kill?.('SIGINT');
      } catch {
        // ignore
      } finally {
        sessions.delete(simulatorUuid);
      }
    }
  };

  try {
    process.on('SIGINT', stopAll);
    process.on('SIGTERM', stopAll);
    process.on('exit', stopAll);
  } catch {
    // Non-Node environments may not support process signals; ignore
  }
}

function parseLastAbsoluteMp4Path(buffer: string | undefined): string | null {
  if (!buffer) return null;
  const matches = [...buffer.matchAll(/(\s|^)(\/[^\s'"]+\.mp4)\b/gi)];
  if (matches.length === 0) return null;
  const last = matches[matches.length - 1];
  return last?.[2] ?? null;
}

function createSessionId(simulatorUuid: string): string {
  return `${simulatorUuid}:${Date.now()}`;
}

/**
 * Start recording video for a simulator using AXe.
 */
export async function startSimulatorVideoCapture(
  params: { simulatorUuid: string; fps?: number },
  executor: CommandExecutor,
  axeHelpers?: AxeHelpers,
): Promise<{ started: boolean; sessionId?: string; warning?: string; error?: string }> {
  const simulatorUuid = params.simulatorUuid;
  if (!simulatorUuid) {
    return { started: false, error: 'simulatorUuid is required' };
  }

  if (sessions.has(simulatorUuid)) {
    return {
      started: false,
      error: 'A video recording session is already active for this simulator. Stop it first.',
    };
  }

  const helpers = axeHelpers ?? {
    getAxePath,
    getBundledAxeEnvironment,
  };

  const axeBinary = helpers.getAxePath();
  if (!axeBinary) {
    return { started: false, error: 'Bundled AXe binary not found' };
  }

  const fps = Number.isFinite(params.fps as number) ? Number(params.fps) : 30;
  const command = [axeBinary, 'record-video', '--udid', simulatorUuid, '--fps', String(fps)];
  const env = helpers.getBundledAxeEnvironment?.() ?? {};

  log('info', `Starting AXe video recording for simulator ${simulatorUuid} at ${fps} fps`);

  const result = await executor(command, 'Start Simulator Video Capture', true, { env }, true);

  if (!result.success || !result.process) {
    return {
      started: false,
      error: result.error ?? 'Failed to start video capture process',
    };
  }

  const child = result.process as ChildProcess;
  const session: Session = {
    process: child,
    sessionId: createSessionId(simulatorUuid),
    startedAt: Date.now(),
    buffer: '',
    ended: false,
  };

  try {
    child.stdout?.on('data', (d: unknown) => {
      try {
        session.buffer += String(d ?? '');
      } catch {
        // ignore
      }
    });
    child.stderr?.on('data', (d: unknown) => {
      try {
        session.buffer += String(d ?? '');
      } catch {
        // ignore
      }
    });
  } catch {
    // ignore stream listener setup failures
  }

  // Track when the child process naturally ends, so stop can short-circuit
  try {
    child.once?.('exit', () => {
      session.ended = true;
    });
    child.once?.('close', () => {
      session.ended = true;
    });
  } catch {
    // ignore
  }

  sessions.set(simulatorUuid, session);
  ensureSignalHandlersAttached();

  return {
    started: true,
    sessionId: session.sessionId,
    warning: fps !== (params.fps ?? 30) ? `FPS coerced to ${fps}` : undefined,
  };
}

/**
 * Stop recording video for a simulator. Returns aggregated output and parsed MP4 path if found.
 */
export async function stopSimulatorVideoCapture(
  params: { simulatorUuid: string },
  executor: CommandExecutor,
): Promise<{
  stopped: boolean;
  sessionId?: string;
  stdout?: string;
  parsedPath?: string;
  error?: string;
}> {
  // Mark executor as used to satisfy lint rule
  void executor;

  const simulatorUuid = params.simulatorUuid;
  if (!simulatorUuid) {
    return { stopped: false, error: 'simulatorUuid is required' };
  }

  const session = sessions.get(simulatorUuid);
  if (!session) {
    return { stopped: false, error: 'No active video recording session for this simulator' };
  }

  const child = session.process as ChildProcess | undefined;

  // Attempt graceful shutdown
  try {
    child?.kill?.('SIGINT');
  } catch {
    try {
      child?.kill?.();
    } catch {
      // ignore
    }
  }

  // Wait for process to close (avoid hanging if it already exited)
  await new Promise<void>((resolve): void => {
    if (!child) return resolve();

    // If process has already ended, resolve immediately
    const alreadyEnded = (session as Session).ended === true;
    const hasExitCode = (child as ChildProcess).exitCode !== null;
    const hasSignal = (child as unknown as { signalCode?: string | null }).signalCode != null;
    if (alreadyEnded || hasExitCode || hasSignal) {
      return resolve();
    }

    let resolved = false;
    const finish = (): void => {
      if (!resolved) {
        resolved = true;
        resolve();
      }
    };
    try {
      child.once('close', finish);
      child.once('exit', finish);
    } catch {
      return finish();
    }
    // Safety timeout to prevent indefinite hangs
    setTimeout(finish, 5000);
  });

  const combinedOutput = session.buffer;
  const parsedPath = parseLastAbsoluteMp4Path(combinedOutput) ?? undefined;

  sessions.delete(simulatorUuid);

  log(
    'info',
    `Stopped AXe video recording for simulator ${simulatorUuid}. ${parsedPath ? `Detected file: ${parsedPath}` : 'No file detected in output.'}`,
  );

  return {
    stopped: true,
    sessionId: session.sessionId,
    stdout: combinedOutput,
    parsedPath,
  };
}

```

--------------------------------------------------------------------------------
/src/mcp/tools/simulator/launch_app_sim.ts:
--------------------------------------------------------------------------------

```typescript
import { z } from 'zod';
import { ToolResponse } from '../../../types/common.ts';
import { log } from '../../../utils/logging/index.ts';
import type { CommandExecutor } from '../../../utils/execution/index.ts';
import { getDefaultCommandExecutor } from '../../../utils/execution/index.ts';
import { nullifyEmptyStrings } from '../../../utils/schema-helpers.ts';
import { createSessionAwareTool } from '../../../utils/typed-tool-factory.ts';

const baseSchemaObject = z.object({
  simulatorId: z
    .string()
    .optional()
    .describe(
      'UUID of the simulator to use (obtained from list_sims). Provide EITHER this OR simulatorName, not both',
    ),
  simulatorName: z
    .string()
    .optional()
    .describe(
      "Name of the simulator (e.g., 'iPhone 16'). Provide EITHER this OR simulatorId, not both",
    ),
  bundleId: z
    .string()
    .describe("Bundle identifier of the app to launch (e.g., 'com.example.MyApp')"),
  args: z.array(z.string()).optional().describe('Additional arguments to pass to the app'),
});

const baseSchema = z.preprocess(nullifyEmptyStrings, baseSchemaObject);

const launchAppSimSchema = baseSchema
  .refine((val) => val.simulatorId !== undefined || val.simulatorName !== undefined, {
    message: 'Either simulatorId or simulatorName is required.',
  })
  .refine((val) => !(val.simulatorId !== undefined && val.simulatorName !== undefined), {
    message: 'simulatorId and simulatorName are mutually exclusive. Provide only one.',
  });

export type LaunchAppSimParams = z.infer<typeof launchAppSimSchema>;

export async function launch_app_simLogic(
  params: LaunchAppSimParams,
  executor: CommandExecutor,
): Promise<ToolResponse> {
  let simulatorId = params.simulatorId;
  let simulatorDisplayName = simulatorId ?? '';

  if (params.simulatorName && !simulatorId) {
    log('info', `Looking up simulator by name: ${params.simulatorName}`);

    const simulatorListResult = await executor(
      ['xcrun', 'simctl', 'list', 'devices', 'available', '--json'],
      'List Simulators',
      true,
    );
    if (!simulatorListResult.success) {
      return {
        content: [
          {
            type: 'text',
            text: `Failed to list simulators: ${simulatorListResult.error}`,
          },
        ],
        isError: true,
      };
    }

    const simulatorsData = JSON.parse(simulatorListResult.output) as {
      devices: Record<string, Array<{ udid: string; name: string }>>;
    };

    let foundSimulator: { udid: string; name: string } | null = null;
    for (const runtime in simulatorsData.devices) {
      const devices = simulatorsData.devices[runtime];
      const simulator = devices.find((device) => device.name === params.simulatorName);
      if (simulator) {
        foundSimulator = simulator;
        break;
      }
    }

    if (!foundSimulator) {
      return {
        content: [
          {
            type: 'text',
            text: `Simulator named "${params.simulatorName}" not found. Use list_sims to see available simulators.`,
          },
        ],
        isError: true,
      };
    }

    simulatorId = foundSimulator.udid;
    simulatorDisplayName = `"${params.simulatorName}" (${foundSimulator.udid})`;
  }

  if (!simulatorId) {
    return {
      content: [
        {
          type: 'text',
          text: 'No simulator identifier provided',
        },
      ],
      isError: true,
    };
  }

  log('info', `Starting xcrun simctl launch request for simulator ${simulatorId}`);

  try {
    const getAppContainerCmd = [
      'xcrun',
      'simctl',
      'get_app_container',
      simulatorId,
      params.bundleId,
      'app',
    ];
    const getAppContainerResult = await executor(
      getAppContainerCmd,
      'Check App Installed',
      true,
      undefined,
    );
    if (!getAppContainerResult.success) {
      return {
        content: [
          {
            type: 'text',
            text: `App is not installed on the simulator. Please use install_app_sim before launching.\n\nWorkflow: build → install → launch.`,
          },
        ],
        isError: true,
      };
    }
  } catch {
    return {
      content: [
        {
          type: 'text',
          text: `App is not installed on the simulator (check failed). Please use install_app_sim before launching.\n\nWorkflow: build → install → launch.`,
        },
      ],
      isError: true,
    };
  }

  try {
    const command = ['xcrun', 'simctl', 'launch', simulatorId, params.bundleId];
    if (params.args && params.args.length > 0) {
      command.push(...params.args);
    }

    const result = await executor(command, 'Launch App in Simulator', true, undefined);

    if (!result.success) {
      return {
        content: [
          {
            type: 'text',
            text: `Launch app in simulator operation failed: ${result.error}`,
          },
        ],
      };
    }

    const userParamName = params.simulatorName ? 'simulatorName' : 'simulatorUuid';
    const userParamValue = params.simulatorName ?? simulatorId;

    return {
      content: [
        {
          type: 'text',
          text: `✅ App launched successfully in simulator ${simulatorDisplayName || simulatorId}.

Next Steps:
1. To see simulator: open_sim()
2. Log capture: start_sim_log_cap({ ${userParamName}: "${userParamValue}", bundleId: "${params.bundleId}" })
   With console: start_sim_log_cap({ ${userParamName}: "${userParamValue}", bundleId: "${params.bundleId}", captureConsole: true })
3. Stop logs: stop_sim_log_cap({ logSessionId: 'SESSION_ID' })`,
        },
      ],
    };
  } catch (error) {
    const errorMessage = error instanceof Error ? error.message : String(error);
    log('error', `Error during launch app in simulator operation: ${errorMessage}`);
    return {
      content: [
        {
          type: 'text',
          text: `Launch app in simulator operation failed: ${errorMessage}`,
        },
      ],
    };
  }
}

const publicSchemaObject = baseSchemaObject.omit({
  simulatorId: true,
  simulatorName: true,
} as const);

export default {
  name: 'launch_app_sim',
  description: 'Launches an app in an iOS simulator.',
  schema: publicSchemaObject.shape,
  handler: createSessionAwareTool<LaunchAppSimParams>({
    internalSchema: launchAppSimSchema as unknown as z.ZodType<LaunchAppSimParams>,
    logicFunction: launch_app_simLogic,
    getExecutor: getDefaultCommandExecutor,
    requirements: [
      { oneOf: ['simulatorId', 'simulatorName'], message: 'Provide simulatorId or simulatorName' },
    ],
    exclusivePairs: [['simulatorId', 'simulatorName']],
  }),
};

```

--------------------------------------------------------------------------------
/src/utils/log_capture.ts:
--------------------------------------------------------------------------------

```typescript
import * as fs from 'fs';
import * as path from 'path';
import * as os from 'os';
import type { ChildProcess } from 'child_process';
import { v4 as uuidv4 } from 'uuid';
import { log } from '../utils/logger.ts';
import { CommandExecutor, getDefaultCommandExecutor } from './command.ts';

/**
 * Log file retention policy:
 * - Old log files (older than LOG_RETENTION_DAYS) are automatically deleted from the temp directory
 * - Cleanup runs on every new log capture start
 */
const LOG_RETENTION_DAYS = 3;
const LOG_FILE_PREFIX = 'xcodemcp_sim_log_';

export interface LogSession {
  processes: ChildProcess[];
  logFilePath: string;
  simulatorUuid: string;
  bundleId: string;
}

export const activeLogSessions: Map<string, LogSession> = new Map();

/**
 * Start a log capture session for an iOS simulator.
 * Returns { sessionId, logFilePath, processes, error? }
 */
export async function startLogCapture(
  params: {
    simulatorUuid: string;
    bundleId: string;
    captureConsole?: boolean;
    args?: string[];
  },
  executor: CommandExecutor = getDefaultCommandExecutor(),
): Promise<{ sessionId: string; logFilePath: string; processes: ChildProcess[]; error?: string }> {
  // Clean up old logs before starting a new session
  await cleanOldLogs();

  const { simulatorUuid, bundleId, captureConsole = false, args = [] } = params;
  const logSessionId = uuidv4();
  const logFileName = `${LOG_FILE_PREFIX}${logSessionId}.log`;
  const logFilePath = path.join(os.tmpdir(), logFileName);

  try {
    await fs.promises.mkdir(os.tmpdir(), { recursive: true });
    await fs.promises.writeFile(logFilePath, '');
    const logStream = fs.createWriteStream(logFilePath, { flags: 'a' });
    const processes: ChildProcess[] = [];
    logStream.write('\n--- Log capture for bundle ID: ' + bundleId + ' ---\n');

    if (captureConsole) {
      const launchCommand = [
        'xcrun',
        'simctl',
        'launch',
        '--console-pty',
        '--terminate-running-process',
        simulatorUuid,
        bundleId,
      ];
      if (args.length > 0) {
        launchCommand.push(...args);
      }

      const stdoutLogResult = await executor(
        launchCommand,
        'Console Log Capture',
        true, // useShell
        undefined, // env
        true, // detached - don't wait for this streaming process to complete
      );

      if (!stdoutLogResult.success) {
        return {
          sessionId: '',
          logFilePath: '',
          processes: [],
          error: stdoutLogResult.error ?? 'Failed to start console log capture',
        };
      }

      stdoutLogResult.process.stdout?.pipe(logStream);
      stdoutLogResult.process.stderr?.pipe(logStream);
      processes.push(stdoutLogResult.process);
    }

    const osLogResult = await executor(
      [
        'xcrun',
        'simctl',
        'spawn',
        simulatorUuid,
        'log',
        'stream',
        '--level=debug',
        '--predicate',
        `subsystem == "${bundleId}"`,
      ],
      'OS Log Capture',
      true, // useShell
      undefined, // env
      true, // detached - don't wait for this streaming process to complete
    );

    if (!osLogResult.success) {
      return {
        sessionId: '',
        logFilePath: '',
        processes: [],
        error: osLogResult.error ?? 'Failed to start OS log capture',
      };
    }

    osLogResult.process.stdout?.pipe(logStream);
    osLogResult.process.stderr?.pipe(logStream);
    processes.push(osLogResult.process);

    for (const process of processes) {
      process.on('close', (code) => {
        log('info', `A log capture process for session ${logSessionId} exited with code ${code}.`);
      });
    }

    activeLogSessions.set(logSessionId, {
      processes,
      logFilePath,
      simulatorUuid,
      bundleId,
    });

    log('info', `Log capture started with session ID: ${logSessionId}`);
    return { sessionId: logSessionId, logFilePath, processes };
  } catch (error) {
    const message = error instanceof Error ? error.message : String(error);
    log('error', `Failed to start log capture: ${message}`);
    return { sessionId: '', logFilePath: '', processes: [], error: message };
  }
}

/**
 * Stop a log capture session and retrieve the log content.
 */
export async function stopLogCapture(
  logSessionId: string,
): Promise<{ logContent: string; error?: string }> {
  const session = activeLogSessions.get(logSessionId);
  if (!session) {
    log('warning', `Log session not found: ${logSessionId}`);
    return { logContent: '', error: `Log capture session not found: ${logSessionId}` };
  }

  try {
    log('info', `Attempting to stop log capture session: ${logSessionId}`);
    const logFilePath = session.logFilePath;
    for (const process of session.processes) {
      if (!process.killed && process.exitCode === null) {
        process.kill('SIGTERM');
      }
    }
    activeLogSessions.delete(logSessionId);
    log(
      'info',
      `Log capture session ${logSessionId} stopped. Log file retained at: ${logFilePath}`,
    );
    await fs.promises.access(logFilePath, fs.constants.R_OK);
    const fileContent = await fs.promises.readFile(logFilePath, 'utf-8');
    log('info', `Successfully read log content from ${logFilePath}`);
    return { logContent: fileContent };
  } catch (error) {
    const message = error instanceof Error ? error.message : String(error);
    log('error', `Failed to stop log capture session ${logSessionId}: ${message}`);
    return { logContent: '', error: message };
  }
}

/**
 * Deletes log files older than LOG_RETENTION_DAYS from the temp directory.
 * Runs quietly; errors are logged but do not throw.
 */
async function cleanOldLogs(): Promise<void> {
  const tempDir = os.tmpdir();
  let files: string[];
  try {
    files = await fs.promises.readdir(tempDir);
  } catch (err) {
    log(
      'warn',
      `Could not read temp dir for log cleanup: ${err instanceof Error ? err.message : String(err)}`,
    );
    return;
  }
  const now = Date.now();
  const retentionMs = LOG_RETENTION_DAYS * 24 * 60 * 60 * 1000;
  await Promise.all(
    files
      .filter((f) => f.startsWith(LOG_FILE_PREFIX) && f.endsWith('.log'))
      .map(async (f) => {
        const filePath = path.join(tempDir, f);
        try {
          const stat = await fs.promises.stat(filePath);
          if (now - stat.mtimeMs > retentionMs) {
            await fs.promises.unlink(filePath);
            log('info', `Deleted old log file: ${filePath}`);
          }
        } catch (err) {
          log(
            'warn',
            `Error during log cleanup for ${filePath}: ${err instanceof Error ? err.message : String(err)}`,
          );
        }
      }),
  );
}

```
Page 3/11FirstPrevNextLast